From 0b64b6371f8e2b3ef8cbae8c06b0629fe206dae6 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 15:18:19 +0800 Subject: [PATCH 001/306] fix pull changes docs --- doc/production/pull-changes.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/production/pull-changes.md b/doc/production/pull-changes.md index b99b39db..0fb5d568 100644 --- a/doc/production/pull-changes.md +++ b/doc/production/pull-changes.md @@ -24,9 +24,8 @@ Now that you have the code, run these commands: bundle install npm install - npm run build rake db:migrate - rake assets:precompile + rake assets:precompile # includes `npm run build` rake perms:fix passenger-config restart-app . From 40cb7606e3dddc68d52d53aae5de66bf154cf147 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 15:21:00 +0800 Subject: [PATCH 002/306] enable metamaps.debug whoops --- app/assets/javascripts/application.js | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index f980da75..6ed8278d 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -44,3 +44,4 @@ //= require ./src/Metamaps.Admin //= require ./src/Metamaps.Import //= require ./src/Metamaps.JIT +//= require ./src/Metamaps.Debug From 4bbb9df5af3a1c8b8832bcaf9f72f8a3486c9bc8 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 14 Sep 2016 10:45:42 +0800 Subject: [PATCH 003/306] can't use ` with uglify --- app/assets/javascripts/src/Metamaps.Debug.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.Debug.js b/app/assets/javascripts/src/Metamaps.Debug.js index 7dc61246..accd93a9 100644 --- a/app/assets/javascripts/src/Metamaps.Debug.js +++ b/app/assets/javascripts/src/Metamaps.Debug.js @@ -6,7 +6,7 @@ Metamaps.Debug = function () { console.debug(Metamaps) - console.debug(`Metamaps Version: ${Metamaps.VERSION}`) + console.debug('Metamaps Version: ' + Metamaps.VERSION) } Metamaps.debug = function () { Metamaps.Debug() From 4723c62b2025ffdb833bb918760bd05af9c74245 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 15 Sep 2016 07:18:15 +0800 Subject: [PATCH 004/306] fix password reset error --- app/controllers/users/passwords_controller.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index b6fa2acb..ee7b8667 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -1,7 +1,11 @@ class Users::PasswordsController < Devise::PasswordsController protected - def after_resetting_password_path_for(resource) - signed_in_root_path(resource) - end + def after_resetting_password_path_for(resource) + signed_in_root_path(resource) + end + + def after_sending_reset_password_instructions_path_for(resource_name) + new_user_session_path if is_navigational_format? + end end From d1c390636a930892fb6a9d26b71bcd9d1b2466a7 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sat, 17 Sep 2016 17:12:35 +0800 Subject: [PATCH 005/306] Get siblings by metacode type returns only topics with that metacode - fix #538 --- app/controllers/topics_controller.rb | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 846c5469..30ac57fd 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -64,13 +64,14 @@ class TopicsController < ApplicationController topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - @alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a - @alltopics.delete_if do |topic| + alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a + alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i } if params[:metacode].present? + alltopics.delete_if do |topic| !topicsAlreadyHas.index(topic.id).nil? end @json = Hash.new(0) - @alltopics.each do |t| + alltopics.each do |t| @json[t.metacode.id] += 1 end @@ -87,6 +88,7 @@ class TopicsController < ApplicationController topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] alltopics = policy_scope(Topic.relatives(@topic.id)).to_a + alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i } if params[:metacode].present? alltopics.delete_if do |topic| !topicsAlreadyHas.index(topic.id.to_s).nil? end From cd31452c79524a69db08aac6688da97f71cc51a3 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Sun, 18 Sep 2016 02:54:44 +0800 Subject: [PATCH 006/306] update readme (#575) * update readme * remove google plus --- README.md | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 26de4e6c..7ba4373a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,21 @@ Metamaps ======= -[![Join the chat at https://gitter.im/metamaps/metamaps](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/metamaps/metamaps?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Build Status](https://travis-ci.org/metamaps/metamaps.svg?branch=develop)](https://travis-ci.org/metamaps/metamaps) -Welcome to the Metamaps GitHub repo. +## What is Metamaps? -## About - -Metamaps is a free and AGPL open source technology for changemakers, innovators, educators and students. It enables individuals and communities to build and visualize their shared knowledge and unlock their collective intelligence. You can find out about more about the project at the [blog][site-blog]. +Metamaps is a free and open source technology for changemakers, innovators, educators and students. It enables individuals and communities to build and visualize their shared knowledge and unlock their collective intelligence. You can find a version of this software running at [metamaps.cc][site-beta], where the technology is being tested in a private beta. -Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch with us at team@metamaps.cc or @metamapps on twitter. +Metamaps is created and maintained by a distributed, nomadic community comprised of technologists, artists and storytellers. You can get in touch by using whichever of these channels you prefer: -To get connected with the community interested in Metamaps, join our [Google+ community][community]. +## Community + +- To send us a personal message or request an invite to the open beta, get in touch with us at team@metamaps.cc or @metamapps on Twitter. +- If you would like to report a bug, please check the [issues][contributing-issues] section in our [contributing instructions][contributing]. +- If you would like to get set up as a developer, that's great! Read on for help getting your development environment set up. ## Installation @@ -46,21 +47,16 @@ OR create a new account at `/join`, and use access code `qwertyui` Start mapping and programming! -We haven't figured out Vagrant for Windows yet, but we have a set of manual instructions here: +We haven't set up instructions for using Vagrant on Windows, but there are instructions for a manual setup here: - [For Windows][windows-installation] -## Contributing +## Contributing guidelines Cloning this repository directly is primarily for those wishing to contribute to our codebase. Check out our [contributing instructions][contributing] to get involved. -## Community - -- If you would like to report a bug, please check the [issues][contributing-issues] section in our [contributing instructions][contributing]. -- To participate in discussions and a public forum about Metamaps, join the [Google+ community][community] -- For contributors, read more instructions in [CONTRIBUTING.md][contributing]. - ## Licensing information + This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or(at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. @@ -69,10 +65,8 @@ The license can be read [here][license]. Copyright (c) 2015 Connor Turland - [site-blog]: http://blog.metamaps.cc [site-beta]: http://metamaps.cc -[community]: https://plus.google.com/u/0/communities/115060009262157699234 [license]: https://github.com/metamaps/metamaps/blob/develop/LICENSE [contributing]: https://github.com/metamaps/metamaps/blob/develop/doc/CONTRIBUTING.md [contributing-issues]: https://github.com/metamaps/metamaps/blob/develop/doc/CONTRIBUTING.md#reporting-bugs-and-other-issues From 698adf69cd4d4cac0dcc3c71ee60e8112c98f7e9 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 17 Sep 2016 14:55:23 -0400 Subject: [PATCH 007/306] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7ba4373a..2ffa9708 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ This program is distributed in the hope that it will be useful, but WITHOUT ANY The license can be read [here][license]. -Copyright (c) 2015 Connor Turland +Copyright (c) 2016 Connor Turland [site-blog]: http://blog.metamaps.cc [site-beta]: http://metamaps.cc From 823c0c59902143038b3846438e51111972717804 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 17 Sep 2016 15:06:54 -0400 Subject: [PATCH 008/306] no room is created if anon user (#642) --- app/assets/javascripts/src/Metamaps.Realtime.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Realtime.js b/app/assets/javascripts/src/Metamaps.Realtime.js index 99ffd097..620a561a 100644 --- a/app/assets/javascripts/src/Metamaps.Realtime.js +++ b/app/assets/javascripts/src/Metamaps.Realtime.js @@ -206,9 +206,11 @@ Metamaps.Realtime = { self.socket.emit('endMapperNotify') $('.collabCompass').remove() self.status = false - self.room.leave() - self.room.chat.$container.hide() - self.room.chat.close() + if (self.room) { + self.room.leave() + self.room.chat.$container.hide() + self.room.chat.close() + } }, turnOn: function (notify) { var self = Metamaps.Realtime From 61e27a4dcb1b41a2d6429891ba844ad06cfd3889 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 17 Sep 2016 23:43:33 +0000 Subject: [PATCH 009/306] height shouldn't stay hard set. fixes 622 --- app/assets/javascripts/src/Metamaps.TopicCard.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/app/assets/javascripts/src/Metamaps.TopicCard.js index 194433db..f1424ed9 100644 --- a/app/assets/javascripts/src/Metamaps.TopicCard.js +++ b/app/assets/javascripts/src/Metamaps.TopicCard.js @@ -25,7 +25,10 @@ Metamaps.TopicCard = { // initialize topic card draggability and resizability $('.showcard').draggable({ - handle: '.metacodeImage' + handle: '.metacodeImage', + stop: function() { + $(this).height('auto') + } }) embedly('on', 'card.rendered', self.embedlyCardRendered) From aace6796f5612b79bf8c1c444be39c035fb438f5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 19 Sep 2016 20:30:34 -0400 Subject: [PATCH 010/306] allow topic carousel to be pinned open (#643) * so that rapid topic creation can happen in succession * close when map closes --- app/assets/images/pincarousel_sprite.png | Bin 0 -> 822 bytes app/assets/javascripts/lib/cloudcarousel.js | 4 ++- app/assets/javascripts/src/JIT.js.erb | 2 +- app/assets/javascripts/src/Metamaps.Create.js | 24 ++++++++++++++-- app/assets/javascripts/src/Metamaps.Map.js | 10 +++++-- app/assets/javascripts/src/Metamaps.Topic.js | 14 ++++++--- app/assets/stylesheets/application.css.erb | 20 +++++++++++++ app/assets/stylesheets/clean.css.erb | 27 ++++++++++++++++-- app/views/maps/_newtopic.html.erb | 8 +++++- app/views/topics/_new.html.erb | 7 ++++- 10 files changed, 101 insertions(+), 15 deletions(-) create mode 100644 app/assets/images/pincarousel_sprite.png diff --git a/app/assets/images/pincarousel_sprite.png b/app/assets/images/pincarousel_sprite.png new file mode 100644 index 0000000000000000000000000000000000000000..e12956ab75962b1f78514e2615709754b35a5947 GIT binary patch literal 822 zcmV-61Ihe}P)rlDTf0sd#h>8bUl6)+ zKiup8IYd{@?|=6 z|2y_QK|k{#cY_zuFpPp>Sq=F10zU`eb<^A_m$$DA3DBS++Y?^447kLZm#GUej$xx& ze{W=2R&(wUA~TS1PdLs)0r*S{9LF7ql0fXwkwZu%0QfCLVBBuE?+XTY>Oz)luuTvy z!|%+p>^|LNuD0-5nmdGK*k}bMV@a5%^MZ3ygvXG6*EIbApszz1;tF8%Qvm;>sim{e zwgUv73m#fR0!U`xalE~#M|W@v9%6U~c;vnc06^m2gv1XF>21Lia~^?R`1=e2{$@G> z$o!LGSli(&X(n*Ao(aI`y^NNSl#hh0$d{3AyYB#`#Cb174Ivv>*G&y=eHlf7vRJ;# z*YW?Q2XeXG3f?J4s;UlQV=~;I77B%AdZ1J)#nuBG8}hoMs9o|Q1Bq@vpHHR-f?#p= z0QPnQA5Xdp@ezs1u7JmIMw+{mG^#B3!^Z>12Mh}3% zE=nIy58(e??{Dh?5b)9etJ4FLq-e6N{@Z%s51hGB8)C_ukpKVy07*qoM6N<$g0V+< AI{*Lx literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/lib/cloudcarousel.js b/app/assets/javascripts/lib/cloudcarousel.js index b37abfd7..bf6259c9 100644 --- a/app/assets/javascripts/lib/cloudcarousel.js +++ b/app/assets/javascripts/lib/cloudcarousel.js @@ -179,7 +179,9 @@ jQuery.browser = browser; { // START METAMAPS CODE $('body').bind('mousewheel',this,function(event, delta) { - if (Metamaps.Create.newTopic.beingCreated && !Metamaps.Create.isSwitchingSet) { + if (Metamaps.Create.newTopic.beingCreated && + !Metamaps.Create.isSwitchingSet && + !Metamaps.Create.newTopic.pinned) { event.data.rotate(delta); return false; } diff --git a/app/assets/javascripts/src/JIT.js.erb b/app/assets/javascripts/src/JIT.js.erb index d31d850c..2cb202dc 100644 --- a/app/assets/javascripts/src/JIT.js.erb +++ b/app/assets/javascripts/src/JIT.js.erb @@ -2454,7 +2454,7 @@ Extras.Classes.Navigation = new Class({ // START METAMAPS CODE e.preventDefault(); if (e.target.id != 'infovis-canvas') return; - if (Metamaps.Create.newTopic.beingCreated) return; + if (Metamaps.Create.newTopic.beingCreated && !Metamaps.Create.newTopic.pinned) return; // END METAMAPS CODE //$.event.stop($.event.get(e, win)); diff --git a/app/assets/javascripts/src/Metamaps.Create.js b/app/assets/javascripts/src/Metamaps.Create.js index bb01d129..6f3bbb62 100644 --- a/app/assets/javascripts/src/Metamaps.Create.js +++ b/app/assets/javascripts/src/Metamaps.Create.js @@ -150,6 +150,17 @@ Metamaps.Create = { $('#topic_name').keyup(function () { Metamaps.Create.newTopic.name = $(this).val() }) + + $('.pinCarousel').click(function() { + if (Metamaps.Create.newTopic.pinned) { + $('.pinCarousel').removeClass('isPinned') + Metamaps.Create.newTopic.pinned = false + } + else { + $('.pinCarousel').addClass('isPinned') + Metamaps.Create.newTopic.pinned = true + } + }) var topicBloodhound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), @@ -204,6 +215,7 @@ Metamaps.Create = { x: null, y: null, addSynapse: false, + pinned: false, open: function () { $('#new_topic').fadeIn('fast', function () { $('#topic_name').focus() @@ -211,10 +223,16 @@ Metamaps.Create = { Metamaps.Create.newTopic.beingCreated = true Metamaps.Create.newTopic.name = '' }, - hide: function () { - $('#new_topic').fadeOut('fast') + hide: function (force) { + if (force || !Metamaps.Create.newTopic.pinned) { + $('#new_topic').fadeOut('fast') + Metamaps.Create.newTopic.beingCreated = false + } + if (force) { + $('.pinCarousel').removeClass('isPinned') + Metamaps.Create.newTopic.pinned = false + } $('#topic_name').typeahead('val', '') - Metamaps.Create.newTopic.beingCreated = false } }, newSynapse: { diff --git a/app/assets/javascripts/src/Metamaps.Map.js b/app/assets/javascripts/src/Metamaps.Map.js index 0964ef51..1c3c638d 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js +++ b/app/assets/javascripts/src/Metamaps.Map.js @@ -136,7 +136,7 @@ Metamaps.Map = { $('.rightclickmenu').remove() Metamaps.TopicCard.hideCard() Metamaps.SynapseCard.hideCard() - Metamaps.Create.newTopic.hide() + Metamaps.Create.newTopic.hide(true) // true means force (and override pinned) Metamaps.Create.newSynapse.hide() Metamaps.Filter.close() Metamaps.Map.InfoBox.close() @@ -284,7 +284,13 @@ Metamaps.Map = { } } - return { + // this is so that if someone has relied on the auto-placement feature on this map, + // it will at least start placing nodes at the first empty spot + // this will only work up to the point in the spiral at which someone manually moved a node + if (Metamaps.Mappings.findWhere({ xloc: nextX, yloc: nextY })) { + return self.getNextCoord() + } + else return { x: nextX, y: nextY } diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index da2e6be3..40a4cd42 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -325,9 +325,12 @@ Metamaps.Topic = { }) Metamaps.Topics.add(topic) + if (Metamaps.Create.newTopic.pinned) { + var nextCoords = Metamaps.Map.getNextCoord() + } var mapping = new Metamaps.Backbone.Mapping({ - xloc: Metamaps.Create.newTopic.x, - yloc: Metamaps.Create.newTopic.y, + xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Metamaps.Create.newTopic.y, mappable_id: topic.cid, mappable_type: 'Topic', }) @@ -347,9 +350,12 @@ Metamaps.Topic = { var topic = self.get(id) + if (Metamaps.Create.newTopic.pinned) { + var nextCoords = Metamaps.Map.getNextCoord() + } var mapping = new Metamaps.Backbone.Mapping({ - xloc: Metamaps.Create.newTopic.x, - yloc: Metamaps.Create.newTopic.y, + xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Metamaps.Create.newTopic.y, mappable_type: 'Topic', mappable_id: topic.id, }) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index 5a4d62d3..d6d80201 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -567,6 +567,26 @@ button.button.btn-no:hover { .openMetacodeSwitcher:hover { background-position: -16px 0; } +.pinCarousel { + cursor: pointer; + display: block; + height: 16px; + width: 16px; + background-image: url(<%= asset_data_uri('pincarousel_sprite.png') %>); + position: absolute; + z-index: 2; + top: 20px; + right: 16px; +} +.pinCarousel:hover { + background-position: 0 -16px; +} +.pinCarousel.isPinned { + background-position: -16px 0; +} +.pinCarousel.isPinned:hover { + background-position: -16px -16px; +} #metacodeImg { height: 120px; } diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index 71985408..b41f14ca 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -458,7 +458,7 @@ } .zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, - .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .starMap:hover .tooltipsAbove { + .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { display: block; } @@ -522,6 +522,29 @@ margin-left: -34px; } +.openMetacodeSwitcher .tooltipsAbove { + left: -50px; + top: -5px; +} +.pinCarousel .tooltipsAbove { + top: -5px; +} +.pinCarousel .tooltipsAbove.helpPin { + left: -24px; +} +.pinCarousel .tooltipsAbove.helpUnpin { + left: -14px; +} +.openMetacodeSwitcher .tooltipsAbove:after { + left: 50%; +} +.pinCarousel .tooltipsAbove.helpPin:after { + left: 46%; +} +.pinCarousel .tooltipsAbove.helpUnpin:after { + left: 42%; +} + .sidebarForkIcon div:after{ left: 45%; } @@ -586,7 +609,7 @@ right: 37% !important; } -.mapInfoIcon div:after, .openCheatsheet div:after, .starMap div:after { +.mapInfoIcon div:after, .openCheatsheet div:after, .starMap div:after, .openMetacodeSwitcher div:after, .pinCarousel div:after { content: ''; position: absolute; top: 76%; diff --git a/app/views/maps/_newtopic.html.erb b/app/views/maps/_newtopic.html.erb index e5263d76..ba3d1797 100644 --- a/app/views/maps/_newtopic.html.erb +++ b/app/views/maps/_newtopic.html.erb @@ -1,5 +1,11 @@ <%= form_for Topic.new, url: topics_url, remote: true do |form| %> -
+
+
Switch Metacodes
+
+
+
Pin Open
+
Unpin
+
<% @metacodes = user_metacodes() %> <% set = get_metacodeset() %> diff --git a/app/views/topics/_new.html.erb b/app/views/topics/_new.html.erb index 49b6a9a8..4f856a81 100644 --- a/app/views/topics/_new.html.erb +++ b/app/views/topics/_new.html.erb @@ -6,7 +6,12 @@
<%= form_for Topic.new, url: topics_url, remote: true do |form| %> -
+
+
Switch Metacodes
+
+
+
Keep Open
+
<% @m = user.settings.metacodes %> <% set = @m[0].include?("metacodeset") ? MetacodeSet.find(@m[0].sub("metacodeset-","").to_i) : false %> From 95151523154117770fc8d07dd6c2e018d1e8efae Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 21:01:36 +0800 Subject: [PATCH 011/306] move auto layout function into its own file --- .../javascripts/src/Metamaps.AutoLayout.js | 75 +++++++++++++++++++ app/assets/javascripts/src/Metamaps.Map.js | 72 +----------------- app/assets/javascripts/src/Metamaps.Topic.js | 2 +- 3 files changed, 78 insertions(+), 71 deletions(-) create mode 100644 app/assets/javascripts/src/Metamaps.AutoLayout.js diff --git a/app/assets/javascripts/src/Metamaps.AutoLayout.js b/app/assets/javascripts/src/Metamaps.AutoLayout.js new file mode 100644 index 00000000..51e105c2 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.AutoLayout.js @@ -0,0 +1,75 @@ +/* global Metamaps */ + +/* + * Metmaaps.AutoLayout.js + * + * Dependencies: none! + */ + +Metamaps.AutoLayout = { + nextX: 0, + nextY: 0, + sideLength: 1, + turnCount: 0, + nextXshift: 1, + nextYshift: 0, + timeToTurn: 0, + + getNextCoord: function () { + var self = Metamaps.AutoLayout + var nextX = self.nextX + var nextY = self.nextY + + var DISTANCE_BETWEEN = 120 + + self.nextX = self.nextX + DISTANCE_BETWEEN * self.nextXshift + self.nextY = self.nextY + DISTANCE_BETWEEN * self.nextYshift + + self.timeToTurn += 1 + // if true, it's time to turn + if (self.timeToTurn === self.sideLength) { + self.turnCount += 1 + // if true, it's time to increase side length + if (self.turnCount % 2 === 0) { + self.sideLength += 1 + } + self.timeToTurn = 0 + + // going right? turn down + if (self.nextXshift == 1 && self.nextYshift == 0) { + self.nextXshift = 0 + self.nextYshift = 1 + } + // going down? turn left + else if (self.nextXshift == 0 && self.nextYshift == 1) { + self.nextXshift = -1 + self.nextYshift = 0 + } + // going left? turn up + else if (self.nextXshift == -1 && self.nextYshift == 0) { + self.nextXshift = 0 + self.nextYshift = -1 + } + // going up? turn right + else if (self.nextXshift == 0 && self.nextYshift == -1) { + self.nextXshift = 1 + self.nextYshift = 0 + } + } + + return { + x: nextX, + y: nextY + } + }, + resetSpiral: function () { + var self = Metamaps.AutoLayout + self.nextX = 0 + self.nextY = 0 + self.nextXshift = 1 + self.nextYshift = 0 + self.sideLength = 1 + self.timeToTurn = 0 + self.turnCount = 0 + } +} diff --git a/app/assets/javascripts/src/Metamaps.Map.js b/app/assets/javascripts/src/Metamaps.Map.js index 1c3c638d..264e3c48 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js +++ b/app/assets/javascripts/src/Metamaps.Map.js @@ -4,6 +4,7 @@ * Metamaps.Map.js.erb * * Dependencies: + * - Metamaps.AutoLayout * - Metamaps.Create * - Metamaps.Erb * - Metamaps.Filter @@ -34,13 +35,6 @@ Metamaps.Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' }, - nextX: 0, - nextY: 0, - sideLength: 1, - turnCount: 0, - nextXshift: 1, - nextYshift: 0, - timeToTurn: 0, init: function () { var self = Metamaps.Map @@ -131,7 +125,7 @@ Metamaps.Map = { end: function () { if (Metamaps.Active.Map) { $('.wrapper').removeClass('canEditMap commonsMap') - Metamaps.Map.resetSpiral() + Metamaps.AutoLayout.resetSpiral() $('.rightclickmenu').remove() Metamaps.TopicCard.hideCard() @@ -242,68 +236,6 @@ Metamaps.Map = { Metamaps.Mappers.add(Metamaps.Active.Mapper) } }, - getNextCoord: function () { - var self = Metamaps.Map - var nextX = self.nextX - var nextY = self.nextY - - var DISTANCE_BETWEEN = 120 - - self.nextX = self.nextX + DISTANCE_BETWEEN * self.nextXshift - self.nextY = self.nextY + DISTANCE_BETWEEN * self.nextYshift - - self.timeToTurn += 1 - // if true, it's time to turn - if (self.timeToTurn === self.sideLength) { - self.turnCount += 1 - // if true, it's time to increase side length - if (self.turnCount % 2 === 0) { - self.sideLength += 1 - } - self.timeToTurn = 0 - - // going right? turn down - if (self.nextXshift == 1 && self.nextYshift == 0) { - self.nextXshift = 0 - self.nextYshift = 1 - } - // going down? turn left - else if (self.nextXshift == 0 && self.nextYshift == 1) { - self.nextXshift = -1 - self.nextYshift = 0 - } - // going left? turn up - else if (self.nextXshift == -1 && self.nextYshift == 0) { - self.nextXshift = 0 - self.nextYshift = -1 - } - // going up? turn right - else if (self.nextXshift == 0 && self.nextYshift == -1) { - self.nextXshift = 1 - self.nextYshift = 0 - } - } - - // this is so that if someone has relied on the auto-placement feature on this map, - // it will at least start placing nodes at the first empty spot - // this will only work up to the point in the spiral at which someone manually moved a node - if (Metamaps.Mappings.findWhere({ xloc: nextX, yloc: nextY })) { - return self.getNextCoord() - } - else return { - x: nextX, - y: nextY - } - }, - resetSpiral: function () { - Metamaps.Map.nextX = 0 - Metamaps.Map.nextY = 0 - Metamaps.Map.nextXshift = 1 - Metamaps.Map.nextYshift = 0 - Metamaps.Map.sideLength = 1 - Metamaps.Map.timeToTurn = 0 - Metamaps.Map.turnCount = 0 - }, exportImage: function () { var canvas = {} diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index 40a4cd42..52fabed3 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -370,7 +370,7 @@ Metamaps.Topic = { var topic = self.get(id) - var nextCoords = Metamaps.Map.getNextCoord() + var nextCoords = Metamaps.AutoLayout.getNextCoord() var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords.x, yloc: nextCoords.y, From ec96d69876da3792a775b507ccd3bb3f3dcb0413 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Tue, 13 Sep 2016 21:02:55 +0800 Subject: [PATCH 012/306] refactor import view: -Paste Input wrapper class to abstract away getting input -Add ability to drop files in PasteInput -Add ability to drop .webloc files or paste a link to create a new topic with that link in the link and desc fields --- app/assets/javascripts/application.js | 2 + app/assets/javascripts/src/Metamaps.Import.js | 64 +++++----- .../javascripts/src/Metamaps.PasteInput.js | 120 ++++++++++++++++++ 3 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 app/assets/javascripts/src/Metamaps.PasteInput.js diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6ed8278d..14f565fa 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -43,5 +43,7 @@ //= require ./src/Metamaps.Mobile //= require ./src/Metamaps.Admin //= require ./src/Metamaps.Import +//= require ./src/Metamaps.AutoLayout +//= require ./src/Metamaps.PasteInput //= require ./src/Metamaps.JIT //= require ./src/Metamaps.Debug diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/app/assets/javascripts/src/Metamaps.Import.js index d7771988..7ebadb37 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -6,7 +6,6 @@ * Dependencies: * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.Famous // TODO remove dependency * - Metamaps.Map * - Metamaps.Mappings * - Metamaps.Metacodes @@ -24,38 +23,30 @@ Metamaps.Import = { ], cidMappings: {}, // to be filled by import_id => cid mappings - init: function () { + handleTSV: function (text) { var self = Metamaps.Import + results = self.parseTabbedString(text) + self.handle(results) + }, - $('body').bind('paste', function (e) { - if (e.target.tagName === 'INPUT') return - if (e.target.tagName === 'TEXTAREA') return + handleJSON: function (text) { + var self = Metamaps.Import + results = JSON.parse(text) + self.handle(results) + }, - var text = e.originalEvent.clipboardData.getData('text/plain') + handle: function(results) { + var self = Metamaps.Import + var topics = results.topics + var synapses = results.synapses - var results - if (text.trimLeft()[0] === '{') { - try { - results = JSON.parse(text) - } catch (e) { - results = false - } - } else { - results = self.parseTabbedString(text) - } - if (results === false) return - - var topics = results.topics - var synapses = results.synapses - - if (topics.length > 0 || synapses.length > 0) { - if (window.confirm('Are you sure you want to create ' + topics.length + - ' new topics and ' + synapses.length + ' new synapses?')) { - self.importTopics(topics) - self.importSynapses(synapses) - } // if + if (topics.length > 0 || synapses.length > 0) { + if (window.confirm('Are you sure you want to create ' + topics.length + + ' new topics and ' + synapses.length + ' new synapses?')) { + self.importTopics(topics) + self.importSynapses(synapses) } // if - }) + } // if }, abort: function (message) { @@ -272,15 +263,22 @@ Metamaps.Import = { console.warn("Couldn't find metacode " + metacode_name + ' so used Wildcard instead.') } + var topic_permission = permission || Metamaps.Active.Map.get('permission') + var defer_to_map_id = permission === topic_permission ? Metamaps.Active.Map.get('id') : null var topic = new Metamaps.Backbone.Topic({ name: name, metacode_id: metacode.id, - permission: permission || Metamaps.Active.Map.get('permission'), - desc: desc || "", - link: link + permission: topic_permission, + defer_to_map_id: defer_to_map_id, + desc: desc || "" }) + topic.set('desc', desc || '') // TODO why is this necessary? + topic.set('link', link) // TODO why is this necessary? Metamaps.Topics.add(topic) - self.cidMappings[import_id] = topic.cid + + if (import_id !== null && import_id !== undefined) { + self.cidMappings[import_id] = topic.cid + } var mapping = new Metamaps.Backbone.Mapping({ xloc: xloc, @@ -293,7 +291,7 @@ Metamaps.Import = { // this function also includes the creation of the topic in the database Metamaps.Topic.renderTopic(mapping, topic, true, true) - Metamaps.Famous.viz.hideInstructions() + Metamaps.GlobalUI.hideDiv('#instructions') }, createSynapseWithParameters: function (desc, category, permission, diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/app/assets/javascripts/src/Metamaps.PasteInput.js new file mode 100644 index 00000000..1b89b0af --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.PasteInput.js @@ -0,0 +1,120 @@ +/* global Metamaps, $ */ + +/* + * Metamaps.PasteInput.js.erb + * + * Dependencies: + * - Metamaps.Import + * - Metamaps.AutoLayout + */ + +Metamaps.PasteInput = { + init: function () { + var self = Metamaps.PasteInput + + // intercept dragged files + // see http://stackoverflow.com/questions/6756583 + window.addEventListener("dragover", function(e){ + e = e || event; + e.preventDefault(); + }, false); + window.addEventListener("drop", function(e){ + e = e || event; + e.preventDefault(); + var coords = Metamaps.Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) + if (e.dataTransfer.files.length > 0) { + var fileReader = new FileReader() + var text = fileReader.readAsText(e.dataTransfer.files[0]) + fileReader.onload = function(e) { + var text = e.currentTarget.result + if (text.substring(0,5) === '(.*)<\/string>[\s\S]*/m, '$1') + } + self.handle(text, coords) + } + } + }, false); + + // allow pasting onto canvas (but don't break existing inputs/textareas) + $('body').bind('paste', function (e) { + if (e.target.tagName === 'INPUT') return + if (e.target.tagName === 'TEXTAREA') return + + var text = e.originalEvent.clipboardData.getData('text/plain').trim() + self.handle(text) + }) + }, + + handle: function(text, coords) { + var self = Metamaps.PasteInput + // thanks to https://github.com/kevva/url-regex + const URL_REGEX = new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$') + + if (text.match(URL_REGEX)) { + self.handleURL(text, coords) + } else if (text[0] === '{') { + self.handleJSON(text) + } else if (text.match(/\t/)) { + self.handleTSV(text) + } else { + // fail silently + } + }, + + handleURL: function (text, coords) { + var title = 'Link' + if (!coords || !coords.x || !coords.y) { + coords = Metamaps.AutoLayout.getNextCoord() + } + + var import_id = null // don't store a cidMapping + var permission = null // use default + + // try { + // // fetch title in 150ms or less + // Promise.race([ + // new Promise(function(resolve, reject) { + // fetch(text).then(function(response) { + // return response.text() + // }).then(function(html) { + // title = html.replace(/[\s\S]*(.*)<\/title>[\s\S]*/m, '$1') + // resolve() + // }) + // }), new Promise(function(resolve, reject) { + // window.setTimeout(function() { + // resolve() + // }, 150) + // }) + // ]).then(function() { + // finish() + // }).catch(function(error) { + // throw error + // }) + // } catch (err) { + // console.warn("Your browser can't fetch the title") // TODO move to webpack to avoid this error + // } + finish() + + function finish() { + Metamaps.Import.createTopicWithParameters( + title, + 'Reference', // metacode - todo fix + permission, + text, // desc - todo load from url? + text, // link - todo fix because this isn't being POSTed + coords.x, + coords.y, + import_id + ) + } + }, + + handleJSON: function (text) { + Metamaps.Import.handleJSON(text) + }, + + handleTSV: function (text) { + Metamaps.Import.handleTSV(text) + } +} From fac59f346f0f5320b7753157ae1297462d48e58d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 21 Sep 2016 10:24:57 +0800 Subject: [PATCH 013/306] fix topic init function --- app/assets/javascripts/src/Metamaps.Backbone.js | 4 ++-- app/assets/javascripts/src/Metamaps.Import.js | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js index d05ffe3a..a37229d2 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/app/assets/javascripts/src/Metamaps.Backbone.js @@ -321,8 +321,8 @@ Metamaps.Backbone.init = function () { if (this.isNew()) { this.set({ 'user_id': Metamaps.Active.Mapper.id, - 'desc': '', - 'link': '', + 'desc': this.get('desc') || '', + 'link': this.get('link') || '', 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' }) } diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/app/assets/javascripts/src/Metamaps.Import.js index 7ebadb37..2ed7e00a 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -270,10 +270,9 @@ Metamaps.Import = { metacode_id: metacode.id, permission: topic_permission, defer_to_map_id: defer_to_map_id, - desc: desc || "" + desc: desc || "", + link: link || "" }) - topic.set('desc', desc || '') // TODO why is this necessary? - topic.set('link', link) // TODO why is this necessary? Metamaps.Topics.add(topic) if (import_id !== null && import_id !== undefined) { From 49084b98dd140c07bb393bec821399e240947353 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 21 Sep 2016 10:48:47 +0800 Subject: [PATCH 014/306] =?UTF-8?q?omg=20import=20bookmarks=20=F0=9F=98=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../javascripts/src/Metamaps.PasteInput.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/app/assets/javascripts/src/Metamaps.PasteInput.js index 1b89b0af..55798587 100644 --- a/app/assets/javascripts/src/Metamaps.PasteInput.js +++ b/app/assets/javascripts/src/Metamaps.PasteInput.js @@ -9,16 +9,19 @@ */ Metamaps.PasteInput = { + // thanks to https://github.com/kevva/url-regex + URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'), + init: function () { var self = Metamaps.PasteInput // intercept dragged files // see http://stackoverflow.com/questions/6756583 - window.addEventListener("dragover", function(e){ + window.addEventListener("dragover", function(e) { e = e || event; e.preventDefault(); }, false); - window.addEventListener("drop", function(e){ + window.addEventListener("drop", function(e) { e = e || event; e.preventDefault(); var coords = Metamaps.Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) @@ -34,6 +37,14 @@ Metamaps.PasteInput = { self.handle(text, coords) } } + // OMG import bookmarks 😍 + if (e.dataTransfer.items.length > 0) { + e.dataTransfer.items[0].getAsString(function(text) { + if (text.match(self.URL_REGEX)) { + self.handle(text, coords) + } + }) + } }, false); // allow pasting onto canvas (but don't break existing inputs/textareas) @@ -48,10 +59,8 @@ Metamaps.PasteInput = { handle: function(text, coords) { var self = Metamaps.PasteInput - // thanks to https://github.com/kevva/url-regex - const URL_REGEX = new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$') - if (text.match(URL_REGEX)) { + if (text.match(self.URL_REGEX)) { self.handleURL(text, coords) } else if (text[0] === '{') { self.handleJSON(text) From 1efd78ad7bbff6fcc761df3ee38b92e39b58eae7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 21 Sep 2016 14:27:49 +0800 Subject: [PATCH 015/306] initial attempt at focussing input field when entering multiple topics --- .../javascripts/src/Metamaps.Backbone.js | 8 +-- app/assets/javascripts/src/Metamaps.Import.js | 9 ++- .../javascripts/src/Metamaps.PasteInput.js | 55 ++++++------------- app/assets/javascripts/src/Metamaps.Topic.js | 19 ++++--- .../javascripts/src/Metamaps.TopicCard.js | 12 ++-- 5 files changed, 48 insertions(+), 55 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/app/assets/javascripts/src/Metamaps.Backbone.js index a37229d2..2c1f58af 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/app/assets/javascripts/src/Metamaps.Backbone.js @@ -63,7 +63,7 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ authorizeToEdit: function (mapper) { if (mapper && ( this.get('permission') === 'commons' || - this.get('collaborator_ids').includes(mapper.get('id')) || + (this.get('collaborator_ids') || []).includes(mapper.get('id')) || this.get('user_id') === mapper.get('id'))) { return true } else { @@ -350,9 +350,9 @@ Metamaps.Backbone.init = function () { }, authorizeToEdit: function (mapper) { if (mapper && - (this.get('calculated_permission') === 'commons' || - this.get('collaborator_ids').includes(mapper.get('id')) || - this.get('user_id') === mapper.get('id'))) { + (this.get('user_id') === mapper.get('id') || + this.get('calculated_permission') === 'commons' || + this.get('collaborator_ids').includes(mapper.get('id')))) { return true } else { return false diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/app/assets/javascripts/src/Metamaps.Import.js index 2ed7e00a..2dee51d0 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/app/assets/javascripts/src/Metamaps.Import.js @@ -254,7 +254,7 @@ Metamaps.Import = { }, createTopicWithParameters: function (name, metacode_name, permission, desc, - link, xloc, yloc, import_id) { + link, xloc, yloc, import_id, opts) { var self = Metamaps.Import $(document).trigger(Metamaps.Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null @@ -271,7 +271,8 @@ Metamaps.Import = { permission: topic_permission, defer_to_map_id: defer_to_map_id, desc: desc || "", - link: link || "" + link: link || "", + calculated_permission: Metamaps.Active.Map.get('permission') }) Metamaps.Topics.add(topic) @@ -288,7 +289,9 @@ Metamaps.Import = { Metamaps.Mappings.add(mapping) // this function also includes the creation of the topic in the database - Metamaps.Topic.renderTopic(mapping, topic, true, true) + Metamaps.Topic.renderTopic(mapping, topic, true, true, { + success: opts.success + }) Metamaps.GlobalUI.hideDiv('#instructions') }, diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/app/assets/javascripts/src/Metamaps.PasteInput.js index 55798587..aaf848d0 100644 --- a/app/assets/javascripts/src/Metamaps.PasteInput.js +++ b/app/assets/javascripts/src/Metamaps.PasteInput.js @@ -80,43 +80,24 @@ Metamaps.PasteInput = { var import_id = null // don't store a cidMapping var permission = null // use default - // try { - // // fetch title in 150ms or less - // Promise.race([ - // new Promise(function(resolve, reject) { - // fetch(text).then(function(response) { - // return response.text() - // }).then(function(html) { - // title = html.replace(/[\s\S]*<title>(.*)<\/title>[\s\S]*/m, '$1') - // resolve() - // }) - // }), new Promise(function(resolve, reject) { - // window.setTimeout(function() { - // resolve() - // }, 150) - // }) - // ]).then(function() { - // finish() - // }).catch(function(error) { - // throw error - // }) - // } catch (err) { - // console.warn("Your browser can't fetch the title") // TODO move to webpack to avoid this error - // } - finish() - - function finish() { - Metamaps.Import.createTopicWithParameters( - title, - 'Reference', // metacode - todo fix - permission, - text, // desc - todo load from url? - text, // link - todo fix because this isn't being POSTed - coords.x, - coords.y, - import_id - ) - } + Metamaps.Import.createTopicWithParameters( + title, + 'Reference', // metacode - todo fix + permission, + text, // desc - todo load from url? + text, // link - todo fix because this isn't being POSTed + coords.x, + coords.y, + import_id, + { + success: function(topic) { + Metamaps.TopicCard.showCard(topic.get('node'), function() { + $('#showcard #titleActivator').click() + .find('textarea, input').focus() + }) + } + } + ) }, handleJSON: function (text) { diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index 52fabed3..9e6782cb 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -186,11 +186,10 @@ Metamaps.Topic = { error: function () {} }) }, - /* - * - * - */ - renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter) { + + // opts is additional options in a hash + // TODO: move createNewInDB and permitCerateSYnapseAfter into opts + renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts) { var self = Metamaps.Topic var nodeOnViz, tempPos @@ -265,18 +264,24 @@ Metamaps.Topic = { }) } - var mappingSuccessCallback = function (mappingModel, response) { + var mappingSuccessCallback = function (mappingModel, response, topicModel) { var newTopicData = { mappingid: mappingModel.id, mappableid: mappingModel.get('mappable_id') } $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + // call a success callback if provided + if (opts.success) { + opts.success(topicModel) + } } var topicSuccessCallback = function (topicModel, response) { if (Metamaps.Active.Map) { mapping.save({ mappable_id: topicModel.id }, { - success: mappingSuccessCallback, + success: function (model, response) { + mappingSuccessCallback(model, response, topicModel) + }, error: function (model, response) { console.log('error saving mapping to database') } diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/app/assets/javascripts/src/Metamaps.TopicCard.js index f1424ed9..1453104d 100644 --- a/app/assets/javascripts/src/Metamaps.TopicCard.js +++ b/app/assets/javascripts/src/Metamaps.TopicCard.js @@ -37,7 +37,7 @@ Metamaps.TopicCard = { * Will open the Topic Card for the node that it's passed * @param {$jit.Graph.Node} node */ - showCard: function (node) { + showCard: function (node, opts) { var self = Metamaps.TopicCard var topic = node.getData('topic') @@ -46,7 +46,11 @@ Metamaps.TopicCard = { self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper) // populate the card that's about to show with the right topics data self.populateShowCard(topic) - $('.showcard').fadeIn('fast') + return $('.showcard').fadeIn('fast', function() { + if (opts.complete) { + opts.complete() + } + }) }, hideCard: function () { var self = Metamaps.TopicCard @@ -413,8 +417,8 @@ Metamaps.TopicCard = { nodeValues.attachments = '' } - var inmapsAr = topic.get('inmaps') - var inmapsLinks = topic.get('inmapsLinks') + var inmapsAr = topic.get('inmaps') || [] + var inmapsLinks = topic.get('inmapsLinks') || [] nodeValues.inmaps = '' if (inmapsAr.length < 6) { for (i = 0; i < inmapsAr.length; i++) { From 3843cab6439d90a16e49418bea289f269cf42d7c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 01:22:40 +0800 Subject: [PATCH 016/306] rails 5 + api v2 + raml api docs (#593) * update Gemfile to rails 5 and ruby 2.3.0 * fiddle with javascripts and add sprockets manifest file * update config directory for rails 5 * fix some errors with controllers/serializers * fix travis and rspec * new serializers renamed to serializers * module Api::V1 * reusable embedding code * add index/collections/paging. overriding most of snorlax now |:) * raml api documentation + rspec tests to verify schemas/examples * add sorting by ?sort and searching by ?q. Add pagination Link headers * api v1 => v2 * fill out synapse api * alphabetize map policy * fix page thing * fill out maps api * formParameters => properties, and fiddle with map api * more raml 1.0 stuff i'm learning about * deprecate v1 api * rails 5 uses ApplicationRecord class for app-wide model config * Update topic spec for api v2 * workaround for user_preference.rb issue * get ready for token api docs. also TODO is mapping api docs * spec out mapping api * map/mapping/synapse spec, plus other bugs * awesome, token specs/apis are done * add sanity checks to the api tests * more cleanup * devise fix * fix starred map error --- Gemfile | 20 +- Gemfile.lock | 211 +++++++++--------- app/assets/config/manifest.js | 6 + app/controllers/api/mappings_controller.rb | 2 - app/controllers/api/maps_controller.rb | 2 - app/controllers/api/restful_controller.rb | 50 ----- app/controllers/api/synapses_controller.rb | 2 - app/controllers/api/tokens_controller.rb | 17 -- app/controllers/api/topics_controller.rb | 2 - .../api/v1/deprecated_controller.rb | 9 + app/controllers/api/v1/mappings_controller.rb | 6 + app/controllers/api/v1/maps_controller.rb | 6 + app/controllers/api/v1/synapses_controller.rb | 6 + app/controllers/api/v1/tokens_controller.rb | 6 + app/controllers/api/v1/topics_controller.rb | 6 + app/controllers/api/v2/mappings_controller.rb | 6 + app/controllers/api/v2/maps_controller.rb | 9 + app/controllers/api/v2/restful_controller.rb | 179 +++++++++++++++ app/controllers/api/v2/sessions_controller.rb | 20 ++ app/controllers/api/v2/synapses_controller.rb | 9 + app/controllers/api/v2/tokens_controller.rb | 11 + app/controllers/api/v2/topics_controller.rb | 6 + app/controllers/main_controller.rb | 4 +- app/controllers/maps_controller.rb | 2 +- .../users/registrations_controller.rb | 5 +- app/models/application_record.rb | 3 + app/models/event.rb | 2 +- app/models/in_metacode_set.rb | 2 +- app/models/map.rb | 2 +- app/models/mapping.rb | 2 +- app/models/message.rb | 2 +- app/models/metacode.rb | 2 +- app/models/metacode_set.rb | 2 +- app/models/synapse.rb | 2 +- app/models/token.rb | 2 +- app/models/topic.rb | 2 +- app/models/user.rb | 4 +- app/models/user_map.rb | 2 +- app/models/user_preference.rb | 10 +- app/models/webhook.rb | 2 +- app/policies/map_policy.rb | 58 ++--- app/policies/mapping_policy.rb | 8 +- app/policies/synapse_policy.rb | 4 + app/policies/topic_policy.rb | 4 + .../api/v2/application_serializer.rb | 29 +++ app/serializers/api/v2/event_serializer.rb | 18 ++ app/serializers/api/v2/map_serializer.rb | 28 +++ app/serializers/api/v2/mapping_serializer.rb | 25 +++ app/serializers/api/v2/metacode_serializer.rb | 11 + app/serializers/api/v2/synapse_serializer.rb | 24 ++ app/serializers/api/v2/token_serializer.rb | 10 + app/serializers/api/v2/topic_serializer.rb | 24 ++ app/serializers/api/v2/user_serializer.rb | 19 ++ app/serializers/api/v2/webhook_serializer.rb | 7 + app/serializers/event_serializer.rb | 15 -- app/serializers/new_map_serializer.rb | 16 -- app/serializers/new_mapping_serializer.rb | 19 -- app/serializers/new_metacode_serializer.rb | 7 - app/serializers/new_synapse_serializer.rb | 14 -- app/serializers/new_topic_serializer.rb | 13 -- app/serializers/new_user_serializer.rb | 15 -- app/serializers/token_serializer.rb | 7 - app/serializers/webhook_serializer.rb | 3 - config/application.rb | 23 +- config/boot.rb | 5 +- config/cable.yml | 9 + config/environment.rb | 8 +- config/environments/production.rb | 51 +---- config/environments/test.rb | 6 +- config/initializers/access_codes.rb | 2 +- .../initializers/active_model_serializers.rb | 1 + .../application_controller_renderer.rb | 6 + config/initializers/assets.rb | 11 + config/initializers/cookies_serializer.rb | 5 + .../initializers/filter_parameter_logging.rb | 4 + config/initializers/inflections.rb | 11 +- config/initializers/kaminari_config.rb | 10 + config/initializers/mime_types.rb | 1 - config/initializers/new_framework_defaults.rb | 24 ++ config/initializers/secret_token.rb | 2 +- config/initializers/session_store.rb | 4 +- config/initializers/wrap_parameters.rb | 8 +- config/puma.rb | 47 ++++ config/routes.rb | 27 ++- config/spring.rb | 7 + db/schema.rb | 92 +++----- doc/api/api.raml | 39 ++++ doc/api/apis/mappings.raml | 68 ++++++ doc/api/apis/maps.raml | 82 +++++++ doc/api/apis/synapses.raml | 82 +++++++ doc/api/apis/tokens.raml | 25 +++ doc/api/apis/topics.raml | 72 ++++++ doc/api/examples/map.json | 27 +++ doc/api/examples/mapping.json | 11 + doc/api/examples/mappings.json | 54 +++++ doc/api/examples/maps.json | 37 +++ doc/api/examples/synapse.json | 13 ++ doc/api/examples/synapses.json | 34 +++ doc/api/examples/token.json | 8 + doc/api/examples/tokens.json | 18 ++ doc/api/examples/topic.json | 13 ++ doc/api/examples/topics.json | 34 +++ doc/api/resourceTypes/base.raml | 35 +++ doc/api/resourceTypes/collection.raml | 22 ++ doc/api/resourceTypes/item.raml | 29 +++ doc/api/schemas/_datetimestamp.json | 4 + doc/api/schemas/_id.json | 4 + doc/api/schemas/_map.json | 67 ++++++ doc/api/schemas/_mapping.json | 41 ++++ doc/api/schemas/_page.json | 38 ++++ doc/api/schemas/_permission.json | 4 + doc/api/schemas/_synapse.json | 42 ++++ doc/api/schemas/_token.json | 24 ++ doc/api/schemas/_topic.json | 43 ++++ doc/api/schemas/map.json | 12 + doc/api/schemas/mapping.json | 12 + doc/api/schemas/mappings.json | 19 ++ doc/api/schemas/maps.json | 19 ++ doc/api/schemas/synapse.json | 12 + doc/api/schemas/synapses.json | 19 ++ doc/api/schemas/token.json | 12 + doc/api/schemas/tokens.json | 19 ++ doc/api/schemas/topic.json | 12 + doc/api/schemas/topics.json | 19 ++ doc/api/traits/orderable.raml | 3 + doc/api/traits/pageable.raml | 7 + doc/api/traits/searchable.raml | 4 + spec/api/v2/mappings_api_spec.rb | 59 +++++ spec/api/v2/maps_api_spec.rb | 59 +++++ spec/api/v2/synapses_api_spec.rb | 59 +++++ spec/api/v2/tokens_api_spec.rb | 44 ++++ spec/api/v2/topics_api_spec.rb | 59 +++++ spec/controllers/mappings_controller_spec.rb | 56 ++--- spec/controllers/maps_controller_spec.rb | 73 +++--- spec/controllers/metacodes_controller_spec.rb | 45 ++-- spec/controllers/synapses_controller_spec.rb | 53 +++-- spec/controllers/topics_controller_spec.rb | 58 ++--- spec/factories/maps.rb | 1 + spec/factories/synapses.rb | 1 + spec/factories/tokens.rb | 6 + spec/factories/topics.rb | 6 +- spec/mailers/map_mailer_spec.rb | 5 - spec/rails_helper.rb | 27 +-- spec/spec_helper.rb | 4 - spec/support/controller_helpers.rb | 23 +- spec/support/pundit.rb | 1 + spec/support/schema_matcher.rb | 35 ++- spec/support/simplecov.rb | 2 + 148 files changed, 2506 insertions(+), 694 deletions(-) create mode 100644 app/assets/config/manifest.js delete mode 100644 app/controllers/api/mappings_controller.rb delete mode 100644 app/controllers/api/maps_controller.rb delete mode 100644 app/controllers/api/restful_controller.rb delete mode 100644 app/controllers/api/synapses_controller.rb delete mode 100644 app/controllers/api/tokens_controller.rb delete mode 100644 app/controllers/api/topics_controller.rb create mode 100644 app/controllers/api/v1/deprecated_controller.rb create mode 100644 app/controllers/api/v1/mappings_controller.rb create mode 100644 app/controllers/api/v1/maps_controller.rb create mode 100644 app/controllers/api/v1/synapses_controller.rb create mode 100644 app/controllers/api/v1/tokens_controller.rb create mode 100644 app/controllers/api/v1/topics_controller.rb create mode 100644 app/controllers/api/v2/mappings_controller.rb create mode 100644 app/controllers/api/v2/maps_controller.rb create mode 100644 app/controllers/api/v2/restful_controller.rb create mode 100644 app/controllers/api/v2/sessions_controller.rb create mode 100644 app/controllers/api/v2/synapses_controller.rb create mode 100644 app/controllers/api/v2/tokens_controller.rb create mode 100644 app/controllers/api/v2/topics_controller.rb create mode 100644 app/models/application_record.rb create mode 100644 app/serializers/api/v2/application_serializer.rb create mode 100644 app/serializers/api/v2/event_serializer.rb create mode 100644 app/serializers/api/v2/map_serializer.rb create mode 100644 app/serializers/api/v2/mapping_serializer.rb create mode 100644 app/serializers/api/v2/metacode_serializer.rb create mode 100644 app/serializers/api/v2/synapse_serializer.rb create mode 100644 app/serializers/api/v2/token_serializer.rb create mode 100644 app/serializers/api/v2/topic_serializer.rb create mode 100644 app/serializers/api/v2/user_serializer.rb create mode 100644 app/serializers/api/v2/webhook_serializer.rb delete mode 100644 app/serializers/event_serializer.rb delete mode 100644 app/serializers/new_map_serializer.rb delete mode 100644 app/serializers/new_mapping_serializer.rb delete mode 100644 app/serializers/new_metacode_serializer.rb delete mode 100644 app/serializers/new_synapse_serializer.rb delete mode 100644 app/serializers/new_topic_serializer.rb delete mode 100644 app/serializers/new_user_serializer.rb delete mode 100644 app/serializers/token_serializer.rb delete mode 100644 app/serializers/webhook_serializer.rb create mode 100644 config/cable.yml create mode 100644 config/initializers/active_model_serializers.rb create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/cookies_serializer.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/kaminari_config.rb create mode 100644 config/initializers/new_framework_defaults.rb create mode 100644 config/puma.rb create mode 100644 config/spring.rb create mode 100644 doc/api/api.raml create mode 100644 doc/api/apis/mappings.raml create mode 100644 doc/api/apis/maps.raml create mode 100644 doc/api/apis/synapses.raml create mode 100644 doc/api/apis/tokens.raml create mode 100644 doc/api/apis/topics.raml create mode 100644 doc/api/examples/map.json create mode 100644 doc/api/examples/mapping.json create mode 100644 doc/api/examples/mappings.json create mode 100644 doc/api/examples/maps.json create mode 100644 doc/api/examples/synapse.json create mode 100644 doc/api/examples/synapses.json create mode 100644 doc/api/examples/token.json create mode 100644 doc/api/examples/tokens.json create mode 100644 doc/api/examples/topic.json create mode 100644 doc/api/examples/topics.json create mode 100644 doc/api/resourceTypes/base.raml create mode 100644 doc/api/resourceTypes/collection.raml create mode 100644 doc/api/resourceTypes/item.raml create mode 100644 doc/api/schemas/_datetimestamp.json create mode 100644 doc/api/schemas/_id.json create mode 100644 doc/api/schemas/_map.json create mode 100644 doc/api/schemas/_mapping.json create mode 100644 doc/api/schemas/_page.json create mode 100644 doc/api/schemas/_permission.json create mode 100644 doc/api/schemas/_synapse.json create mode 100644 doc/api/schemas/_token.json create mode 100644 doc/api/schemas/_topic.json create mode 100644 doc/api/schemas/map.json create mode 100644 doc/api/schemas/mapping.json create mode 100644 doc/api/schemas/mappings.json create mode 100644 doc/api/schemas/maps.json create mode 100644 doc/api/schemas/synapse.json create mode 100644 doc/api/schemas/synapses.json create mode 100644 doc/api/schemas/token.json create mode 100644 doc/api/schemas/tokens.json create mode 100644 doc/api/schemas/topic.json create mode 100644 doc/api/schemas/topics.json create mode 100644 doc/api/traits/orderable.raml create mode 100644 doc/api/traits/pageable.raml create mode 100644 doc/api/traits/searchable.raml create mode 100644 spec/api/v2/mappings_api_spec.rb create mode 100644 spec/api/v2/maps_api_spec.rb create mode 100644 spec/api/v2/synapses_api_spec.rb create mode 100644 spec/api/v2/tokens_api_spec.rb create mode 100644 spec/api/v2/topics_api_spec.rb create mode 100644 spec/factories/tokens.rb delete mode 100644 spec/mailers/map_mailer_spec.rb create mode 100644 spec/support/pundit.rb create mode 100644 spec/support/simplecov.rb diff --git a/Gemfile b/Gemfile index 6be2271b..4c58772c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,28 +1,27 @@ source 'https://rubygems.org' ruby '2.3.0' -gem 'rails' +gem 'rails', '~> 5.0.0' -gem 'active_model_serializers', '~> 0.8.1' +gem 'active_model_serializers' gem 'aws-sdk', '< 2.0' -gem 'best_in_place' # in-place editing -gem 'delayed_job', '~> 4.0.2' -gem 'delayed_job_active_record', '~> 4.0.1' +gem 'best_in_place' +gem 'delayed_job' +gem 'delayed_job_active_record' gem 'devise' -gem 'doorkeeper' +gem 'doorkeeper', '~> 4.0.0.rc4' gem 'dotenv-rails' gem 'exception_notification' gem 'formtastic' gem 'formula' gem 'httparty' gem 'json' -gem 'kaminari' # pagination -gem 'paperclip' +gem 'kaminari' +gem 'paperclip', '~> 4.3.6' gem 'pg' gem 'pundit' gem 'pundit_extra' gem 'rack-cors' -gem 'rails3-jquery-autocomplete' gem 'redis' gem 'slack-notifier' gem 'snorlax' @@ -31,12 +30,12 @@ gem 'uservoice-ruby' gem 'jquery-rails' gem 'jquery-ui-rails' gem 'jbuilder' +gem 'rails3-jquery-autocomplete' group :assets do gem 'coffee-rails' gem 'sass-rails' gem 'uglifier' - # gem 'therubyracer' end group :production do @@ -57,7 +56,6 @@ group :development, :test do gem 'binding_of_caller' gem 'pry-byebug' gem 'pry-rails' - gem 'quiet_assets' gem 'tunemygc' gem 'rubocop' end diff --git a/Gemfile.lock b/Gemfile.lock index c385bd81..c2fd0d28 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,45 +1,50 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.2.6) - actionpack (= 4.2.6) - actionview (= 4.2.6) - activejob (= 4.2.6) + actioncable (5.0.0) + actionpack (= 5.0.0) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.0) + actionpack (= 5.0.0) + actionview (= 5.0.0) + activejob (= 5.0.0) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.6) - actionview (= 4.2.6) - activesupport (= 4.2.6) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) + actionpack (5.0.0) + actionview (= 5.0.0) + activesupport (= 5.0.0) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.6) - activesupport (= 4.2.6) + actionview (5.0.0) + activesupport (= 5.0.0) builder (~> 3.1) erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - active_model_serializers (0.8.3) - activemodel (>= 3.0) - activejob (4.2.6) - activesupport (= 4.2.6) - globalid (>= 0.3.0) - activemodel (4.2.6) - activesupport (= 4.2.6) - builder (~> 3.1) - activerecord (4.2.6) - activemodel (= 4.2.6) - activesupport (= 4.2.6) - arel (~> 6.0) - activesupport (4.2.6) + active_model_serializers (0.10.1) + actionpack (>= 4.1, < 6) + activemodel (>= 4.1, < 6) + jsonapi (~> 0.1.1.beta2) + railties (>= 4.1, < 6) + activejob (5.0.0) + activesupport (= 5.0.0) + globalid (>= 0.3.6) + activemodel (5.0.0) + activesupport (= 5.0.0) + activerecord (5.0.0) + activemodel (= 5.0.0) + activesupport (= 5.0.0) + arel (~> 7.0) + activesupport (5.0.0) + concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) - json (~> 1.7, >= 1.7.7) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.3.8) - arel (6.0.3) + arel (7.1.1) ast (2.3.0) aws-sdk (1.66.0) aws-sdk-v1 (= 1.66.0) @@ -56,7 +61,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - brakeman (3.3.2) + brakeman (3.3.3) builder (3.2.2) byebug (9.0.5) climate_control (0.0.3) @@ -64,21 +69,21 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.1) - coffee-rails (4.1.1) + coffee-rails (4.2.1) coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.1.x) + railties (>= 4.0.0, < 5.2.x) coffee-script (2.4.1) coffee-script-source execjs coffee-script-source (1.10.0) concurrent-ruby (1.0.2) debug_inspector (0.0.2) - delayed_job (4.0.6) - activesupport (>= 3.0, < 5.0) - delayed_job_active_record (4.0.3) - activerecord (>= 3.0, < 5.0) - delayed_job (>= 3.0, < 4.1) - devise (4.1.1) + delayed_job (4.1.2) + activesupport (>= 3.0, < 5.1) + delayed_job_active_record (4.1.1) + activerecord (>= 3.0, < 5.1) + delayed_job (>= 3.0, < 5) + devise (4.2.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) railties (>= 4.1.0, < 5.1) @@ -86,16 +91,16 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - doorkeeper (3.1.0) - railties (>= 3.2) + doorkeeper (4.0.0) + railties (>= 4.2) dotenv (2.1.1) dotenv-rails (2.1.1) dotenv (= 2.1.1) railties (>= 4.0, < 5.1) erubis (2.7.0) - exception_notification (4.1.4) - actionmailer (~> 4.0) - activesupport (~> 4.0) + exception_notification (4.2.1) + actionmailer (>= 4.0, < 6) + activesupport (>= 4.0, < 6) execjs (2.7.0) ezcrypto (0.7.2) factory_girl (4.7.0) @@ -107,13 +112,12 @@ GEM actionpack (>= 3.2.13) formula (1.1.1) rails (> 3.0.0) - globalid (0.3.6) + globalid (0.3.7) activesupport (>= 4.1.0) - httparty (0.13.7) - json (~> 1.8) + httparty (0.14.0) multi_xml (>= 0.5.2) i18n (0.7.0) - jbuilder (2.5.0) + jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) jquery-rails (4.1.1) @@ -125,6 +129,8 @@ GEM json (1.8.3) json-schema (2.6.2) addressable (~> 2.3.8) + jsonapi (0.1.1.beta2) + json (~> 1.8) kaminari (0.17.0) actionpack (>= 3.0.0) activesupport (>= 3.0.0) @@ -141,6 +147,7 @@ GEM minitest (5.9.0) multi_json (1.12.1) multi_xml (0.5.5) + nio4r (1.2.1) nokogiri (1.6.8) mini_portile2 (~> 2.1.0) pkg-config (~> 1.1.7) @@ -157,7 +164,7 @@ GEM pg (0.18.4) pkg-config (1.1.7) powerpack (0.1.1) - pry (0.10.3) + pry (0.10.4) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) @@ -169,29 +176,25 @@ GEM pundit (1.1.0) activesupport (>= 3.0.0) pundit_extra (0.2.0) - quiet_assets (1.1.0) - railties (>= 3.1, < 5.0) - rack (1.6.4) + rack (2.0.1) rack-cors (0.4.0) rack-test (0.6.3) rack (>= 1.0) - rails (4.2.6) - actionmailer (= 4.2.6) - actionpack (= 4.2.6) - actionview (= 4.2.6) - activejob (= 4.2.6) - activemodel (= 4.2.6) - activerecord (= 4.2.6) - activesupport (= 4.2.6) + rails (5.0.0) + actioncable (= 5.0.0) + actionmailer (= 5.0.0) + actionpack (= 5.0.0) + actionview (= 5.0.0) + activejob (= 5.0.0) + activemodel (= 5.0.0) + activerecord (= 5.0.0) + activesupport (= 5.0.0) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.6) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.7) - activesupport (>= 4.2.0.beta, < 5.0) + railties (= 5.0.0) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.1) + activesupport (>= 4.2.0, < 6.0) nokogiri (~> 1.6.0) - rails-deprecated_sanitizer (>= 1.0.1) rails-html-sanitizer (1.0.3) loofah (~> 2.0) rails3-jquery-autocomplete (1.0.15) @@ -201,34 +204,35 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (4.2.6) - actionpack (= 4.2.6) - activesupport (= 4.2.6) + railties (5.0.0) + actionpack (= 5.0.0) + activesupport (= 5.0.0) + method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) rake (11.2.2) - redis (3.3.0) + redis (3.3.1) responders (2.2.0) railties (>= 4.2.0, < 5.1) - rspec-core (3.4.4) - rspec-support (~> 3.4.0) - rspec-expectations (3.4.0) + rspec-core (3.5.2) + rspec-support (~> 3.5.0) + rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-mocks (3.4.1) + rspec-support (~> 3.5.0) + rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.4.0) - rspec-rails (3.4.2) - actionpack (>= 3.0, < 4.3) - activesupport (>= 3.0, < 4.3) - railties (>= 3.0, < 4.3) - rspec-core (~> 3.4.0) - rspec-expectations (~> 3.4.0) - rspec-mocks (~> 3.4.0) - rspec-support (~> 3.4.0) - rspec-support (3.4.1) - rubocop (0.41.1) + rspec-support (~> 3.5.0) + rspec-rails (3.5.1) + actionpack (>= 3.0) + activesupport (>= 3.0) + railties (>= 3.0) + rspec-core (~> 3.5.0) + rspec-expectations (~> 3.5.0) + rspec-mocks (~> 3.5.0) + rspec-support (~> 3.5.0) + rspec-support (3.5.0) + rubocop (0.42.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -236,8 +240,8 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) sass (3.4.22) - sass-rails (5.0.4) - railties (>= 4.0.0, < 5.0) + sass-rails (5.0.5) + railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) sprockets-rails (>= 2.0, < 4.0) @@ -253,20 +257,20 @@ GEM slop (3.6.0) snorlax (0.1.6) rails (> 4.1) - sprockets (3.6.0) + sprockets (3.6.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.0.4) + sprockets-rails (3.1.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) thor (0.19.1) thread_safe (0.3.5) tilt (2.0.5) - tunemygc (1.0.65) + tunemygc (1.0.68) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.0) + uglifier (3.0.1) execjs (>= 0.3.0, < 3) unicode-display_width (1.1.0) uservoice-ruby (0.0.11) @@ -275,22 +279,25 @@ GEM oauth (>= 0.4.7) warden (1.2.6) rack (>= 1.0) + websocket-driver (0.6.4) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.2) PLATFORMS ruby DEPENDENCIES - active_model_serializers (~> 0.8.1) + active_model_serializers aws-sdk (< 2.0) best_in_place better_errors binding_of_caller brakeman coffee-rails - delayed_job (~> 4.0.2) - delayed_job_active_record (~> 4.0.1) + delayed_job + delayed_job_active_record devise - doorkeeper + doorkeeper (~> 4.0.0.rc4) dotenv-rails exception_notification factory_girl_rails @@ -303,15 +310,14 @@ DEPENDENCIES json json-schema kaminari - paperclip + paperclip (~> 4.3.6) pg pry-byebug pry-rails pundit pundit_extra - quiet_assets rack-cors - rails + rails (~> 5.0.0) rails3-jquery-autocomplete rails_12factor redis @@ -326,5 +332,8 @@ DEPENDENCIES uglifier uservoice-ruby +RUBY VERSION + ruby 2.3.0p0 + BUNDLED WITH - 1.11.2 + 1.12.5 diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js new file mode 100644 index 00000000..72a7189c --- /dev/null +++ b/app/assets/config/manifest.js @@ -0,0 +1,6 @@ +// JS and CSS bundles +//= link_directory ../javascripts .js +//= link_directory ../stylesheets .css + +// Other +//= link_tree ../images diff --git a/app/controllers/api/mappings_controller.rb b/app/controllers/api/mappings_controller.rb deleted file mode 100644 index 15fde6bc..00000000 --- a/app/controllers/api/mappings_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::MappingsController < API::RestfulController -end diff --git a/app/controllers/api/maps_controller.rb b/app/controllers/api/maps_controller.rb deleted file mode 100644 index bb2d553d..00000000 --- a/app/controllers/api/maps_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::MapsController < API::RestfulController -end diff --git a/app/controllers/api/restful_controller.rb b/app/controllers/api/restful_controller.rb deleted file mode 100644 index 5b6c41da..00000000 --- a/app/controllers/api/restful_controller.rb +++ /dev/null @@ -1,50 +0,0 @@ -class API::RestfulController < ActionController::Base - include Pundit - include PunditExtra - - snorlax_used_rest! - - load_and_authorize_resource only: [:show, :update, :destroy] - - def create - instantiate_resource - resource.user = current_user - authorize resource - create_action - respond_with_resource - end - - private - - def resource_serializer - "new_#{resource_name}_serializer".camelize.constantize - end - - def accessible_records - if current_user - visible_records - else - public_records - end - end - - def current_user - super || token_user || doorkeeper_user || nil - end - - def token_user - token = params[:access_token] - access_token = Token.find_by_token(token) - @token_user ||= access_token.user if access_token - end - - def doorkeeper_user - return unless doorkeeper_token.present? - doorkeeper_render_error unless valid_doorkeeper_token? - @doorkeeper_user ||= User.find(doorkeeper_token.resource_owner_id) - end - - def permitted_params - @permitted_params ||= PermittedParams.new(params) - end -end diff --git a/app/controllers/api/synapses_controller.rb b/app/controllers/api/synapses_controller.rb deleted file mode 100644 index 47cb6056..00000000 --- a/app/controllers/api/synapses_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::SynapsesController < API::RestfulController -end diff --git a/app/controllers/api/tokens_controller.rb b/app/controllers/api/tokens_controller.rb deleted file mode 100644 index cea6ac5f..00000000 --- a/app/controllers/api/tokens_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -class Api::TokensController < API::RestfulController - def my_tokens - raise Pundit::NotAuthorizedError unless current_user - instantiate_collection page_collection: false, timeframe_collection: false - respond_with_collection - end - - private - - def resource_serializer - "#{resource_name}_serializer".camelize.constantize - end - - def visible_records - current_user.tokens - end -end diff --git a/app/controllers/api/topics_controller.rb b/app/controllers/api/topics_controller.rb deleted file mode 100644 index 4ccc619c..00000000 --- a/app/controllers/api/topics_controller.rb +++ /dev/null @@ -1,2 +0,0 @@ -class Api::TopicsController < API::RestfulController -end diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb new file mode 100644 index 00000000..ed68b897 --- /dev/null +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -0,0 +1,9 @@ +module Api + module V1 + class DeprecatedController < ApplicationController + def method_missing + render json: { error: "/api/v1 is deprecated! Please use /api/v2 instead." } + end + end + end +end diff --git a/app/controllers/api/v1/mappings_controller.rb b/app/controllers/api/v1/mappings_controller.rb new file mode 100644 index 00000000..35c7d6bd --- /dev/null +++ b/app/controllers/api/v1/mappings_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class MappingsController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/maps_controller.rb b/app/controllers/api/v1/maps_controller.rb new file mode 100644 index 00000000..056810f1 --- /dev/null +++ b/app/controllers/api/v1/maps_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class MapsController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/synapses_controller.rb b/app/controllers/api/v1/synapses_controller.rb new file mode 100644 index 00000000..e2111e95 --- /dev/null +++ b/app/controllers/api/v1/synapses_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class SynapsesController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb new file mode 100644 index 00000000..c96b1065 --- /dev/null +++ b/app/controllers/api/v1/tokens_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class TokensController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v1/topics_controller.rb b/app/controllers/api/v1/topics_controller.rb new file mode 100644 index 00000000..e974fff3 --- /dev/null +++ b/app/controllers/api/v1/topics_controller.rb @@ -0,0 +1,6 @@ +module Api + module V1 + class TopicsController < DeprecatedController + end + end +end diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb new file mode 100644 index 00000000..7f0d9513 --- /dev/null +++ b/app/controllers/api/v2/mappings_controller.rb @@ -0,0 +1,6 @@ +module Api + module V2 + class MappingsController < RestfulController + end + end +end diff --git a/app/controllers/api/v2/maps_controller.rb b/app/controllers/api/v2/maps_controller.rb new file mode 100644 index 00000000..fd54fa7b --- /dev/null +++ b/app/controllers/api/v2/maps_controller.rb @@ -0,0 +1,9 @@ +module Api + module V2 + class MapsController < RestfulController + def searchable_columns + [:name, :desc] + end + end + end +end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb new file mode 100644 index 00000000..e73f21b8 --- /dev/null +++ b/app/controllers/api/v2/restful_controller.rb @@ -0,0 +1,179 @@ +module Api + module V2 + class RestfulController < ActionController::Base + include Pundit + include PunditExtra + + snorlax_used_rest! + + before_action :load_resource, only: [:show, :update, :destroy] + after_action :verify_authorized + + def index + authorize resource_class + instantiate_collection + respond_with_collection + end + + def create + instantiate_resource + resource.user = current_user if current_user.present? + authorize resource + create_action + respond_with_resource + end + + def destroy + destroy_action + head :no_content + end + + private + + def accessible_records + if current_user + visible_records + else + public_records + end + end + + def current_user + super || token_user || doorkeeper_user || nil + end + + def load_resource + super + authorize resource + end + + def resource_serializer + "Api::V2::#{resource_name.camelize}Serializer".constantize + end + + def respond_with_resource(scope: default_scope, serializer: resource_serializer, root: serializer_root) + if resource.errors.empty? + render json: resource, scope: scope, serializer: serializer, root: root + else + respond_with_errors + end + end + + def respond_with_collection(resources: collection, scope: default_scope, serializer: resource_serializer, root: serializer_root) + render json: resources, scope: scope, each_serializer: serializer, root: root, meta: pagination(resources), meta_key: :page + end + + def default_scope + { + embeds: embeds + } + end + + def embeds + (params[:embed] || '').split(',').map(&:to_sym) + end + + def token_user + token = params[:access_token] + access_token = Token.find_by_token(token) + @token_user ||= access_token.user if access_token + end + + def doorkeeper_user + return unless doorkeeper_token.present? + doorkeeper_render_error unless valid_doorkeeper_token? + @doorkeeper_user ||= User.find(doorkeeper_token.resource_owner_id) + end + + def permitted_params + @permitted_params ||= PermittedParams.new(params) + end + + def serializer_root + 'data' + end + + def pagination(collection) + per = (params[:per] || 25).to_i + current_page = (params[:page] || 1).to_i + total_pages = (collection.total_count.to_f / per).ceil + prev_page = current_page > 1 ? current_page - 1 : 0 + next_page = current_page < total_pages ? current_page + 1 : 0 + + base_url = request.base_url + request.path + nxt = request.query_parameters.merge(page: next_page).map{|x| x.join('=')}.join('&') + prev = request.query_parameters.merge(page: prev_page).map{|x| x.join('=')}.join('&') + last = request.query_parameters.merge(page: total_pages).map{|x| x.join('=')}.join('&') + response.headers['Link'] = [ + %(<#{base_url}?#{nxt}>; rel="next"), + %(<#{base_url}?#{prev}>; rel="prev"), + %(<#{base_url}?#{last}>; rel="last") + ].join(',') + response.headers['X-Total-Pages'] = collection.total_pages.to_s + response.headers['X-Total-Count'] = collection.total_count.to_s + response.headers['X-Per-Page'] = per.to_s + + { + current_page: current_page, + next_page: next_page, + prev_page: prev_page, + total_pages: total_pages, + total_count: collection.total_count, + per: per + } + end + + def instantiate_collection + collection = accessible_records + collection = yield collection if block_given? + collection = search_by_q(collection) if params[:q] + collection = order_by_sort(collection) if params[:sort] + collection = collection.page(params[:page]).per(params[:per]) + self.collection = collection + end + + # override this method to explicitly set searchable columns + def searchable_columns + columns = resource_class.columns.select do |column| + column.type == :text || column.type == :string + end + columns.map(&:name) + end + + # thanks to http://stackoverflow.com/questions/4430578 + def search_by_q(collection) + table = resource_class.arel_table + safe_query = "%#{params[:q].gsub(/[%_]/, '\\\\\0')}%" + search_column = -> (column) { table[column].matches(safe_query) } + + condition = searchable_columns.reduce(nil) do |prev, column| + next search_column.(column) if prev.nil? + search_column.(column).or(prev) + end + puts collection.where(condition).to_sql + collection.where(condition) + end + + def order_by_sort(collection) + builder = collection + sorts = params[:sort].split(',') + sorts.each do |sort| + direction = sort.starts_with?('-') ? 'desc' : 'asc' + sort = sort.sub(/^-/, '') + if resource_class.columns.map(&:name).include?(sort) + builder = builder.order(sort => direction) + end + end + return builder + end + + def visible_records + policy_scope(resource_class) + end + + def public_records + policy_scope(resource_class) + end + end + end +end diff --git a/app/controllers/api/v2/sessions_controller.rb b/app/controllers/api/v2/sessions_controller.rb new file mode 100644 index 00000000..3aefa214 --- /dev/null +++ b/app/controllers/api/v2/sessions_controller.rb @@ -0,0 +1,20 @@ +module Api + module V2 + class SessionsController < ApplicationController + def create + @user = User.find_by(email: params[:email]) + if @user && @user.valid_password(params[:password]) + sign_in(@user) + render json: @user + else + render json: { error: 'Error' } + end + end + + def destroy + sign_out + head :no_content + end + end + end +end diff --git a/app/controllers/api/v2/synapses_controller.rb b/app/controllers/api/v2/synapses_controller.rb new file mode 100644 index 00000000..6572997d --- /dev/null +++ b/app/controllers/api/v2/synapses_controller.rb @@ -0,0 +1,9 @@ +module Api + module V2 + class SynapsesController < RestfulController + def searchable_columns + [:desc] + end + end + end +end diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb new file mode 100644 index 00000000..6eeb102b --- /dev/null +++ b/app/controllers/api/v2/tokens_controller.rb @@ -0,0 +1,11 @@ +module Api + module V2 + class TokensController < RestfulController + def my_tokens + authorize resource_class + instantiate_collection + respond_with_collection + end + end + end +end diff --git a/app/controllers/api/v2/topics_controller.rb b/app/controllers/api/v2/topics_controller.rb new file mode 100644 index 00000000..74fa7105 --- /dev/null +++ b/app/controllers/api/v2/topics_controller.rb @@ -0,0 +1,6 @@ +module Api + module V2 + class TopicsController < RestfulController + end + end +end diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 1164d42e..01304328 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -163,8 +163,8 @@ class MainController < ApplicationController @synapses = [] end - # limit to 5 results - @synapses = @synapses.slice(0, 5) + #limit to 5 results + @synapses = @synapses.to_a.slice(0,5) render json: autocomplete_synapse_array_json(@synapses) end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index ee3e6549..7c4a74a7 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,6 +1,6 @@ class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] - after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps, :events] + after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] respond_to :html, :json, :csv diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index efd6b42d..8895cfd2 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -15,11 +15,10 @@ class Users::RegistrationsController < Devise::RegistrationsController private def configure_sign_up_params - devise_parameter_sanitizer.for(:sign_up) << [:name, :joinedwithcode] + devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode]) end def configure_account_update_params - puts devise_parameter_sanitizer_for(:account_update) - devise_parameter_sanitizer.for(:account_update) << [:image] + devise_parameter_sanitizer.permit(:account_update, keys: [:image]) end end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..10a4cba8 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,3 @@ +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end diff --git a/app/models/event.rb b/app/models/event.rb index 67606aa2..90407314 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,4 +1,4 @@ -class Event < ActiveRecord::Base +class Event < ApplicationRecord KINDS = %w(user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map).freeze # has_many :notifications, dependent: :destroy diff --git a/app/models/in_metacode_set.rb b/app/models/in_metacode_set.rb index c1b1ca33..de1f2514 100644 --- a/app/models/in_metacode_set.rb +++ b/app/models/in_metacode_set.rb @@ -1,4 +1,4 @@ -class InMetacodeSet < ActiveRecord::Base +class InMetacodeSet < ApplicationRecord belongs_to :metacode, class_name: 'Metacode', foreign_key: 'metacode_id' belongs_to :metacode_set, class_name: 'MetacodeSet', foreign_key: 'metacode_set_id' end diff --git a/app/models/map.rb b/app/models/map.rb index bf5757d3..f59eb790 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -1,4 +1,4 @@ -class Map < ActiveRecord::Base +class Map < ApplicationRecord belongs_to :user has_many :topicmappings, -> { Mapping.topicmapping }, class_name: :Mapping, dependent: :destroy diff --git a/app/models/mapping.rb b/app/models/mapping.rb index ceb15538..eba7a6d2 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,4 +1,4 @@ -class Mapping < ActiveRecord::Base +class Mapping < ApplicationRecord scope :topicmapping, -> { where(mappable_type: :Topic) } scope :synapsemapping, -> { where(mappable_type: :Synapse) } diff --git a/app/models/message.rb b/app/models/message.rb index 597caeb7..348c5d4e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,4 +1,4 @@ -class Message < ActiveRecord::Base +class Message < ApplicationRecord belongs_to :user belongs_to :resource, polymorphic: true diff --git a/app/models/metacode.rb b/app/models/metacode.rb index bc0bef7b..9b05bee5 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -1,4 +1,4 @@ -class Metacode < ActiveRecord::Base +class Metacode < ApplicationRecord has_many :in_metacode_sets has_many :metacode_sets, through: :in_metacode_sets has_many :topics diff --git a/app/models/metacode_set.rb b/app/models/metacode_set.rb index cc672784..c52811fd 100644 --- a/app/models/metacode_set.rb +++ b/app/models/metacode_set.rb @@ -1,4 +1,4 @@ -class MetacodeSet < ActiveRecord::Base +class MetacodeSet < ApplicationRecord belongs_to :user has_many :in_metacode_sets has_many :metacodes, through: :in_metacode_sets diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 710cb029..afd40a25 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -1,4 +1,4 @@ -class Synapse < ActiveRecord::Base +class Synapse < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' diff --git a/app/models/token.rb b/app/models/token.rb index 1dac3fde..9103aebc 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,4 +1,4 @@ -class Token < ActiveRecord::Base +class Token < ApplicationRecord belongs_to :user before_create :assign_token diff --git a/app/models/topic.rb b/app/models/topic.rb index a91c75fc..c250338b 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,4 +1,4 @@ -class Topic < ActiveRecord::Base +class Topic < ApplicationRecord include TopicsHelper belongs_to :user diff --git a/app/models/user.rb b/app/models/user.rb index 1f091499..876e10cd 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,6 +1,6 @@ require 'open-uri' -class User < ActiveRecord::Base +class User < ApplicationRecord has_many :topics has_many :synapses has_many :maps @@ -80,7 +80,7 @@ class User < ActiveRecord::Base end def starred_map?(map) - return !!self.stars.index{|s| s.map_id == map.id } + return self.stars.where(map_id: map.id).exists? end def settings diff --git a/app/models/user_map.rb b/app/models/user_map.rb index 5e91ecc2..c48cfb96 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -1,4 +1,4 @@ -class UserMap < ActiveRecord::Base +class UserMap < ApplicationRecord belongs_to :map belongs_to :user end diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index a87dc679..3aadbdb3 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -4,8 +4,14 @@ class UserPreference def initialize array = [] %w(Action Aim Idea Question Note Wildcard Subject).each do |m| - metacode = Metacode.find_by_name(m) - array.push(metacode.id.to_s) if metacode + begin + metacode = Metacode.find_by_name(m) + array.push(metacode.id.to_s) if metacode + rescue ActiveRecord::StatementInvalid + if m == 'Action' + Rails.logger.warn("TODO: remove this travis workaround in user_preference.rb") + end + end end @metacodes = array end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 86d2333d..6389398e 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -1,4 +1,4 @@ -class Webhook < ActiveRecord::Base +class Webhook < ApplicationRecord belongs_to :hookable, polymorphic: true validates :uri, presence: true diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index bf511869..0a2b33ce 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -12,19 +12,7 @@ class MapPolicy < ApplicationPolicy end end - def activemaps? - user.blank? # redirect to root url if authenticated for some reason - end - - def featuredmaps? - true - end - - def mymaps? - user.present? - end - - def usermaps? + def index? true end @@ -32,18 +20,6 @@ class MapPolicy < ApplicationPolicy record.permission == 'commons' || record.permission == 'public' || record.collaborators.include?(user) || record.user == user end - def export? - show? - end - - def events? - show? - end - - def contains? - show? - end - def create? user.present? end @@ -52,11 +28,39 @@ class MapPolicy < ApplicationPolicy user.present? && (record.permission == 'commons' || record.collaborators.include?(user) || record.user == user) end + def destroy? + record.user == user || admin_override + end + def access? # note that this is to edit access user.present? && record.user == user end + def activemaps? + user.blank? # redirect to root url if authenticated for some reason + end + + def contains? + show? + end + + def events? + show? + end + + def export? + show? + end + + def featuredmaps? + true + end + + def mymaps? + user.present? + end + def star? unstar? end @@ -69,7 +73,7 @@ class MapPolicy < ApplicationPolicy update? end - def destroy? - record.user == user || admin_override + def usermaps? + true end end diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 07b6d0c5..1cd99783 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -8,13 +8,17 @@ class MappingPolicy < ApplicationPolicy visible = %w(public commons) permission = 'maps.permission IN (?)' if user - scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) + scope.joins(:map).where(permission, visible).or(scope.joins(:map).where(user_id: user.id)) else - scope.where(permission, visible) + scope.joins(:map).where(permission, visible) end end end + def index? + true + end + def show? map_policy.show? && mappable_policy.try(:show?) end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 97d993f5..310b3947 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -11,6 +11,10 @@ class SynapsePolicy < ApplicationPolicy end end + def index? + true # really only for the API. should be policy scoped! + end + def create? user.present? # TODO: add validation against whether you can see both topics diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index a8d5df2a..7bca6770 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -11,6 +11,10 @@ class TopicPolicy < ApplicationPolicy end end + def index? + user.present? + end + def create? user.present? end diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb new file mode 100644 index 00000000..f943646c --- /dev/null +++ b/app/serializers/api/v2/application_serializer.rb @@ -0,0 +1,29 @@ +module Api + module V2 + class ApplicationSerializer < ActiveModel::Serializer + def self.embeddable + {} + end + + def embeds + @embeds ||= (scope[:embeds] || []).select { |e| self.class.embeddable.keys.include?(e) } + end + + def self.embed_dat + embeddable.each_pair do |key, opts| + attr = opts.delete(:attr) || key + if attr.to_s.pluralize == attr.to_s + attribute "#{attr.to_s.singularize}_ids".to_sym, opts.merge(unless: -> { embeds.include?(key) }) do + object.send(attr).map(&:id) + end + has_many attr, opts.merge(if: -> { embeds.include?(key) }) + else + id_opts = opts.merge(key: "#{key}_id") + attribute "#{attr}_id".to_sym, id_opts.merge(unless: -> { embeds.include?(key) }) + attribute key, opts.merge(if: -> { embeds.include?(key) }) + end + end + end + end + end +end diff --git a/app/serializers/api/v2/event_serializer.rb b/app/serializers/api/v2/event_serializer.rb new file mode 100644 index 00000000..644598cf --- /dev/null +++ b/app/serializers/api/v2/event_serializer.rb @@ -0,0 +1,18 @@ +module Api + module V2 + class EventSerializer < ApplicationSerializer + attributes :id, :sequence_id, :kind, :map_id, :created_at + + has_one :actor, serializer: UserSerializer, root: 'users' + has_one :map, serializer: MapSerializer + + def actor + object.user || object.eventable.try(:user) + end + + def map + object.eventable.try(:map) || object.eventable.map + end + end + end +end diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb new file mode 100644 index 00000000..438f97ee --- /dev/null +++ b/app/serializers/api/v2/map_serializer.rb @@ -0,0 +1,28 @@ +module Api + module V2 + class MapSerializer < ApplicationSerializer + attributes :id, + :name, + :desc, + :permission, + :screenshot, + :created_at, + :updated_at + + def self.embeddable + { + user: {}, + topics: {}, + synapses: {}, + mappings: {}, + contributors: { serializer: UserSerializer }, + collaborators: { serializer: UserSerializer } + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb new file mode 100644 index 00000000..dc36421e --- /dev/null +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -0,0 +1,25 @@ +module Api + module V2 + class MappingSerializer < ApplicationSerializer + attributes :id, + :created_at, + :updated_at, + :mappable_id, + :mappable_type + + attribute :xloc, if: -> { object.mappable_type == 'Topic' } + attribute :yloc, if: -> { object.mappable_type == 'Topic' } + + def self.embeddable + { + user: {}, + map: {} + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/metacode_serializer.rb b/app/serializers/api/v2/metacode_serializer.rb new file mode 100644 index 00000000..4f4daa35 --- /dev/null +++ b/app/serializers/api/v2/metacode_serializer.rb @@ -0,0 +1,11 @@ +module Api + module V2 + class MetacodeSerializer < ApplicationSerializer + attributes :id, + :name, + :manual_icon, + :color, + :aws_icon + end + end +end diff --git a/app/serializers/api/v2/synapse_serializer.rb b/app/serializers/api/v2/synapse_serializer.rb new file mode 100644 index 00000000..9ef86660 --- /dev/null +++ b/app/serializers/api/v2/synapse_serializer.rb @@ -0,0 +1,24 @@ +module Api + module V2 + class SynapseSerializer < ApplicationSerializer + attributes :id, + :desc, + :category, + :permission, + :created_at, + :updated_at + + def self.embeddable + { + topic1: { attr: :node1, serializer: TopicSerializer }, + topic2: { attr: :node2, serializer: TopicSerializer }, + user: {} + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/token_serializer.rb b/app/serializers/api/v2/token_serializer.rb new file mode 100644 index 00000000..18d15d15 --- /dev/null +++ b/app/serializers/api/v2/token_serializer.rb @@ -0,0 +1,10 @@ +module Api + module V2 + class TokenSerializer < ApplicationSerializer + attributes :id, + :token, + :description, + :created_at + end + end +end diff --git a/app/serializers/api/v2/topic_serializer.rb b/app/serializers/api/v2/topic_serializer.rb new file mode 100644 index 00000000..48d1d6de --- /dev/null +++ b/app/serializers/api/v2/topic_serializer.rb @@ -0,0 +1,24 @@ +module Api + module V2 + class TopicSerializer < ApplicationSerializer + attributes :id, + :name, + :desc, + :link, + :permission, + :created_at, + :updated_at + + def self.embeddable + { + user: {}, + metacode: {} + } + end + + self.class_eval do + embed_dat + end + end + end +end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb new file mode 100644 index 00000000..fdfffae0 --- /dev/null +++ b/app/serializers/api/v2/user_serializer.rb @@ -0,0 +1,19 @@ +module Api + module V2 + class UserSerializer < ApplicationSerializer + attributes :id, + :name, + :avatar, + :is_admin, + :generation + + def avatar + object.image.url(:sixtyfour) + end + + def is_admin + object.admin + end + end + end +end diff --git a/app/serializers/api/v2/webhook_serializer.rb b/app/serializers/api/v2/webhook_serializer.rb new file mode 100644 index 00000000..59d60283 --- /dev/null +++ b/app/serializers/api/v2/webhook_serializer.rb @@ -0,0 +1,7 @@ +module Api + module V2 + class WebhookSerializer < ApplicationSerializer + attributes :text, :username, :icon_url # , :attachments + end + end +end diff --git a/app/serializers/event_serializer.rb b/app/serializers/event_serializer.rb deleted file mode 100644 index 0e87cd44..00000000 --- a/app/serializers/event_serializer.rb +++ /dev/null @@ -1,15 +0,0 @@ -class EventSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, :sequence_id, :kind, :map_id, :created_at - - has_one :actor, serializer: NewUserSerializer, root: 'users' - has_one :map, serializer: NewMapSerializer - - def actor - object.user || object.eventable.try(:user) - end - - def map - object.eventable.try(:map) || object.eventable.map - end -end diff --git a/app/serializers/new_map_serializer.rb b/app/serializers/new_map_serializer.rb deleted file mode 100644 index c323b09d..00000000 --- a/app/serializers/new_map_serializer.rb +++ /dev/null @@ -1,16 +0,0 @@ -class NewMapSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :name, - :desc, - :permission, - :screenshot, - :created_at, - :updated_at - - has_many :topics, serializer: NewTopicSerializer - has_many :synapses, serializer: NewSynapseSerializer - has_many :mappings, serializer: NewMappingSerializer - has_many :contributors, root: :users, serializer: NewUserSerializer - has_many :collaborators, root: :users, serializer: NewUserSerializer -end diff --git a/app/serializers/new_mapping_serializer.rb b/app/serializers/new_mapping_serializer.rb deleted file mode 100644 index 3ef9a8b6..00000000 --- a/app/serializers/new_mapping_serializer.rb +++ /dev/null @@ -1,19 +0,0 @@ -class NewMappingSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :xloc, - :yloc, - :created_at, - :updated_at, - :mappable_id, - :mappable_type - - has_one :user, serializer: NewUserSerializer - has_one :map, serializer: NewMapSerializer - - def filter(keys) - keys.delete(:xloc) unless object.mappable_type == 'Topic' - keys.delete(:yloc) unless object.mappable_type == 'Topic' - keys - end -end diff --git a/app/serializers/new_metacode_serializer.rb b/app/serializers/new_metacode_serializer.rb deleted file mode 100644 index b20f25b6..00000000 --- a/app/serializers/new_metacode_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class NewMetacodeSerializer < ActiveModel::Serializer - attributes :id, - :name, - :manual_icon, - :color, - :aws_icon -end diff --git a/app/serializers/new_synapse_serializer.rb b/app/serializers/new_synapse_serializer.rb deleted file mode 100644 index 5cdf644d..00000000 --- a/app/serializers/new_synapse_serializer.rb +++ /dev/null @@ -1,14 +0,0 @@ -class NewSynapseSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :desc, - :category, - :weight, - :permission, - :created_at, - :updated_at - - has_one :topic1, root: :topics, serializer: NewTopicSerializer - has_one :topic2, root: :topics, serializer: NewTopicSerializer - has_one :user, serializer: NewUserSerializer -end diff --git a/app/serializers/new_topic_serializer.rb b/app/serializers/new_topic_serializer.rb deleted file mode 100644 index 2eb718df..00000000 --- a/app/serializers/new_topic_serializer.rb +++ /dev/null @@ -1,13 +0,0 @@ -class NewTopicSerializer < ActiveModel::Serializer - embed :ids, include: true - attributes :id, - :name, - :desc, - :link, - :permission, - :created_at, - :updated_at - - has_one :user, serializer: NewUserSerializer - has_one :metacode, serializer: NewMetacodeSerializer -end diff --git a/app/serializers/new_user_serializer.rb b/app/serializers/new_user_serializer.rb deleted file mode 100644 index 62796f31..00000000 --- a/app/serializers/new_user_serializer.rb +++ /dev/null @@ -1,15 +0,0 @@ -class NewUserSerializer < ActiveModel::Serializer - attributes :id, - :name, - :avatar, - :is_admin, - :generation - - def avatar - object.image.url(:sixtyfour) - end - - def is_admin - object.admin - end -end diff --git a/app/serializers/token_serializer.rb b/app/serializers/token_serializer.rb deleted file mode 100644 index 4d593c0e..00000000 --- a/app/serializers/token_serializer.rb +++ /dev/null @@ -1,7 +0,0 @@ -class TokenSerializer < ActiveModel::Serializer - attributes :id, - :token, - :description, - :created_at, - :updated_at -end diff --git a/app/serializers/webhook_serializer.rb b/app/serializers/webhook_serializer.rb deleted file mode 100644 index 8108c86c..00000000 --- a/app/serializers/webhook_serializer.rb +++ /dev/null @@ -1,3 +0,0 @@ -class WebhookSerializer < ActiveModel::Serializer - attributes :text, :username, :icon_url # , :attachments -end diff --git a/config/application.rb b/config/application.rb index afebfd6d..b80306c5 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,4 @@ -require File.expand_path('../boot', __FILE__) +require_relative 'boot' require 'csv' require 'rails/all' @@ -15,21 +15,6 @@ module Metamaps # Custom directories with classes and modules you want to be autoloadable. config.autoload_paths << Rails.root.join('app', 'services') - # Only load the plugins named here, in the order given (default is alphabetical). - # :all can be used as a placeholder for all plugins not explicitly named. - # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - - # Activate observers that should always be running. - # config.active_record.observers = :cacher, :garbage_collector, :forum_observer - - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - # config.time_zone = 'Central Time (US & Canada)' - - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de - # Configure the default encoding used in templates for Ruby 1.9. config.encoding = 'utf-8' @@ -43,11 +28,6 @@ module Metamaps # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] - # Use SQL instead of Active Record's schema dumper when creating the database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - # config.active_record.schema_format = :sql - # Enable the asset pipeline config.assets.initialize_on_precompile = false @@ -57,7 +37,6 @@ module Metamaps config.generators do |g| g.test_framework :rspec end - config.active_record.raise_in_transactional_callbacks = true # pundit errors return 403 FORBIDDEN config.action_dispatch.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden diff --git a/config/boot.rb b/config/boot.rb index 4add3ee3..e49b6649 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,5 +1,6 @@ require 'rubygems' require 'rails/commands/server' + module Rails class Server def default_options @@ -9,6 +10,6 @@ module Rails end # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' diff --git a/config/cable.yml b/config/cable.yml new file mode 100644 index 00000000..0bbde6f7 --- /dev/null +++ b/config/cable.yml @@ -0,0 +1,9 @@ +development: + adapter: async + +test: + adapter: async + +production: + adapter: redis + url: redis://localhost:6379/1 diff --git a/config/environment.rb b/config/environment.rb index 6e9ad9e4..426333bb 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,5 +1,5 @@ -# Load the rails application -require File.expand_path('../application', __FILE__) +# Load the Rails application. +require_relative 'application' -# Initialize the rails application -Metamaps::Application.initialize! +# Initialize the Rails application. +Rails.application.initialize! diff --git a/config/environments/production.rb b/config/environments/production.rb index 5ad26ad3..24ceed21 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,9 +1,8 @@ -Metamaps::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb config.log_level = :warn config.eager_load = true - config.assets.js_compressor = :uglifier # Code is not reloaded between requests config.cache_classes = true @@ -13,12 +12,12 @@ Metamaps::Application.configure do config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_files = true + config.public_file_server.enabled = false + # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false - # Compress JavaScripts and CSS - config.assets.compress = true + config.assets.js_compressor = :uglifier # S3 file storage config.paperclip_defaults = { @@ -37,7 +36,6 @@ Metamaps::Application.configure do port: ENV['SMTP_PORT'], user_name: ENV['SMTP_USERNAME'], password: ENV['SMTP_PASSWORD'], - # domain: ENV['SMTP_DOMAIN'] authentication: 'plain', enable_starttls_auto: true, openssl_verify_mode: 'none' @@ -46,54 +44,13 @@ Metamaps::Application.configure do # Don't care if the mailer can't send config.action_mailer.raise_delivery_errors = true - # Don't fallback to assets pipeline if a precompiled asset is missed - config.assets.compile = false - # Generate digests for assets URLs config.assets.digest = true - # Defaults to Rails.root.join("public/assets") - # config.assets.manifest = YOUR_PATH - - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true - - # See everything in the log (default is :info) - # config.log_level = :debug - - # Prepend all log lines with the following tags - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - - # Use a different cache store in production - # config.cache_store = :mem_cache_store - - # Enable serving of images, stylesheets, and JavaScripts from an asset server - # config.action_controller.asset_host = "http://assets.example.com" - - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile += %w( ) - - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - - # Enable threaded mode - # config.threadsafe! - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to # the I18n.default_locale when a translation can not be found) config.i18n.fallbacks = true # Send deprecation notices to registered listeners config.active_support.deprecation = :notify - - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - # config.active_record.auto_explain_threshold_in_seconds = 0.5 end diff --git a/config/environments/test.rb b/config/environments/test.rb index 4980b34c..dac060f1 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -10,8 +10,10 @@ Metamaps::Application.configure do config.cache_classes = true # Configure static asset server for tests with Cache-Control for performance - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } # Show full error reports and disable caching config.consider_all_requests_local = true diff --git a/config/initializers/access_codes.rb b/config/initializers/access_codes.rb index 66ace9a1..4a220c97 100644 --- a/config/initializers/access_codes.rb +++ b/config/initializers/access_codes.rb @@ -1,4 +1,4 @@ $codes = [] -if ActiveRecord::Base.connection.table_exists? 'users' +if ActiveRecord::Base.connection.data_source_exists? 'users' $codes = ActiveRecord::Base.connection.execute('SELECT code FROM users').map { |user| user['code'] } end diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb new file mode 100644 index 00000000..aba3586b --- /dev/null +++ b/config/initializers/active_model_serializers.rb @@ -0,0 +1 @@ +ActiveModelSerializers.config.adapter = :json diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 00000000..51639b67 --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,6 @@ +# Be sure to restart your server when you modify this file. + +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index b84c4e25..31897cf4 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1 +1,12 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '2.0' +Rails.application.config.assets.quiet = true + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. Rails.application.config.assets.precompile += %w( webpacked/metamaps.bundle.js ) diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000..f51a497e --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :hybrid diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..4a994e1e --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 5d8d9be2..ac033bf9 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,15 +1,16 @@ # Be sure to restart your server when you modify this file. -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end -# + # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections do |inflect| +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb new file mode 100644 index 00000000..b1d87b01 --- /dev/null +++ b/config/initializers/kaminari_config.rb @@ -0,0 +1,10 @@ +Kaminari.configure do |config| + # config.default_per_page = 25 + # config.max_per_page = nil + # config.window = 4 + # config.outer_window = 0 + # config.left = 0 + # config.right = 0 + # config.page_method_name = :page + # config.param_name = :page +end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 54890c6e..c7b0c86d 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -2,6 +2,5 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone Mime::Type.register 'application/xls', :xls diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb new file mode 100644 index 00000000..0706cafd --- /dev/null +++ b/config/initializers/new_framework_defaults.rb @@ -0,0 +1,24 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Read the Rails 5.0 release notes for more info on each option. + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = true + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = true + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = true + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = true + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = false + +# Configure SSL options to enable HSTS with subdomains. Previous versions had false. +Rails.application.config.ssl_options = { hsts: { subdomains: true } } diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 83877c08..e7f18911 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -4,4 +4,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Metamaps::Application.config.secret_key_base = ENV['SECRET_KEY_BASE'] +Rails.application.config.secret_key_base = ENV['SECRET_KEY_BASE'] diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 757d66cc..d2dc13b6 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,8 +1,8 @@ # Be sure to restart your server when you modify this file. -Metamaps::Application.config.session_store :cookie_store, key: '_Metamaps_session' +Rails.application.config.session_store :cookie_store, key: '_Metamaps_session' # Use the database for sessions instead of the cookie-based default, # which shouldn't be used to store highly confidential information # (create the session table with "rails generate session_migration") -# Metamaps::Application.config.session_store :active_record_store +# Rails.application.config.session_store :active_record_store diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 999df201..36bb3e27 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -8,7 +8,7 @@ ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end -# Disable root element in JSON by default. -ActiveSupport.on_load(:active_record) do - self.include_root_in_json = false -end +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 00000000..c7f311f8 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,47 @@ +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + +# Specifies the `port` that Puma will listen on to receive requests, default is 3000. +# +port ENV.fetch("PORT") { 3000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked webserver processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# +# workers ENV.fetch("WEB_CONCURRENCY") { 2 } + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. If you use this option +# you need to make sure to reconnect any threads in the `on_worker_boot` +# block. +# +# preload_app! + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted this block will be run, if you are using `preload_app!` +# option you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, Ruby +# cannot share connections between processes. +# +# on_worker_boot do +# ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +# end + +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/routes.rb b/config/routes.rb index 83f03051..38fe274e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,13 +9,26 @@ Metamaps::Application.routes.draw do get 'search/mappers', to: 'main#searchmappers', as: :searchmappers get 'search/synapses', to: 'main#searchsynapses', as: :searchsynapses - namespace :api, path: '/api/v1', defaults: { format: :json } do - resources :maps, only: [:create, :show, :update, :destroy] - resources :synapses, only: [:create, :show, :update, :destroy] - resources :topics, only: [:create, :show, :update, :destroy] - resources :mappings, only: [:create, :show, :update, :destroy] - resources :tokens, only: [:create, :destroy] do - get :my_tokens, on: :collection + namespace :api, path: '/api', default: { format: :json } do + namespace :v2, path: '/v2' do + resources :maps, only: [:index, :create, :show, :update, :destroy] + resources :synapses, only: [:index, :create, :show, :update, :destroy] + resources :topics, only: [:index, :create, :show, :update, :destroy] + resources :mappings, only: [:index, :create, :show, :update, :destroy] + resources :tokens, only: [:create, :destroy] do + get :my_tokens, on: :collection + end + end + namespace :v1, path: '/v1' do + # api v1 routes all lead to a deprecation error method + # see app/controllers/api/v1/deprecated_controller.rb + resources :maps, only: [:create, :show, :update, :destroy] + resources :synapses, only: [:create, :show, :update, :destroy] + resources :topics, only: [:create, :show, :update, :destroy] + resources :mappings, only: [:create, :show, :update, :destroy] + resources :tokens, only: [:create, :destroy] do + get :my_tokens, on: :collection + end end end diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 00000000..be72de67 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,7 @@ +%w( + .ruby-version + .ruby-gemset + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } diff --git a/db/schema.rb b/db/schema.rb index 6b586a0d..ea06b679 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,4 +1,3 @@ -# encoding: UTF-8 # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -28,10 +27,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "queue" t.datetime "created_at" t.datetime "updated_at" + t.index ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree end - add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree - create_table "events", force: :cascade do |t| t.string "kind", limit: 255 t.integer "eventable_id" @@ -41,24 +39,22 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.integer "sequence_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree + t.index ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree + t.index ["map_id"], name: "index_events_on_map_id", using: :btree + t.index ["sequence_id"], name: "index_events_on_sequence_id", using: :btree + t.index ["user_id"], name: "index_events_on_user_id", using: :btree end - add_index "events", ["eventable_type", "eventable_id"], name: "index_events_on_eventable_type_and_eventable_id", using: :btree - add_index "events", ["map_id", "sequence_id"], name: "index_events_on_map_id_and_sequence_id", unique: true, using: :btree - add_index "events", ["map_id"], name: "index_events_on_map_id", using: :btree - add_index "events", ["sequence_id"], name: "index_events_on_sequence_id", using: :btree - add_index "events", ["user_id"], name: "index_events_on_user_id", using: :btree - create_table "in_metacode_sets", force: :cascade do |t| t.integer "metacode_id" t.integer "metacode_set_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["metacode_id"], name: "index_in_metacode_sets_on_metacode_id", using: :btree + t.index ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree end - add_index "in_metacode_sets", ["metacode_id"], name: "index_in_metacode_sets_on_metacode_id", using: :btree - add_index "in_metacode_sets", ["metacode_set_id"], name: "index_in_metacode_sets_on_metacode_set_id", using: :btree - create_table "mappings", force: :cascade do |t| t.text "category" t.integer "xloc" @@ -71,14 +67,13 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "updated_at", null: false t.integer "mappable_id" t.string "mappable_type" + t.index ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree + t.index ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree + t.index ["map_id"], name: "index_mappings_on_map_id", using: :btree + t.index ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree + t.index ["user_id"], name: "index_mappings_on_user_id", using: :btree end - add_index "mappings", ["map_id", "synapse_id"], name: "index_mappings_on_map_id_and_synapse_id", using: :btree - add_index "mappings", ["map_id", "topic_id"], name: "index_mappings_on_map_id_and_topic_id", using: :btree - add_index "mappings", ["map_id"], name: "index_mappings_on_map_id", using: :btree - add_index "mappings", ["mappable_id", "mappable_type"], name: "index_mappings_on_mappable_id_and_mappable_type", using: :btree - add_index "mappings", ["user_id"], name: "index_mappings_on_user_id", using: :btree - create_table "maps", force: :cascade do |t| t.text "name" t.boolean "arranged" @@ -92,10 +87,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "screenshot_content_type" t.integer "screenshot_file_size" t.datetime "screenshot_updated_at" + t.index ["user_id"], name: "index_maps_on_user_id", using: :btree end - add_index "maps", ["user_id"], name: "index_maps_on_user_id", using: :btree - create_table "messages", force: :cascade do |t| t.text "message" t.integer "user_id" @@ -103,12 +97,11 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "resource_type" t.datetime "created_at" t.datetime "updated_at" + t.index ["resource_id"], name: "index_messages_on_resource_id", using: :btree + t.index ["resource_type"], name: "index_messages_on_resource_type", using: :btree + t.index ["user_id"], name: "index_messages_on_user_id", using: :btree end - add_index "messages", ["resource_id"], name: "index_messages_on_resource_id", using: :btree - add_index "messages", ["resource_type"], name: "index_messages_on_resource_type", using: :btree - add_index "messages", ["user_id"], name: "index_messages_on_user_id", using: :btree - create_table "metacode_sets", force: :cascade do |t| t.string "name" t.text "desc" @@ -116,10 +109,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.boolean "mapperContributed" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree end - add_index "metacode_sets", ["user_id"], name: "index_metacode_sets_on_user_id", using: :btree - create_table "metacodes", force: :cascade do |t| t.text "name" t.string "manual_icon" @@ -141,10 +133,9 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "created_at", null: false t.datetime "revoked_at" t.string "scopes" + t.index ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree end - add_index "oauth_access_grants", ["token"], name: "index_oauth_access_grants_on_token", unique: true, using: :btree - create_table "oauth_access_tokens", force: :cascade do |t| t.integer "resource_owner_id" t.integer "application_id" @@ -154,12 +145,11 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "revoked_at" t.datetime "created_at", null: false t.string "scopes" + t.index ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree + t.index ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree + t.index ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree end - add_index "oauth_access_tokens", ["refresh_token"], name: "index_oauth_access_tokens_on_refresh_token", unique: true, using: :btree - add_index "oauth_access_tokens", ["resource_owner_id"], name: "index_oauth_access_tokens_on_resource_owner_id", using: :btree - add_index "oauth_access_tokens", ["token"], name: "index_oauth_access_tokens_on_token", unique: true, using: :btree - create_table "oauth_applications", force: :cascade do |t| t.string "name", null: false t.string "uid", null: false @@ -168,20 +158,18 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.string "scopes", default: "", null: false t.datetime "created_at" t.datetime "updated_at" + t.index ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree end - add_index "oauth_applications", ["uid"], name: "index_oauth_applications_on_uid", unique: true, using: :btree - create_table "stars", force: :cascade do |t| t.integer "user_id" t.integer "map_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["map_id"], name: "index_stars_on_map_id", using: :btree + t.index ["user_id"], name: "index_stars_on_user_id", using: :btree end - add_index "stars", ["map_id"], name: "index_stars_on_map_id", using: :btree - add_index "stars", ["user_id"], name: "index_stars_on_user_id", using: :btree - create_table "synapses", force: :cascade do |t| t.text "desc" t.text "category" @@ -193,24 +181,22 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "defer_to_map_id" + t.index ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree + t.index ["node1_id"], name: "index_synapses_on_node1_id", using: :btree + t.index ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree + t.index ["node2_id"], name: "index_synapses_on_node2_id", using: :btree + t.index ["user_id"], name: "index_synapses_on_user_id", using: :btree end - add_index "synapses", ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree - add_index "synapses", ["node1_id"], name: "index_synapses_on_node1_id", using: :btree - add_index "synapses", ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree - add_index "synapses", ["node2_id"], name: "index_synapses_on_node2_id", using: :btree - add_index "synapses", ["user_id"], name: "index_synapses_on_user_id", using: :btree - create_table "tokens", force: :cascade do |t| t.string "token" t.string "description" t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_tokens_on_user_id", using: :btree end - add_index "tokens", ["user_id"], name: "index_tokens_on_user_id", using: :btree - create_table "topics", force: :cascade do |t| t.text "name" t.text "desc" @@ -229,21 +215,19 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.integer "audio_file_size" t.datetime "audio_updated_at" t.integer "defer_to_map_id" + t.index ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree + t.index ["user_id"], name: "index_topics_on_user_id", using: :btree end - add_index "topics", ["metacode_id"], name: "index_topics_on_metacode_id", using: :btree - add_index "topics", ["user_id"], name: "index_topics_on_user_id", using: :btree - create_table "user_maps", force: :cascade do |t| t.integer "user_id" t.integer "map_id" t.datetime "created_at" t.datetime "updated_at" + t.index ["map_id"], name: "index_user_maps_on_map_id", using: :btree + t.index ["user_id"], name: "index_user_maps_on_user_id", using: :btree end - add_index "user_maps", ["map_id"], name: "index_user_maps_on_map_id", using: :btree - add_index "user_maps", ["user_id"], name: "index_user_maps_on_user_id", using: :btree - create_table "users", force: :cascade do |t| t.string "name" t.string "email" @@ -272,19 +256,17 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.integer "image_file_size" t.datetime "image_updated_at" t.integer "generation" + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree end - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree - create_table "webhooks", force: :cascade do |t| t.integer "hookable_id" t.string "hookable_type" t.string "kind", null: false t.string "uri", null: false t.text "event_types", default: [], array: true + t.index ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree end - add_index "webhooks", ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree - add_foreign_key "tokens", "users" end diff --git a/doc/api/api.raml b/doc/api/api.raml new file mode 100644 index 00000000..d61e66ac --- /dev/null +++ b/doc/api/api.raml @@ -0,0 +1,39 @@ +#%RAML 1.0 +--- +title: Metamaps +version: v2 +baseUri: http://metamaps.cc/api/v2 +mediaType: application/json + +securitySchemes: + - oauth_2_0: + description: | + OAuth 2.0 implementation + type: OAuth 2.0 + settings: + authorizationUri: https://metamaps.cc/api/v2/oauth/authorize + accessTokenUri: https://metamaps.cc/api/v2/oauth/token + authorizationGrants: [ authorization_code, password, client_credentials, implicit, refresh_token ] + +traits: + - pageable: !include traits/pageable.raml + - orderable: !include traits/orderable.raml + - searchable: !include traits/searchable.raml + +schemas: + - topic: !include schemas/_topic.json + - synapse: !include schemas/_synapse.json + - map: !include schemas/_map.json + - mapping: !include schemas/_mapping.json + - token: !include schemas/_token.json + +resourceTypes: + - base: !include resourceTypes/base.raml + - item: !include resourceTypes/item.raml + - collection: !include resourceTypes/collection.raml + +/topics: !include apis/topics.raml +/synapses: !include apis/synapses.raml +/maps: !include apis/maps.raml +/mappings: !include apis/mappings.raml +/tokens: !include apis/tokens.raml diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml new file mode 100644 index 00000000..8b72b4df --- /dev/null +++ b/doc/api/apis/mappings.raml @@ -0,0 +1,68 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/mappings.json +post: + body: + application/json: + properties: + mappable_id: + description: id of the topic/synapse to be mapped + mappable_type: + description: Topic or Synapse + map_id: + description: id of the map + xloc: + description: (for Topic mappings only) x location on the canvas + yloc: + description: (for Topic mappings only) y location on the canvas + responses: + 201: + body: + application/json: + example: !include ../examples/mapping.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/mapping.json + put: + body: + application/json: + properties: + mappable_id: + description: id of the topic/synapse to be mapped + mappable_type: + description: Topic or Synapse + map_id: + description: id of the map + responses: + 200: + body: + application/json: + example: !include ../examples/mapping.json + patch: + body: + application/json: + properties: + mappable_id: + description: id of the topic/synapse to be mapped + mappable_type: + description: Topic or Synapse + map_id: + description: id of the map + responses: + 200: + body: + application/json: + example: !include ../examples/mapping.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml new file mode 100644 index 00000000..c5499a33 --- /dev/null +++ b/doc/api/apis/maps.raml @@ -0,0 +1,82 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/maps.json +post: + body: + application/json: + properties: + name: + description: name + desc: + description: description + permission: + description: commons, public, or private + screenshot: + description: url to a screenshot of the map + contributor_ids: + description: the topic being linked from + collaborator_ids: + description: the topic being linked to + responses: + 201: + body: + application/json: + example: !include ../examples/map.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/map.json + put: + body: + application/json: + properties: + name: + description: name + desc: + description: description + permission: + description: commons, public, or private + screenshot: + description: url to a screenshot of the map + contributor_ids: + description: the topic being linked from + collaborator_ids: + description: the topic being linked to + responses: + 200: + body: + application/json: + example: !include ../examples/map.json + patch: + body: + application/json: + properties: + name: + description: name + desc: + description: description + permission: + description: commons, public, or private + screenshot: + description: url to a screenshot of the map + contributor_ids: + description: the topic being linked from + collaborator_ids: + description: the topic being linked to + responses: + 200: + body: + application/json: + example: !include ../examples/map.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml new file mode 100644 index 00000000..3169c712 --- /dev/null +++ b/doc/api/apis/synapses.raml @@ -0,0 +1,82 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/synapses.json +post: + body: + application/json: + properties: + desc: + description: name + category: + description: from to or both + permission: + description: commons, public, or private + topic1_id: + description: the topic being linked from + topic2_id: + description: the topic being linked to + user_id: + description: the creator of the topic + responses: + 201: + body: + application/json: + example: !include ../examples/synapse.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/synapse.json + put: + body: + application/json: + properties: + desc: + description: name + category: + description: from-to or both + permission: + description: commons, public, or private + topic1_id: + description: the topic being linked from + topic2_id: + description: the topic being linked to + user_id: + description: the creator of the topic + responses: + 200: + body: + application/json: + example: !include ../examples/synapse.json + patch: + body: + application/json: + properties: + desc: + description: name + category: + description: from-to or both + permission: + description: commons, public, or private + topic1_id: + description: the topic being linked from + topic2_id: + description: the topic being linked to + user_id: + description: the creator of the topic + responses: + 200: + body: + application/json: + example: !include ../examples/synapse.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml new file mode 100644 index 00000000..9f471615 --- /dev/null +++ b/doc/api/apis/tokens.raml @@ -0,0 +1,25 @@ +type: collection +post: + body: + application/json: + properties: + description: + description: short string describing this token + responses: + 201: + body: + application/json: + example: !include ../examples/token.json +/my_tokens: + get: + responses: + 200: + body: + application/json: + example: !include ../examples/tokens.json +/{id}: + type: item + delete: + responses: + 204: + description: No content diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml new file mode 100644 index 00000000..7c214dd2 --- /dev/null +++ b/doc/api/apis/topics.raml @@ -0,0 +1,72 @@ +type: collection +get: + responses: + 200: + body: + application/json: + example: !include ../examples/topics.json +post: + body: + application/json: + properties: + name: + description: name + desc: + description: description + link: + description: (optional) link to content on the web + permission: + description: commons, public, or private + metacode_id: + description: Topic's metacode + responses: + 201: + body: + application/json: + example: !include ../examples/topic.json +/{id}: + type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/topic.json + put: + body: + application/json: + properties: + name: + description: name + desc: + description: description + link: + description: (optional) link to content on the web + permission: + description: commons, public, or private + responses: + 200: + body: + application/json: + example: !include ../examples/topic.json + patch: + body: + application/json: + properties: + name: + description: name + desc: + description: description + link: + description: (optional) link to content on the web + permission: + description: commons, public, or private + responses: + 200: + body: + application/json: + example: !include ../examples/topic.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/examples/map.json b/doc/api/examples/map.json new file mode 100644 index 00000000..fe3796ca --- /dev/null +++ b/doc/api/examples/map.json @@ -0,0 +1,27 @@ +{ + "data": { + "id": 2, + "name": "Emergent Network Phenomena", + "desc": "Example map for the API", + "permission": "commons", + "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "created_at": "2016-03-26T08:02:05.379Z", + "updated_at": "2016-03-27T07:20:18.047Z", + "topic_ids": [ + 58, + 59 + ], + "synapse_ids": [ + 2 + ], + "mapping_ids": [ + 94, + 95, + 96 + ], + "collaborator_ids": [], + "contributor_ids": [ + 2 + ] + } +} diff --git a/doc/api/examples/mapping.json b/doc/api/examples/mapping.json new file mode 100644 index 00000000..c4aa87bf --- /dev/null +++ b/doc/api/examples/mapping.json @@ -0,0 +1,11 @@ +{ + "data": { + "id": 4, + "created_at": "2016-03-25T08:44:21.337Z", + "updated_at": "2016-03-25T08:44:21.337Z", + "mappable_id": 1, + "mappable_type": "Synapse", + "user_id": 1, + "map_id": 1 + } +} diff --git a/doc/api/examples/mappings.json b/doc/api/examples/mappings.json new file mode 100644 index 00000000..5a4a99c3 --- /dev/null +++ b/doc/api/examples/mappings.json @@ -0,0 +1,54 @@ +{ + "data": [ + { + "created_at": "2016-03-25T08:44:07.152Z", + "id": 1, + "map_id": 1, + "mappable_id": 1, + "mappable_type": "Topic", + "updated_at": "2016-03-25T08:44:07.152Z", + "user_id": 1, + "xloc": -271, + "yloc": 22 + }, + { + "created_at": "2016-03-25T08:44:13.907Z", + "id": 2, + "map_id": 1, + "mappable_id": 2, + "mappable_type": "Topic", + "updated_at": "2016-03-25T08:44:13.907Z", + "user_id": 1, + "xloc": -12, + "yloc": 61 + }, + { + "created_at": "2016-03-25T08:44:19.333Z", + "id": 3, + "map_id": 1, + "mappable_id": 3, + "mappable_type": "Topic", + "updated_at": "2016-03-25T08:44:19.333Z", + "user_id": 1, + "xloc": -93, + "yloc": -90 + }, + { + "created_at": "2016-03-25T08:44:21.337Z", + "id": 4, + "map_id": 1, + "mappable_id": 1, + "mappable_type": "Synapse", + "updated_at": "2016-03-25T08:44:21.337Z", + "user_id": 1 + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "per": 4, + "prev_page": 0, + "total_count": 303, + "total_pages": 76 + } +} diff --git a/doc/api/examples/maps.json b/doc/api/examples/maps.json new file mode 100644 index 00000000..8b963990 --- /dev/null +++ b/doc/api/examples/maps.json @@ -0,0 +1,37 @@ +{ + "data": [ + { + "id": 2, + "name": "Emergent Network Phenomena", + "desc": "Example map for the API", + "permission": "commons", + "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "created_at": "2016-03-26T08:02:05.379Z", + "updated_at": "2016-03-27T07:20:18.047Z", + "topic_ids": [ + 58, + 59 + ], + "synapse_ids": [ + 2 + ], + "mapping_ids": [ + 94, + 95, + 96 + ], + "collaborator_ids": [], + "contributor_ids": [ + 2 + ] + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 5, + "total_count": 5, + "per": 1 + } +} diff --git a/doc/api/examples/synapse.json b/doc/api/examples/synapse.json new file mode 100644 index 00000000..0de4acb3 --- /dev/null +++ b/doc/api/examples/synapse.json @@ -0,0 +1,13 @@ +{ + "data": { + "id": 2, + "desc": "hello", + "category": "from-to", + "permission": "commons", + "created_at": "2016-03-26T08:02:17.994Z", + "updated_at": "2016-03-26T08:02:17.994Z", + "topic1_id": 5, + "topic2_id": 6, + "user_id": 2 + } +} diff --git a/doc/api/examples/synapses.json b/doc/api/examples/synapses.json new file mode 100644 index 00000000..1bcb00c2 --- /dev/null +++ b/doc/api/examples/synapses.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": 2, + "desc": "hello", + "category": "from-to", + "permission": "commons", + "created_at": "2016-03-26T08:02:17.994Z", + "updated_at": "2016-03-26T08:02:17.994Z", + "topic1_id": 1, + "topic2_id": 2, + "user_id": 2 + }, + { + "id": 6, + "desc": "nice", + "category": "both", + "permission": "public", + "created_at": "2016-03-26T08:05:31.563Z", + "updated_at": "2016-03-26T08:05:31.563Z", + "topic1_id": 2, + "topic2_id": 3, + "user_id": 2 + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 71, + "total_count": 142, + "per": 2 + } +} diff --git a/doc/api/examples/token.json b/doc/api/examples/token.json new file mode 100644 index 00000000..14f559ea --- /dev/null +++ b/doc/api/examples/token.json @@ -0,0 +1,8 @@ +{ + "data": { + "id": 1, + "token": "VeI0qAe2bf2ytnrTRxmywsH0VSwuyjK5", + "description": "Personal token for in-browser testing", + "created_at": "2016-09-06T03:47:56.553Z" + } +} diff --git a/doc/api/examples/tokens.json b/doc/api/examples/tokens.json new file mode 100644 index 00000000..6d05cffc --- /dev/null +++ b/doc/api/examples/tokens.json @@ -0,0 +1,18 @@ +{ + "data": [ + { + "id": 1, + "token": "VeI0qAe2bf2ytnrTRxmywsH0VSwuyjK5", + "description": "Personal token for in-browser testing", + "created_at": "2016-09-06T03:47:56.553Z" + } + ], + "page": { + "current_page": 1, + "next_page": 0, + "prev_page": 0, + "total_pages": 1, + "total_count": 1, + "per": 25 + } +} diff --git a/doc/api/examples/topic.json b/doc/api/examples/topic.json new file mode 100644 index 00000000..90e702a2 --- /dev/null +++ b/doc/api/examples/topic.json @@ -0,0 +1,13 @@ +{ + "data": { + "id": 670, + "name": "Junto feedback and enhancements map", + "desc": "", + "link": "", + "permission": "commons", + "created_at": "2016-07-02T09:23:30.397Z", + "updated_at": "2016-07-02T09:23:30.397Z", + "user_id": 2, + "metacode_id": 36 + } +} diff --git a/doc/api/examples/topics.json b/doc/api/examples/topics.json new file mode 100644 index 00000000..d4eba53e --- /dev/null +++ b/doc/api/examples/topics.json @@ -0,0 +1,34 @@ +{ + "data": [ + { + "id": 670, + "name": "Junto feedback and enhancements map", + "desc": "", + "link": "", + "permission": "commons", + "created_at": "2016-07-02T09:23:30.397Z", + "updated_at": "2016-07-02T09:23:30.397Z", + "user_id": 2, + "metacode_id": 36 + }, + { + "id": 60, + "name": "View others on map in realtime", + "desc": "", + "link": "", + "permission": "commons", + "created_at": "2016-03-31T01:20:26.734Z", + "updated_at": "2016-03-31T01:20:26.734Z", + "user_id": 2, + "metacode_id": 8 + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 249, + "total_count": 497, + "per": 2 + } +} diff --git a/doc/api/resourceTypes/base.raml b/doc/api/resourceTypes/base.raml new file mode 100644 index 00000000..47ca56e3 --- /dev/null +++ b/doc/api/resourceTypes/base.raml @@ -0,0 +1,35 @@ +get?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +put?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +patch?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +post?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error +delete?: + responses: + 400: + description: Invalid request or params + body: + application/json: + schema: error diff --git a/doc/api/resourceTypes/collection.raml b/doc/api/resourceTypes/collection.raml new file mode 100644 index 00000000..d54e6c0c --- /dev/null +++ b/doc/api/resourceTypes/collection.raml @@ -0,0 +1,22 @@ +type: base +get?: + description: Get all <<resourcePathName>> + queryParameters: + page: + description: The page number + type: integer + per: + description: Number of records per page + type: integer + responses: + 200: + body: + application/json: + schema: <<resourcePathName>> +post?: + description: Create a new <<resourcePathName | !singularize>> + responses: + 200: + body: + application/json: + schema: <<resourcePathName | !singularize>> diff --git a/doc/api/resourceTypes/item.raml b/doc/api/resourceTypes/item.raml new file mode 100644 index 00000000..1abf040e --- /dev/null +++ b/doc/api/resourceTypes/item.raml @@ -0,0 +1,29 @@ +get?: + description: Get a <<resourcePathName | !singularize>> + responses: + 200: + body: + application/json: + schema: <<resourcePathName | !singularize>> +put?: + description: Update a <<resourcePathName | !singularize>> + responses: + 201: + description: Update success + body: + application/json: + schema: <<resourcePathName | !singularize>> +patch?: + description: Update a <<resourcePathName | !singularize>> + responses: + 201: + description: Update success + body: + application/json: + schema: <<resourcePathName | !singularize>> +delete?: + description: Delete a <<resourcePathName | !singularize>> + responses: + 204: + description: Removed +type: base diff --git a/doc/api/schemas/_datetimestamp.json b/doc/api/schemas/_datetimestamp.json new file mode 100644 index 00000000..fd9a76a4 --- /dev/null +++ b/doc/api/schemas/_datetimestamp.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "format": "date-time" +} diff --git a/doc/api/schemas/_id.json b/doc/api/schemas/_id.json new file mode 100644 index 00000000..d94ff818 --- /dev/null +++ b/doc/api/schemas/_id.json @@ -0,0 +1,4 @@ +{ + "type": "integer", + "minimum": 1 +} diff --git a/doc/api/schemas/_map.json b/doc/api/schemas/_map.json new file mode 100644 index 00000000..469b4dbe --- /dev/null +++ b/doc/api/schemas/_map.json @@ -0,0 +1,67 @@ +{ + "name": "Map", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "permission": { + "$ref": "_permission.json" + }, + "screenshot": { + "format": "uri", + "type": "string" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "topic_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "synapse_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "mapping_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "contributor_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + }, + "collaborator_ids": { + "type": "array", + "items": { + "$ref": "_id.json" + } + } + }, + "required": [ + "id", + "name", + "desc", + "permission", + "screenshot", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/_mapping.json b/doc/api/schemas/_mapping.json new file mode 100644 index 00000000..5a3b06a6 --- /dev/null +++ b/doc/api/schemas/_mapping.json @@ -0,0 +1,41 @@ +{ + "name": "Mapping", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "mappable_id": { + "$ref": "_id.json" + }, + "mappable_type": { + "type": "string", + "pattern": "(Topic|Synapse)" + }, + "xloc": { + "type": "integer" + }, + "yloc": { + "type": "integer" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "map_id": { + "$ref": "_id.json" + }, + "user_id": { + "$ref": "_id.json" + } + }, + "required": [ + "id", + "mappable_id", + "mappable_type", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/_page.json b/doc/api/schemas/_page.json new file mode 100644 index 00000000..635f0286 --- /dev/null +++ b/doc/api/schemas/_page.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "properties": { + "current_page": { + "type": "integer", + "minimum": 1 + }, + "next_page": { + "type": "integer", + "minimum": 0 + }, + "prev_page": { + "type": "integer", + "minimum": 0 + }, + "total_pages": { + "type": "integer", + "minimum": 0 + }, + "total_count": { + "type": "integer", + "minimum": 0 + }, + "per": { + "type": "integer", + "minimum": 0 + } + }, + "required": [ + "current_page", + "next_page", + "prev_page", + "total_pages", + "total_count", + "per" + ] +} + diff --git a/doc/api/schemas/_permission.json b/doc/api/schemas/_permission.json new file mode 100644 index 00000000..5c94fc81 --- /dev/null +++ b/doc/api/schemas/_permission.json @@ -0,0 +1,4 @@ +{ + "type": "string", + "pattern": "(commons|private|public)" +} diff --git a/doc/api/schemas/_synapse.json b/doc/api/schemas/_synapse.json new file mode 100644 index 00000000..dea238e2 --- /dev/null +++ b/doc/api/schemas/_synapse.json @@ -0,0 +1,42 @@ +{ + "name": "Synapse", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "desc": { + "type": "string" + }, + "category": { + "type": "string", + "pattern": "(from-to|both)" + }, + "permission": { + "$ref": "_permission.json" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "topic1_id": { + "$ref": "_id.json" + }, + "topic2_id": { + "$ref": "_id.json" + }, + "user_id": { + "$ref": "_id.json" + } + }, + "required": [ + "id", + "desc", + "category", + "permission", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/_token.json b/doc/api/schemas/_token.json new file mode 100644 index 00000000..62a44b3c --- /dev/null +++ b/doc/api/schemas/_token.json @@ -0,0 +1,24 @@ +{ + "name": "Token", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "token": { + "type": "string" + }, + "description": { + "type": "string" + }, + "created_at": { + "$ref": "_datetimestamp.json" + } + }, + "required": [ + "id", + "token", + "description", + "created_at" + ] +} diff --git a/doc/api/schemas/_topic.json b/doc/api/schemas/_topic.json new file mode 100644 index 00000000..e9ccf67b --- /dev/null +++ b/doc/api/schemas/_topic.json @@ -0,0 +1,43 @@ +{ + "name": "Topic", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "desc": { + "type": "string" + }, + "link": { + "format": "uri", + "type": "string" + }, + "permission": { + "$ref": "_permission.json" + }, + "created_at": { + "$ref": "_datetimestamp.json" + }, + "updated_at": { + "$ref": "_datetimestamp.json" + }, + "user_id": { + "$ref": "_id.json" + }, + "metacode_id": { + "$ref": "_id.json" + } + }, + "required": [ + "id", + "name", + "desc", + "link", + "permission", + "created_at", + "updated_at" + ] +} diff --git a/doc/api/schemas/map.json b/doc/api/schemas/map.json new file mode 100644 index 00000000..0a7ece7e --- /dev/null +++ b/doc/api/schemas/map.json @@ -0,0 +1,12 @@ +{ + "name": "Map Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_map.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/mapping.json b/doc/api/schemas/mapping.json new file mode 100644 index 00000000..f0e91ace --- /dev/null +++ b/doc/api/schemas/mapping.json @@ -0,0 +1,12 @@ +{ + "name": "Mapping Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_mapping.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/mappings.json b/doc/api/schemas/mappings.json new file mode 100644 index 00000000..37976a70 --- /dev/null +++ b/doc/api/schemas/mappings.json @@ -0,0 +1,19 @@ +{ + "name": "Mappings", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_mapping.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/maps.json b/doc/api/schemas/maps.json new file mode 100644 index 00000000..39698b96 --- /dev/null +++ b/doc/api/schemas/maps.json @@ -0,0 +1,19 @@ +{ + "name": "Maps", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_map.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/synapse.json b/doc/api/schemas/synapse.json new file mode 100644 index 00000000..5f916976 --- /dev/null +++ b/doc/api/schemas/synapse.json @@ -0,0 +1,12 @@ +{ + "name": "Synapse Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_synapse.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/synapses.json b/doc/api/schemas/synapses.json new file mode 100644 index 00000000..dd41cc53 --- /dev/null +++ b/doc/api/schemas/synapses.json @@ -0,0 +1,19 @@ +{ + "name": "Synapses", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_synapse.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/token.json b/doc/api/schemas/token.json new file mode 100644 index 00000000..85be81a5 --- /dev/null +++ b/doc/api/schemas/token.json @@ -0,0 +1,12 @@ +{ + "name": "Token Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_token.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/tokens.json b/doc/api/schemas/tokens.json new file mode 100644 index 00000000..5ea5bdce --- /dev/null +++ b/doc/api/schemas/tokens.json @@ -0,0 +1,19 @@ +{ + "name": "Tokens", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_token.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/topic.json b/doc/api/schemas/topic.json new file mode 100644 index 00000000..170670b1 --- /dev/null +++ b/doc/api/schemas/topic.json @@ -0,0 +1,12 @@ +{ + "name": "Topic Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_topic.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/topics.json b/doc/api/schemas/topics.json new file mode 100644 index 00000000..643e7607 --- /dev/null +++ b/doc/api/schemas/topics.json @@ -0,0 +1,19 @@ +{ + "name": "Topics", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_topic.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/traits/orderable.raml b/doc/api/traits/orderable.raml new file mode 100644 index 00000000..708736ab --- /dev/null +++ b/doc/api/traits/orderable.raml @@ -0,0 +1,3 @@ +queryParameters: + sort: + description: The name of the field to sort by, prefixed by "-" to sort descending diff --git a/doc/api/traits/pageable.raml b/doc/api/traits/pageable.raml new file mode 100644 index 00000000..88165861 --- /dev/null +++ b/doc/api/traits/pageable.raml @@ -0,0 +1,7 @@ +queryParameters: + page: + description: The page number + type: integer + per: + description: Number of records per page + type: integer diff --git a/doc/api/traits/searchable.raml b/doc/api/traits/searchable.raml new file mode 100644 index 00000000..53ae8525 --- /dev/null +++ b/doc/api/traits/searchable.raml @@ -0,0 +1,4 @@ +queryParameters: + q: + description: The search string to query by + type: string diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb new file mode 100644 index 00000000..4a1e3298 --- /dev/null +++ b/spec/api/v2/mappings_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'mappings API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:mapping) { create(:mapping, user: user) } + + it 'GET /api/v2/mappings' do + create_list(:mapping, 5) + get '/api/v2/mappings', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mappings) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/mappings/:id' do + get "/api/v2/mappings/#{mapping.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mapping) + expect(JSON.parse(response.body)['data']['id']).to eq mapping.id + end + + it 'POST /api/v2/mappings' do + post '/api/v2/mappings', params: { mapping: mapping.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mapping) + expect(Mapping.count).to eq 2 + end + + it 'PATCH /api/v2/mappings/:id' do + patch "/api/v2/mappings/#{mapping.id}", params: { mapping: mapping.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:mapping) + end + + it 'DELETE /api/v2/mappings/:id' do + delete "/api/v2/mappings/#{mapping.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Mapping.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:mapping) } + let(:collection) { get_json_example(:mappings) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:mapping) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:mappings) + end + end +end diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb new file mode 100644 index 00000000..7356ca72 --- /dev/null +++ b/spec/api/v2/maps_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'maps API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:map) { create(:map, user: user) } + + it 'GET /api/v2/maps' do + create_list(:map, 5) + get '/api/v2/maps', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:maps) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/maps/:id' do + get "/api/v2/maps/#{map.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + expect(JSON.parse(response.body)['data']['id']).to eq map.id + end + + it 'POST /api/v2/maps' do + post '/api/v2/maps', params: { map: map.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + expect(Map.count).to eq 2 + end + + it 'PATCH /api/v2/maps/:id' do + patch "/api/v2/maps/#{map.id}", params: { map: map.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + end + + it 'DELETE /api/v2/maps/:id' do + delete "/api/v2/maps/#{map.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Map.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:map) } + let(:collection) { get_json_example(:maps) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:map) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:maps) + end + end +end diff --git a/spec/api/v2/synapses_api_spec.rb b/spec/api/v2/synapses_api_spec.rb new file mode 100644 index 00000000..f232b879 --- /dev/null +++ b/spec/api/v2/synapses_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'synapses API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:synapse) { create(:synapse, user: user) } + + it 'GET /api/v2/synapses' do + create_list(:synapse, 5) + get '/api/v2/synapses', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapses) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/synapses/:id' do + get "/api/v2/synapses/#{synapse.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapse) + expect(JSON.parse(response.body)['data']['id']).to eq synapse.id + end + + it 'POST /api/v2/synapses' do + post '/api/v2/synapses', params: { synapse: synapse.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapse) + expect(Synapse.count).to eq 2 + end + + it 'PATCH /api/v2/synapses/:id' do + patch "/api/v2/synapses/#{synapse.id}", params: { synapse: synapse.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:synapse) + end + + it 'DELETE /api/v2/synapses/:id' do + delete "/api/v2/synapses/#{synapse.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Synapse.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:synapse) } + let(:collection) { get_json_example(:synapses) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:synapse) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:synapses) + end + end +end diff --git a/spec/api/v2/tokens_api_spec.rb b/spec/api/v2/tokens_api_spec.rb new file mode 100644 index 00000000..c2e480a5 --- /dev/null +++ b/spec/api/v2/tokens_api_spec.rb @@ -0,0 +1,44 @@ +require 'rails_helper' + +RSpec.describe 'tokens API', type: :request do + let(:user) { create(:user, admin: true) } + let(:auth_token) { create(:token, user: user).token } + let(:token) { create(:token, user: user) } + + it 'GET /api/v2/tokens/my_tokens' do + create_list(:token, 5, user: user) + get '/api/v2/tokens/my_tokens', params: { access_token: auth_token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:tokens) + expect(Token.count).to eq 6 # 5 + the extra auth token; let(:token) wasn't used + end + + it 'POST /api/v2/tokens' do + post '/api/v2/tokens', params: { token: token.attributes, access_token: auth_token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:token) + expect(Token.count).to eq 3 # auth_token, token, and the new POST-ed token + end + + it 'DELETE /api/v2/tokens/:id' do + delete "/api/v2/tokens/#{token.id}", params: { access_token: auth_token } + + expect(response).to have_http_status(:no_content) + expect(Token.count).to eq 1 # the extra auth token + end + + context 'RAML example' do + let(:resource) { get_json_example(:token) } + let(:collection) { get_json_example(:tokens) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:token) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:tokens) + end + end +end diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb new file mode 100644 index 00000000..4781348a --- /dev/null +++ b/spec/api/v2/topics_api_spec.rb @@ -0,0 +1,59 @@ +require 'rails_helper' + +RSpec.describe 'topics API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:topic) { create(:topic, user: user) } + + it 'GET /api/v2/topics' do + create_list(:topic, 5) + get '/api/v2/topics', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topics) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/topics/:id' do + get "/api/v2/topics/#{topic.id}" + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topic) + expect(JSON.parse(response.body)['data']['id']).to eq topic.id + end + + it 'POST /api/v2/topics' do + post '/api/v2/topics', params: { topic: topic.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topic) + expect(Topic.count).to eq 2 + end + + it 'PATCH /api/v2/topics/:id' do + patch "/api/v2/topics/#{topic.id}", params: { topic: topic.attributes, access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:topic) + end + + it 'DELETE /api/v2/topics/:id' do + delete "/api/v2/topics/#{topic.id}", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(Topic.count).to eq 0 + end + + context 'RAML example' do + let(:resource) { get_json_example(:topic) } + let(:collection) { get_json_example(:topics) } + + it 'resource matches schema' do + expect(resource).to match_json_schema(:topic) + end + + it 'collection matches schema' do + expect(collection).to match_json_schema(:topics) + end + end +end diff --git a/spec/controllers/mappings_controller_spec.rb b/spec/controllers/mappings_controller_spec.rb index ecf726c2..bcd2b97f 100644 --- a/spec/controllers/mappings_controller_spec.rb +++ b/spec/controllers/mappings_controller_spec.rb @@ -1,39 +1,40 @@ require 'rails_helper' RSpec.describe MappingsController, type: :controller do - let!(:mapping) { create(:mapping) } + let(:user) { create(:user) } + let!(:mapping) { create(:mapping, user: user) } let(:valid_attributes) { mapping.attributes.except('id') } - let(:invalid_attributes) { { xloc: 0 } } + let(:invalid_attributes) { { id: mapping.id } } before :each do - sign_in - end - - describe 'GET #show' do - it 'assigns the requested mapping as @mapping' do - get :show, id: mapping.to_param - expect(assigns(:mapping)).to eq(mapping) - end + sign_in user end describe 'POST #create' do context 'with valid params' do it 'creates a new Mapping' do expect do - post :create, mapping: valid_attributes + post :create, params: { + mapping: valid_attributes + } end.to change(Mapping, :count).by(1) end it 'assigns a newly created mapping as @mapping' do - post :create, mapping: valid_attributes - expect(assigns(:mapping)).to be_a(Mapping) - expect(assigns(:mapping)).to be_persisted + post :create, params: { + mapping: valid_attributes + } + expect(comparable(Mapping.last)).to eq comparable(mapping) end end context 'with invalid params' do it 'assigns a newly created but unsaved mapping as @mapping' do - post :create, mapping: invalid_attributes - expect(assigns(:mapping)).to be_a_new(Mapping) + post :create, params: { + mapping: invalid_attributes + } + # for some reason, the first mapping is still persisted + # TODO: fixme?? + expect(Mapping.count).to eq 1 end end end @@ -43,23 +44,26 @@ RSpec.describe MappingsController, type: :controller do let(:new_attributes) { build(:mapping_random_location).attributes.except('id') } it 'updates the requested mapping' do - put :update, - id: mapping.to_param, mapping: new_attributes + put :update, params: { + id: mapping.to_param, mapping: new_attributes + } mapping.reload end it 'assigns the requested mapping as @mapping' do - put :update, - id: mapping.to_param, mapping: valid_attributes - expect(assigns(:mapping)).to eq(mapping) + put :update, params: { + id: mapping.to_param, mapping: valid_attributes + } + expect(Mapping.last).to eq mapping end end context 'with invalid params' do it 'assigns the mapping as @mapping' do - put :update, - id: mapping.to_param, mapping: invalid_attributes - expect(assigns(:mapping)).to eq(mapping) + put :update, params: { + id: mapping.to_param, mapping: invalid_attributes + } + expect(Mapping.last).to eq mapping end end end @@ -67,7 +71,9 @@ RSpec.describe MappingsController, type: :controller do describe 'DELETE #destroy' do it 'destroys the requested mapping' do expect do - delete :destroy, id: mapping.to_param + delete :destroy, params: { + id: mapping.to_param + } end.to change(Mapping, :count).by(-1) end end diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 60b52ec1..278ec559 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -5,50 +5,33 @@ RSpec.describe MapsController, type: :controller do let(:valid_attributes) { map.attributes.except(:id) } let(:invalid_attributes) { { permission: :commons } } before :each do - sign_in - end - - describe 'GET #index' do - it 'viewable maps as @maps' do - get :activemaps - expect(assigns(:maps)).to eq([map]) - end - end - - describe 'GET #contains' do - it 'returns json matching schema' do - get :contains, id: map.to_param, format: :json - expect(response.body).to match_json_schema(:map_contains) - end - end - - describe 'GET #show' do - it 'assigns the requested map as @map' do - get :show, id: map.to_param - expect(assigns(:map)).to eq(map) - end + sign_in create(:user) end describe 'POST #create' do context 'with valid params' do it 'creates a new Map' do - map.reload expect do - post :create, valid_attributes.merge(format: :json) + post :create, format: :json, params: { + map: valid_attributes + } end.to change(Map, :count).by(1) end it 'assigns a newly created map as @map' do - post :create, valid_attributes.merge(format: :json) - expect(assigns(:map)).to be_a(Map) - expect(assigns(:map)).to be_persisted + post :create, format: :json, params: { + map: valid_attributes + } + expect(Map.last).to eq map end end context 'with invalid params' do it 'assigns a newly created but unsaved map as @map' do - post :create, invalid_attributes.merge(format: :json) - expect(assigns(:map)).to be_a_new(Map) + post :create, format: :json, params: { + map: invalid_attributes + } + expect(Map.count).to eq 0 end end end @@ -58,24 +41,28 @@ RSpec.describe MapsController, type: :controller do let(:new_attributes) { { name: 'Uncool map', permission: :private } } it 'updates the requested map' do - put :update, - id: map.to_param, map: new_attributes, format: :json - expect(assigns(:map).name).to eq 'Uncool map' - expect(assigns(:map).permission).to eq 'private' + put :update, format: :json, params: { + id: map.to_param, map: new_attributes + } + map.reload + expect(map.name).to eq 'Uncool map' + expect(map.permission).to eq 'private' end it 'assigns the requested map as @map' do - put :update, - id: map.to_param, map: valid_attributes, format: :json - expect(assigns(:map)).to eq(map) + put :update, format: :json, params: { + id: map.to_param, map: valid_attributes + } + expect(Map.last).to eq map end end context 'with invalid params' do it 'assigns the map as @map' do - put :update, - id: map.to_param, map: invalid_attributes, format: :json - expect(assigns(:map)).to eq(map) + put :update, format: :json, params: { + id: map.to_param, map: invalid_attributes + } + expect(Map.last).to eq map end end end @@ -87,7 +74,9 @@ RSpec.describe MapsController, type: :controller do it 'prevents deletion by non-owners' do unowned_map.reload expect do - delete :destroy, id: unowned_map.to_param, format: :json + delete :destroy, format: :json, params: { + id: unowned_map.to_param + } end.to change(Map, :count).by(0) expect(response.body).to eq '' expect(response.status).to eq 403 @@ -96,7 +85,9 @@ RSpec.describe MapsController, type: :controller do it 'deletes owned map' do owned_map.reload # ensure it's in the database expect do - delete :destroy, id: owned_map.to_param, format: :json + delete :destroy, format: :json, params: { + id: owned_map.to_param + } end.to change(Map, :count).by(-1) expect(response.body).to eq '' expect(response.status).to eq 204 diff --git a/spec/controllers/metacodes_controller_spec.rb b/spec/controllers/metacodes_controller_spec.rb index 06434d49..cb4116d4 100644 --- a/spec/controllers/metacodes_controller_spec.rb +++ b/spec/controllers/metacodes_controller_spec.rb @@ -10,43 +10,33 @@ RSpec.describe MetacodesController, type: :controller do describe 'GET #index' do it 'assigns all metacodes as @metacodes' do metacode.reload # ensure it's created - get :index, {} + get :index expect(Metacode.all.to_a).to eq([metacode]) end end - describe 'GET #new' do - it 'assigns a new metacode as @metacode' do - get :new, format: :json - expect(assigns(:metacode)).to be_a_new(Metacode) - end - end - - describe 'GET #edit' do - it 'assigns the requested metacode as @metacode' do - get :edit, id: metacode.to_param - expect(assigns(:metacode)).to eq(metacode) - end - end - describe 'POST #create' do context 'with valid params' do it 'creates a new Metacode' do metacode.reload # ensure it's present to start expect do - post :create, metacode: valid_attributes + post :create, params: { + metacode: valid_attributes + } end.to change(Metacode, :count).by(1) end it 'has the correct attributes' do - post :create, metacode: valid_attributes - # expect(Metacode.last.attributes.expect(:id)).to eq(metacode.attributes.except(:id)) - expect(assigns(:metacode)).to be_a(Metacode) - expect(assigns(:metacode)).to be_persisted + post :create, params: { + metacode: valid_attributes + } + expect(comparable(Metacode.last)).to eq(comparable(metacode)) end it 'redirects to the metacode index' do - post :create, metacode: valid_attributes + post :create, params: { + metacode: valid_attributes + } expect(response).to redirect_to(metacodes_url) end end @@ -62,8 +52,9 @@ RSpec.describe MetacodesController, type: :controller do end it 'updates the requested metacode' do - put :update, - id: metacode.to_param, metacode: new_attributes + put :update, params: { + id: metacode.to_param, metacode: new_attributes + } metacode.reload expect(metacode.icon).to eq 'https://newimages.ca/cool-image.jpg' expect(metacode.color).to eq '#ffffff' @@ -75,13 +66,17 @@ RSpec.describe MetacodesController, type: :controller do context 'not admin' do it 'denies access to create' do sign_in create(:user, admin: false) - post :create, metacode: valid_attributes + post :create, params: { + metacode: valid_attributes + } expect(response).to redirect_to root_url end it 'denies access to update' do sign_in create(:user, admin: false) - post :update, id: metacode.to_param, metacode: valid_attributes + post :update, params: { + id: metacode.to_param, metacode: valid_attributes + } expect(response).to redirect_to root_url end end diff --git a/spec/controllers/synapses_controller_spec.rb b/spec/controllers/synapses_controller_spec.rb index 55b2addb..15d91250 100644 --- a/spec/controllers/synapses_controller_spec.rb +++ b/spec/controllers/synapses_controller_spec.rb @@ -5,14 +5,7 @@ RSpec.describe SynapsesController, type: :controller do let(:valid_attributes) { synapse.attributes.except('id') } let(:invalid_attributes) { { permission: :invalid_lol } } before :each do - sign_in - end - - describe 'GET #show' do - it 'assigns the requested synapse as @synapse' do - get :show, id: synapse.to_param, format: :json - expect(assigns(:synapse)).to eq(synapse) - end + sign_in create(:user) end describe 'POST #create' do @@ -20,25 +13,32 @@ RSpec.describe SynapsesController, type: :controller do it 'creates a new Synapse' do synapse.reload # ensure it's present expect do - post :create, synapse: valid_attributes, format: :json + post :create, format: :json, params: { + synapse: valid_attributes + } end.to change(Synapse, :count).by(1) end it 'assigns a newly created synapse as @synapse' do - post :create, synapse: valid_attributes, format: :json - expect(assigns(:synapse)).to be_a(Synapse) - expect(assigns(:synapse)).to be_persisted + post :create, format: :json, params: { + synapse: valid_attributes + } + expect(comparable(Synapse.last)).to eq comparable(synapse) end it 'returns 201 CREATED' do - post :create, synapse: valid_attributes, format: :json + post :create, format: :json, params: { + synapse: valid_attributes + } expect(response.status).to eq 201 end end context 'with invalid params' do it 'returns 422 UNPROCESSABLE ENTITY' do - post :create, synapse: invalid_attributes, format: :json + post :create, format: :json, params: { + synapse: invalid_attributes + } expect(response.status).to eq 422 end end @@ -53,8 +53,9 @@ RSpec.describe SynapsesController, type: :controller do end it 'updates the requested synapse' do - put :update, - id: synapse.to_param, synapse: new_attributes, format: :json + put :update, format: :json, params: { + id: synapse.to_param, synapse: new_attributes + } synapse.reload expect(synapse.desc).to eq 'My new description' expect(synapse.category).to eq 'both' @@ -62,17 +63,19 @@ RSpec.describe SynapsesController, type: :controller do end it 'returns 204 NO CONTENT' do - put :update, - id: synapse.to_param, synapse: valid_attributes, format: :json + put :update, format: :json, params: { + id: synapse.to_param, synapse: valid_attributes + } expect(response.status).to eq 204 end end context 'with invalid params' do it 'assigns the synapse as @synapse' do - put :update, - id: synapse.to_param, synapse: invalid_attributes, format: :json - expect(assigns(:synapse)).to eq(synapse) + put :update, format: :json, params: { + id: synapse.to_param, synapse: invalid_attributes + } + expect(Synapse.last).to eq synapse end end end @@ -83,12 +86,16 @@ RSpec.describe SynapsesController, type: :controller do it 'destroys the requested synapse' do synapse.reload # ensure it's present expect do - delete :destroy, id: synapse.to_param, format: :json + delete :destroy, format: :json, params: { + id: synapse.to_param + } end.to change(Synapse, :count).by(-1) end it 'returns 204 NO CONTENT' do - delete :destroy, id: synapse.to_param, format: :json + delete :destroy, format: :json, params: { + id: synapse.to_param + } expect(response.status).to eq 204 end end diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index b6081701..315b931f 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -5,14 +5,7 @@ RSpec.describe TopicsController, type: :controller do let(:valid_attributes) { topic.attributes.except('id') } let(:invalid_attributes) { { permission: :invalid_lol } } before :each do - sign_in - end - - describe 'GET #show' do - it 'assigns the requested topic as @topic' do - get :show, id: topic.to_param, format: :json - expect(assigns(:topic)).to eq(topic) - end + sign_in create(:user) end describe 'POST #create' do @@ -20,26 +13,33 @@ RSpec.describe TopicsController, type: :controller do it 'creates a new Topic' do topic.reload # ensure it's created expect do - post :create, topic: valid_attributes, format: :json + post :create, format: :json, params: { + topic: valid_attributes + } end.to change(Topic, :count).by(1) end it 'assigns a newly created topic as @topic' do - post :create, topic: valid_attributes, format: :json - expect(assigns(:topic)).to be_a(Topic) - expect(assigns(:topic)).to be_persisted + post :create, format: :json, params: { + topic: valid_attributes + } + expect(comparable(Topic.last)).to eq comparable(topic) end it 'returns 201 CREATED' do - post :create, topic: valid_attributes, format: :json + post :create, format: :json, params: { + topic: valid_attributes + } expect(response.status).to eq 201 end end context 'with invalid params' do it 'assigns a newly created but unsaved topic as @topic' do - post :create, topic: invalid_attributes, format: :json - expect(assigns(:topic)).to be_a_new(Topic) + post :create, format: :json, params: { + topic: invalid_attributes + } + expect(Topic.count).to eq 0 end end end @@ -54,8 +54,9 @@ RSpec.describe TopicsController, type: :controller do end it 'updates the requested topic' do - put :update, - id: topic.to_param, topic: new_attributes, format: :json + put :update, format: :json, params: { + id: topic.to_param, topic: new_attributes + } topic.reload expect(topic.name).to eq 'Cool Topic with no number' expect(topic.desc).to eq 'This is a cool topic.' @@ -64,23 +65,26 @@ RSpec.describe TopicsController, type: :controller do end it 'assigns the requested topic as @topic' do - put :update, - id: topic.to_param, topic: valid_attributes, format: :json - expect(assigns(:topic)).to eq(topic) + put :update, format: :json, params: { + id: topic.to_param, topic: valid_attributes + } + expect(Topic.last).to eq(topic) end it 'returns status of no content' do - put :update, - id: topic.to_param, topic: valid_attributes, format: :json + put :update, format: :json, params: { + id: topic.to_param, topic: valid_attributes + } expect(response.status).to eq 204 end end context 'with invalid params' do it 'assigns the topic as @topic' do - put :update, - id: topic.to_param, topic: invalid_attributes, format: :json - expect(assigns(:topic)).to eq(topic) + put :update, format: :json, params: { + id: topic.to_param, topic: invalid_attributes + } + expect(Topic.last).to eq topic end end end @@ -90,7 +94,9 @@ RSpec.describe TopicsController, type: :controller do it 'destroys the requested topic' do owned_topic.reload # ensure it's there expect do - delete :destroy, id: owned_topic.to_param, format: :json + delete :destroy, format: :json, params: { + id: owned_topic.to_param + } end.to change(Topic, :count).by(-1) expect(response.body).to eq '' expect(response.status).to eq 204 diff --git a/spec/factories/maps.rb b/spec/factories/maps.rb index a786d109..14450c00 100644 --- a/spec/factories/maps.rb +++ b/spec/factories/maps.rb @@ -3,6 +3,7 @@ FactoryGirl.define do sequence(:name) { |n| "Cool Map ##{n}" } permission :commons arranged { false } + desc '' user end end diff --git a/spec/factories/synapses.rb b/spec/factories/synapses.rb index 4454a7a4..db82fc39 100644 --- a/spec/factories/synapses.rb +++ b/spec/factories/synapses.rb @@ -6,5 +6,6 @@ FactoryGirl.define do association :topic1, factory: :topic association :topic2, factory: :topic user + weight 1 # todo drop this column end end diff --git a/spec/factories/tokens.rb b/spec/factories/tokens.rb new file mode 100644 index 00000000..3970d76f --- /dev/null +++ b/spec/factories/tokens.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :token do + user + description '' + end +end diff --git a/spec/factories/topics.rb b/spec/factories/topics.rb index 17c69a25..f4c73f4c 100644 --- a/spec/factories/topics.rb +++ b/spec/factories/topics.rb @@ -1,8 +1,10 @@ FactoryGirl.define do factory :topic do - sequence(:name) { |n| "Cool Topic ##{n}" } - permission :commons user metacode + permission :commons + sequence(:name) { |n| "Cool Topic ##{n}" } + sequence(:desc) { |n| "topic desc #{n}" } + sequence(:link) { |n| "https://metamaps.cc/maps/#{n}" } end end diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb deleted file mode 100644 index 9c398b20..00000000 --- a/spec/mailers/map_mailer_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe MapMailer, type: :mailer do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 23f21101..d14d6dbe 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,45 +1,26 @@ -# This file is copied to spec/ when you run 'rails generate rspec:install' ENV['RAILS_ENV'] ||= 'test' +require 'spec_helper' require File.expand_path('../../config/environment', __FILE__) +require 'rspec/rails' # Prevent database truncation if the environment is production if Rails.env.production? abort('The Rails environment is running in production mode!') end -require 'spec_helper' -require 'rspec/rails' -# Add additional requires below this line. Rails is not loaded until this point! - # require all support files Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } +ActiveRecord::Migration.maintain_test_schema! + RSpec.configure do |config| # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures config.fixture_path = "#{::Rails.root}/spec/fixtures" - # If you're not using ActiveRecord, or you'd prefer not to run each of your - # examples within a transaction, remove the following line or assign false - # instead of true. config.use_transactional_fixtures = true - # RSpec Rails can automatically mix in different behaviours to your tests - # based on their file location, for example enabling you to call `get` and - # `post` in specs under `spec/controllers`. - # - # You can disable this behaviour by removing the line below, and instead - # explicitly tag your specs with their type, e.g.: - # - # RSpec.describe UsersController, :type => :controller do - # # ... - # end - # - # The different available types are documented in the features, such as in - # https://relishapp.com/rspec/rspec-rails/docs config.infer_spec_type_from_file_location! - config.include Devise::TestHelpers, type: :controller - config.include ControllerHelpers, type: :controller config.include Shoulda::Matchers::ActiveModel, type: :model config.include Shoulda::Matchers::ActiveRecord, type: :model end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d4028602..a2b164b2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,3 @@ -require 'simplecov' -require 'support/controller_helpers' -require 'pundit/rspec' - RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/spec/support/controller_helpers.rb b/spec/support/controller_helpers.rb index a8364f91..1672479f 100644 --- a/spec/support/controller_helpers.rb +++ b/spec/support/controller_helpers.rb @@ -3,24 +3,17 @@ require 'devise' module ControllerHelpers - # rubocop:disable Metrics/AbcSize - def sign_in(user = create(:user)) - if user.nil? # simulate unauthenticated - allow(request.env['warden']).to( - receive(:authenticate!).and_throw(:warden, scope: :user) - ) - else # simulate authenticated - allow_message_expectations_on_nil - allow(request.env['warden']).to( - receive(:authenticate!).and_return(user) - ) - end - allow(controller).to receive(:current_user).and_return(user) + extend ActiveSupport::Concern + + included do + include Devise::Test::ControllerHelpers + end + + def comparable(model) + model.attributes.except('id', 'created_at', 'updated_at') end - # rubocop:enable Metrics/AbcSize end RSpec.configure do |config| - config.include Devise::TestHelpers, type: :controller config.include ControllerHelpers, type: :controller end diff --git a/spec/support/pundit.rb b/spec/support/pundit.rb new file mode 100644 index 00000000..1fd8e296 --- /dev/null +++ b/spec/support/pundit.rb @@ -0,0 +1 @@ +require 'pundit/rspec' diff --git a/spec/support/schema_matcher.rb b/spec/support/schema_matcher.rb index b2a89352..207c5fa6 100644 --- a/spec/support/schema_matcher.rb +++ b/spec/support/schema_matcher.rb @@ -1,7 +1,32 @@ -RSpec::Matchers.define :match_json_schema do |schema| - match do |json| - schema_directory = Rails.root.join('spec', 'schemas').to_s - schema_path = "#{schema_directory}/#{schema}.json" - JSON::Validator.validate!(schema_path, json) +RSpec::Matchers.define :match_json_schema do |schema_name| + match do |response| + schema_directory = Rails.root.join('doc', 'api', 'schemas').to_s + schema = "#{schema_directory}/#{schema_name}.json" + + # schema customizations + schema = JSON.parse(File.read(schema)) + schema = update_file_refs(schema) + + data = JSON.parse(response.body) + JSON::Validator.validate!(schema, data, validate_schema: true) end end + +def get_json_example(resource) + filepath = "#{Rails.root}/doc/api/examples/#{resource}.json" + OpenStruct.new(body: File.read(filepath)) +end + +# add full paths to file references +def update_file_refs(schema) + schema.each_pair do |key, value| + schema[key] = if value.is_a? Hash + update_file_refs(value) + elsif key == '$ref' + "#{Rails.root}/doc/api/schemas/#{value}" + else + value + end + end + schema +end diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb new file mode 100644 index 00000000..8017e897 --- /dev/null +++ b/spec/support/simplecov.rb @@ -0,0 +1,2 @@ +require 'simplecov' + From 8b19c9e340482d16b703d1a089baa9c5b434e5bb Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 01:24:14 +0800 Subject: [PATCH 017/306] automatic versioning via git (#621) --- config/initializers/version.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 19139a4a..94f54a37 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,2 @@ -METAMAPS_VERSION = '2.9.0'.freeze -METAMAPS_LAST_UPDATED = 'Sept 1, 2016'.freeze +METAMAPS_VERSION = "2 build `git log -1 --pretty=%H`".freeze +METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad' --date=format:'%b %d, %Y'`.freeze From 2219e0d0dd7250ae6ca5630ea5a15ab45e8948fe Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 21 Sep 2016 14:53:17 -0400 Subject: [PATCH 018/306] Update Metamaps.Topic.js --- app/assets/javascripts/src/Metamaps.Topic.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/app/assets/javascripts/src/Metamaps.Topic.js index 9e6782cb..a0ebfa82 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/app/assets/javascripts/src/Metamaps.Topic.js @@ -331,7 +331,7 @@ Metamaps.Topic = { Metamaps.Topics.add(topic) if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.Map.getNextCoord() + var nextCoords = Metamaps.AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, @@ -356,7 +356,7 @@ Metamaps.Topic = { var topic = self.get(id) if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.Map.getNextCoord() + var nextCoords = Metamaps.AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, From a4d31241a8cebbbe47579304360b5bf9173ade3a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 14:25:49 +0800 Subject: [PATCH 019/306] move Metamaps code into webpack --- app/assets/javascripts/application.js | 32 +- .../javascripts/src/Metamaps.Erb.js.erb | 18 + .../javascripts/src/Metamaps.GlobalUI.js | 717 ------------------ .../src/Metamaps}/Metamaps.Account.js | 1 + .../src/Metamaps}/Metamaps.Admin.js | 1 + .../src/Metamaps}/Metamaps.AutoLayout.js | 1 + .../src/Metamaps}/Metamaps.Backbone.js | 1 + .../src/Metamaps}/Metamaps.Control.js | 1 + .../src/Metamaps}/Metamaps.Create.js | 1 + .../src/Metamaps}/Metamaps.Debug.js | 2 + .../src/Metamaps}/Metamaps.Filter.js | 1 + frontend/src/Metamaps/Metamaps.GlobalUI.js | 680 +++++++++++++++++ .../src/Metamaps}/Metamaps.Import.js | 1 + .../src/Metamaps}/Metamaps.JIT.js | 3 +- .../src/Metamaps}/Metamaps.Listeners.js | 1 + .../src/Metamaps}/Metamaps.Map.js | 1 + .../src/Metamaps}/Metamaps.Mapper.js | 1 + .../src/Metamaps}/Metamaps.Mobile.js | 1 + .../src/Metamaps}/Metamaps.Organize.js | 1 + .../src/Metamaps}/Metamaps.PasteInput.js | 1 + .../src/Metamaps/Metamaps.ReactComponents.js | 7 + .../src/Metamaps}/Metamaps.Realtime.js | 2 + .../src/Metamaps}/Metamaps.Router.js | 1 + .../src/Metamaps}/Metamaps.Synapse.js | 1 + .../src/Metamaps}/Metamaps.SynapseCard.js | 1 + .../src/Metamaps}/Metamaps.Topic.js | 1 + .../src/Metamaps}/Metamaps.TopicCard.js | 7 +- .../src/Metamaps}/Metamaps.Util.js | 1 + .../src/Metamaps}/Metamaps.Views.js | 1 + .../src/Metamaps}/Metamaps.Visualize.js | 1 + .../src/Metamaps/index.js | 49 +- frontend/src/index.js | 13 +- 32 files changed, 775 insertions(+), 776 deletions(-) create mode 100644 app/assets/javascripts/src/Metamaps.Erb.js.erb delete mode 100644 app/assets/javascripts/src/Metamaps.GlobalUI.js rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Account.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Admin.js (97%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.AutoLayout.js (97%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Backbone.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Control.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Create.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Debug.js (73%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Filter.js (99%) create mode 100644 frontend/src/Metamaps/Metamaps.GlobalUI.js rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Import.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.JIT.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Listeners.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Map.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Mapper.js (91%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Mobile.js (95%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Organize.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.PasteInput.js (98%) create mode 100644 frontend/src/Metamaps/Metamaps.ReactComponents.js rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Realtime.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Router.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Synapse.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.SynapseCard.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Topic.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.TopicCard.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Util.js (99%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Views.js (98%) rename {app/assets/javascripts/src => frontend/src/Metamaps}/Metamaps.Visualize.js (99%) rename app/assets/javascripts/src/Metamaps.js.erb => frontend/src/Metamaps/index.js (60%) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 14f565fa..03dac4fb 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -13,37 +13,11 @@ //= require jquery //= require jquery-ui //= require jquery_ujs -//= require ./webpacked/metamaps.bundle //= require_directory ./lib -//= require ./src/Metamaps.GlobalUI -//= require ./src/Metamaps.Router -//= require ./src/Metamaps.Backbone -//= require ./src/Metamaps.Views +//= require ./src/JIT +//= require ./src/Metamaps.Erb +//= require ./webpacked/metamaps.bundle //= require ./src/views/chatView //= require ./src/views/videoView //= require ./src/views/room -//= require ./src/JIT //= require ./src/check-canvas-support -//= require ./src/Metamaps -//= require ./src/Metamaps.Create -//= require ./src/Metamaps.TopicCard -//= require ./src/Metamaps.SynapseCard -//= require ./src/Metamaps.Visualize -//= require ./src/Metamaps.Util -//= require ./src/Metamaps.Realtime -//= require ./src/Metamaps.Control -//= require ./src/Metamaps.Filter -//= require ./src/Metamaps.Listeners -//= require ./src/Metamaps.Organize -//= require ./src/Metamaps.Topic -//= require ./src/Metamaps.Synapse -//= require ./src/Metamaps.Map -//= require ./src/Metamaps.Account -//= require ./src/Metamaps.Mapper -//= require ./src/Metamaps.Mobile -//= require ./src/Metamaps.Admin -//= require ./src/Metamaps.Import -//= require ./src/Metamaps.AutoLayout -//= require ./src/Metamaps.PasteInput -//= require ./src/Metamaps.JIT -//= require ./src/Metamaps.Debug diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb new file mode 100644 index 00000000..90eba5e5 --- /dev/null +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -0,0 +1,18 @@ +/* global Metamaps */ + +/* + * Metamaps.Erb.js.erb + */ + +/* erb variables from rails */ +window.Metamaps = window.Metamaps || {} +Metamaps.Erb = {} +Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' +Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' +Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' +Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' +Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' +Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> +Metamaps.VERSION = '<%= METAMAPS_VERSION %>' diff --git a/app/assets/javascripts/src/Metamaps.GlobalUI.js b/app/assets/javascripts/src/Metamaps.GlobalUI.js deleted file mode 100644 index a6466fdc..00000000 --- a/app/assets/javascripts/src/Metamaps.GlobalUI.js +++ /dev/null @@ -1,717 +0,0 @@ -var Metamaps = window.Metamaps || {}; // this variable declaration defines a Javascript object that will contain all the variables and functions used by us, broken down into 'sub-modules' that look something like this -/* - -* unless you are on a page with the Javascript InfoVis Toolkit (Topic or Map) the only section in the metamaps -* object will be these -GlobalUI -Active -Maps -Mappers -Backbone - -* all these get added when you are on a page with the Javascript Infovis Toolkit -Settings -Touch -Mouse -Selected -Metacodes -Topics -Synapses -Mappings -Create -TopicCard -SynapseCard -Visualize -Util -Realtime -Control -Filter -Listeners -Organize -Map -Mapper -Topic -Synapse -JIT -*/ - -Metamaps.Active = { - Map: null, - Topic: null, - Mapper: null -}; -Metamaps.Maps = {}; - -$(document).ready(function () { - // initialize all the modules - for (var prop in Metamaps) { - // this runs the init function within each sub-object on the Metamaps one - if (Metamaps.hasOwnProperty(prop) && - Metamaps[prop] != null && - Metamaps[prop].hasOwnProperty('init') && - typeof (Metamaps[prop].init) == 'function' - ) { - Metamaps[prop].init() - } - } - // load whichever page you are on - if (Metamaps.currentSection === "explore") { - var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - - Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) - if (Metamaps.currentPage === "mapper") { - Metamaps.Views.exploreMaps.fetchUserThenRender() - } - else { - Metamaps.Views.exploreMaps.render() - } - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Metamaps.Views.exploreMaps.render() - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.Active.Map || Metamaps.Active.Topic) { - Metamaps.Loading.show() - Metamaps.JIT.prepareVizData() - Metamaps.GlobalUI.showDiv('#infovis') - } -}); - -Metamaps.GlobalUI = { - notifyTimeout: null, - lightbox: null, - init: function () { - var self = Metamaps.GlobalUI; - - self.Search.init(); - self.CreateMap.init(); - self.Account.init(); - - if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) - - //bind lightbox clicks - $('.openLightbox').click(function (event) { - self.openLightbox($(this).attr('data-open')); - event.preventDefault(); - return false; - }); - - $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); - - // initialize global backbone models and collections - if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); - - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; - var mapperCollection = []; - var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; - if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models; - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; - } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; - Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); - Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); - Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); - // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); - Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); - Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); - }, - showDiv: function (selector) { - $(selector).show() - $(selector).animate({ - opacity: 1 - }, 200, 'easeOutCubic') - }, - hideDiv: function (selector) { - $(selector).animate({ - opacity: 0 - }, 200, 'easeInCubic', function () { $(this).hide() }) - }, - openLightbox: function (which) { - var self = Metamaps.GlobalUI; - - $('.lightboxContent').hide(); - $('#' + which).show(); - - self.lightbox = which; - - $('#lightbox_overlay').show(); - - var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; - // animate the content in from the bottom - $('#lightbox_main').animate({ - 'top': '50%', - 'margin-top': heightOfContent - }, 200, 'easeOutCubic'); - - // fade the black overlay in - $('#lightbox_screen').animate({ - 'opacity': '0.42' - }, 200); - - if (which == "switchMetacodes") { - Metamaps.Create.isSwitchingSet = true; - } - }, - - closeLightbox: function (event) { - var self = Metamaps.GlobalUI; - - if (event) event.preventDefault(); - - // animate the lightbox content offscreen - $('#lightbox_main').animate({ - 'top': '100%', - 'margin-top': '0' - }, 200, 'easeInCubic'); - - // fade the black overlay out - $('#lightbox_screen').animate({ - 'opacity': '0.0' - }, 200, function () { - $('#lightbox_overlay').hide(); - }); - - if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map'); - if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { - Metamaps.Create.cancelMetacodeSetSwitch(); - } - self.lightbox = null; - }, - notifyUser: function (message, leaveOpen) { - var self = Metamaps.GlobalUI; - - $('#toast').html(message) - self.showDiv('#toast') - clearTimeout(self.notifyTimeOut); - if (!leaveOpen) { - self.notifyTimeOut = setTimeout(function () { - self.hideDiv('#toast') - }, 8000); - } - }, - clearNotify: function() { - var self = Metamaps.GlobalUI; - - clearTimeout(self.notifyTimeOut); - self.hideDiv('#toast') - }, - shareInvite: function(inviteLink) { - window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); - } -}; - -Metamaps.GlobalUI.CreateMap = { - newMap: null, - emptyMapForm: "", - emptyForkMapForm: "", - topicsToMap: [], - synapsesToMap: [], - init: function () { - var self = Metamaps.GlobalUI.CreateMap; - - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - - self.bindFormEvents(); - - self.emptyMapForm = $('#new_map').html(); - - }, - bindFormEvents: function () { - var self = Metamaps.GlobalUI.CreateMap; - - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { - if (event.keyCode === 13) self.submit() - }) - - $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault(); - Metamaps.GlobalUI.closeLightbox(); - }); - $('.new_map button.submitMap').unbind().bind('click', self.submit); - - // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission); - }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function(){ - $(this).remove(); - }); - }, - generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; - stringStart += id; - stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; - var page = Metamaps.Active.Map ? 'map' : 'page'; - var stringEnd = "</a></div>"; - return stringStart + page + stringEnd; - }, - switchPermission: function () { - var self = Metamaps.GlobalUI.CreateMap; - - self.newMap.set('permission', $(this).attr('data-permission')); - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); - $(this).find('.mapPermIcon').addClass('selected'); - - var permText = $(this).find('.tip').html(); - $(this).parents('.new_map').find('.permText').html(permText); - }, - submit: function (event) { - if (event) event.preventDefault(); - - var self = Metamaps.GlobalUI.CreateMap; - - if (Metamaps.GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap); - self.newMap.set('synapsesToMap', self.synapsesToMap); - } - - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); - - self.newMap.set('name', $form.find('#map_name').val()); - self.newMap.set('desc', $form.find('#map_desc').val()); - - if (self.newMap.get('name').length===0){ - self.throwMapNameError(); - return; - } - - self.newMap.save(null, { - success: self.success - // TODO add error message - }); - - Metamaps.GlobalUI.closeLightbox(); - Metamaps.GlobalUI.notifyUser('Working...'); - }, - throwMapNameError: function () { - var self = Metamaps.GlobalUI.CreateMap; - - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); - - var message = $("<div class='feedback_message'>Please enter a map name...</div>"); - - $form.find('#map_name').after(message); - setTimeout(function(){ - message.fadeOut('fast', function(){ - message.remove(); - }); - }, 5000); - }, - success: function (model) { - var self = Metamaps.GlobalUI.CreateMap; - - //push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model); - - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var form = $(formId); - - Metamaps.GlobalUI.clearNotify(); - $('#wrapper').append(self.generateSuccessMessage(model.id)); - - }, - reset: function (id) { - var self = Metamaps.GlobalUI.CreateMap; - - var form = $('#' + id); - - if (id === "fork_map") { - self.topicsToMap = []; - self.synapsesToMap = []; - form.html(self.emptyForkMapForm); - } - else { - form.html(self.emptyMapForm); - } - - self.bindFormEvents(); - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - - return false; - }, -}; - - -Metamaps.GlobalUI.Account = { - isOpen: false, - changing: false, - init: function () { - var self = Metamaps.GlobalUI.Account; - - $('.sidebarAccountIcon').click(self.toggleBox); - $('.sidebarAccountBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); - }, - toggleBox: function (event) { - var self = Metamaps.GlobalUI.Account; - - if (self.isOpen) self.close(); - else self.open(); - - event.stopPropagation(); - }, - open: function () { - var self = Metamaps.GlobalUI.Account; - - Metamaps.Filter.close(); - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); - - - if (!self.isOpen && !self.changing) { - self.changing = true; - $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - $('.sidebarAccountBox #user_email').focus(); - }); - } - }, - close: function () { - var self = Metamaps.GlobalUI.Account; - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); - if (!self.changing) { - self.changing = true; - $('.sidebarAccountBox #user_email').blur(); - $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); - } - } -}; - - - -Metamaps.GlobalUI.Search = { - locked: false, - isOpen: false, - limitTopicsToMe: false, - limitMapsToMe: false, - timeOut: null, - changing: false, - optionsInitialized: false, - init: function () { - var self = Metamaps.GlobalUI.Search; - - var loader = new CanvasLoader('searchLoading'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(24); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default - - // bind the hover events - $(".sidebarSearch").hover(function () { - self.open() - }, function () { - self.close(800, false) - }); - - $('.sidebarSearchIcon').click(function (e) { - $('.sidebarSearchField').focus(); - }); - $('.sidebarSearch').click(function (e) { - e.stopPropagation(); - }); - $('body').click(function (e) { - self.close(0, false); - }); - - // open if the search is closed and user hits ctrl+/ - // close if they hit ESC - $('body').bind('keyup', function (e) { - switch (e.which) { - case 191: - if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true); // true for focus - } - break; - case 27: - if (self.isOpen) { - self.close(0, true); - } - break; - - default: - break; //console.log(e.which); - } - }); - - self.startTypeahead(); - }, - lock: function() { - var self = Metamaps.GlobalUI.Search; - self.locked = true; - }, - unlock: function() { - var self = Metamaps.GlobalUI.Search; - self.locked = false; - }, - open: function (focus) { - var self = Metamaps.GlobalUI.Search; - - clearTimeout(self.timeOut); - if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true; - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '400px' - }, 300, function () { - if (focus) $('.sidebarSearchField').focus(); - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 10px 3px 10px', - width: '380px' - }); - self.changing = false; - self.isOpen = true; - }); - } - }, - close: function (closeAfter, bypass) { - // for now - return - - var self = Metamaps.GlobalUI.Search; - - self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { - self.changing = true; - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 0 3px 0', - width: '400px' - }); - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '0' - }, 300, function () { - $('.sidebarSearchField').typeahead('val', ''); - $('.sidebarSearchField').blur(); - self.changing = false; - self.isOpen = false; - }); - } - }, closeAfter); - }, - startTypeahead: function () { - var self = Metamaps.GlobalUI.Search; - - var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; - var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; - var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; - - var topics = { - name: 'topics', - limit: 9999, - - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ - value: "No results", - label: "No results", - typeImageURL: Metamaps.Erb['icons/wildcard.png'], - rtype: "noresult" - }); - }, - header: topicheader, - suggestion: function(s) { - return Hogan.compile($('#topicSearchTemplate').html()).render(s); - }, - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/topics', - prepare: function(query, settings) { - settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitTopicsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); - } - return settings; - }, - }, - }), - }; - - var maps = { - name: 'maps', - limit: 9999, - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult" - }); - }, - header: mapheader, - suggestion: function(s) { - return Hogan.compile($('#mapSearchTemplate').html()).render(s); - }, - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/maps', - prepare: function(query, settings) { - settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitMapsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); - } - return settings; - }, - }, - }), - }; - - var mappers = { - name: 'mappers', - limit: 9999, - display: function(s) { return s.label; }, - templates: { - notFound: function(s) { - return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult", - profile: Metamaps.Erb['user.png'] - }); - }, - header: mapperheader, - suggestion: function(s) { - return Hogan.compile($('#mapperSearchTemplate').html()).render(s); - }, - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY', - }, - }), - }; - - // Take all that crazy setup data and put it together into one beautiful typeahead call! - $('.sidebarSearchField').typeahead( - { - highlight: true, - }, - [topics, maps, mappers] - ); - - //Set max height of the search results box to prevent it from covering bottom left footer - $('.sidebarSearchField').bind('typeahead:render', function (event) { - self.initSearchOptions(); - self.hideLoader(); - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); - if (self.limitTopicsToMe) { - $('#limitTopicsToMe').prop('checked', true); - } - if (self.limitMapsToMe) { - $('#limitMapsToMe').prop('checked', true); - } - }); - $(window).resize(function () { - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); - }); - - // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); - - // don't do it, if they clicked on a 'addToMap' button - $('.sidebarSearch button.addToMap').click(function (event) { - event.stopPropagation(); - }); - - // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function(){ - if ($('.sidebarSearchField.tt-input').val() === '') { - self.hideLoader(); - } else { - self.showLoader(); - } - }); - - }, - handleResultClick: function (event, datum, dataset) { - var self = Metamaps.GlobalUI.Search; - - self.hideLoader(); - - if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { - self.close(0, true); - var win; - if (datum.rtype == "topic") { - Metamaps.Router.topics(datum.id); - } else if (datum.rtype == "map") { - Metamaps.Router.maps(datum.id); - } else if (datum.rtype == "mapper") { - Metamaps.Router.explore("mapper", datum.id); - } - } - }, - initSearchOptions: function () { - var self = Metamaps.GlobalUI.Search; - - function toggleResultSet(set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); - if (s.is(':visible')) { - s.hide(); - $(this).removeClass('minimizeResults').addClass('maximizeResults'); - } else { - s.show(); - $(this).removeClass('maximizeResults').addClass('minimizeResults'); - } - } - - $('.limitToMe').unbind().bind("change", function (e) { - if ($(this).attr('id') == 'limitTopicsToMe') { - self.limitTopicsToMe = !self.limitTopicsToMe; - } - if ($(this).attr('id') == 'limitMapsToMe') { - self.limitMapsToMe = !self.limitMapsToMe; - } - - // set the value of the search equal to itself to retrigger the - // autocomplete event - var searchQuery = $('.sidebarSearchField.tt-input').val(); - $(".sidebarSearchField").typeahead('val', '') - .typeahead('val', searchQuery); - }); - - // when the user clicks minimize section, hide the results for that section - $('.minimizeMapperResults').unbind().click(function (e) { - toggleResultSet.call(this, 'mappers'); - }); - $('.minimizeTopicResults').unbind().click(function (e) { - toggleResultSet.call(this, 'topics'); - }); - $('.minimizeMapResults').unbind().click(function (e) { - toggleResultSet.call(this, 'maps'); - }); - }, - hideLoader: function () { - $('#searchLoading').hide(); - }, - showLoader: function () { - $('#searchLoading').show(); - } -}; diff --git a/app/assets/javascripts/src/Metamaps.Account.js b/frontend/src/Metamaps/Metamaps.Account.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Account.js rename to frontend/src/Metamaps/Metamaps.Account.js index a2286ad8..66348481 100644 --- a/app/assets/javascripts/src/Metamaps.Account.js +++ b/frontend/src/Metamaps/Metamaps.Account.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Admin.js b/frontend/src/Metamaps/Metamaps.Admin.js similarity index 97% rename from app/assets/javascripts/src/Metamaps.Admin.js rename to frontend/src/Metamaps/Metamaps.Admin.js index a0192012..8dcaed5b 100644 --- a/app/assets/javascripts/src/Metamaps.Admin.js +++ b/frontend/src/Metamaps/Metamaps.Admin.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.AutoLayout.js b/frontend/src/Metamaps/Metamaps.AutoLayout.js similarity index 97% rename from app/assets/javascripts/src/Metamaps.AutoLayout.js rename to frontend/src/Metamaps/Metamaps.AutoLayout.js index 51e105c2..2360204b 100644 --- a/app/assets/javascripts/src/Metamaps.AutoLayout.js +++ b/frontend/src/Metamaps/Metamaps.AutoLayout.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps */ /* diff --git a/app/assets/javascripts/src/Metamaps.Backbone.js b/frontend/src/Metamaps/Metamaps.Backbone.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Backbone.js rename to frontend/src/Metamaps/Metamaps.Backbone.js index 2c1f58af..3c991e0b 100644 --- a/app/assets/javascripts/src/Metamaps.Backbone.js +++ b/frontend/src/Metamaps/Metamaps.Backbone.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, _, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Control.js b/frontend/src/Metamaps/Metamaps.Control.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Control.js rename to frontend/src/Metamaps/Metamaps.Control.js index da6854c2..33623927 100644 --- a/app/assets/javascripts/src/Metamaps.Control.js +++ b/frontend/src/Metamaps/Metamaps.Control.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Create.js b/frontend/src/Metamaps/Metamaps.Create.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Create.js rename to frontend/src/Metamaps/Metamaps.Create.js index 6f3bbb62..1bd4216f 100644 --- a/app/assets/javascripts/src/Metamaps.Create.js +++ b/frontend/src/Metamaps/Metamaps.Create.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Debug.js b/frontend/src/Metamaps/Metamaps.Debug.js similarity index 73% rename from app/assets/javascripts/src/Metamaps.Debug.js rename to frontend/src/Metamaps/Metamaps.Debug.js index accd93a9..7bc71979 100644 --- a/app/assets/javascripts/src/Metamaps.Debug.js +++ b/frontend/src/Metamaps/Metamaps.Debug.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* * Metamaps.Debug.js.erb * @@ -10,4 +11,5 @@ Metamaps.Debug = function () { } Metamaps.debug = function () { Metamaps.Debug() +window.Metamaps = window.Metamaps || {} } diff --git a/app/assets/javascripts/src/Metamaps.Filter.js b/frontend/src/Metamaps/Metamaps.Filter.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Filter.js rename to frontend/src/Metamaps/Metamaps.Filter.js index 1dba099c..367918e6 100644 --- a/app/assets/javascripts/src/Metamaps.Filter.js +++ b/frontend/src/Metamaps/Metamaps.Filter.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/frontend/src/Metamaps/Metamaps.GlobalUI.js b/frontend/src/Metamaps/Metamaps.GlobalUI.js new file mode 100644 index 00000000..d5fe6caa --- /dev/null +++ b/frontend/src/Metamaps/Metamaps.GlobalUI.js @@ -0,0 +1,680 @@ +window.Metamaps = window.Metamaps || {}; + +Metamaps.Active = Metamaps.Active || { + Map: null, + Topic: null, + Mapper: null +}; +Metamaps.Maps = Metamaps.Maps || {} + +$(document).ready(function () { + // initialize all the modules + for (var prop in Metamaps) { + // this runs the init function within each sub-object on the Metamaps one + if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && + Metamaps[prop].hasOwnProperty('init') && + typeof (Metamaps[prop].init) == 'function' + ) { + Metamaps[prop].init() + } + } + // load whichever page you are on + if (Metamaps.currentSection === "explore") { + var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + + Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) + if (Metamaps.currentPage === "mapper") { + Metamaps.Views.exploreMaps.fetchUserThenRender() + } + else { + Metamaps.Views.exploreMaps.render() + } + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Metamaps.Views.exploreMaps.render() + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.Active.Map || Metamaps.Active.Topic) { + Metamaps.Loading.show() + Metamaps.JIT.prepareVizData() + Metamaps.GlobalUI.showDiv('#infovis') + } +}); + +Metamaps.GlobalUI = { + notifyTimeout: null, + lightbox: null, + init: function () { + var self = Metamaps.GlobalUI; + + self.Search.init(); + self.CreateMap.init(); + self.Account.init(); + + if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) + + //bind lightbox clicks + $('.openLightbox').click(function (event) { + self.openLightbox($(this).attr('data-open')); + event.preventDefault(); + return false; + }); + + $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); + + // initialize global backbone models and collections + if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); + + var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; + var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; + var mapperCollection = []; + var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; + if (Metamaps.Maps.Mapper) { + mapperCollection = Metamaps.Maps.Mapper.models; + mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; + } + var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; + var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; + Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); + Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); + // 'Mapper' refers to another mapper + Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); + Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); + Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); + }, + showDiv: function (selector) { + $(selector).show() + $(selector).animate({ + opacity: 1 + }, 200, 'easeOutCubic') + }, + hideDiv: function (selector) { + $(selector).animate({ + opacity: 0 + }, 200, 'easeInCubic', function () { $(this).hide() }) + }, + openLightbox: function (which) { + var self = Metamaps.GlobalUI; + + $('.lightboxContent').hide(); + $('#' + which).show(); + + self.lightbox = which; + + $('#lightbox_overlay').show(); + + var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; + // animate the content in from the bottom + $('#lightbox_main').animate({ + 'top': '50%', + 'margin-top': heightOfContent + }, 200, 'easeOutCubic'); + + // fade the black overlay in + $('#lightbox_screen').animate({ + 'opacity': '0.42' + }, 200); + + if (which == "switchMetacodes") { + Metamaps.Create.isSwitchingSet = true; + } + }, + + closeLightbox: function (event) { + var self = Metamaps.GlobalUI; + + if (event) event.preventDefault(); + + // animate the lightbox content offscreen + $('#lightbox_main').animate({ + 'top': '100%', + 'margin-top': '0' + }, 200, 'easeInCubic'); + + // fade the black overlay out + $('#lightbox_screen').animate({ + 'opacity': '0.0' + }, 200, function () { + $('#lightbox_overlay').hide(); + }); + + if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map'); + if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map'); + if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { + Metamaps.Create.cancelMetacodeSetSwitch(); + } + self.lightbox = null; + }, + notifyUser: function (message, leaveOpen) { + var self = Metamaps.GlobalUI; + + $('#toast').html(message) + self.showDiv('#toast') + clearTimeout(self.notifyTimeOut); + if (!leaveOpen) { + self.notifyTimeOut = setTimeout(function () { + self.hideDiv('#toast') + }, 8000); + } + }, + clearNotify: function() { + var self = Metamaps.GlobalUI; + + clearTimeout(self.notifyTimeOut); + self.hideDiv('#toast') + }, + shareInvite: function(inviteLink) { + window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + } +}; + +Metamaps.GlobalUI.CreateMap = { + newMap: null, + emptyMapForm: "", + emptyForkMapForm: "", + topicsToMap: [], + synapsesToMap: [], + init: function () { + var self = Metamaps.GlobalUI.CreateMap; + + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + + self.bindFormEvents(); + + self.emptyMapForm = $('#new_map').html(); + + }, + bindFormEvents: function () { + var self = Metamaps.GlobalUI.CreateMap; + + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { + if (event.keyCode === 13) self.submit() + }) + + $('.new_map button.cancel').unbind().bind('click', function (event) { + event.preventDefault(); + Metamaps.GlobalUI.closeLightbox(); + }); + $('.new_map button.submitMap').unbind().bind('click', self.submit); + + // bind permission changer events on the createMap form + $('.permIcon').unbind().bind('click', self.switchPermission); + }, + closeSuccess: function () { + $('#mapCreatedSuccess').fadeOut(300, function(){ + $(this).remove(); + }); + }, + generateSuccessMessage: function (id) { + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; + stringStart += id; + stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; + var page = Metamaps.Active.Map ? 'map' : 'page'; + var stringEnd = "</a></div>"; + return stringStart + page + stringEnd; + }, + switchPermission: function () { + var self = Metamaps.GlobalUI.CreateMap; + + self.newMap.set('permission', $(this).attr('data-permission')); + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); + $(this).find('.mapPermIcon').addClass('selected'); + + var permText = $(this).find('.tip').html(); + $(this).parents('.new_map').find('.permText').html(permText); + }, + submit: function (event) { + if (event) event.preventDefault(); + + var self = Metamaps.GlobalUI.CreateMap; + + if (Metamaps.GlobalUI.lightbox === 'forkmap') { + self.newMap.set('topicsToMap', self.topicsToMap); + self.newMap.set('synapsesToMap', self.synapsesToMap); + } + + var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); + + self.newMap.set('name', $form.find('#map_name').val()); + self.newMap.set('desc', $form.find('#map_desc').val()); + + if (self.newMap.get('name').length===0){ + self.throwMapNameError(); + return; + } + + self.newMap.save(null, { + success: self.success + // TODO add error message + }); + + Metamaps.GlobalUI.closeLightbox(); + Metamaps.GlobalUI.notifyUser('Working...'); + }, + throwMapNameError: function () { + var self = Metamaps.GlobalUI.CreateMap; + + var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); + + var message = $("<div class='feedback_message'>Please enter a map name...</div>"); + + $form.find('#map_name').after(message); + setTimeout(function(){ + message.fadeOut('fast', function(){ + message.remove(); + }); + }, 5000); + }, + success: function (model) { + var self = Metamaps.GlobalUI.CreateMap; + + //push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model); + + var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var form = $(formId); + + Metamaps.GlobalUI.clearNotify(); + $('#wrapper').append(self.generateSuccessMessage(model.id)); + + }, + reset: function (id) { + var self = Metamaps.GlobalUI.CreateMap; + + var form = $('#' + id); + + if (id === "fork_map") { + self.topicsToMap = []; + self.synapsesToMap = []; + form.html(self.emptyForkMapForm); + } + else { + form.html(self.emptyMapForm); + } + + self.bindFormEvents(); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + + return false; + }, +}; + + +Metamaps.GlobalUI.Account = { + isOpen: false, + changing: false, + init: function () { + var self = Metamaps.GlobalUI.Account; + + $('.sidebarAccountIcon').click(self.toggleBox); + $('.sidebarAccountBox').click(function(event){ + event.stopPropagation(); + }); + $('body').click(self.close); + }, + toggleBox: function (event) { + var self = Metamaps.GlobalUI.Account; + + if (self.isOpen) self.close(); + else self.open(); + + event.stopPropagation(); + }, + open: function () { + var self = Metamaps.GlobalUI.Account; + + Metamaps.Filter.close(); + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); + + + if (!self.isOpen && !self.changing) { + self.changing = true; + $('.sidebarAccountBox').fadeIn(200, function () { + self.changing = false; + self.isOpen = true; + $('.sidebarAccountBox #user_email').focus(); + }); + } + }, + close: function () { + var self = Metamaps.GlobalUI.Account; + + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); + if (!self.changing) { + self.changing = true; + $('.sidebarAccountBox #user_email').blur(); + $('.sidebarAccountBox').fadeOut(200, function () { + self.changing = false; + self.isOpen = false; + }); + } + } +}; + +Metamaps.GlobalUI.Search = { + locked: false, + isOpen: false, + limitTopicsToMe: false, + limitMapsToMe: false, + timeOut: null, + changing: false, + optionsInitialized: false, + init: function () { + var self = Metamaps.GlobalUI.Search; + + var loader = new CanvasLoader('searchLoading'); + loader.setColor('#4fb5c0'); // default is '#000000' + loader.setDiameter(24); // default is 40 + loader.setDensity(41); // default is 40 + loader.setRange(0.9); // default is 1.3 + loader.show(); // Hidden by default + + // bind the hover events + $(".sidebarSearch").hover(function () { + self.open() + }, function () { + self.close(800, false) + }); + + $('.sidebarSearchIcon').click(function (e) { + $('.sidebarSearchField').focus(); + }); + $('.sidebarSearch').click(function (e) { + e.stopPropagation(); + }); + $('body').click(function (e) { + self.close(0, false); + }); + + // open if the search is closed and user hits ctrl+/ + // close if they hit ESC + $('body').bind('keyup', function (e) { + switch (e.which) { + case 191: + if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { + self.open(true); // true for focus + } + break; + case 27: + if (self.isOpen) { + self.close(0, true); + } + break; + + default: + break; //console.log(e.which); + } + }); + + self.startTypeahead(); + }, + lock: function() { + var self = Metamaps.GlobalUI.Search; + self.locked = true; + }, + unlock: function() { + var self = Metamaps.GlobalUI.Search; + self.locked = false; + }, + open: function (focus) { + var self = Metamaps.GlobalUI.Search; + + clearTimeout(self.timeOut); + if (!self.isOpen && !self.changing && !self.locked) { + self.changing = true; + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '400px' + }, 300, function () { + if (focus) $('.sidebarSearchField').focus(); + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 10px 3px 10px', + width: '380px' + }); + self.changing = false; + self.isOpen = true; + }); + } + }, + close: function (closeAfter, bypass) { + // for now + return + + var self = Metamaps.GlobalUI.Search; + + self.timeOut = setTimeout(function () { + if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { + self.changing = true; + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 0 3px 0', + width: '400px' + }); + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '0' + }, 300, function () { + $('.sidebarSearchField').typeahead('val', ''); + $('.sidebarSearchField').blur(); + self.changing = false; + self.isOpen = false; + }); + } + }, closeAfter); + }, + startTypeahead: function () { + var self = Metamaps.GlobalUI.Search; + + var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; + var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; + var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; + + var topics = { + name: 'topics', + limit: 9999, + + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ + value: "No results", + label: "No results", + typeImageURL: Metamaps.Erb['icons/wildcard.png'], + rtype: "noresult" + }); + }, + header: topicheader, + suggestion: function(s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/topics', + prepare: function(query, settings) { + settings.url += '?term=' + query; + if (Metamaps.Active.Mapper && self.limitTopicsToMe) { + settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + } + return settings; + }, + }, + }), + }; + + var maps = { + name: 'maps', + limit: 9999, + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult" + }); + }, + header: mapheader, + suggestion: function(s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/maps', + prepare: function(query, settings) { + settings.url += '?term=' + query; + if (Metamaps.Active.Mapper && self.limitMapsToMe) { + settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + } + return settings; + }, + }, + }), + }; + + var mappers = { + name: 'mappers', + limit: 9999, + display: function(s) { return s.label; }, + templates: { + notFound: function(s) { + return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ + value: "No results", + label: "No results", + rtype: "noresult", + profile: Metamaps.Erb['user.png'] + }); + }, + header: mapperheader, + suggestion: function(s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s); + }, + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY', + }, + }), + }; + + // Take all that crazy setup data and put it together into one beautiful typeahead call! + $('.sidebarSearchField').typeahead( + { + highlight: true, + }, + [topics, maps, mappers] + ); + + //Set max height of the search results box to prevent it from covering bottom left footer + $('.sidebarSearchField').bind('typeahead:render', function (event) { + self.initSearchOptions(); + self.hideLoader(); + var h = $(window).height(); + $(".tt-dropdown-menu").css('max-height', h - 100); + if (self.limitTopicsToMe) { + $('#limitTopicsToMe').prop('checked', true); + } + if (self.limitMapsToMe) { + $('#limitMapsToMe').prop('checked', true); + } + }); + $(window).resize(function () { + var h = $(window).height(); + $(".tt-dropdown-menu").css('max-height', h - 100); + }); + + // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); + + // don't do it, if they clicked on a 'addToMap' button + $('.sidebarSearch button.addToMap').click(function (event) { + event.stopPropagation(); + }); + + // make sure that when you click on 'limit to me' or 'toggle section' it works + $('.sidebarSearchField.tt-input').keyup(function(){ + if ($('.sidebarSearchField.tt-input').val() === '') { + self.hideLoader(); + } else { + self.showLoader(); + } + }); + + }, + handleResultClick: function (event, datum, dataset) { + var self = Metamaps.GlobalUI.Search; + + self.hideLoader(); + + if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { + self.close(0, true); + var win; + if (datum.rtype == "topic") { + Metamaps.Router.topics(datum.id); + } else if (datum.rtype == "map") { + Metamaps.Router.maps(datum.id); + } else if (datum.rtype == "mapper") { + Metamaps.Router.explore("mapper", datum.id); + } + } + }, + initSearchOptions: function () { + var self = Metamaps.GlobalUI.Search; + + function toggleResultSet(set) { + var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); + if (s.is(':visible')) { + s.hide(); + $(this).removeClass('minimizeResults').addClass('maximizeResults'); + } else { + s.show(); + $(this).removeClass('maximizeResults').addClass('minimizeResults'); + } + } + + $('.limitToMe').unbind().bind("change", function (e) { + if ($(this).attr('id') == 'limitTopicsToMe') { + self.limitTopicsToMe = !self.limitTopicsToMe; + } + if ($(this).attr('id') == 'limitMapsToMe') { + self.limitMapsToMe = !self.limitMapsToMe; + } + + // set the value of the search equal to itself to retrigger the + // autocomplete event + var searchQuery = $('.sidebarSearchField.tt-input').val(); + $(".sidebarSearchField").typeahead('val', '') + .typeahead('val', searchQuery); + }); + + // when the user clicks minimize section, hide the results for that section + $('.minimizeMapperResults').unbind().click(function (e) { + toggleResultSet.call(this, 'mappers'); + }); + $('.minimizeTopicResults').unbind().click(function (e) { + toggleResultSet.call(this, 'topics'); + }); + $('.minimizeMapResults').unbind().click(function (e) { + toggleResultSet.call(this, 'maps'); + }); + }, + hideLoader: function () { + $('#searchLoading').hide(); + }, + showLoader: function () { + $('#searchLoading').show(); + } +} diff --git a/app/assets/javascripts/src/Metamaps.Import.js b/frontend/src/Metamaps/Metamaps.Import.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Import.js rename to frontend/src/Metamaps/Metamaps.Import.js index 2dee51d0..426071f0 100644 --- a/app/assets/javascripts/src/Metamaps.Import.js +++ b/frontend/src/Metamaps/Metamaps.Import.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.JIT.js b/frontend/src/Metamaps/Metamaps.JIT.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.JIT.js rename to frontend/src/Metamaps/Metamaps.JIT.js index d5e82081..cfdd921d 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js +++ b/frontend/src/Metamaps/Metamaps.JIT.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} Metamaps.JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', @@ -819,7 +820,7 @@ Metamaps.JIT = { } } // - temp = eventInfo.getNode() + let temp = eventInfo.getNode() if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned Metamaps.tempNode2 = temp diff --git a/app/assets/javascripts/src/Metamaps.Listeners.js b/frontend/src/Metamaps/Metamaps.Listeners.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Listeners.js rename to frontend/src/Metamaps/Metamaps.Listeners.js index 948893cb..e6c4e1b9 100644 --- a/app/assets/javascripts/src/Metamaps.Listeners.js +++ b/frontend/src/Metamaps/Metamaps.Listeners.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Map.js b/frontend/src/Metamaps/Metamaps.Map.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Map.js rename to frontend/src/Metamaps/Metamaps.Map.js index 264e3c48..e925a92c 100644 --- a/app/assets/javascripts/src/Metamaps.Map.js +++ b/frontend/src/Metamaps/Metamaps.Map.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Mapper.js b/frontend/src/Metamaps/Metamaps.Mapper.js similarity index 91% rename from app/assets/javascripts/src/Metamaps.Mapper.js rename to frontend/src/Metamaps/Metamaps.Mapper.js index 7d565479..f8a530b8 100644 --- a/app/assets/javascripts/src/Metamaps.Mapper.js +++ b/frontend/src/Metamaps/Metamaps.Mapper.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Mobile.js b/frontend/src/Metamaps/Metamaps.Mobile.js similarity index 95% rename from app/assets/javascripts/src/Metamaps.Mobile.js rename to frontend/src/Metamaps/Metamaps.Mobile.js index 1a55f081..fcd76b2f 100644 --- a/app/assets/javascripts/src/Metamaps.Mobile.js +++ b/frontend/src/Metamaps/Metamaps.Mobile.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Organize.js b/frontend/src/Metamaps/Metamaps.Organize.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Organize.js rename to frontend/src/Metamaps/Metamaps.Organize.js index b2463280..220cb83a 100644 --- a/app/assets/javascripts/src/Metamaps.Organize.js +++ b/frontend/src/Metamaps/Metamaps.Organize.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.PasteInput.js b/frontend/src/Metamaps/Metamaps.PasteInput.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.PasteInput.js rename to frontend/src/Metamaps/Metamaps.PasteInput.js index aaf848d0..3e933e41 100644 --- a/app/assets/javascripts/src/Metamaps.PasteInput.js +++ b/frontend/src/Metamaps/Metamaps.PasteInput.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/frontend/src/Metamaps/Metamaps.ReactComponents.js b/frontend/src/Metamaps/Metamaps.ReactComponents.js new file mode 100644 index 00000000..a1de0f40 --- /dev/null +++ b/frontend/src/Metamaps/Metamaps.ReactComponents.js @@ -0,0 +1,7 @@ +window.Metamaps = window.Metamaps || {} + +import Maps from '../components/Maps' + +Metamaps.ReactComponents = { + Maps +} diff --git a/app/assets/javascripts/src/Metamaps.Realtime.js b/frontend/src/Metamaps/Metamaps.Realtime.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Realtime.js rename to frontend/src/Metamaps/Metamaps.Realtime.js index 620a561a..0b62648f 100644 --- a/app/assets/javascripts/src/Metamaps.Realtime.js +++ b/frontend/src/Metamaps/Metamaps.Realtime.js @@ -1,3 +1,5 @@ +window.Metamaps = window.Metamaps || {} + /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Router.js b/frontend/src/Metamaps/Metamaps.Router.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Router.js rename to frontend/src/Metamaps/Metamaps.Router.js index 3ef30986..417c9b9e 100644 --- a/app/assets/javascripts/src/Metamaps.Router.js +++ b/frontend/src/Metamaps/Metamaps.Router.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Synapse.js b/frontend/src/Metamaps/Metamaps.Synapse.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Synapse.js rename to frontend/src/Metamaps/Metamaps.Synapse.js index ceed219d..20cf0f9c 100644 --- a/app/assets/javascripts/src/Metamaps.Synapse.js +++ b/frontend/src/Metamaps/Metamaps.Synapse.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.SynapseCard.js b/frontend/src/Metamaps/Metamaps.SynapseCard.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.SynapseCard.js rename to frontend/src/Metamaps/Metamaps.SynapseCard.js index f71601e5..aff207a9 100644 --- a/app/assets/javascripts/src/Metamaps.SynapseCard.js +++ b/frontend/src/Metamaps/Metamaps.SynapseCard.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Topic.js b/frontend/src/Metamaps/Metamaps.Topic.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Topic.js rename to frontend/src/Metamaps/Metamaps.Topic.js index a0ebfa82..faa8b336 100644 --- a/app/assets/javascripts/src/Metamaps.Topic.js +++ b/frontend/src/Metamaps/Metamaps.Topic.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.TopicCard.js b/frontend/src/Metamaps/Metamaps.TopicCard.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.TopicCard.js rename to frontend/src/Metamaps/Metamaps.TopicCard.js index 1453104d..fc007f3b 100644 --- a/app/assets/javascripts/src/Metamaps.TopicCard.js +++ b/frontend/src/Metamaps/Metamaps.TopicCard.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -421,18 +422,18 @@ Metamaps.TopicCard = { var inmapsLinks = topic.get('inmapsLinks') || [] nodeValues.inmaps = '' if (inmapsAr.length < 6) { - for (i = 0; i < inmapsAr.length; i++) { + for (let i = 0; i < inmapsAr.length; i++) { var url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>' } } else { - for (i = 0; i < 5; i++) { + for (let i = 0; i < 5; i++) { var url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '<li><a href="' + url + '">' + inmapsAr[i] + '</a></li>' } extra = inmapsAr.length - 5 nodeValues.inmaps += '<li><span class="showMore">See ' + extra + ' more...</span></li>' - for (i = 5; i < inmapsAr.length; i++) { + for (let i = 5; i < inmapsAr.length; i++) { var url = '/maps/' + inmapsLinks[i] nodeValues.inmaps += '<li class="hideExtra extraText"><a href="' + url + '">' + inmapsAr[i] + '</a></li>' } diff --git a/app/assets/javascripts/src/Metamaps.Util.js b/frontend/src/Metamaps/Metamaps.Util.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Util.js rename to frontend/src/Metamaps/Metamaps.Util.js index e150d3bb..9ff9c470 100644 --- a/app/assets/javascripts/src/Metamaps.Util.js +++ b/frontend/src/Metamaps/Metamaps.Util.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps */ /* diff --git a/app/assets/javascripts/src/Metamaps.Views.js b/frontend/src/Metamaps/Metamaps.Views.js similarity index 98% rename from app/assets/javascripts/src/Metamaps.Views.js rename to frontend/src/Metamaps/Metamaps.Views.js index d027d22c..eb5fdb7c 100644 --- a/app/assets/javascripts/src/Metamaps.Views.js +++ b/frontend/src/Metamaps/Metamaps.Views.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* diff --git a/app/assets/javascripts/src/Metamaps.Visualize.js b/frontend/src/Metamaps/Metamaps.Visualize.js similarity index 99% rename from app/assets/javascripts/src/Metamaps.Visualize.js rename to frontend/src/Metamaps/Metamaps.Visualize.js index 7168c03a..f5ce8c79 100644 --- a/app/assets/javascripts/src/Metamaps.Visualize.js +++ b/frontend/src/Metamaps/Metamaps.Visualize.js @@ -1,3 +1,4 @@ +window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* * Metamaps.Visualize diff --git a/app/assets/javascripts/src/Metamaps.js.erb b/frontend/src/Metamaps/index.js similarity index 60% rename from app/assets/javascripts/src/Metamaps.js.erb rename to frontend/src/Metamaps/index.js index 839d701e..bf54483d 100644 --- a/app/assets/javascripts/src/Metamaps.js.erb +++ b/frontend/src/Metamaps/index.js @@ -1,26 +1,10 @@ -/* global Metamaps */ - -/* - * Metamaps.js.erb - */ +window.Metamaps = window.Metamaps || {} // TODO eliminate these 5 top-level variables Metamaps.panningInt = null Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null -Metamaps.VERSION = '<%= METAMAPS_VERSION %>' - -/* erb variables from rails */ -Metamaps.Erb = {} -Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' -Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' -Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' -Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' -Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' -Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' -Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' -Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.Settings = { embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages @@ -65,10 +49,39 @@ Metamaps.Mouse = { Metamaps.Selected = { reset: function () { var self = Metamaps.Selected - self.Nodes = [] self.Edges = [] }, Nodes: [], Edges: [] } + +require('./Metamaps.Account') +require('./Metamaps.Admin') +require('./Metamaps.AutoLayout') +require('./Metamaps.Backbone') +require('./Metamaps.Control') +require('./Metamaps.Create') +require('./Metamaps.Debug') +require('./Metamaps.Filter') +require('./Metamaps.GlobalUI') +require('./Metamaps.Import') +require('./Metamaps.JIT') +require('./Metamaps.Listeners') +require('./Metamaps.Map') +require('./Metamaps.Mapper') +require('./Metamaps.Mobile') +require('./Metamaps.Organize') +require('./Metamaps.PasteInput') +require('./Metamaps.Realtime') +require('./Metamaps.Router') +require('./Metamaps.Synapse') +require('./Metamaps.SynapseCard') +require('./Metamaps.Topic') +require('./Metamaps.TopicCard') +require('./Metamaps.Util') +require('./Metamaps.Views') +require('./Metamaps.Visualize') +require('./Metamaps.ReactComponents') + +export default window.Metamaps diff --git a/frontend/src/index.js b/frontend/src/index.js index 0556f4c1..e5705512 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -2,17 +2,14 @@ import React from 'react' import ReactDOM from 'react-dom' import Backbone from 'backbone' import _ from 'underscore' -import Maps from './components/Maps.js' -// this is optional really, if we import components directly React will be -// in the bundle, so we won't need a global reference +import Metamaps from './Metamaps' + +// create global references to some libraries window.React = React window.ReactDOM = ReactDOM -Backbone.$ = window.$ +Backbone.$ = window.$ // jquery from rails window.Backbone = Backbone window._ = _ -window.Metamaps = window.Metamaps || {} -window.Metamaps.ReactComponents = { - Maps -} +window.Metamaps = Metamaps From d02c836805e36b0611872a04f481a759e902b58a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 14:35:14 +0800 Subject: [PATCH 020/306] remove Metamaps from filenames --- .../{Metamaps.Account.js => Account.js} | 0 .../Metamaps/{Metamaps.Admin.js => Admin.js} | 0 .../{Metamaps.AutoLayout.js => AutoLayout.js} | 0 .../{Metamaps.Backbone.js => Backbone.js} | 0 frontend/src/Metamaps/Constants.js | 57 +++++++++ .../{Metamaps.Control.js => Control.js} | 0 .../{Metamaps.Create.js => Create.js} | 0 .../Metamaps/{Metamaps.Debug.js => Debug.js} | 0 .../{Metamaps.Filter.js => Filter.js} | 0 .../{Metamaps.GlobalUI.js => GlobalUI.js} | 0 .../{Metamaps.Import.js => Import.js} | 0 .../src/Metamaps/{Metamaps.JIT.js => JIT.js} | 1 + .../{Metamaps.Listeners.js => Listeners.js} | 0 .../src/Metamaps/{Metamaps.Map.js => Map.js} | 0 .../{Metamaps.Mapper.js => Mapper.js} | 0 .../{Metamaps.Mobile.js => Mobile.js} | 0 .../{Metamaps.Organize.js => Organize.js} | 0 .../{Metamaps.PasteInput.js => PasteInput.js} | 0 ....ReactComponents.js => ReactComponents.js} | 0 .../{Metamaps.Realtime.js => Realtime.js} | 0 .../{Metamaps.Router.js => Router.js} | 0 .../{Metamaps.Synapse.js => Synapse.js} | 0 ...Metamaps.SynapseCard.js => SynapseCard.js} | 0 .../Metamaps/{Metamaps.Topic.js => Topic.js} | 0 .../{Metamaps.TopicCard.js => TopicCard.js} | 0 .../Metamaps/{Metamaps.Util.js => Util.js} | 0 .../Metamaps/{Metamaps.Views.js => Views.js} | 0 .../{Metamaps.Visualize.js => Visualize.js} | 0 frontend/src/Metamaps/index.js | 110 +++++------------- 29 files changed, 86 insertions(+), 82 deletions(-) rename frontend/src/Metamaps/{Metamaps.Account.js => Account.js} (100%) rename frontend/src/Metamaps/{Metamaps.Admin.js => Admin.js} (100%) rename frontend/src/Metamaps/{Metamaps.AutoLayout.js => AutoLayout.js} (100%) rename frontend/src/Metamaps/{Metamaps.Backbone.js => Backbone.js} (100%) create mode 100644 frontend/src/Metamaps/Constants.js rename frontend/src/Metamaps/{Metamaps.Control.js => Control.js} (100%) rename frontend/src/Metamaps/{Metamaps.Create.js => Create.js} (100%) rename frontend/src/Metamaps/{Metamaps.Debug.js => Debug.js} (100%) rename frontend/src/Metamaps/{Metamaps.Filter.js => Filter.js} (100%) rename frontend/src/Metamaps/{Metamaps.GlobalUI.js => GlobalUI.js} (100%) rename frontend/src/Metamaps/{Metamaps.Import.js => Import.js} (100%) rename frontend/src/Metamaps/{Metamaps.JIT.js => JIT.js} (99%) rename frontend/src/Metamaps/{Metamaps.Listeners.js => Listeners.js} (100%) rename frontend/src/Metamaps/{Metamaps.Map.js => Map.js} (100%) rename frontend/src/Metamaps/{Metamaps.Mapper.js => Mapper.js} (100%) rename frontend/src/Metamaps/{Metamaps.Mobile.js => Mobile.js} (100%) rename frontend/src/Metamaps/{Metamaps.Organize.js => Organize.js} (100%) rename frontend/src/Metamaps/{Metamaps.PasteInput.js => PasteInput.js} (100%) rename frontend/src/Metamaps/{Metamaps.ReactComponents.js => ReactComponents.js} (100%) rename frontend/src/Metamaps/{Metamaps.Realtime.js => Realtime.js} (100%) rename frontend/src/Metamaps/{Metamaps.Router.js => Router.js} (100%) rename frontend/src/Metamaps/{Metamaps.Synapse.js => Synapse.js} (100%) rename frontend/src/Metamaps/{Metamaps.SynapseCard.js => SynapseCard.js} (100%) rename frontend/src/Metamaps/{Metamaps.Topic.js => Topic.js} (100%) rename frontend/src/Metamaps/{Metamaps.TopicCard.js => TopicCard.js} (100%) rename frontend/src/Metamaps/{Metamaps.Util.js => Util.js} (100%) rename frontend/src/Metamaps/{Metamaps.Views.js => Views.js} (100%) rename frontend/src/Metamaps/{Metamaps.Visualize.js => Visualize.js} (100%) diff --git a/frontend/src/Metamaps/Metamaps.Account.js b/frontend/src/Metamaps/Account.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Account.js rename to frontend/src/Metamaps/Account.js diff --git a/frontend/src/Metamaps/Metamaps.Admin.js b/frontend/src/Metamaps/Admin.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Admin.js rename to frontend/src/Metamaps/Admin.js diff --git a/frontend/src/Metamaps/Metamaps.AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.AutoLayout.js rename to frontend/src/Metamaps/AutoLayout.js diff --git a/frontend/src/Metamaps/Metamaps.Backbone.js b/frontend/src/Metamaps/Backbone.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Backbone.js rename to frontend/src/Metamaps/Backbone.js diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js new file mode 100644 index 00000000..f56c463a --- /dev/null +++ b/frontend/src/Metamaps/Constants.js @@ -0,0 +1,57 @@ +window.Metamaps = window.Metamaps || {} + +// TODO eliminate these 5 top-level variables +Metamaps.panningInt = null +Metamaps.tempNode = null +Metamaps.tempInit = false +Metamaps.tempNode2 = null + +Metamaps.Settings = { + embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages + sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database + colors: { + background: '#344A58', + synapses: { + normal: '#888888', + hover: '#888888', + selected: '#FFFFFF' + }, + topics: { + selected: '#FFFFFF' + }, + labels: { + background: '#18202E', + text: '#DDD' + } + }, +} + +Metamaps.Touch = { + touchPos: null, // this stores the x and y values of a current touch event + touchDragNode: null // this stores a reference to a JIT node that is being dragged +} + +Metamaps.Mouse = { + didPan: false, + didBoxZoom: false, + changeInX: 0, + changeInY: 0, + edgeHoveringOver: false, + boxStartCoordinates: false, + boxEndCoordinates: false, + synapseStartCoordinates: [], + synapseEndCoordinates: null, + lastNodeClick: 0, + lastCanvasClick: 0, + DOUBLE_CLICK_TOLERANCE: 300 +} + +Metamaps.Selected = { + reset: function () { + var self = Metamaps.Selected + self.Nodes = [] + self.Edges = [] + }, + Nodes: [], + Edges: [] +} diff --git a/frontend/src/Metamaps/Metamaps.Control.js b/frontend/src/Metamaps/Control.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Control.js rename to frontend/src/Metamaps/Control.js diff --git a/frontend/src/Metamaps/Metamaps.Create.js b/frontend/src/Metamaps/Create.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Create.js rename to frontend/src/Metamaps/Create.js diff --git a/frontend/src/Metamaps/Metamaps.Debug.js b/frontend/src/Metamaps/Debug.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Debug.js rename to frontend/src/Metamaps/Debug.js diff --git a/frontend/src/Metamaps/Metamaps.Filter.js b/frontend/src/Metamaps/Filter.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Filter.js rename to frontend/src/Metamaps/Filter.js diff --git a/frontend/src/Metamaps/Metamaps.GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.GlobalUI.js rename to frontend/src/Metamaps/GlobalUI.js diff --git a/frontend/src/Metamaps/Metamaps.Import.js b/frontend/src/Metamaps/Import.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Import.js rename to frontend/src/Metamaps/Import.js diff --git a/frontend/src/Metamaps/Metamaps.JIT.js b/frontend/src/Metamaps/JIT.js similarity index 99% rename from frontend/src/Metamaps/Metamaps.JIT.js rename to frontend/src/Metamaps/JIT.js index cfdd921d..57ab60bf 100644 --- a/frontend/src/Metamaps/Metamaps.JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,5 @@ window.Metamaps = window.Metamaps || {} + Metamaps.JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', diff --git a/frontend/src/Metamaps/Metamaps.Listeners.js b/frontend/src/Metamaps/Listeners.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Listeners.js rename to frontend/src/Metamaps/Listeners.js diff --git a/frontend/src/Metamaps/Metamaps.Map.js b/frontend/src/Metamaps/Map.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Map.js rename to frontend/src/Metamaps/Map.js diff --git a/frontend/src/Metamaps/Metamaps.Mapper.js b/frontend/src/Metamaps/Mapper.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Mapper.js rename to frontend/src/Metamaps/Mapper.js diff --git a/frontend/src/Metamaps/Metamaps.Mobile.js b/frontend/src/Metamaps/Mobile.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Mobile.js rename to frontend/src/Metamaps/Mobile.js diff --git a/frontend/src/Metamaps/Metamaps.Organize.js b/frontend/src/Metamaps/Organize.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Organize.js rename to frontend/src/Metamaps/Organize.js diff --git a/frontend/src/Metamaps/Metamaps.PasteInput.js b/frontend/src/Metamaps/PasteInput.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.PasteInput.js rename to frontend/src/Metamaps/PasteInput.js diff --git a/frontend/src/Metamaps/Metamaps.ReactComponents.js b/frontend/src/Metamaps/ReactComponents.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.ReactComponents.js rename to frontend/src/Metamaps/ReactComponents.js diff --git a/frontend/src/Metamaps/Metamaps.Realtime.js b/frontend/src/Metamaps/Realtime.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Realtime.js rename to frontend/src/Metamaps/Realtime.js diff --git a/frontend/src/Metamaps/Metamaps.Router.js b/frontend/src/Metamaps/Router.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Router.js rename to frontend/src/Metamaps/Router.js diff --git a/frontend/src/Metamaps/Metamaps.Synapse.js b/frontend/src/Metamaps/Synapse.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Synapse.js rename to frontend/src/Metamaps/Synapse.js diff --git a/frontend/src/Metamaps/Metamaps.SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.SynapseCard.js rename to frontend/src/Metamaps/SynapseCard.js diff --git a/frontend/src/Metamaps/Metamaps.Topic.js b/frontend/src/Metamaps/Topic.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Topic.js rename to frontend/src/Metamaps/Topic.js diff --git a/frontend/src/Metamaps/Metamaps.TopicCard.js b/frontend/src/Metamaps/TopicCard.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.TopicCard.js rename to frontend/src/Metamaps/TopicCard.js diff --git a/frontend/src/Metamaps/Metamaps.Util.js b/frontend/src/Metamaps/Util.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Util.js rename to frontend/src/Metamaps/Util.js diff --git a/frontend/src/Metamaps/Metamaps.Views.js b/frontend/src/Metamaps/Views.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Views.js rename to frontend/src/Metamaps/Views.js diff --git a/frontend/src/Metamaps/Metamaps.Visualize.js b/frontend/src/Metamaps/Visualize.js similarity index 100% rename from frontend/src/Metamaps/Metamaps.Visualize.js rename to frontend/src/Metamaps/Visualize.js diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index bf54483d..ef50b564 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,87 +1,33 @@ window.Metamaps = window.Metamaps || {} -// TODO eliminate these 5 top-level variables -Metamaps.panningInt = null -Metamaps.tempNode = null -Metamaps.tempInit = false -Metamaps.tempNode2 = null +import './Constants' -Metamaps.Settings = { - embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages - sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database - colors: { - background: '#344A58', - synapses: { - normal: '#888888', - hover: '#888888', - selected: '#FFFFFF' - }, - topics: { - selected: '#FFFFFF' - }, - labels: { - background: '#18202E', - text: '#DDD' - } - }, -} - -Metamaps.Touch = { - touchPos: null, // this stores the x and y values of a current touch event - touchDragNode: null // this stores a reference to a JIT node that is being dragged -} - -Metamaps.Mouse = { - didPan: false, - didBoxZoom: false, - changeInX: 0, - changeInY: 0, - edgeHoveringOver: false, - boxStartCoordinates: false, - boxEndCoordinates: false, - synapseStartCoordinates: [], - synapseEndCoordinates: null, - lastNodeClick: 0, - lastCanvasClick: 0, - DOUBLE_CLICK_TOLERANCE: 300 -} - -Metamaps.Selected = { - reset: function () { - var self = Metamaps.Selected - self.Nodes = [] - self.Edges = [] - }, - Nodes: [], - Edges: [] -} - -require('./Metamaps.Account') -require('./Metamaps.Admin') -require('./Metamaps.AutoLayout') -require('./Metamaps.Backbone') -require('./Metamaps.Control') -require('./Metamaps.Create') -require('./Metamaps.Debug') -require('./Metamaps.Filter') -require('./Metamaps.GlobalUI') -require('./Metamaps.Import') -require('./Metamaps.JIT') -require('./Metamaps.Listeners') -require('./Metamaps.Map') -require('./Metamaps.Mapper') -require('./Metamaps.Mobile') -require('./Metamaps.Organize') -require('./Metamaps.PasteInput') -require('./Metamaps.Realtime') -require('./Metamaps.Router') -require('./Metamaps.Synapse') -require('./Metamaps.SynapseCard') -require('./Metamaps.Topic') -require('./Metamaps.TopicCard') -require('./Metamaps.Util') -require('./Metamaps.Views') -require('./Metamaps.Visualize') -require('./Metamaps.ReactComponents') +import './Account' +import './Admin' +import './AutoLayout' +import './Backbone' +import './Control' +import './Create' +import './Debug' +import './Filter' +import './GlobalUI' +import './Import' +import './JIT' +import './Listeners' +import './Map' +import './Mapper' +import './Mobile' +import './Organize' +import './PasteInput' +import './Realtime' +import './Router' +import './Synapse' +import './SynapseCard' +import './Topic' +import './TopicCard' +import './Util' +import './Views' +import './Visualize' +import './ReactComponents' export default window.Metamaps From 03446f548aad8f6956d4339f35a12dec2b88007f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 15:21:59 +0800 Subject: [PATCH 021/306] start making the code modular. many files still need global scape --- frontend/src/Metamaps/Account.js | 2 + frontend/src/Metamaps/Admin.js | 13 +- frontend/src/Metamaps/AutoLayout.js | 13 +- frontend/src/Metamaps/Backbone.js | 2 + frontend/src/Metamaps/Constants.js | 1 - frontend/src/Metamaps/Control.js | 44 +-- frontend/src/Metamaps/Create.js | 74 ++-- frontend/src/Metamaps/Debug.js | 19 +- frontend/src/Metamaps/Filter.js | 51 +-- frontend/src/Metamaps/GlobalUI.js | 2 + frontend/src/Metamaps/Import.js | 19 +- frontend/src/Metamaps/JIT.js | 92 ++--- frontend/src/Metamaps/Listeners.js | 7 +- frontend/src/Metamaps/Map.js | 3 + frontend/src/Metamaps/Mapper.js | 26 +- frontend/src/Metamaps/Mobile.js | 7 +- frontend/src/Metamaps/Organize.js | 8 +- frontend/src/Metamaps/PasteInput.js | 9 +- frontend/src/Metamaps/ReactComponents.js | 6 +- frontend/src/Metamaps/Realtime.js | 154 ++++---- frontend/src/Metamaps/Router.js | 430 +++++++++++------------ frontend/src/Metamaps/Synapse.js | 11 +- frontend/src/Metamaps/SynapseCard.js | 15 +- frontend/src/Metamaps/Topic.js | 7 +- frontend/src/Metamaps/TopicCard.js | 29 +- frontend/src/Metamaps/Util.js | 9 +- frontend/src/Metamaps/Views.js | 15 +- frontend/src/Metamaps/Visualize.js | 14 +- frontend/src/Metamaps/index.js | 82 +++-- 29 files changed, 598 insertions(+), 566 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 66348481..95a1a69f 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -121,3 +121,5 @@ Metamaps.Account = { $('#user_password_confirmation').val('') } } + +export default Metamaps.Account diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index 8dcaed5b..10cbc6d8 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -1,13 +1,6 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps, $ */ +/* global $ */ -/* - * Metamaps.Admin.js.erb - * - * Dependencies: none! - */ - -Metamaps.Admin = { +const Admin = { selectMetacodes: [], allMetacodes: [], init: function () { @@ -53,3 +46,5 @@ Metamaps.Admin = { } } } + +export default Admin diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index 2360204b..386b61ef 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -1,13 +1,4 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps */ - -/* - * Metmaaps.AutoLayout.js - * - * Dependencies: none! - */ - -Metamaps.AutoLayout = { +const AutoLayout = { nextX: 0, nextY: 0, sideLength: 1, @@ -74,3 +65,5 @@ Metamaps.AutoLayout = { self.turnCount = 0 } } + +export default AutoLayout diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone.js index 3c991e0b..ce62c6be 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone.js @@ -695,3 +695,5 @@ Metamaps.Backbone.init = function () { } self.attachCollectionEvents() }; // end Metamaps.Backbone.init + +export default Metamaps.Backbone diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index f56c463a..a79054e6 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -1,7 +1,6 @@ window.Metamaps = window.Metamaps || {} // TODO eliminate these 5 top-level variables -Metamaps.panningInt = null Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 33623927..b9df0d2c 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,12 +1,10 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* - * Metamaps.Control.js.erb + * Metamaps.Control.js * * Dependencies: * - Metamaps.Active - * - Metamaps.Control * - Metamaps.Filter * - Metamaps.GlobalUI * - Metamaps.JIT @@ -20,7 +18,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Control = { +const Control = { init: function () {}, selectNode: function (node, e) { var filtered = node.getData('alpha') === 0 @@ -34,7 +32,7 @@ Metamaps.Control = { var l = Metamaps.Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { var node = Metamaps.Selected.Nodes[i] - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) } Metamaps.Visualize.mGraph.plot() }, @@ -64,8 +62,8 @@ Metamaps.Control = { var r = confirm(text + 'Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.') if (r == true) { - Metamaps.Control.deleteSelectedEdges() - Metamaps.Control.deleteSelectedNodes() + Control.deleteSelectedEdges() + Control.deleteSelectedNodes() } }, deleteSelectedNodes: function () { // refers to deleting topics permanently @@ -81,7 +79,7 @@ Metamaps.Control = { var l = Metamaps.Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { var node = Metamaps.Selected.Nodes[i] - Metamaps.Control.deleteNode(node.id) + Control.deleteNode(node.id) } }, deleteNode: function (nodeid) { // refers to deleting topics permanently @@ -106,7 +104,7 @@ Metamaps.Control = { $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ mappableid: mappableid }]) - Metamaps.Control.hideNode(nodeid) + Control.hideNode(nodeid) } else { Metamaps.GlobalUI.notifyUser('Only topics you created can be deleted') } @@ -120,7 +118,7 @@ Metamaps.Control = { _.each(nodeids, function(nodeid) { if (Metamaps.Active.Topic.id !== nodeid) { Metamaps.Topics.remove(nodeid) - Metamaps.Control.hideNode(nodeid) + Control.hideNode(nodeid) } }) return @@ -139,7 +137,7 @@ Metamaps.Control = { for (i = l - 1; i >= 0; i -= 1) { node = Metamaps.Selected.Nodes[i] - Metamaps.Control.removeNode(node.id) + Control.removeNode(node.id) } }, removeNode: function (nodeid) { // refers to removing topics permanently from a map @@ -161,7 +159,7 @@ Metamaps.Control = { $(document).trigger(Metamaps.JIT.events.removeTopic, [{ mappableid: mappableid }]) - Metamaps.Control.hideNode(nodeid) + Control.hideNode(nodeid) }, hideSelectedNodes: function () { var l = Metamaps.Selected.Nodes.length, @@ -170,14 +168,14 @@ Metamaps.Control = { for (i = l - 1; i >= 0; i -= 1) { node = Metamaps.Selected.Nodes[i] - Metamaps.Control.hideNode(node.id) + Control.hideNode(node.id) } }, hideNode: function (nodeid) { var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) var graph = Metamaps.Visualize.mGraph - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) node.setData('alpha', 0, 'end') node.eachAdjacency(function (adj) { @@ -218,7 +216,7 @@ Metamaps.Control = { var l = Metamaps.Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { var edge = Metamaps.Selected.Edges[i] - Metamaps.Control.deselectEdge(edge) + Control.deselectEdge(edge) } Metamaps.Visualize.mGraph.plot() }, @@ -258,7 +256,7 @@ Metamaps.Control = { for (var i = l - 1; i >= 0; i -= 1) { edge = Metamaps.Selected.Edges[i] - Metamaps.Control.deleteEdge(edge) + Control.deleteEdge(edge) } }, deleteEdge: function (edge) { @@ -279,7 +277,7 @@ Metamaps.Control = { var permToDelete = Metamaps.Active.Mapper.id === synapse.get('user_id') || Metamaps.Active.Mapper.get('admin') if (permToDelete) { if (edge.getData('synapses').length - 1 === 0) { - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } var mappableid = synapse.id synapse.destroy() @@ -315,7 +313,7 @@ Metamaps.Control = { for (i = l - 1; i >= 0; i -= 1) { edge = Metamaps.Selected.Edges[i] - Metamaps.Control.removeEdge(edge) + Control.removeEdge(edge) } Metamaps.Selected.Edges = [ ] }, @@ -330,7 +328,7 @@ Metamaps.Control = { } if (edge.getData('mappings').length - 1 === 0) { - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 @@ -357,7 +355,7 @@ Metamaps.Control = { i for (i = l - 1; i >= 0; i -= 1) { edge = Metamaps.Selected.Edges[i] - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } Metamaps.Selected.Edges = [ ] }, @@ -365,7 +363,7 @@ Metamaps.Control = { var from = edge.nodeFrom.id var to = edge.nodeTo.id edge.setData('alpha', 0, 'end') - Metamaps.Control.deselectEdge(edge) + Control.deselectEdge(edge) Metamaps.Visualize.mGraph.fx.animate({ modes: ['edge-property:alpha'], duration: 500 @@ -449,4 +447,6 @@ Metamaps.Control = { Metamaps.GlobalUI.notifyUser(message) Metamaps.Visualize.mGraph.plot() }, -}; // end Metamaps.Control +} + +export default Control diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1bd4216f..1348e9d2 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -15,7 +15,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Create = { +const Create = { isSwitchingSet: false, // indicates whether the metacode set switch lightbox is open selectedMetacodeSet: null, selectedMetacodeSetIndex: null, @@ -24,7 +24,7 @@ Metamaps.Create = { selectedMetacodes: [], newSelectedMetacodes: [], init: function () { - var self = Metamaps.Create + var self = Create self.newTopic.init() self.newSynapse.init() @@ -37,7 +37,7 @@ Metamaps.Create = { $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab }, toggleMetacodeSelected: function () { - var self = Metamaps.Create + var self = Create if ($(this).attr('class') != 'toggledOff') { $(this).addClass('toggledOff') @@ -52,29 +52,29 @@ Metamaps.Create = { } }, updateMetacodeSet: function (set, index, custom) { - if (custom && Metamaps.Create.newSelectedMetacodes.length == 0) { + if (custom && Create.newSelectedMetacodes.length == 0) { alert('Please select at least one metacode to use!') return false } var codesToSwitchToIds var metacodeModels = new Metamaps.Backbone.MetacodeCollection() - Metamaps.Create.selectedMetacodeSetIndex = index - Metamaps.Create.selectedMetacodeSet = 'metacodeset-' + set + Create.selectedMetacodeSetIndex = index + Create.selectedMetacodeSet = 'metacodeset-' + set if (!custom) { codesToSwitchToIds = $('#metacodeSwitchTabs' + set).attr('data-metacodes').split(',') $('.customMetacodeList li').addClass('toggledOff') - Metamaps.Create.selectedMetacodes = [] - Metamaps.Create.selectedMetacodeNames = [] - Metamaps.Create.newSelectedMetacodes = [] - Metamaps.Create.newSelectedMetacodeNames = [] + Create.selectedMetacodes = [] + Create.selectedMetacodeNames = [] + Create.newSelectedMetacodes = [] + Create.newSelectedMetacodeNames = [] } else if (custom) { // uses .slice to avoid setting the two arrays to the same actual array - Metamaps.Create.selectedMetacodes = Metamaps.Create.newSelectedMetacodes.slice(0) - Metamaps.Create.selectedMetacodeNames = Metamaps.Create.newSelectedMetacodeNames.slice(0) - codesToSwitchToIds = Metamaps.Create.selectedMetacodes.slice(0) + Create.selectedMetacodes = Create.newSelectedMetacodes.slice(0) + Create.selectedMetacodeNames = Create.newSelectedMetacodeNames.slice(0) + codesToSwitchToIds = Create.selectedMetacodes.slice(0) } // sort by name @@ -106,7 +106,7 @@ Metamaps.Create = { var mdata = { 'metacodes': { - 'value': custom ? Metamaps.Create.selectedMetacodes.toString() : Metamaps.Create.selectedMetacodeSet + 'value': custom ? Create.selectedMetacodes.toString() : Create.selectedMetacodeSet } } $.ajax({ @@ -124,7 +124,7 @@ Metamaps.Create = { }, cancelMetacodeSetSwitch: function () { - var self = Metamaps.Create + var self = Create self.isSwitchingSet = false if (self.selectedMetacodeSet != 'metacodeset-custom') { @@ -149,17 +149,17 @@ Metamaps.Create = { newTopic: { init: function () { $('#topic_name').keyup(function () { - Metamaps.Create.newTopic.name = $(this).val() + Create.newTopic.name = $(this).val() }) $('.pinCarousel').click(function() { - if (Metamaps.Create.newTopic.pinned) { + if (Create.newTopic.pinned) { $('.pinCarousel').removeClass('isPinned') - Metamaps.Create.newTopic.pinned = false + Create.newTopic.pinned = false } else { $('.pinCarousel').addClass('isPinned') - Metamaps.Create.newTopic.pinned = true + Create.newTopic.pinned = true } }) @@ -221,24 +221,24 @@ Metamaps.Create = { $('#new_topic').fadeIn('fast', function () { $('#topic_name').focus() }) - Metamaps.Create.newTopic.beingCreated = true - Metamaps.Create.newTopic.name = '' + Create.newTopic.beingCreated = true + Create.newTopic.name = '' }, hide: function (force) { - if (force || !Metamaps.Create.newTopic.pinned) { + if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') - Metamaps.Create.newTopic.beingCreated = false + Create.newTopic.beingCreated = false } if (force) { $('.pinCarousel').removeClass('isPinned') - Metamaps.Create.newTopic.pinned = false + Create.newTopic.pinned = false } $('#topic_name').typeahead('val', '') } }, newSynapse: { init: function () { - var self = Metamaps.Create.newSynapse + var self = Create.newSynapse var synapseBloodhound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), @@ -254,7 +254,7 @@ Metamaps.Create = { remote: { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', prepare: function (query, settings) { - var self = Metamaps.Create.newSynapse + var self = Create.newSynapse if (Metamaps.Selected.Nodes.length < 2) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) return settings @@ -300,13 +300,13 @@ Metamaps.Create = { if (e.keyCode === BACKSPACE && $(this).val() === '' || e.keyCode === DELETE && $(this).val() === '' || e.keyCode === ESC) { - Metamaps.Create.newSynapse.hide() + Create.newSynapse.hide() } // if - Metamaps.Create.newSynapse.description = $(this).val() + Create.newSynapse.description = $(this).val() }) $('#synapse_desc').focusout(function () { - if (Metamaps.Create.newSynapse.beingCreated) { + if (Create.newSynapse.beingCreated) { Metamaps.Synapse.createSynapseLocally() } }) @@ -315,7 +315,7 @@ Metamaps.Create = { if (datum.id) { // if they clicked on an existing synapse get it Metamaps.Synapse.getSynapseFromAutocomplete(datum.id) } else { - Metamaps.Create.newSynapse.description = datum.value + Create.newSynapse.description = datum.value Metamaps.Synapse.createSynapseLocally() } }) @@ -329,17 +329,19 @@ Metamaps.Create = { $('#new_synapse').fadeIn(100, function () { $('#synapse_desc').focus() }) - Metamaps.Create.newSynapse.beingCreated = true + Create.newSynapse.beingCreated = true }, hide: function () { $('#new_synapse').fadeOut('fast') $('#synapse_desc').typeahead('val', '') - Metamaps.Create.newSynapse.beingCreated = false - Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newSynapse.topic1id = 0 - Metamaps.Create.newSynapse.topic2id = 0 + Create.newSynapse.beingCreated = false + Create.newTopic.addSynapse = false + Create.newSynapse.topic1id = 0 + Create.newSynapse.topic2id = 0 Metamaps.Mouse.synapseStartCoordinates = [] Metamaps.Visualize.mGraph.plot() }, } -}; // end Metamaps.Create +} + +export default Create diff --git a/frontend/src/Metamaps/Debug.js b/frontend/src/Metamaps/Debug.js index 7bc71979..e8e40e69 100644 --- a/frontend/src/Metamaps/Debug.js +++ b/frontend/src/Metamaps/Debug.js @@ -1,15 +1,6 @@ -window.Metamaps = window.Metamaps || {} -/* - * Metamaps.Debug.js.erb - * - * Dependencies: none! - */ +const Debug = () => { + console.debug(window.Metamaps) + console.debug(`Metamaps Version: ${window.Metamaps.VERSION}`) +} -Metamaps.Debug = function () { - console.debug(Metamaps) - console.debug('Metamaps Version: ' + Metamaps.VERSION) -} -Metamaps.debug = function () { - Metamaps.Debug() -window.Metamaps = window.Metamaps || {} -} +export default Debug diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 367918e6..cc21f7e2 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -16,7 +15,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Topics * - Metamaps.Visualize */ -Metamaps.Filter = { +const Filter = { filters: { name: '', metacodes: [], @@ -31,7 +30,7 @@ Metamaps.Filter = { isOpen: false, changing: false, init: function () { - var self = Metamaps.Filter + var self = Filter $('.sidebarFilterIcon').click(self.toggleBox) @@ -46,7 +45,7 @@ Metamaps.Filter = { self.getFilterData() }, toggleBox: function (event) { - var self = Metamaps.Filter + var self = Filter if (self.isOpen) self.close() else self.open() @@ -54,7 +53,7 @@ Metamaps.Filter = { event.stopPropagation() }, open: function () { - var self = Metamaps.Filter + var self = Filter Metamaps.GlobalUI.Account.close() $('.sidebarFilterIcon div').addClass('hide') @@ -70,7 +69,7 @@ Metamaps.Filter = { } }, close: function () { - var self = Metamaps.Filter + var self = Filter $('.sidebarFilterIcon div').removeClass('hide') if (!self.changing) { @@ -83,7 +82,7 @@ Metamaps.Filter = { } }, reset: function () { - var self = Metamaps.Filter + var self = Filter self.filters.metacodes = [] self.filters.mappers = [] @@ -103,7 +102,7 @@ Metamaps.Filter = { But what these function do is load this data into three accessible array within java : metacodes, mappers and synapses */ getFilterData: function () { - var self = Metamaps.Filter + var self = Filter var metacode, mapper, synapse @@ -126,7 +125,7 @@ Metamaps.Filter = { }) }, bindLiClicks: function () { - var self = Metamaps.Filter + var self = Filter $('#filter_by_metacode ul li').unbind().click(self.toggleMetacode) $('#filter_by_mapper ul li').unbind().click(self.toggleMapper) $('#filter_by_synapse ul li').unbind().click(self.toggleSynapse) @@ -137,7 +136,7 @@ Metamaps.Filter = { @param */ updateFilters: function (collection, propertyToCheck, correlatedModel, filtersToUse, listToModify) { - var self = Metamaps.Filter + var self = Filter var newList = [] var removed = [] @@ -212,11 +211,11 @@ Metamaps.Filter = { self.bindLiClicks() }, checkMetacodes: function () { - var self = Metamaps.Filter + var self = Filter self.updateFilters('Topics', 'metacode_id', 'Metacodes', 'metacodes', 'metacode') }, checkMappers: function () { - var self = Metamaps.Filter + var self = Filter var onMap = Metamaps.Active.Map ? true : false if (onMap) { self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper') @@ -226,11 +225,11 @@ Metamaps.Filter = { } }, checkSynapses: function () { - var self = Metamaps.Filter + var self = Filter self.updateFilters('Synapses', 'desc', 'Synapses', 'synapses', 'synapse') }, filterAllMetacodes: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_metacode ul li').addClass('toggledOff') $('.showAllMetacodes').removeClass('active') $('.hideAllMetacodes').addClass('active') @@ -238,7 +237,7 @@ Metamaps.Filter = { self.passFilters() }, filterNoMetacodes: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_metacode ul li').removeClass('toggledOff') $('.showAllMetacodes').addClass('active') $('.hideAllMetacodes').removeClass('active') @@ -246,7 +245,7 @@ Metamaps.Filter = { self.passFilters() }, filterAllMappers: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_mapper ul li').addClass('toggledOff') $('.showAllMappers').removeClass('active') $('.hideAllMappers').addClass('active') @@ -254,7 +253,7 @@ Metamaps.Filter = { self.passFilters() }, filterNoMappers: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_mapper ul li').removeClass('toggledOff') $('.showAllMappers').addClass('active') $('.hideAllMappers').removeClass('active') @@ -262,7 +261,7 @@ Metamaps.Filter = { self.passFilters() }, filterAllSynapses: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_synapse ul li').addClass('toggledOff') $('.showAllSynapses').removeClass('active') $('.hideAllSynapses').addClass('active') @@ -270,7 +269,7 @@ Metamaps.Filter = { self.passFilters() }, filterNoSynapses: function (e) { - var self = Metamaps.Filter + var self = Filter $('#filter_by_synapse ul li').removeClass('toggledOff') $('.showAllSynapses').addClass('active') $('.hideAllSynapses').removeClass('active') @@ -281,7 +280,7 @@ Metamaps.Filter = { // to reduce code redundancy // gets called in the context of a list item in a filter box toggleLi: function (whichToFilter) { - var self = Metamaps.Filter, index + var self = Filter, index var id = $(this).attr('data-id') if (self.visible[whichToFilter].indexOf(id) == -1) { self.visible[whichToFilter].push(id) @@ -294,7 +293,7 @@ Metamaps.Filter = { self.passFilters() }, toggleMetacode: function () { - var self = Metamaps.Filter + var self = Filter self.toggleLi.call(this, 'metacodes') if (self.visible.metacodes.length === self.filters.metacodes.length) { @@ -310,7 +309,7 @@ Metamaps.Filter = { } }, toggleMapper: function () { - var self = Metamaps.Filter + var self = Filter self.toggleLi.call(this, 'mappers') if (self.visible.mappers.length === self.filters.mappers.length) { @@ -326,7 +325,7 @@ Metamaps.Filter = { } }, toggleSynapse: function () { - var self = Metamaps.Filter + var self = Filter self.toggleLi.call(this, 'synapses') if (self.visible.synapses.length === self.filters.synapses.length) { @@ -342,7 +341,7 @@ Metamaps.Filter = { } }, passFilters: function () { - var self = Metamaps.Filter + var self = Filter var visible = self.visible var passesMetacode, passesMapper, passesSynapse @@ -464,4 +463,6 @@ Metamaps.Filter = { duration: 200 }) } -}; // end Metamaps.Filter +} + +export default Filter diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index d5fe6caa..e4522c2f 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -678,3 +678,5 @@ Metamaps.GlobalUI.Search = { $('#searchLoading').show(); } } + +export default Metamaps.GlobalUI diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 426071f0..e963ca32 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -14,7 +13,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Topics */ -Metamaps.Import = { +const Import = { // note that user is not imported topicWhitelist: [ 'id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission' @@ -25,19 +24,19 @@ Metamaps.Import = { cidMappings: {}, // to be filled by import_id => cid mappings handleTSV: function (text) { - var self = Metamaps.Import + var self = Import results = self.parseTabbedString(text) self.handle(results) }, handleJSON: function (text) { - var self = Metamaps.Import + var self = Import results = JSON.parse(text) self.handle(results) }, handle: function(results) { - var self = Metamaps.Import + var self = Import var topics = results.topics var synapses = results.synapses @@ -61,7 +60,7 @@ Metamaps.Import = { }, parseTabbedString: function (text) { - var self = Metamaps.Import + var self = Import // determine line ending and split lines var delim = '\n' @@ -187,7 +186,7 @@ Metamaps.Import = { }, importTopics: function (parsedTopics) { - var self = Metamaps.Import + var self = Import // up to 25 topics: scale 100 // up to 81 topics: scale 200 @@ -220,7 +219,7 @@ Metamaps.Import = { }, importSynapses: function (parsedSynapses) { - var self = Metamaps.Import + var self = Import parsedSynapses.forEach(function (synapse) { // only createSynapseWithParameters once both topics are persisted @@ -256,7 +255,7 @@ Metamaps.Import = { createTopicWithParameters: function (name, metacode_name, permission, desc, link, xloc, yloc, import_id, opts) { - var self = Metamaps.Import + var self = Import $(document).trigger(Metamaps.Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null if (metacode === null) { @@ -325,3 +324,5 @@ Metamaps.Import = { Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true) } } + +export default Import diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 57ab60bf..a93e3341 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,6 +1,8 @@ -window.Metamaps = window.Metamaps || {} +/* global Metamaps */ -Metamaps.JIT = { +let panningInt + +const JIT = { events: { topicDrag: 'Metamaps:JIT:events:topicDrag', newTopic: 'Metamaps:JIT:events:newTopic', @@ -18,7 +20,7 @@ Metamaps.JIT = { * This method will bind the event handlers it is interested and initialize the class. */ init: function () { - var self = Metamaps.JIT + var self = JIT $('.zoomIn').click(self.zoomIn) $('.zoomOut').click(self.zoomOut) @@ -94,7 +96,7 @@ Metamaps.JIT = { return [jitReady, synapsesToRemove] }, prepareVizData: function () { - var self = Metamaps.JIT + var self = JIT var mapping // reset/empty vizData @@ -148,7 +150,7 @@ Metamaps.JIT = { var color = Metamaps.Settings.colors.synapses.normal canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color } - Metamaps.JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) + JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) // check for edge label in data var desc = synapse.get('desc') @@ -253,7 +255,7 @@ Metamaps.JIT = { duration: 800, onComplete: function () { Metamaps.Visualize.mGraph.busy = false - $(document).trigger(Metamaps.JIT.events.animationDone) + $(document).trigger(JIT.events.animationDone) } }, animateFDLayout: { @@ -323,26 +325,26 @@ Metamaps.JIT = { enable: true, enableForEdges: true, onMouseMove: function (node, eventInfo, e) { - Metamaps.JIT.onMouseMoveHandler(node, eventInfo, e) + JIT.onMouseMoveHandler(node, eventInfo, e) // console.log('called mouse move handler') }, // Update node positions when dragged onDragMove: function (node, eventInfo, e) { - Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e) + JIT.onDragMoveTopicHandler(node, eventInfo, e) // console.log('called drag move handler') }, onDragEnd: function (node, eventInfo, e) { - Metamaps.JIT.onDragEndTopicHandler(node, eventInfo, e, false) + JIT.onDragEndTopicHandler(node, eventInfo, e, false) // console.log('called drag end handler') }, onDragCancel: function (node, eventInfo, e) { - Metamaps.JIT.onDragCancelHandler(node, eventInfo, e, false) + JIT.onDragCancelHandler(node, eventInfo, e, false) }, // Implement the same handler for touchscreens onTouchStart: function (node, eventInfo, e) {}, // Implement the same handler for touchscreens onTouchMove: function (node, eventInfo, e) { - Metamaps.JIT.onDragMoveTopicHandler(node, eventInfo, e) + JIT.onDragMoveTopicHandler(node, eventInfo, e) }, // Implement the same handler for touchscreens onTouchEnd: function (node, eventInfo, e) {}, @@ -361,7 +363,7 @@ Metamaps.JIT = { var bS = Metamaps.Mouse.boxStartCoordinates var bE = Metamaps.Mouse.boxEndCoordinates if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { - Metamaps.JIT.zoomToBox(e) + JIT.zoomToBox(e) return } else { Metamaps.Mouse.boxStartCoordinates = null @@ -373,7 +375,7 @@ Metamaps.JIT = { if (e.shiftKey) { Metamaps.Visualize.mGraph.busy = false Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() - Metamaps.JIT.selectWithBox(e) + JIT.selectWithBox(e) // console.log('called select with box') return } @@ -383,13 +385,13 @@ Metamaps.JIT = { // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { - Metamaps.JIT.selectEdgeOnClickHandler(node, e) + JIT.selectEdgeOnClickHandler(node, e) // console.log('called selectEdgeOnClickHandler') } else if (node && !node.nodeFrom) { - Metamaps.JIT.selectNodeOnClickHandler(node, e) + JIT.selectNodeOnClickHandler(node, e) // console.log('called selectNodeOnClickHandler') } else { - Metamaps.JIT.canvasClickHandler(eventInfo.getPos(), e) + JIT.canvasClickHandler(eventInfo.getPos(), e) // console.log('called canvasClickHandler') } // if }, @@ -401,7 +403,7 @@ Metamaps.JIT = { if (Metamaps.Mouse.boxStartCoordinates) { Metamaps.Visualize.mGraph.busy = false Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() - Metamaps.JIT.selectWithBox(e) + JIT.selectWithBox(e) return } @@ -409,9 +411,9 @@ Metamaps.JIT = { // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { - Metamaps.JIT.selectEdgeOnRightClickHandler(node, e) + JIT.selectEdgeOnRightClickHandler(node, e) } else if (node && !node.nodeFrom) { - Metamaps.JIT.selectNodeOnRightClickHandler(node, e) + JIT.selectNodeOnRightClickHandler(node, e) } else { // console.log('right clicked on open space') } @@ -455,7 +457,7 @@ Metamaps.JIT = { // if the topic has a link, draw a small image to indicate that var hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null - var linkImage = Metamaps.JIT.topicLinkImage + var linkImage = JIT.topicLinkImage var linkImageLoaded = linkImage.complete || (typeof linkImage.naturalWidth !== 'undefined' && linkImage.naturalWidth !== 0) @@ -465,7 +467,7 @@ Metamaps.JIT = { // if the topic has a desc, draw a small image to indicate that var hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null - var descImage = Metamaps.JIT.topicDescImage + var descImage = JIT.topicDescImage var descImageLoaded = descImage.complete || (typeof descImage.naturalWidth !== 'undefined' && descImage.naturalWidth !== 0) @@ -500,7 +502,7 @@ Metamaps.JIT = { edgeSettings: { 'customEdge': { 'render': function (adj, canvas) { - Metamaps.JIT.edgeRender(adj, canvas) + JIT.edgeRender(adj, canvas) }, 'contains': function (adj, pos) { var from = adj.nodeFrom.pos.getc(), @@ -667,7 +669,7 @@ Metamaps.JIT = { Metamaps.Visualize.mGraph.plot() }, // onMouseLeave onMouseMoveHandler: function (node, eventInfo, e) { - var self = Metamaps.JIT + var self = JIT if (Metamaps.Visualize.mGraph.busy) return @@ -721,7 +723,7 @@ Metamaps.JIT = { Metamaps.Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { - var self = Metamaps.JIT + var self = JIT // this is used to send nodes that are moving to // other realtime collaborators on the same map @@ -751,7 +753,7 @@ Metamaps.JIT = { // to be the same as on other collaborators // maps positionsToSend[topic.id] = pos - $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]) + $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } else { var len = Metamaps.Selected.Nodes.length @@ -782,7 +784,7 @@ Metamaps.JIT = { } // for if (Metamaps.Active.Map) { - $(document).trigger(Metamaps.JIT.events.topicDrag, [positionsToSend]) + $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } // if @@ -1201,7 +1203,7 @@ Metamaps.JIT = { selectNodeOnClickHandler: function (node, e) { if (Metamaps.Visualize.mGraph.busy) return - var self = Metamaps.JIT + var self = JIT // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { @@ -1222,7 +1224,7 @@ Metamaps.JIT = { } else { // wait a certain length of time, then check again, then run this code setTimeout(function () { - if (!Metamaps.JIT.nodeWasDoubleClicked()) { + if (!JIT.nodeWasDoubleClicked()) { var nodeAlreadySelected = node.selected if (!e.shiftKey) { @@ -1403,7 +1405,7 @@ Metamaps.JIT = { var fetch_sent = false $('.rc-siblings').hover(function () { if (!fetch_sent) { - Metamaps.JIT.populateRightClickSiblings(node) + JIT.populateRightClickSiblings(node) fetch_sent = true } }) @@ -1414,7 +1416,7 @@ Metamaps.JIT = { }) }, // selectNodeOnRightClickHandler, populateRightClickSiblings: function (node) { - var self = Metamaps.JIT + var self = JIT // depending on how many topics are selected, do different things @@ -1456,7 +1458,7 @@ Metamaps.JIT = { selectEdgeOnClickHandler: function (adj, e) { if (Metamaps.Visualize.mGraph.busy) return - var self = Metamaps.JIT + var self = JIT // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { @@ -1471,7 +1473,7 @@ Metamaps.JIT = { } else { // wait a certain length of time, then check again, then run this code setTimeout(function () { - if (!Metamaps.JIT.nodeWasDoubleClicked()) { + if (!JIT.nodeWasDoubleClicked()) { var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1 if (!e.shiftKey) { @@ -1611,17 +1613,17 @@ Metamaps.JIT = { easing = 1 // frictional value easing = 1 - window.clearInterval(Metamaps.panningInt) - Metamaps.panningInt = setInterval(function () { + window.clearInterval(panningInt) + panningInt = setInterval(function () { myTimer() }, 1) function myTimer () { Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) - $(document).trigger(Metamaps.JIT.events.pan) + $(document).trigger(JIT.events.pan) easing = easing * 0.75 - if (easing < 0.1) window.clearInterval(Metamaps.panningInt) + if (easing < 0.1) window.clearInterval(panningInt) } }, // SmoothPanning renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { @@ -1666,7 +1668,7 @@ Metamaps.JIT = { ctx.stroke() }, // renderMidArrow renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { - var self = Metamaps.JIT + var self = JIT var directionCat = synapse.get('category') var direction = synapse.getDirection() @@ -1720,11 +1722,11 @@ Metamaps.JIT = { }, // renderEdgeArrows zoomIn: function (event) { Metamaps.Visualize.mGraph.canvas.scale(1.25, 1.25) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) }, zoomOut: function (event) { Metamaps.Visualize.mGraph.canvas.scale(0.8, 0.8) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) }, centerMap: function (canvas) { var offsetScale = canvas.scaleOffsetX @@ -1743,7 +1745,7 @@ Metamaps.JIT = { eY = Metamaps.Mouse.boxEndCoordinates.y var canvas = Metamaps.Visualize.mGraph.canvas - Metamaps.JIT.centerMap(canvas) + JIT.centerMap(canvas) var height = $(document).height(), width = $(document).width() @@ -1770,14 +1772,14 @@ Metamaps.JIT = { var cogY = (sY + eY) / 2 canvas.translate(-1 * cogX, -1 * cogY) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) Metamaps.Mouse.boxStartCoordinates = false Metamaps.Mouse.boxEndCoordinates = false Metamaps.Visualize.mGraph.plot() }, zoomExtents: function (event, canvas, denySelected) { - Metamaps.JIT.centerMap(canvas) + JIT.centerMap(canvas) var height = canvas.getSize().height, width = canvas.getSize().width, maxX, minX, maxY, minY, counter = 0 @@ -1847,7 +1849,7 @@ Metamaps.JIT = { canvas.scale(scaleMultiplier, scaleMultiplier) } - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) } else if (nodes.length == 1) { nodes.forEach(function (n) { @@ -1855,8 +1857,10 @@ Metamaps.JIT = { y = n.pos.y canvas.translate(-1 * x, -1 * y) - $(document).trigger(Metamaps.JIT.events.zoom, [event]) + $(document).trigger(JIT.events.zoom, [event]) }) } } } + +export default JIT diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index e6c4e1b9..af244961 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -10,7 +9,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.JIT * - Metamaps.Visualize */ -Metamaps.Listeners = { +const Listeners = { init: function () { var self = this $(document).on('keydown', function (e) { @@ -120,4 +119,6 @@ Metamaps.Listeners = { Metamaps.Topic.fetchRelatives(nodes) } } -}; // end Metamaps.Listeners +} + +export default Listeners diff --git a/frontend/src/Metamaps/Map.js b/frontend/src/Metamaps/Map.js index e925a92c..690c2a6d 100644 --- a/frontend/src/Metamaps/Map.js +++ b/frontend/src/Metamaps/Map.js @@ -1,4 +1,5 @@ window.Metamaps = window.Metamaps || {} + /* global Metamaps, $ */ /* @@ -753,3 +754,5 @@ Metamaps.Map.InfoBox = { } } }; // end Metamaps.Map.InfoBox + +export default Metamaps.Map diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index f8a530b8..114d4f8c 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,21 +1,21 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps, $ */ +/* global Metamaps */ /* - * Metamaps.Mapper.js.erb - * - * Dependencies: none! + * Dependencies: + * - Metamaps.Backbone */ - -Metamaps.Mapper = { +const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve get: function (id, callback) { - return $.ajax({ - url: '/users/' + id + '.json', - success: function (data) { - callback(new Metamaps.Backbone.Mapper(data)) - } + return fetch(`/users/${id}.json`, { + }).then(response => { + if (!response.ok) throw response + return response.json() + }).then(payload => { + callback(new Metamaps.Backbone.Mapper(payload)) }) } -}; // end Metamaps.Mapper +} + +export default Mapper diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index fcd76b2f..e062ca45 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -9,7 +8,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Map */ -Metamaps.Mobile = { +const Mobile = { init: function () { var self = Metamaps.Mobile @@ -23,7 +22,7 @@ Metamaps.Mobile = { $('#header_content').width($(document).width() - 70) }, liClick: function () { - var self = Metamaps.Mobile + var self = Mobile $('#header_content').html($(this).text()) self.toggleMenu() }, @@ -36,3 +35,5 @@ Metamaps.Mobile = { } } } + +export default Mobile diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index 220cb83a..71905568 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -7,8 +6,7 @@ window.Metamaps = window.Metamaps || {} * Dependencies: * - Metamaps.Visualize */ -Metamaps.Organize = { - init: function () {}, +const Organize = { arrange: function (layout, centerNode) { // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin if (layout == 'grid') { @@ -115,4 +113,6 @@ Metamaps.Organize = { var newOriginY = (lowY + highY) / 2 } else alert('please call function with a valid layout dammit!') } -}; // end Metamaps.Organize +} + +export default Organize diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 3e933e41..9676e783 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -9,12 +8,12 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.AutoLayout */ -Metamaps.PasteInput = { +const PasteInput = { // thanks to https://github.com/kevva/url-regex URL_REGEX: new RegExp('^(?:(?:(?:[a-z]+:)?//)|www\.)(?:\S+(?::\S*)?@)?(?:localhost|(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,}))\.?)(?::\d{2,5})?(?:[/?#][^\s"]*)?$'), init: function () { - var self = Metamaps.PasteInput + var self = PasteInput // intercept dragged files // see http://stackoverflow.com/questions/6756583 @@ -59,7 +58,7 @@ Metamaps.PasteInput = { }, handle: function(text, coords) { - var self = Metamaps.PasteInput + var self = PasteInput if (text.match(self.URL_REGEX)) { self.handleURL(text, coords) @@ -109,3 +108,5 @@ Metamaps.PasteInput = { Metamaps.Import.handleTSV(text) } } + +export default PasteInput diff --git a/frontend/src/Metamaps/ReactComponents.js b/frontend/src/Metamaps/ReactComponents.js index a1de0f40..a2495245 100644 --- a/frontend/src/Metamaps/ReactComponents.js +++ b/frontend/src/Metamaps/ReactComponents.js @@ -1,7 +1,7 @@ -window.Metamaps = window.Metamaps || {} - import Maps from '../components/Maps' -Metamaps.ReactComponents = { +const ReactComponents = { Maps } + +export default ReactComponents diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 0b62648f..35d00f06 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,5 +1,3 @@ -window.Metamaps = window.Metamaps || {} - /* global Metamaps, $ */ /* @@ -26,7 +24,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Realtime = { +const Realtime = { videoId: 'video-wrapper', socket: null, webrtc: null, @@ -39,7 +37,7 @@ Metamaps.Realtime = { inConversation: false, localVideo: null, init: function () { - var self = Metamaps.Realtime + var self = Realtime self.addJuntoListeners() @@ -102,7 +100,7 @@ Metamaps.Realtime = { } // if Metamaps.Active.Mapper }, addJuntoListeners: function () { - var self = Metamaps.Realtime + var self = Realtime $(document).on(Metamaps.Views.chatView.events.openTray, function () { $('.main').addClass('compressed') @@ -128,7 +126,7 @@ Metamaps.Realtime = { }) }, handleVideoAdded: function (v, id) { - var self = Metamaps.Realtime + var self = Realtime self.positionVideos() v.setParent($('#wrapper')) v.$container.find('.video-cutoff').css({ @@ -137,7 +135,7 @@ Metamaps.Realtime = { $('#wrapper').append(v.$container) }, positionVideos: function () { - var self = Metamaps.Realtime + var self = Realtime var videoIds = Object.keys(self.room.videos) var numOfVideos = videoIds.length var numOfVideosToPosition = _.filter(videoIds, function (id) { @@ -169,7 +167,7 @@ Metamaps.Realtime = { } // do self first - var myVideo = Metamaps.Realtime.localVideo.view + var myVideo = Realtime.localVideo.view if (!myVideo.manuallyPositioned) { myVideo.$container.css({ top: yFormula() + 'px', @@ -187,7 +185,7 @@ Metamaps.Realtime = { }) }, startActiveMap: function () { - var self = Metamaps.Realtime + var self = Realtime if (Metamaps.Active.Map && Metamaps.Active.Mapper) { if (Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)) { @@ -200,7 +198,7 @@ Metamaps.Realtime = { } }, endActiveMap: function () { - var self = Metamaps.Realtime + var self = Realtime $(document).off('.map') self.socket.removeAllListeners() @@ -215,7 +213,7 @@ Metamaps.Realtime = { } }, turnOn: function (notify) { - var self = Metamaps.Realtime + var self = Realtime if (notify) self.sendRealtimeOn() self.status = true @@ -238,11 +236,11 @@ Metamaps.Realtime = { self.room.chat.addParticipant(self.activeMapper) }, checkForACallToJoin: function () { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }) }, promptToJoin: function () { - var self = Metamaps.Realtime + var self = Realtime var notifyText = "There's a conversation happening, want to join?" notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' @@ -251,7 +249,7 @@ Metamaps.Realtime = { self.room.conversationInProgress() }, conversationHasBegun: function () { - var self = Metamaps.Realtime + var self = Realtime if (self.inConversation) return var notifyText = "There's a conversation starting, want to join?" @@ -261,7 +259,7 @@ Metamaps.Realtime = { self.room.conversationInProgress() }, countOthersInConversation: function () { - var self = Metamaps.Realtime + var self = Realtime var count = 0 for (var key in self.mappersOnMap) { @@ -270,7 +268,7 @@ Metamaps.Realtime = { return count }, mapperJoinedCall: function (id) { - var self = Metamaps.Realtime + var self = Realtime var mapper = self.mappersOnMap[id] if (mapper) { @@ -285,7 +283,7 @@ Metamaps.Realtime = { } }, mapperLeftCall: function (id) { - var self = Metamaps.Realtime + var self = Realtime var mapper = self.mappersOnMap[id] if (mapper) { @@ -305,7 +303,7 @@ Metamaps.Realtime = { } }, callEnded: function () { - var self = Metamaps.Realtime + var self = Realtime self.room.conversationEnding() self.room.leaveVideoOnly() @@ -324,7 +322,7 @@ Metamaps.Realtime = { self.webrtc.webrtc.localStreams = [] }, invitedToCall: function (inviter) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.room.chat.sound.play('sessioninvite') @@ -337,7 +335,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(notifyText, true) }, invitedToJoin: function (inviter) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.room.chat.sound.play('sessioninvite') @@ -349,7 +347,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(notifyText, true) }, acceptCall: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callAccepted', { mapid: Metamaps.Active.Map.id, @@ -361,7 +359,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, denyCall: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callDenied', { mapid: Metamaps.Active.Map.id, @@ -371,7 +369,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, denyInvite: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('inviteDenied', { mapid: Metamaps.Active.Map.id, @@ -381,7 +379,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, inviteACall: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('inviteACall', { mapid: Metamaps.Active.Map.id, inviter: Metamaps.Active.Mapper.id, @@ -391,7 +389,7 @@ Metamaps.Realtime = { Metamaps.GlobalUI.clearNotify() }, inviteToJoin: function (userid) { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('inviteToJoin', { mapid: Metamaps.Active.Map.id, inviter: Metamaps.Active.Mapper.id, @@ -400,7 +398,7 @@ Metamaps.Realtime = { self.room.chat.invitationPending(userid) }, callAccepted: function (userid) { - var self = Metamaps.Realtime + var self = Realtime var username = self.mappersOnMap[userid].name Metamaps.GlobalUI.notifyUser('Conversation starting...') @@ -408,21 +406,21 @@ Metamaps.Realtime = { self.room.chat.invitationAnswered(userid) }, callDenied: function (userid) { - var self = Metamaps.Realtime + var self = Realtime var username = self.mappersOnMap[userid].name Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, inviteDenied: function (userid) { - var self = Metamaps.Realtime + var self = Realtime var username = self.mappersOnMap[userid].name Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, joinCall: function () { - var self = Metamaps.Realtime + var self = Realtime self.webrtc.off('readyToCall') self.webrtc.once('readyToCall', function () { @@ -446,7 +444,7 @@ Metamaps.Realtime = { self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id) }, leaveCall: function () { - var self = Metamaps.Realtime + var self = Realtime self.socket.emit('mapperLeftCall', { mapid: Metamaps.Active.Map.id, @@ -465,7 +463,7 @@ Metamaps.Realtime = { } }, turnOff: function (silent) { - var self = Metamaps.Realtime + var self = Realtime if (self.status) { if (!silent) self.sendRealtimeOff() @@ -479,8 +477,8 @@ Metamaps.Realtime = { } }, setupSocket: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var myId = Metamaps.Active.Mapper.id socket.emit('newMapperNotify', { @@ -614,14 +612,14 @@ Metamaps.Realtime = { $(document).on(Metamaps.Views.room.events.newMessage + '.map', sendNewMessage) }, attachMapListener: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket socket.on('mapChangeFromServer', self.mapChange) }, sendRealtimeOn: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // send this new mapper back your details, and the awareness that you're online var update = { @@ -632,8 +630,8 @@ Metamaps.Realtime = { socket.emit('notifyStartRealtime', update) }, sendRealtimeOff: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // send this new mapper back your details, and the awareness that you're online var update = { @@ -644,8 +642,8 @@ Metamaps.Realtime = { socket.emit('notifyStopRealtime', update) }, updateMapperList: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -675,8 +673,8 @@ Metamaps.Realtime = { } }, newPeerOnMap: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -743,8 +741,8 @@ Metamaps.Realtime = { }) }, lostPeerOnMap: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -763,8 +761,8 @@ Metamaps.Realtime = { } }, newCollaborator: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -777,8 +775,8 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime') }, lostCollaborator: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket // data.userid // data.username @@ -791,15 +789,15 @@ Metamaps.Realtime = { Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime') }, updatePeerCoords: function (data) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket self.mappersOnMap[data.userid].coords = {x: data.usercoords.x,y: data.usercoords.y} self.positionPeerIcon(data.userid) }, positionPeerIcons: function () { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket if (self.status) { // if i have realtime turned on for (var key in self.mappersOnMap) { @@ -811,8 +809,8 @@ Metamaps.Realtime = { } }, positionPeerIcon: function (id) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var boundary = self.chatOpen ? '#wrapper' : document var mapper = self.mappersOnMap[id] @@ -848,8 +846,8 @@ Metamaps.Realtime = { } }, limitPixelsToScreen: function (pixels) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var boundary = self.chatOpen ? '#wrapper' : document var xLimit, yLimit @@ -866,8 +864,8 @@ Metamaps.Realtime = { return {x: xLimit,y: yLimit} }, sendCoords: function (coords) { - var self = Metamaps.Realtime - var socket = Metamaps.Realtime.socket + var self = Realtime + var socket = Realtime.socket var map = Metamaps.Active.Map var mapper = Metamaps.Active.Mapper @@ -882,7 +880,7 @@ Metamaps.Realtime = { } }, sendTopicDrag: function (positions) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map && self.status) { @@ -891,7 +889,7 @@ Metamaps.Realtime = { } }, topicDrag: function (positions) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var topic @@ -907,7 +905,7 @@ Metamaps.Realtime = { } }, sendTopicChange: function (topic) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var data = { @@ -929,7 +927,7 @@ Metamaps.Realtime = { } }, sendSynapseChange: function (synapse) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var data = { @@ -952,7 +950,7 @@ Metamaps.Realtime = { } }, sendMapChange: function (map) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var data = { @@ -989,7 +987,7 @@ Metamaps.Realtime = { }, // newMessage sendNewMessage: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket var message = data.attributes @@ -997,14 +995,14 @@ Metamaps.Realtime = { socket.emit('newMessage', message) }, newMessage: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket self.room.addMessages(new Metamaps.Backbone.MessageCollection(data)) }, // newTopic sendNewTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map && self.status) { @@ -1016,7 +1014,7 @@ Metamaps.Realtime = { newTopic: function (data) { var topic, mapping, mapper, mapperCallback, cancel - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1063,7 +1061,7 @@ Metamaps.Realtime = { }, // removeTopic sendDeleteTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1072,7 +1070,7 @@ Metamaps.Realtime = { }, // removeTopic sendRemoveTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1081,7 +1079,7 @@ Metamaps.Realtime = { } }, removeTopic: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1097,7 +1095,7 @@ Metamaps.Realtime = { }, // newSynapse sendNewSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1109,7 +1107,7 @@ Metamaps.Realtime = { newSynapse: function (data) { var topic1, topic2, node1, node2, synapse, mapping, cancel - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1160,7 +1158,7 @@ Metamaps.Realtime = { }, // deleteSynapse sendDeleteSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1170,7 +1168,7 @@ Metamaps.Realtime = { }, // removeSynapse sendRemoveSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (Metamaps.Active.Map) { @@ -1179,7 +1177,7 @@ Metamaps.Realtime = { } }, removeSynapse: function (data) { - var self = Metamaps.Realtime + var self = Realtime var socket = self.socket if (!self.status) return @@ -1202,4 +1200,6 @@ Metamaps.Realtime = { Metamaps.Mappings.remove(mapping) } }, -}; // end Metamaps.Realtime +} + +export default Realtime diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 417c9b9e..8aacadd1 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -16,231 +16,231 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -;(function () { - var Router = Backbone.Router.extend({ - routes: { - '': 'home', // #home - 'explore/:section': 'explore', // #explore/active - 'explore/:section/:id': 'explore', // #explore/mapper/1234 - 'maps/:id': 'maps' // #maps/7 - }, - home: function () { - clearTimeout(Metamaps.Router.timeoutId) +const _Router = Backbone.Router.extend({ + routes: { + '': 'home', // #home + 'explore/:section': 'explore', // #explore/active + 'explore/:section/:id': 'explore', // #explore/mapper/1234 + 'maps/:id': 'maps' // #maps/7 + }, + home: function () { + clearTimeout(Metamaps.Router.timeoutId) - if (Metamaps.Active.Mapper) document.title = 'Explore Active Maps | Metamaps' - else document.title = 'Home | Metamaps' + if (Metamaps.Active.Mapper) document.title = 'Explore Active Maps | Metamaps' + else document.title = 'Home | Metamaps' - Metamaps.Router.currentSection = '' - Metamaps.Router.currentPage = '' - $('.wrapper').removeClass('mapPage topicPage') + Metamaps.Router.currentSection = '' + Metamaps.Router.currentPage = '' + $('.wrapper').removeClass('mapPage topicPage') - var classes = Metamaps.Active.Mapper ? 'homePage explorePage' : 'homePage' - $('.wrapper').addClass(classes) + var classes = Metamaps.Active.Mapper ? 'homePage explorePage' : 'homePage' + $('.wrapper').addClass(classes) - var navigate = function () { - Metamaps.Router.timeoutId = setTimeout(function () { - Metamaps.Router.navigate('') - }, 300) - } + var navigate = function () { + Metamaps.Router.timeoutId = setTimeout(function () { + Metamaps.Router.navigate('') + }, 300) + } - // all this only for the logged in home page - if (Metamaps.Active.Mapper) { - $('.homeButton a').attr('href', '/') - Metamaps.GlobalUI.hideDiv('#yield') - - Metamaps.GlobalUI.showDiv('#explore') - - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - if (Metamaps.Maps.Active.length === 0) { - Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render - } else { - Metamaps.Views.exploreMaps.render(navigate) - } - } else { - // logged out home page - Metamaps.GlobalUI.hideDiv('#explore') - Metamaps.GlobalUI.showDiv('#yield') - Metamaps.Router.timeoutId = setTimeout(navigate, 500) - } - - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null - }, - explore: function (section, id) { - clearTimeout(Metamaps.Router.timeoutId) - - // just capitalize the variable section - // either 'featured', 'mapper', or 'active' - var capitalize = section.charAt(0).toUpperCase() + section.slice(1) - - if (section === 'shared' || section === 'featured' || section === 'active' || section === 'starred') { - document.title = 'Explore ' + capitalize + ' Maps | Metamaps' - } else if (section === 'mapper') { - $.ajax({ - url: '/users/' + id + '.json', - success: function (response) { - document.title = response.name + ' | Metamaps' - }, - error: function () {} - }) - } else if (section === 'mine') { - document.title = 'Explore My Maps | Metamaps' - } - - if (Metamaps.Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) - $('.wrapper').removeClass('homePage mapPage topicPage') - $('.wrapper').addClass('explorePage') - - Metamaps.Router.currentSection = 'explore' - Metamaps.Router.currentPage = section - - // this will mean it's a mapper page being loaded - if (id) { - if (Metamaps.Maps.Mapper.mapperId !== id) { - // empty the collection if we are trying to load the maps - // collection of a different mapper than we had previously - Metamaps.Maps.Mapper.reset() - Metamaps.Maps.Mapper.page = 1 - } - Metamaps.Maps.Mapper.mapperId = id - } - - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) - - var navigate = function () { - var path = '/explore/' + Metamaps.Router.currentPage - - // alter url if for mapper profile page - if (Metamaps.Router.currentPage === 'mapper') { - path += '/' + Metamaps.Maps.Mapper.mapperId - } - - Metamaps.Router.navigate(path) - } - var navigateTimeout = function () { - Metamaps.Router.timeoutId = setTimeout(navigate, 300) - } - if (Metamaps.Maps[capitalize].length === 0) { - Metamaps.Loading.show() - setTimeout(function () { - Metamaps.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render - }, 300) // wait 300 milliseconds till the other animations are done to do the fetch - } else { - if (id) { - Metamaps.Views.exploreMaps.fetchUserThenRender(navigateTimeout) - } else { - Metamaps.Views.exploreMaps.render(navigateTimeout) - } - } + // all this only for the logged in home page + if (Metamaps.Active.Mapper) { + $('.homeButton a').attr('href', '/') + Metamaps.GlobalUI.hideDiv('#yield') Metamaps.GlobalUI.showDiv('#explore') - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null - }, - maps: function (id) { - clearTimeout(Metamaps.Router.timeoutId) - document.title = 'Map ' + id + ' | Metamaps' - - Metamaps.Router.currentSection = 'map' - Metamaps.Router.currentPage = id - - $('.wrapper').removeClass('homePage explorePage topicPage') - $('.wrapper').addClass('mapPage') - // another class will be added to wrapper if you - // can edit this map '.canEditMap' - - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') - - // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) - } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Topic.end() - Metamaps.Active.Topic = null - - Metamaps.Loading.show() - Metamaps.Map.end() - Metamaps.Map.launch(id) - }, - topics: function (id) { - clearTimeout(Metamaps.Router.timeoutId) - - document.title = 'Topic ' + id + ' | Metamaps' - - Metamaps.Router.currentSection = 'topic' - Metamaps.Router.currentPage = id - - $('.wrapper').removeClass('homePage explorePage mapPage') - $('.wrapper').addClass('topicPage') - - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') - - // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) - } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Map.end() - Metamaps.Active.Map = null - - Metamaps.Topic.end() - Metamaps.Topic.launch(id) - } - }) - - Metamaps.Router = new Router() - Metamaps.Router.currentPage = '' - Metamaps.Router.currentSection = undefined - Metamaps.Router.timeoutId = undefined - - Metamaps.Router.intercept = function (evt) { - var segments - - var href = { - prop: $(this).prop('href'), - attr: $(this).attr('href') - } - var root = window.location.protocol + '//' + window.location.host + Backbone.history.options.root - - if (href.prop && href.prop === root) href.attr = '' - - if (href.prop && href.prop.slice(0, root.length) === root) { - evt.preventDefault() - - segments = href.attr.split('/') - segments.splice(0, 1) // pop off the element created by the first / - - if (href.attr === '') { - Metamaps.Router.home() + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + if (Metamaps.Maps.Active.length === 0) { + Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { - Metamaps.Router[segments[0]](segments[1], segments[2]) + Metamaps.Views.exploreMaps.render(navigate) + } + } else { + // logged out home page + Metamaps.GlobalUI.hideDiv('#explore') + Metamaps.GlobalUI.showDiv('#yield') + Metamaps.Router.timeoutId = setTimeout(navigate, 500) + } + + Metamaps.GlobalUI.hideDiv('#infovis') + Metamaps.GlobalUI.hideDiv('#instructions') + Metamaps.Map.end() + Metamaps.Topic.end() + Metamaps.Active.Map = null + Metamaps.Active.Topic = null + }, + explore: function (section, id) { + clearTimeout(Metamaps.Router.timeoutId) + + // just capitalize the variable section + // either 'featured', 'mapper', or 'active' + var capitalize = section.charAt(0).toUpperCase() + section.slice(1) + + if (section === 'shared' || section === 'featured' || section === 'active' || section === 'starred') { + document.title = 'Explore ' + capitalize + ' Maps | Metamaps' + } else if (section === 'mapper') { + $.ajax({ + url: '/users/' + id + '.json', + success: function (response) { + document.title = response.name + ' | Metamaps' + }, + error: function () {} + }) + } else if (section === 'mine') { + document.title = 'Explore My Maps | Metamaps' + } + + if (Metamaps.Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) + $('.wrapper').removeClass('homePage mapPage topicPage') + $('.wrapper').addClass('explorePage') + + Metamaps.Router.currentSection = 'explore' + Metamaps.Router.currentPage = section + + // this will mean it's a mapper page being loaded + if (id) { + if (Metamaps.Maps.Mapper.mapperId !== id) { + // empty the collection if we are trying to load the maps + // collection of a different mapper than we had previously + Metamaps.Maps.Mapper.reset() + Metamaps.Maps.Mapper.page = 1 + } + Metamaps.Maps.Mapper.mapperId = id + } + + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) + + var navigate = function () { + var path = '/explore/' + Metamaps.Router.currentPage + + // alter url if for mapper profile page + if (Metamaps.Router.currentPage === 'mapper') { + path += '/' + Metamaps.Maps.Mapper.mapperId + } + + Metamaps.Router.navigate(path) + } + var navigateTimeout = function () { + Metamaps.Router.timeoutId = setTimeout(navigate, 300) + } + if (Metamaps.Maps[capitalize].length === 0) { + Metamaps.Loading.show() + setTimeout(function () { + Metamaps.Maps[capitalize].getMaps(navigate) // this will trigger an explore maps render + }, 300) // wait 300 milliseconds till the other animations are done to do the fetch + } else { + if (id) { + Metamaps.Views.exploreMaps.fetchUserThenRender(navigateTimeout) + } else { + Metamaps.Views.exploreMaps.render(navigateTimeout) } } - } - Metamaps.Router.init = function () { - Backbone.history.start({ - silent: true, - pushState: true, - root: '/' - }) - $(document).on('click', 'a[data-router="true"]', Metamaps.Router.intercept) + Metamaps.GlobalUI.showDiv('#explore') + Metamaps.GlobalUI.hideDiv('#yield') + Metamaps.GlobalUI.hideDiv('#infovis') + Metamaps.GlobalUI.hideDiv('#instructions') + Metamaps.Map.end() + Metamaps.Topic.end() + Metamaps.Active.Map = null + Metamaps.Active.Topic = null + }, + maps: function (id) { + clearTimeout(Metamaps.Router.timeoutId) + + document.title = 'Map ' + id + ' | Metamaps' + + Metamaps.Router.currentSection = 'map' + Metamaps.Router.currentPage = id + + $('.wrapper').removeClass('homePage explorePage topicPage') + $('.wrapper').addClass('mapPage') + // another class will be added to wrapper if you + // can edit this map '.canEditMap' + + Metamaps.GlobalUI.hideDiv('#yield') + Metamaps.GlobalUI.hideDiv('#explore') + + // clear the visualization, if there was one, before showing its div again + if (Metamaps.Visualize.mGraph) { + Metamaps.Visualize.mGraph.graph.empty() + Metamaps.Visualize.mGraph.plot() + Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + } + Metamaps.GlobalUI.showDiv('#infovis') + Metamaps.Topic.end() + Metamaps.Active.Topic = null + + Metamaps.Loading.show() + Metamaps.Map.end() + Metamaps.Map.launch(id) + }, + topics: function (id) { + clearTimeout(Metamaps.Router.timeoutId) + + document.title = 'Topic ' + id + ' | Metamaps' + + Metamaps.Router.currentSection = 'topic' + Metamaps.Router.currentPage = id + + $('.wrapper').removeClass('homePage explorePage mapPage') + $('.wrapper').addClass('topicPage') + + Metamaps.GlobalUI.hideDiv('#yield') + Metamaps.GlobalUI.hideDiv('#explore') + + // clear the visualization, if there was one, before showing its div again + if (Metamaps.Visualize.mGraph) { + Metamaps.Visualize.mGraph.graph.empty() + Metamaps.Visualize.mGraph.plot() + Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + } + Metamaps.GlobalUI.showDiv('#infovis') + Metamaps.Map.end() + Metamaps.Active.Map = null + + Metamaps.Topic.end() + Metamaps.Topic.launch(id) } -})() +}) + +const Router = new _Router() +Router.currentPage = '' +Router.currentSection = undefined +Router.timeoutId = undefined + +Router.intercept = function (evt) { + var segments + + var href = { + prop: $(this).prop('href'), + attr: $(this).attr('href') + } + var root = window.location.protocol + '//' + window.location.host + Backbone.history.options.root + + if (href.prop && href.prop === root) href.attr = '' + + if (href.prop && href.prop.slice(0, root.length) === root) { + evt.preventDefault() + + segments = href.attr.split('/') + segments.splice(0, 1) // pop off the element created by the first / + + if (href.attr === '') { + Metamaps.Router.home() + } else { + Metamaps.Router[segments[0]](segments[1], segments[2]) + } + } +} + +Router.init = function () { + Backbone.history.start({ + silent: true, + pushState: true, + root: '/' + }) + $(document).on('click', 'a[data-router="true"]', Metamaps.Router.intercept) +} + +export default Router diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 20cf0f9c..5258de3b 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -18,7 +17,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Synapse = { +const Synapse = { // this function is to retrieve a synapse JSON object from the database // @param id = the id of the synapse to retrieve get: function (id, callback) { @@ -98,7 +97,7 @@ Metamaps.Synapse = { } }, createSynapseLocally: function () { - var self = Metamaps.Synapse, + var self = Synapse, topic1, topic2, node1, @@ -145,7 +144,7 @@ Metamaps.Synapse = { Metamaps.Create.newSynapse.hide() }, getSynapseFromAutocomplete: function (id) { - var self = Metamaps.Synapse, + var self = Synapse, topic1, topic2, node1, @@ -167,4 +166,6 @@ Metamaps.Synapse = { self.renderSynapse(mapping, synapse, node1, node2, true) } -}; // end Metamaps.Synapse +} + +export default Synapse diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index aff207a9..93ebb646 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -10,10 +9,10 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Mapper * - Metamaps.Visualize */ -Metamaps.SynapseCard = { +const SynapseCard = { openSynapseCard: null, showCard: function (edge, e) { - var self = Metamaps.SynapseCard + var self = SynapseCard // reset so we don't interfere with other edges, but first, save its x and y var myX = $('#edit_synapse').css('left') @@ -59,11 +58,11 @@ Metamaps.SynapseCard = { hideCard: function () { $('#edit_synapse').remove() - Metamaps.SynapseCard.openSynapseCard = null + SynapseCard.openSynapseCard = null }, populateShowCard: function (edge, synapse) { - var self = Metamaps.SynapseCard + var self = SynapseCard self.add_synapse_count(edge) self.add_desc_form(synapse) @@ -154,7 +153,7 @@ Metamaps.SynapseCard = { var index = parseInt($(this).attr('data-synapse-index')) edge.setData('displayIndex', index) Metamaps.Visualize.mGraph.plot() - Metamaps.SynapseCard.showCard(edge, false) + SynapseCard.showCard(edge, false) }) } }, @@ -286,4 +285,6 @@ Metamaps.SynapseCard = { }) } // if } // add_direction_form -}; // end Metamaps.SynapseCard +} + +export default SynapseCard diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index faa8b336..8de14c34 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -28,7 +27,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.tempNode2 */ -Metamaps.Topic = { +const Topic = { // this function is to retrieve a topic JSON object from the database // @param id = the id of the topic to retrieve get: function (id, callback) { @@ -393,4 +392,6 @@ Metamaps.Topic = { event.preventDefault() return false } -}; // end Metamaps.Topic +} + +export default Topic diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index fc007f3b..5a7f1920 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -13,16 +12,16 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Util * - Metamaps.Visualize */ -Metamaps.TopicCard = { +const TopicCard = { openTopicCard: null, // stores the topic that's currently open authorizedToEdit: false, // stores boolean for edit permission for open topic card init: function () { - var self = Metamaps.TopicCard + var self = TopicCard // initialize best_in_place editing $('.authenticated div.permission.canEdit .best_in_place').best_in_place() - Metamaps.TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()) + TopicCard.generateShowcardHTML = Hogan.compile($('#topicCardTemplate').html()) // initialize topic card draggability and resizability $('.showcard').draggable({ @@ -39,7 +38,7 @@ Metamaps.TopicCard = { * @param {$jit.Graph.Node} node */ showCard: function (node, opts) { - var self = Metamaps.TopicCard + var self = TopicCard var topic = node.getData('topic') @@ -54,14 +53,14 @@ Metamaps.TopicCard = { }) }, hideCard: function () { - var self = Metamaps.TopicCard + var self = TopicCard $('.showcard').fadeOut('fast') self.openTopicCard = null self.authorizedToEdit = false }, embedlyCardRendered: function (iframe) { - var self = Metamaps.TopicCard + var self = TopicCard $('#embedlyLinkLoader').hide() @@ -78,7 +77,7 @@ Metamaps.TopicCard = { } }, removeLink: function () { - var self = Metamaps.TopicCard + var self = TopicCard self.openTopicCard.save({ link: null }) @@ -88,7 +87,7 @@ Metamaps.TopicCard = { $('.CardOnGraph').removeClass('hasAttachment') }, bindShowCardListeners: function (topic) { - var self = Metamaps.TopicCard + var self = TopicCard var showCard = document.getElementById('showcard') var authorized = self.authorizedToEdit @@ -350,13 +349,13 @@ Metamaps.TopicCard = { }) }, handleInvalidLink: function () { - var self = Metamaps.TopicCard + var self = TopicCard self.removeLink() Metamaps.GlobalUI.notifyUser('Invalid link') }, populateShowCard: function (topic) { - var self = Metamaps.TopicCard + var self = TopicCard var showCard = document.getElementById('showcard') @@ -380,12 +379,12 @@ Metamaps.TopicCard = { showCard.appendChild(perm) } - Metamaps.TopicCard.bindShowCardListeners(topic) + TopicCard.bindShowCardListeners(topic) }, generateShowcardHTML: null, // will be initialized into a Hogan template within init function // generateShowcardHTML buildObject: function (topic) { - var self = Metamaps.TopicCard + var self = TopicCard var nodeValues = {} @@ -456,4 +455,6 @@ Metamaps.TopicCard = { nodeValues.desc = (topic.get('desc') == '' && authorized) ? desc_nil : topic.get('desc') return nodeValues } -}; // end Metamaps.TopicCard +} + +export default TopicCard diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 9ff9c470..e835a15b 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps */ /* @@ -8,7 +7,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Util = { +const Util = { // helper function to determine how many lines are needed // Line Splitter Function // copyright Stephen Chapman, 19th April 2006 @@ -92,7 +91,7 @@ Metamaps.Util = { var r = (Math.round(Math.random() * 127) + 127).toString(16) var g = (Math.round(Math.random() * 127) + 127).toString(16) var b = (Math.round(Math.random() * 127) + 127).toString(16) - return Metamaps.Util.colorLuminance('#' + r + g + b, -0.4) + return Util.colorLuminance('#' + r + g + b, -0.4) }, // darkens a hex value by 'lum' percentage colorLuminance: function (hex, lum) { @@ -128,4 +127,6 @@ Metamaps.Util = { checkURLisYoutubeVideo: function (url) { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) } -}; // end Metamaps.Util +} + +export default Util diff --git a/frontend/src/Metamaps/Views.js b/frontend/src/Metamaps/Views.js index eb5fdb7c..90cd466d 100644 --- a/frontend/src/Metamaps/Views.js +++ b/frontend/src/Metamaps/Views.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* @@ -10,10 +9,10 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.ReactComponents */ -Metamaps.Views = { +const Views = { exploreMaps: { setCollection: function (collection) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (self.collection) { self.collection.off('add', self.render) @@ -26,7 +25,7 @@ Metamaps.Views = { self.collection.on('errorOnFetch', self.handleError) }, render: function (mapperObj, cb) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (typeof mapperObj === 'function') { cb = mapperObj @@ -51,7 +50,7 @@ Metamaps.Views = { Metamaps.Loading.hide() }, loadMore: function () { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (self.collection.page != "loadedAll") { self.collection.getMaps() @@ -59,7 +58,7 @@ Metamaps.Views = { else self.render() }, handleSuccess: function (cb) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps if (self.collection && self.collection.id === 'mapper') { self.fetchUserThenRender(cb) @@ -71,7 +70,7 @@ Metamaps.Views = { console.log('error loading maps!') // TODO }, fetchUserThenRender: function (cb) { - var self = Metamaps.Views.exploreMaps + var self = Views.exploreMaps // first load the mapper object and then call the render function $.ajax({ @@ -86,3 +85,5 @@ Metamaps.Views = { } } } + +export default Views diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index f5ce8c79..5e99519f 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,4 +1,3 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ /* * Metamaps.Visualize @@ -13,16 +12,15 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.TopicCard * - Metamaps.Topics * - Metamaps.Touch - * - Metamaps.Visualize */ -Metamaps.Visualize = { +const Visualize = { mGraph: null, // a reference to the graph object. cameraPosition: null, // stores the camera position when using a 3D visualization type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created init: function () { - var self = Metamaps.Visualize + var self = Visualize // disable awkward dragging of the canvas element that would sometimes happen $('#infovis-canvas').on('dragstart', function (event) { event.preventDefault() @@ -48,7 +46,7 @@ Metamaps.Visualize = { }) }, computePositions: function () { - var self = Metamaps.Visualize, + var self = Visualize, mapping if (self.type == 'RGraph') { @@ -112,7 +110,7 @@ Metamaps.Visualize = { * */ render: function () { - var self = Metamaps.Visualize, RGraphSettings, FDSettings + var self = Visualize, RGraphSettings, FDSettings if (self.type == 'RGraph' && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { // clear the previous canvas from #infovis @@ -217,4 +215,6 @@ Metamaps.Visualize = { } }, 800) } -}; // end Metamaps.Visualize +} + +export default Visualize diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index ef50b564..926529ca 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -2,32 +2,60 @@ window.Metamaps = window.Metamaps || {} import './Constants' -import './Account' -import './Admin' -import './AutoLayout' -import './Backbone' -import './Control' -import './Create' -import './Debug' -import './Filter' -import './GlobalUI' -import './Import' -import './JIT' -import './Listeners' -import './Map' -import './Mapper' -import './Mobile' -import './Organize' -import './PasteInput' -import './Realtime' -import './Router' -import './Synapse' -import './SynapseCard' -import './Topic' -import './TopicCard' -import './Util' -import './Views' -import './Visualize' -import './ReactComponents' +import Account from './Account' +import Admin from './Admin' +import AutoLayout from './AutoLayout' +import Backbone from './Backbone' +import Control from './Control' +import Create from './Create' +import Debug from './Debug' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import Import from './Import' +import JIT from './JIT' +import Listeners from './Listeners' +import Map from './Map' +import Mapper from './Mapper' +import Mobile from './Mobile' +import Organize from './Organize' +import PasteInput from './PasteInput' +import Realtime from './Realtime' +import Router from './Router' +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' +import ReactComponents from './ReactComponents' + +Metamaps.Account = Account +Metamaps.Admin = Admin +Metamaps.AutoLayout = AutoLayout +Metamaps.Backbone = Backbone +Metamaps.Control = Control +Metamaps.Create = Create +Metamaps.Debug = Debug +Metamaps.Filter = Filter +Metamaps.GlobalUI = GlobalUI +Metamaps.Import = Import +Metamaps.JIT = JIT +Metamaps.Listeners = Listeners +Metamaps.Map = Map +Metamaps.Mapper = Mapper +Metamaps.Mobile = Mobile +Metamaps.Organize = Organize +Metamaps.PasteInput = PasteInput +Metamaps.Realtime = Realtime +Metamaps.ReactComponents = ReactComponents +Metamaps.Router = Router +Metamaps.Synapse = Synapse +Metamaps.SynapseCard = SynapseCard +Metamaps.Topic = Topic +Metamaps.TopicCard = TopicCard +Metamaps.Util = Util +Metamaps.Views = Views +Metamaps.Visualize = Visualize export default window.Metamaps From 7f83f86460d9c9b3eb5e1dc201d0689ea04b8084 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 15:29:55 +0800 Subject: [PATCH 022/306] refactor a bit, make a comment about the Constants file --- frontend/src/Metamaps/Constants.js | 12 ++- frontend/src/Metamaps/GlobalUI.js | 133 ++++++++++------------------- frontend/src/Metamaps/index.js | 38 +++++++++ 3 files changed, 93 insertions(+), 90 deletions(-) diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index a79054e6..ab62ba14 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -1,10 +1,20 @@ window.Metamaps = window.Metamaps || {} -// TODO eliminate these 5 top-level variables +// TODO everything in this file should be moved into one of the other modules +// Either as a local constant, or as a local constant with a globally available getter/setter + Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null +Metamaps.Active = Metamaps.Active || { + Map: null, + Topic: null, + Mapper: null +}; + +Metamaps.Maps = Metamaps.Maps || {} + Metamaps.Settings = { embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index e4522c2f..5abf25ee 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,54 +1,10 @@ window.Metamaps = window.Metamaps || {}; -Metamaps.Active = Metamaps.Active || { - Map: null, - Topic: null, - Mapper: null -}; -Metamaps.Maps = Metamaps.Maps || {} - -$(document).ready(function () { - // initialize all the modules - for (var prop in Metamaps) { - // this runs the init function within each sub-object on the Metamaps one - if (Metamaps.hasOwnProperty(prop) && - Metamaps[prop] != null && - Metamaps[prop].hasOwnProperty('init') && - typeof (Metamaps[prop].init) == 'function' - ) { - Metamaps[prop].init() - } - } - // load whichever page you are on - if (Metamaps.currentSection === "explore") { - var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - - Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) - if (Metamaps.currentPage === "mapper") { - Metamaps.Views.exploreMaps.fetchUserThenRender() - } - else { - Metamaps.Views.exploreMaps.render() - } - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Metamaps.Views.exploreMaps.render() - Metamaps.GlobalUI.showDiv('#explore') - } - else if (Metamaps.Active.Map || Metamaps.Active.Topic) { - Metamaps.Loading.show() - Metamaps.JIT.prepareVizData() - Metamaps.GlobalUI.showDiv('#infovis') - } -}); - -Metamaps.GlobalUI = { +const GlobalUI = { notifyTimeout: null, lightbox: null, init: function () { - var self = Metamaps.GlobalUI; + var self = GlobalUI; self.Search.init(); self.CreateMap.init(); @@ -99,7 +55,7 @@ Metamaps.GlobalUI = { }, 200, 'easeInCubic', function () { $(this).hide() }) }, openLightbox: function (which) { - var self = Metamaps.GlobalUI; + var self = GlobalUI; $('.lightboxContent').hide(); $('#' + which).show(); @@ -126,7 +82,7 @@ Metamaps.GlobalUI = { }, closeLightbox: function (event) { - var self = Metamaps.GlobalUI; + var self = GlobalUI; if (event) event.preventDefault(); @@ -143,15 +99,15 @@ Metamaps.GlobalUI = { $('#lightbox_overlay').hide(); }); - if (self.lightbox === 'forkmap') Metamaps.GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') Metamaps.GlobalUI.CreateMap.reset('new_map'); + if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); + if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { Metamaps.Create.cancelMetacodeSetSwitch(); } self.lightbox = null; }, notifyUser: function (message, leaveOpen) { - var self = Metamaps.GlobalUI; + var self = GlobalUI; $('#toast').html(message) self.showDiv('#toast') @@ -163,7 +119,7 @@ Metamaps.GlobalUI = { } }, clearNotify: function() { - var self = Metamaps.GlobalUI; + var self = GlobalUI; clearTimeout(self.notifyTimeOut); self.hideDiv('#toast') @@ -171,16 +127,16 @@ Metamaps.GlobalUI = { shareInvite: function(inviteLink) { window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); } -}; +} -Metamaps.GlobalUI.CreateMap = { +GlobalUI.CreateMap = { newMap: null, emptyMapForm: "", emptyForkMapForm: "", topicsToMap: [], synapsesToMap: [], init: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); @@ -190,7 +146,7 @@ Metamaps.GlobalUI.CreateMap = { }, bindFormEvents: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { if (event.keyCode === 13) self.submit() @@ -198,7 +154,7 @@ Metamaps.GlobalUI.CreateMap = { $('.new_map button.cancel').unbind().bind('click', function (event) { event.preventDefault(); - Metamaps.GlobalUI.closeLightbox(); + GlobalUI.closeLightbox(); }); $('.new_map button.submitMap').unbind().bind('click', self.submit); @@ -213,14 +169,14 @@ Metamaps.GlobalUI.CreateMap = { generateSuccessMessage: function (id) { var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; stringStart += id; - stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; + stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; var page = Metamaps.Active.Map ? 'map' : 'page'; var stringEnd = "</a></div>"; return stringStart + page + stringEnd; }, switchPermission: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; self.newMap.set('permission', $(this).attr('data-permission')); $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); @@ -232,14 +188,14 @@ Metamaps.GlobalUI.CreateMap = { submit: function (event) { if (event) event.preventDefault(); - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; - if (Metamaps.GlobalUI.lightbox === 'forkmap') { + if (GlobalUI.lightbox === 'forkmap') { self.newMap.set('topicsToMap', self.topicsToMap); self.newMap.set('synapsesToMap', self.synapsesToMap); } - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; var $form = $(formId); self.newMap.set('name', $form.find('#map_name').val()); @@ -255,13 +211,13 @@ Metamaps.GlobalUI.CreateMap = { // TODO add error message }); - Metamaps.GlobalUI.closeLightbox(); - Metamaps.GlobalUI.notifyUser('Working...'); + GlobalUI.closeLightbox(); + GlobalUI.notifyUser('Working...'); }, throwMapNameError: function () { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; var $form = $(formId); var message = $("<div class='feedback_message'>Please enter a map name...</div>"); @@ -274,20 +230,20 @@ Metamaps.GlobalUI.CreateMap = { }, 5000); }, success: function (model) { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; //push the new map onto the collection of 'my maps' Metamaps.Maps.Mine.add(model); - var formId = Metamaps.GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; var form = $(formId); - Metamaps.GlobalUI.clearNotify(); + GlobalUI.clearNotify(); $('#wrapper').append(self.generateSuccessMessage(model.id)); }, reset: function (id) { - var self = Metamaps.GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; var form = $('#' + id); @@ -305,14 +261,13 @@ Metamaps.GlobalUI.CreateMap = { return false; }, -}; +} - -Metamaps.GlobalUI.Account = { +GlobalUI.Account = { isOpen: false, changing: false, init: function () { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; $('.sidebarAccountIcon').click(self.toggleBox); $('.sidebarAccountBox').click(function(event){ @@ -321,7 +276,7 @@ Metamaps.GlobalUI.Account = { $('body').click(self.close); }, toggleBox: function (event) { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; if (self.isOpen) self.close(); else self.open(); @@ -329,7 +284,7 @@ Metamaps.GlobalUI.Account = { event.stopPropagation(); }, open: function () { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; Metamaps.Filter.close(); $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); @@ -345,7 +300,7 @@ Metamaps.GlobalUI.Account = { } }, close: function () { - var self = Metamaps.GlobalUI.Account; + var self = GlobalUI.Account; $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); if (!self.changing) { @@ -357,9 +312,9 @@ Metamaps.GlobalUI.Account = { }); } } -}; +} -Metamaps.GlobalUI.Search = { +GlobalUI.Search = { locked: false, isOpen: false, limitTopicsToMe: false, @@ -368,7 +323,7 @@ Metamaps.GlobalUI.Search = { changing: false, optionsInitialized: false, init: function () { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; var loader = new CanvasLoader('searchLoading'); loader.setColor('#4fb5c0'); // default is '#000000' @@ -417,15 +372,15 @@ Metamaps.GlobalUI.Search = { self.startTypeahead(); }, lock: function() { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.locked = true; }, unlock: function() { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.locked = false; }, open: function (focus) { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; clearTimeout(self.timeOut); if (!self.isOpen && !self.changing && !self.locked) { @@ -447,7 +402,7 @@ Metamaps.GlobalUI.Search = { // for now return - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.timeOut = setTimeout(function () { if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { @@ -468,7 +423,7 @@ Metamaps.GlobalUI.Search = { }, closeAfter); }, startTypeahead: function () { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; @@ -615,7 +570,7 @@ Metamaps.GlobalUI.Search = { }, handleResultClick: function (event, datum, dataset) { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; self.hideLoader(); @@ -632,7 +587,7 @@ Metamaps.GlobalUI.Search = { } }, initSearchOptions: function () { - var self = Metamaps.GlobalUI.Search; + var self = GlobalUI.Search; function toggleResultSet(set) { var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); @@ -679,4 +634,4 @@ Metamaps.GlobalUI.Search = { } } -export default Metamaps.GlobalUI +export default GlobalUI diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 926529ca..21d2af3a 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,3 +1,4 @@ +/* global $ */ window.Metamaps = window.Metamaps || {} import './Constants' @@ -58,4 +59,41 @@ Metamaps.Util = Util Metamaps.Views = Views Metamaps.Visualize = Visualize +$(document).ready(function () { + // initialize all the modules + for (var prop in Metamaps) { + // this runs the init function within each sub-object on the Metamaps one + if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && + Metamaps[prop].hasOwnProperty('init') && + typeof (Metamaps[prop].init) == 'function' + ) { + Metamaps[prop].init() + } + } + // load whichever page you are on + if (Metamaps.currentSection === "explore") { + var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + + Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) + if (Metamaps.currentPage === "mapper") { + Metamaps.Views.exploreMaps.fetchUserThenRender() + } + else { + Metamaps.Views.exploreMaps.render() + } + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { + Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Metamaps.Views.exploreMaps.render() + Metamaps.GlobalUI.showDiv('#explore') + } + else if (Metamaps.Active.Map || Metamaps.Active.Topic) { + Metamaps.Loading.show() + Metamaps.JIT.prepareVizData() + Metamaps.GlobalUI.showDiv('#infovis') + } +}); + export default window.Metamaps From d97b5c297729bb9420fac3ce61dd3659d78bd5a8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 15:32:08 +0800 Subject: [PATCH 023/306] make Util modular --- frontend/src/Metamaps/Util.js | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index e835a15b..9eb715de 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,11 +1,4 @@ -/* global Metamaps */ - -/* - * Metamaps.Util.js - * - * Dependencies: - * - Metamaps.Visualize - */ +import Visualize from './Visualize' const Util = { // helper function to determine how many lines are needed @@ -45,8 +38,8 @@ const Util = { return Math.sqrt(Math.pow((p2.x - p1.x), 2) + Math.pow((p2.y - p1.y), 2)) }, coordsToPixels: function (coords) { - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, + if (Visualize.mGraph) { + var canvas = Visualize.mGraph.canvas, s = canvas.getSize(), p = canvas.getPos(), ox = canvas.translateOffsetX, @@ -67,8 +60,8 @@ const Util = { }, pixelsToCoords: function (pixels) { var coords - if (Metamaps.Visualize.mGraph) { - var canvas = Metamaps.Visualize.mGraph.canvas, + if (Visualize.mGraph) { + var canvas = Visualize.mGraph.canvas, s = canvas.getSize(), p = canvas.getPos(), ox = canvas.translateOffsetX, From c0f63abc59eaf2cd4d105e6bb766c4105beafc04 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 16:30:20 +0800 Subject: [PATCH 024/306] upgrade testing to es6 --- frontend/test/Metamaps.Import.spec.js | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index 8dcf8e97..68946bea 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -1,13 +1,13 @@ /* global describe, it */ -const chai = require('chai') -const expect = chai.expect -Metamaps = {} -require('../../app/assets/javascripts/src/Metamaps.Import') +import chai from 'chai' +import Import from '../src/Metamaps/Import' + +const { expect } = chai describe('Metamaps.Import.js', function () { it('has a topic whitelist', function () { - expect(Metamaps.Import.topicWhitelist).to.deep.equal( + expect(Import.topicWhitelist).to.deep.equal( ['id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission'] ) }) diff --git a/package.json b/package.json index a2227300..495b4f4b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "webpack", "build:watch": "webpack --watch", - "test": "mocha frontend/test || (echo 'Run `npm install` to setup testing' && false)" + "test": "mocha --compilers js:babel-core/register frontend/test || (echo 'Run `npm install` to setup testing' && false)" }, "repository": { "type": "git", From 0a109895f77ca0bae9c6f5065e464aa5c06adead Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 16:32:10 +0800 Subject: [PATCH 025/306] merge realtime/package.json into top level package.json --- doc/production/first-deploy.md | 2 -- doc/production/pull-changes.md | 4 +--- package.json | 2 ++ realtime/package.json | 10 ---------- 4 files changed, 3 insertions(+), 15 deletions(-) delete mode 100644 realtime/package.json diff --git a/doc/production/first-deploy.md b/doc/production/first-deploy.md index cf98eda4..cc3a1f4a 100644 --- a/doc/production/first-deploy.md +++ b/doc/production/first-deploy.md @@ -87,8 +87,6 @@ server to see what problems show up: sudo npm install -g forever (crontab -u metamaps -l 2>/dev/null; echo "@reboot $(which forever) --append -l /home/metamaps/logs/forever.realtime.log start /home/metamaps/metamaps/realtime/realtime-server.js") | crontab -u metamaps - - cd /home/metamaps/metamaps/realtime - npm install mkdir -p /home/metamaps/logs forever --append -l /home/metamaps/logs/forever.realtime.log \ start /home/metamaps/metamaps/realtime/realtime-server.js diff --git a/doc/production/pull-changes.md b/doc/production/pull-changes.md index 0fb5d568..30f41cf5 100644 --- a/doc/production/pull-changes.md +++ b/doc/production/pull-changes.md @@ -29,9 +29,7 @@ Now that you have the code, run these commands: rake perms:fix passenger-config restart-app . - cd realtime - npm install - forever list #find the uid, e.g. xQKv + forever list #find the uid of the realtime server, e.g. xQKv forever restart xQKv sudo service metamaps_delayed_job restart diff --git a/package.json b/package.json index 495b4f4b..925323ac 100644 --- a/package.json +++ b/package.json @@ -27,9 +27,11 @@ "chai": "^3.5.0", "jquery": "1.12.1", "mocha": "^3.0.2", + "node-uuid": "1.2.0", "react": "^15.3.0", "react-dom": "^15.3.0", "requirejs": "^2.1.1", + "socket.io": "0.9.12", "underscore": "^1.4.4", "webpack": "^1.13.1" } diff --git a/realtime/package.json b/realtime/package.json deleted file mode 100644 index 5b5b08f4..00000000 --- a/realtime/package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "RoR-real-time", - "description": "providing real-time sychronization for ruby on rails", - "version": "0.0.1", - "private": true, - "dependencies": { - "socket.io": "0.9.12", - "node-uuid": "1.2.0" - } -} From 056213415772c38b738e64223502bc5cb6b493c0 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:00:12 +0800 Subject: [PATCH 026/306] low hanging fruit Here is my TODO list: already done ==> Account.js <== ==> Admin.js <== ==> AutoLayout.js <== ==> Listeners.js <== ==> Mapper.js <== ==> Organize.js <== ==> PasteInput.js <== ==> ReactComponents.js <== ==> Util.js <== TODO (I think) simple to make modular ==> Backbone.js <== ==> Control.js <== ==> Create.js <== ==> Filter.js <== ==> Import.js <== ==> Mobile.js <== ==> Synapse.js <== ==> SynapseCard.js <== ==> Topic.js <== ==> TopicCard.js <== ==> Views.js <== ==> Visualize.js <== TODO hard to make modular ==> Constants.js <== ==> Debug.js <== ==> GlobalUI.js <== ==> JIT.js <== ==> Map.js <== ==> Realtime.js <== ==> Router.js <== --- frontend/src/Metamaps/Account.js | 28 +++++++++-------------- frontend/src/Metamaps/Admin.js | 10 ++++----- frontend/src/Metamaps/Listeners.js | 2 -- frontend/src/Metamaps/Mapper.js | 8 ++----- frontend/src/Metamaps/Mobile.js | 2 +- frontend/src/Metamaps/Organize.js | 35 +++++++++++++---------------- frontend/src/Metamaps/PasteInput.js | 19 ++++++---------- 7 files changed, 41 insertions(+), 63 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 95a1a69f..f424019f 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,14 +1,6 @@ -window.Metamaps = window.Metamaps || {} -/* global Metamaps, $ */ +/* uses window.Metamaps.Erb */ -/* - * Metamaps.Account.js.erb - * - * Dependencies: - * - Metamaps.Erb - */ - -Metamaps.Account = { +const Account = { listenersInitialized: false, init: function () { var self = Metamaps.Account @@ -20,24 +12,24 @@ Metamaps.Account = { self.listenersInitialized = true }, toggleChangePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageMenu').toggle() if (!self.listenersInitialized) self.initListeners() }, openChangePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageMenu').show() if (!self.listenersInitialized) self.initListeners() }, closeChangePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageMenu').hide() }, showLoading: function () { - var self = Metamaps.Account + var self = Account var loader = new CanvasLoader('accountPageLoading') loader.setColor('#4FC059'); // default is '#000000' @@ -48,7 +40,7 @@ Metamaps.Account = { $('#accountPageLoading').show() }, showImagePreview: function () { - var self = Metamaps.Account + var self = Account var file = $('#user_image')[0].files[0] @@ -94,10 +86,10 @@ Metamaps.Account = { } }, removePicture: function () { - var self = Metamaps.Account + var self = Account $('.userImageDiv canvas').remove() - $('.userImageDiv img').attr('src', Metamaps.Erb['user.png']).show() + $('.userImageDiv img').attr('src', window.Metamaps.Erb['user.png']).show() $('.userImageMenu').hide() var input = $('#user_image') @@ -122,4 +114,4 @@ Metamaps.Account = { } } -export default Metamaps.Account +export default Account diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index 10cbc6d8..5d080c2e 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -4,26 +4,26 @@ const Admin = { selectMetacodes: [], allMetacodes: [], init: function () { - var self = Metamaps.Admin + var self = Admin $('#metacodes_value').val(self.selectMetacodes.toString()) }, selectAll: function () { - var self = Metamaps.Admin + var self = Admin $('.editMetacodes li').removeClass('toggledOff') self.selectMetacodes = self.allMetacodes.slice(0) $('#metacodes_value').val(self.selectMetacodes.toString()) }, deselectAll: function () { - var self = Metamaps.Admin + var self = Admin $('.editMetacodes li').addClass('toggledOff') self.selectMetacodes = [] $('#metacodes_value').val(0) }, liClickHandler: function () { - var self = Metamaps.Admin + var self = Admin if ($(this).attr('class') != 'toggledOff') { $(this).addClass('toggledOff') @@ -38,7 +38,7 @@ const Admin = { } }, validate: function () { - var self = Metamaps.Admin + var self = Admin if (self.selectMetacodes.length == 0) { alert('Would you pretty please select at least one metacode for the set?') diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index af244961..1c56b679 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,8 +1,6 @@ /* global Metamaps, $ */ /* - * Metamaps.Listeners.js.erb - * * Dependencies: * - Metamaps.Active * - Metamaps.Control diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index 114d4f8c..ac93c34d 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,9 +1,5 @@ -/* global Metamaps */ +import Backbone from './Backbone' -/* - * Dependencies: - * - Metamaps.Backbone - */ const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve @@ -13,7 +9,7 @@ const Mapper = { if (!response.ok) throw response return response.json() }).then(payload => { - callback(new Metamaps.Backbone.Mapper(payload)) + callback(new Backbone.Mapper(payload)) }) } } diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index e062ca45..9074f521 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -10,7 +10,7 @@ const Mobile = { init: function () { - var self = Metamaps.Mobile + var self = Mobile $('#menu_icon').click(self.toggleMenu) $('#mobile_menu li a').click(self.liClick) diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index 71905568..ee29c2b8 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,21 +1,18 @@ -/* global Metamaps, $ */ +/* global $ */ + +import Visualize from './Visualize' +import JIT from './JIT' -/* - * Metamaps.Organize.js.erb - * - * Dependencies: - * - Metamaps.Visualize - */ const Organize = { arrange: function (layout, centerNode) { // first option for layout to implement is 'grid', will do an evenly spaced grid with its center at the 0,0 origin if (layout == 'grid') { - var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization + var numNodes = _.size(Visualize.mGraph.graph.nodes); // this will always be an integer, the # of nodes on your graph visualization var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid var GRIDSPACE = 400 var row = 0 var column = 0 - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { if (column == numColumns) { column = 0 row += 1 @@ -26,14 +23,14 @@ const Organize = { n.setPos(newPos, 'end') column += 1 }) - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'grid_full') { // this will always be an integer, the # of nodes on your graph visualization - var numNodes = _.size(Metamaps.Visualize.mGraph.graph.nodes) + var numNodes = _.size(Visualize.mGraph.graph.nodes) // var numColumns = Math.floor(Math.sqrt(numNodes)) // the number of columns to make an even grid // var GRIDSPACE = 400 - var height = Metamaps.Visualize.mGraph.canvas.getSize(0).height - var width = Metamaps.Visualize.mGraph.canvas.getSize(0).width + var height = Visualize.mGraph.canvas.getSize(0).height + var width = Visualize.mGraph.canvas.getSize(0).width var totalArea = height * width var cellArea = totalArea / numNodes var ratio = height / width @@ -44,7 +41,7 @@ const Organize = { var totalCells = row * column if (totalCells) - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { if (column == numColumns) { column = 0 row += 1 @@ -55,7 +52,7 @@ const Organize = { n.setPos(newPos, 'end') column += 1 }) - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'radial') { var centerX = centerNode.getPos().x var centerY = centerNode.getPos().y @@ -87,16 +84,16 @@ const Organize = { }) } radial(centerNode, 1, 0) - Metamaps.Visualize.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'center_viewport') { var lowX = 0, lowY = 0, highX = 0, highY = 0 - var oldOriginX = Metamaps.Visualize.mGraph.canvas.translateOffsetX - var oldOriginY = Metamaps.Visualize.mGraph.canvas.translateOffsetY + var oldOriginX = Visualize.mGraph.canvas.translateOffsetX + var oldOriginY = Visualize.mGraph.canvas.translateOffsetY - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { if (n.id === 1) { lowX = n.getPos().x lowY = n.getPos().y diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 9676e783..ebe1d944 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -1,12 +1,7 @@ -/* global Metamaps, $ */ +/* global $ */ -/* - * Metamaps.PasteInput.js.erb - * - * Dependencies: - * - Metamaps.Import - * - Metamaps.AutoLayout - */ +import AutoLayout from './AutoLayout' +import Import from './Import' const PasteInput = { // thanks to https://github.com/kevva/url-regex @@ -74,13 +69,13 @@ const PasteInput = { handleURL: function (text, coords) { var title = 'Link' if (!coords || !coords.x || !coords.y) { - coords = Metamaps.AutoLayout.getNextCoord() + coords = AutoLayout.getNextCoord() } var import_id = null // don't store a cidMapping var permission = null // use default - Metamaps.Import.createTopicWithParameters( + Import.createTopicWithParameters( title, 'Reference', // metacode - todo fix permission, @@ -101,11 +96,11 @@ const PasteInput = { }, handleJSON: function (text) { - Metamaps.Import.handleJSON(text) + Import.handleJSON(text) }, handleTSV: function (text) { - Metamaps.Import.handleTSV(text) + Import.handleTSV(text) } } From 8f100d99cb82bb3204f264fd6212619d7dbf9603 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:05:28 +0800 Subject: [PATCH 027/306] start to do stuff that may/may not work --- frontend/src/Metamaps/Active.js | 7 +++++++ frontend/src/Metamaps/Constants.js | 5 ----- frontend/src/Metamaps/index.js | 11 ++++++----- 3 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 frontend/src/Metamaps/Active.js diff --git a/frontend/src/Metamaps/Active.js b/frontend/src/Metamaps/Active.js new file mode 100644 index 00000000..c61a8bb9 --- /dev/null +++ b/frontend/src/Metamaps/Active.js @@ -0,0 +1,7 @@ +const Active = { + Map: null, + Topic: null, + Mapper: null +}; + +export default Active diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index ab62ba14..e887f24c 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -7,11 +7,6 @@ Metamaps.tempNode = null Metamaps.tempInit = false Metamaps.tempNode2 = null -Metamaps.Active = Metamaps.Active || { - Map: null, - Topic: null, - Mapper: null -}; Metamaps.Maps = Metamaps.Maps || {} diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 21d2af3a..598533c0 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,9 +1,9 @@ /* global $ */ -window.Metamaps = window.Metamaps || {} import './Constants' import Account from './Account' +import Active from './Active' import Admin from './Admin' import AutoLayout from './AutoLayout' import Backbone from './Backbone' @@ -32,6 +32,7 @@ import Visualize from './Visualize' import ReactComponents from './ReactComponents' Metamaps.Account = Account +Metamaps.Active = Active Metamaps.Admin = Admin Metamaps.AutoLayout = AutoLayout Metamaps.Backbone = Backbone @@ -59,9 +60,9 @@ Metamaps.Util = Util Metamaps.Views = Views Metamaps.Visualize = Visualize -$(document).ready(function () { +document.addEventListener("DOMContentLoaded", function() { // initialize all the modules - for (var prop in Metamaps) { + for (const prop in Metamaps) { // this runs the init function within each sub-object on the Metamaps one if (Metamaps.hasOwnProperty(prop) && Metamaps[prop] != null && @@ -73,7 +74,7 @@ $(document).ready(function () { } // load whichever page you are on if (Metamaps.currentSection === "explore") { - var capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) if (Metamaps.currentPage === "mapper") { @@ -96,4 +97,4 @@ $(document).ready(function () { } }); -export default window.Metamaps +export default Metamaps From 9c1543de6467fb5eab9d059e111d4e533b6436c0 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:08:53 +0800 Subject: [PATCH 028/306] move some variables into JIT --- frontend/src/Metamaps/Constants.js | 5 --- frontend/src/Metamaps/JIT.js | 58 ++++++++++++++++-------------- frontend/src/Metamaps/Topic.js | 15 ++++---- 3 files changed, 37 insertions(+), 41 deletions(-) diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js index e887f24c..9d0818ab 100644 --- a/frontend/src/Metamaps/Constants.js +++ b/frontend/src/Metamaps/Constants.js @@ -3,11 +3,6 @@ window.Metamaps = window.Metamaps || {} // TODO everything in this file should be moved into one of the other modules // Either as a local constant, or as a local constant with a globally available getter/setter -Metamaps.tempNode = null -Metamaps.tempInit = false -Metamaps.tempNode2 = null - - Metamaps.Maps = Metamaps.Maps || {} Metamaps.Settings = { diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index a93e3341..ec8195de 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -3,6 +3,10 @@ let panningInt const JIT = { + tempInit: false, + tempNode: null, + tempNode2: null, + events: { topicDrag: 'Metamaps:JIT:events:topicDrag', newTopic: 'Metamaps:JIT:events:newTopic', @@ -795,9 +799,9 @@ const JIT = { } // if it's a right click or holding down alt, start synapse creation ->third option is for firefox else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { - if (Metamaps.tempInit == false) { - Metamaps.tempNode = node - Metamaps.tempInit = true + if (JIT.tempInit == false) { + JIT.tempNode = node + JIT.tempInit = true Metamaps.Create.newTopic.hide() Metamaps.Create.newSynapse.hide() @@ -813,8 +817,8 @@ const JIT = { } } else { Metamaps.Mouse.synapseStartCoordinates = [{ - x: Metamaps.tempNode.pos.getc().x, - y: Metamaps.tempNode.pos.getc().y + x: JIT.tempNode.pos.getc().x, + y: JIT.tempNode.pos.getc().y }] } Metamaps.Mouse.synapseEndCoordinates = { @@ -825,11 +829,11 @@ const JIT = { // let temp = eventInfo.getNode() if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned - Metamaps.tempNode2 = temp + JIT.tempNode2 = temp Metamaps.Mouse.synapseEndCoordinates = { - x: Metamaps.tempNode2.pos.getc().x, - y: Metamaps.tempNode2.pos.getc().y + x: JIT.tempNode2.pos.getc().x, + y: JIT.tempNode2.pos.getc().y } // before making the highlighted one bigger, make sure all the others are regular size @@ -839,7 +843,7 @@ const JIT = { temp.setData('dim', 35, 'current') Metamaps.Visualize.mGraph.plot() } else if (!temp) { - Metamaps.tempNode2 = null + JIT.tempNode2 = null Metamaps.Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current') }) @@ -867,10 +871,10 @@ const JIT = { } }, // onDragMoveTopicHandler onDragCancelHandler: function (node, eventInfo, e) { - Metamaps.tempNode = null - if (Metamaps.tempNode2) Metamaps.tempNode2.setData('dim', 25, 'current') - Metamaps.tempNode2 = null - Metamaps.tempInit = false + JIT.tempNode = null + if (JIT.tempNode2) JIT.tempNode2.setData('dim', 25, 'current') + JIT.tempNode2 = null + JIT.tempInit = false // reset the draw synapse positions to false Metamaps.Mouse.synapseStartCoordinates = [] Metamaps.Mouse.synapseEndCoordinates = null @@ -879,27 +883,27 @@ const JIT = { onDragEndTopicHandler: function (node, eventInfo, e) { var midpoint = {}, pixelPos, mapping - if (Metamaps.tempInit && Metamaps.tempNode2 == null) { + if (JIT.tempInit && JIT.tempNode2 == null) { // this means you want to add a new topic, and then a synapse Metamaps.Create.newTopic.addSynapse = true Metamaps.Create.newTopic.open() - } else if (Metamaps.tempInit && Metamaps.tempNode2 != null) { + } else if (JIT.tempInit && JIT.tempNode2 != null) { // this means you want to create a synapse between two existing topics Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id - Metamaps.Create.newSynapse.topic2id = Metamaps.tempNode2.getData('topic').id - Metamaps.tempNode2.setData('dim', 25, 'current') + Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id + Metamaps.Create.newSynapse.topic2id = JIT.tempNode2.getData('topic').id + JIT.tempNode2.setData('dim', 25, 'current') Metamaps.Visualize.mGraph.plot() - midpoint.x = Metamaps.tempNode.pos.getc().x + (Metamaps.tempNode2.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2 - midpoint.y = Metamaps.tempNode.pos.getc().y + (Metamaps.tempNode2.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2 + midpoint.x = JIT.tempNode.pos.getc().x + (JIT.tempNode2.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 + midpoint.y = JIT.tempNode.pos.getc().y + (JIT.tempNode2.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 pixelPos = Metamaps.Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') Metamaps.Create.newSynapse.open() - Metamaps.tempNode = null - Metamaps.tempNode2 = null - Metamaps.tempInit = false - } else if (!Metamaps.tempInit && node && !node.nodeFrom) { + JIT.tempNode = null + JIT.tempNode2 = null + JIT.tempInit = false + } else if (!JIT.tempInit && node && !node.nodeFrom) { // this means you dragged an existing node, autosave that to the database // check whether to save mappings @@ -977,9 +981,9 @@ const JIT = { // reset the draw synapse positions to false Metamaps.Mouse.synapseStartCoordinates = [] Metamaps.Mouse.synapseEndCoordinates = null - Metamaps.tempInit = false - Metamaps.tempNode = null - Metamaps.tempNode2 = null + JIT.tempInit = false + JIT.tempNode = null + JIT.tempNode2 = null if (!e.ctrlKey && !e.shiftKey) { Metamaps.Control.deselectAllEdges() Metamaps.Control.deselectAllNodes() diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 8de14c34..0ebcc118 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -22,9 +22,6 @@ * - Metamaps.Topics * - Metamaps.Util * - Metamaps.Visualize - * - Metamaps.tempInit - * - Metamaps.tempNode - * - Metamaps.tempNode2 */ const Topic = { @@ -218,11 +215,11 @@ const Topic = { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') } if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { - Metamaps.Create.newSynapse.topic1id = Metamaps.tempNode.getData('topic').id + Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id // position the form - midpoint.x = Metamaps.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - Metamaps.tempNode.pos.getc().x) / 2 - midpoint.y = Metamaps.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - Metamaps.tempNode.pos.getc().y) / 2 + midpoint.x = JIT.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 + midpoint.y = JIT.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 pixelPos = Metamaps.Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') @@ -232,9 +229,9 @@ const Topic = { modes: ['node-property:dim'], duration: 500, onComplete: function () { - Metamaps.tempNode = null - Metamaps.tempNode2 = null - Metamaps.tempInit = false + JIT.tempNode = null + JIT.tempNode2 = null + JIT.tempInit = false } }) } else { From 8ed2b3ffc17d7206672853d0dedac7b8dacb64b2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:14:34 +0800 Subject: [PATCH 029/306] remove Constants.js --- frontend/src/Metamaps/Constants.js | 56 ------------------------------ frontend/src/Metamaps/Mouse.js | 16 +++++++++ frontend/src/Metamaps/Selected.js | 11 ++++++ frontend/src/Metamaps/Settings.js | 21 +++++++++++ frontend/src/Metamaps/Visualize.js | 6 ++-- frontend/src/Metamaps/index.js | 9 +++-- 6 files changed, 58 insertions(+), 61 deletions(-) delete mode 100644 frontend/src/Metamaps/Constants.js create mode 100644 frontend/src/Metamaps/Mouse.js create mode 100644 frontend/src/Metamaps/Selected.js create mode 100644 frontend/src/Metamaps/Settings.js diff --git a/frontend/src/Metamaps/Constants.js b/frontend/src/Metamaps/Constants.js deleted file mode 100644 index 9d0818ab..00000000 --- a/frontend/src/Metamaps/Constants.js +++ /dev/null @@ -1,56 +0,0 @@ -window.Metamaps = window.Metamaps || {} - -// TODO everything in this file should be moved into one of the other modules -// Either as a local constant, or as a local constant with a globally available getter/setter - -Metamaps.Maps = Metamaps.Maps || {} - -Metamaps.Settings = { - embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages - sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database - colors: { - background: '#344A58', - synapses: { - normal: '#888888', - hover: '#888888', - selected: '#FFFFFF' - }, - topics: { - selected: '#FFFFFF' - }, - labels: { - background: '#18202E', - text: '#DDD' - } - }, -} - -Metamaps.Touch = { - touchPos: null, // this stores the x and y values of a current touch event - touchDragNode: null // this stores a reference to a JIT node that is being dragged -} - -Metamaps.Mouse = { - didPan: false, - didBoxZoom: false, - changeInX: 0, - changeInY: 0, - edgeHoveringOver: false, - boxStartCoordinates: false, - boxEndCoordinates: false, - synapseStartCoordinates: [], - synapseEndCoordinates: null, - lastNodeClick: 0, - lastCanvasClick: 0, - DOUBLE_CLICK_TOLERANCE: 300 -} - -Metamaps.Selected = { - reset: function () { - var self = Metamaps.Selected - self.Nodes = [] - self.Edges = [] - }, - Nodes: [], - Edges: [] -} diff --git a/frontend/src/Metamaps/Mouse.js b/frontend/src/Metamaps/Mouse.js new file mode 100644 index 00000000..9989bc20 --- /dev/null +++ b/frontend/src/Metamaps/Mouse.js @@ -0,0 +1,16 @@ +const Mouse = { + didPan: false, + didBoxZoom: false, + changeInX: 0, + changeInY: 0, + edgeHoveringOver: false, + boxStartCoordinates: false, + boxEndCoordinates: false, + synapseStartCoordinates: [], + synapseEndCoordinates: null, + lastNodeClick: 0, + lastCanvasClick: 0, + DOUBLE_CLICK_TOLERANCE: 300 +} + +export default Mouse diff --git a/frontend/src/Metamaps/Selected.js b/frontend/src/Metamaps/Selected.js new file mode 100644 index 00000000..396270ab --- /dev/null +++ b/frontend/src/Metamaps/Selected.js @@ -0,0 +1,11 @@ +const Selected = { + reset: function () { + var self = Metamaps.Selected + self.Nodes = [] + self.Edges = [] + }, + Nodes: [], + Edges: [] +} + +export default Selected diff --git a/frontend/src/Metamaps/Settings.js b/frontend/src/Metamaps/Settings.js new file mode 100644 index 00000000..687a6629 --- /dev/null +++ b/frontend/src/Metamaps/Settings.js @@ -0,0 +1,21 @@ +const Settings = { + embed: false, // indicates that the app is on a page that is optimized for embedding in iFrames on other web pages + sandbox: false, // puts the app into a mode (when true) where it only creates data locally, and isn't writing it to the database + colors: { + background: '#344A58', + synapses: { + normal: '#888888', + hover: '#888888', + selected: '#FFFFFF' + }, + topics: { + selected: '#FFFFFF' + }, + labels: { + background: '#18202E', + text: '#DDD' + } + }, +} + +export default Settings diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 5e99519f..4aa65772 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -11,7 +11,6 @@ * - Metamaps.Synapses * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Touch */ const Visualize = { @@ -19,6 +18,7 @@ const Visualize = { cameraPosition: null, // stores the camera position when using a 3D visualization type: 'ForceDirected', // the type of graph we're building, could be "RGraph", "ForceDirected", or "ForceDirected3D" loadLater: false, // indicates whether there is JSON that should be loaded right in the offset, or whether to wait till the first topic is created + touchDragNode: null, init: function () { var self = Visualize // disable awkward dragging of the canvas element that would sometimes happen @@ -40,9 +40,9 @@ const Visualize = { // prevent touch events on the canvas from default behaviour $('#infovis-canvas').bind('touchend touchcancel', function (event) { lastDist = 0 - if (!self.mGraph.events.touchMoved && !Metamaps.Touch.touchDragNode) Metamaps.TopicCard.hideCurrentCard() + if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) Metamaps.TopicCard.hideCurrentCard() self.mGraph.events.touched = self.mGraph.events.touchMoved = false - Metamaps.Touch.touchDragNode = false + Visualize.touchDragNode = false }) }, computePositions: function () { diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 598533c0..37e93492 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,7 +1,5 @@ /* global $ */ -import './Constants' - import Account from './Account' import Active from './Active' import Admin from './Admin' @@ -18,10 +16,13 @@ import Listeners from './Listeners' import Map from './Map' import Mapper from './Mapper' import Mobile from './Mobile' +import Mouse from './Mouse' import Organize from './Organize' import PasteInput from './PasteInput' import Realtime from './Realtime' import Router from './Router' +import Selected from './Selected' +import Settings from './Settings' import Synapse from './Synapse' import SynapseCard from './SynapseCard' import Topic from './Topic' @@ -45,13 +46,17 @@ Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners Metamaps.Map = Map +Metamaps.Maps = {} Metamaps.Mapper = Mapper Metamaps.Mobile = Mobile +Metamaps.Mouse = Mouse Metamaps.Organize = Organize Metamaps.PasteInput = PasteInput Metamaps.Realtime = Realtime Metamaps.ReactComponents = ReactComponents Metamaps.Router = Router +Metamaps.Selected = Selected +Metamaps.Settings = Settings Metamaps.Synapse = Synapse Metamaps.SynapseCard = SynapseCard Metamaps.Topic = Topic From 0065b201c7b12c7eb7d76db7a33ee225e6e87094 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 17:36:47 +0800 Subject: [PATCH 030/306] make more code modular --- frontend/src/Metamaps/Control.js | 187 ++++++++++++++------------- frontend/src/Metamaps/Create.js | 26 ++-- frontend/src/Metamaps/Filter.js | 25 ++-- frontend/src/Metamaps/Import.js | 13 +- frontend/src/Metamaps/Mobile.js | 15 +-- frontend/src/Metamaps/SynapseCard.js | 35 +++-- frontend/src/Metamaps/Topic.js | 37 +++--- frontend/src/Metamaps/TopicCard.js | 25 ++-- frontend/src/Metamaps/Views.js | 10 +- frontend/src/Metamaps/Visualize.js | 48 +++---- 10 files changed, 209 insertions(+), 212 deletions(-) diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index b9df0d2c..3eecb126 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,21 +1,22 @@ /* global Metamaps, $ */ +import Active from './Active' +import Filter from './Filter' +import JIT from './JIT' +import Mouse from './Mouse' +import Selected from './Selected' +import Settings from './Settings' +import Visualize from './Visualize' + /* * Metamaps.Control.js * * Dependencies: - * - Metamaps.Active - * - Metamaps.Filter * - Metamaps.GlobalUI - * - Metamaps.JIT * - Metamaps.Mappings * - Metamaps.Metacodes - * - Metamaps.Mouse - * - Metamaps.Selected - * - Metamaps.Settings * - Metamaps.Synapses * - Metamaps.Topics - * - Metamaps.Visualize */ const Control = { @@ -23,37 +24,37 @@ const Control = { selectNode: function (node, e) { var filtered = node.getData('alpha') === 0 - if (filtered || Metamaps.Selected.Nodes.indexOf(node) != -1) return + if (filtered || Selected.Nodes.indexOf(node) != -1) return node.selected = true node.setData('dim', 30, 'current') - Metamaps.Selected.Nodes.push(node) + Selected.Nodes.push(node) }, deselectAllNodes: function () { - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var node = Metamaps.Selected.Nodes[i] + var node = Selected.Nodes[i] Control.deselectNode(node) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, deselectNode: function (node) { delete node.selected node.setData('dim', 25, 'current') // remove the node - Metamaps.Selected.Nodes.splice( - Metamaps.Selected.Nodes.indexOf(node), 1) + Selected.Nodes.splice( + Selected.Nodes.indexOf(node), 1) }, deleteSelected: function () { - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var n = Metamaps.Selected.Nodes.length - var e = Metamaps.Selected.Edges.length + var n = Selected.Nodes.length + var e = Selected.Edges.length var ntext = n == 1 ? '1 topic' : n + ' topics' var etext = e == 1 ? '1 synapse' : e + ' synapses' var text = 'You have ' + ntext + ' and ' + etext + ' selected. ' - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -67,41 +68,41 @@ const Control = { } }, deleteSelectedNodes: function () { // refers to deleting topics permanently - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') return } - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var node = Metamaps.Selected.Nodes[i] + var node = Selected.Nodes[i] Control.deleteNode(node.id) } }, deleteNode: function (nodeid) { // refers to deleting topics permanently - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') return } - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + var node = Visualize.mGraph.graph.getNode(nodeid) var topic = node.getData('topic') - var permToDelete = Metamaps.Active.Mapper.id === topic.get('user_id') || Metamaps.Active.Mapper.get('admin') + var permToDelete = Active.Mapper.id === topic.get('user_id') || Active.Mapper.get('admin') if (permToDelete) { var mappableid = topic.id var mapping = node.getData('mapping') topic.destroy() Metamaps.Mappings.remove(mapping) - $(document).trigger(Metamaps.JIT.events.deleteTopic, [{ + $(document).trigger(JIT.events.deleteTopic, [{ mappableid: mappableid }]) Control.hideNode(nodeid) @@ -110,25 +111,25 @@ const Control = { } }, removeSelectedNodes: function () { // refers to removing topics permanently from a map - if (Metamaps.Active.Topic) { + if (Active.Topic) { // hideNode will handle synapses as well - var nodeids = _.map(Metamaps.Selected.Nodes, function(node) { + var nodeids = _.map(Selected.Nodes, function(node) { return node.id }) _.each(nodeids, function(nodeid) { - if (Metamaps.Active.Topic.id !== nodeid) { + if (Active.Topic.id !== nodeid) { Metamaps.Topics.remove(nodeid) Control.hideNode(nodeid) } }) return } - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var l = Metamaps.Selected.Nodes.length, + var l = Selected.Nodes.length, i, node, - authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -136,15 +137,15 @@ const Control = { } for (i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] Control.removeNode(node.id) } }, removeNode: function (nodeid) { // refers to removing topics permanently from a map - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) + var node = Visualize.mGraph.graph.getNode(nodeid) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -156,24 +157,24 @@ const Control = { var mapping = node.getData('mapping') mapping.destroy() Metamaps.Topics.remove(topic) - $(document).trigger(Metamaps.JIT.events.removeTopic, [{ + $(document).trigger(JIT.events.removeTopic, [{ mappableid: mappableid }]) Control.hideNode(nodeid) }, hideSelectedNodes: function () { - var l = Metamaps.Selected.Nodes.length, + var l = Selected.Nodes.length, i, node for (i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] Control.hideNode(node.id) } }, hideNode: function (nodeid) { - var node = Metamaps.Visualize.mGraph.graph.getNode(nodeid) - var graph = Metamaps.Visualize.mGraph + var node = Visualize.mGraph.graph.getNode(nodeid) + var graph = Visualize.mGraph Control.deselectNode(node) @@ -181,73 +182,73 @@ const Control = { node.eachAdjacency(function (adj) { adj.setData('alpha', 0, 'end') }) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['node-property:alpha', 'edge-property:alpha' ], duration: 500 }) setTimeout(function () { - if (nodeid == Metamaps.Visualize.mGraph.root) { // && Metamaps.Visualize.type === "RGraph" + if (nodeid == Visualize.mGraph.root) { // && Visualize.type === "RGraph" var newroot = _.find(graph.graph.nodes, function (n) { return n.id !== nodeid; }) graph.root = newroot ? newroot.id : null } - Metamaps.Visualize.mGraph.graph.removeNode(nodeid) + Visualize.mGraph.graph.removeNode(nodeid) }, 500) - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkMappers() + Filter.checkMetacodes() + Filter.checkMappers() }, selectEdge: function (edge) { var filtered = edge.getData('alpha') === 0; // don't select if the edge is filtered - if (filtered || Metamaps.Selected.Edges.indexOf(edge) != -1) return + if (filtered || Selected.Edges.indexOf(edge) != -1) return - var width = Metamaps.Mouse.edgeHoveringOver === edge ? 4 : 2 + var width = Mouse.edgeHoveringOver === edge ? 4 : 2 edge.setDataset('current', { showDesc: true, lineWidth: width, - color: Metamaps.Settings.colors.synapses.selected + color: Settings.colors.synapses.selected }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() - Metamaps.Selected.Edges.push(edge) + Selected.Edges.push(edge) }, deselectAllEdges: function () { - var l = Metamaps.Selected.Edges.length + var l = Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { - var edge = Metamaps.Selected.Edges[i] + var edge = Selected.Edges[i] Control.deselectEdge(edge) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, deselectEdge: function (edge) { edge.setData('showDesc', false, 'current') edge.setDataset('current', { lineWidth: 2, - color: Metamaps.Settings.colors.synapses.normal + color: Settings.colors.synapses.normal }) - if (Metamaps.Mouse.edgeHoveringOver == edge) { + if (Mouse.edgeHoveringOver == edge) { edge.setDataset('current', { showDesc: true, lineWidth: 4 }) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() // remove the edge - Metamaps.Selected.Edges.splice( - Metamaps.Selected.Edges.indexOf(edge), 1) + Selected.Edges.splice( + Selected.Edges.indexOf(edge), 1) }, deleteSelectedEdges: function () { // refers to deleting topics permanently var edge, - l = Metamaps.Selected.Edges.length + l = Selected.Edges.length - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -255,14 +256,14 @@ const Control = { } for (var i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] Control.deleteEdge(edge) } }, deleteEdge: function (edge) { - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -274,7 +275,7 @@ const Control = { var synapse = edge.getData('synapses')[index] var mapping = edge.getData('mappings')[index] - var permToDelete = Metamaps.Active.Mapper.id === synapse.get('user_id') || Metamaps.Active.Mapper.get('admin') + var permToDelete = Active.Mapper.id === synapse.get('user_id') || Active.Mapper.get('admin') if (permToDelete) { if (edge.getData('synapses').length - 1 === 0) { Control.hideEdge(edge) @@ -289,7 +290,7 @@ const Control = { if (edge.getData('displayIndex')) { delete edge.data.$displayIndex } - $(document).trigger(Metamaps.JIT.events.deleteSynapse, [{ + $(document).trigger(JIT.events.deleteSynapse, [{ mappableid: mappableid }]) } else { @@ -298,13 +299,13 @@ const Control = { }, removeSelectedEdges: function () { // Topic view is handled by removeSelectedNodes - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var l = Metamaps.Selected.Edges.length, + var l = Selected.Edges.length, i, edge - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -312,15 +313,15 @@ const Control = { } for (i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] Control.removeEdge(edge) } - Metamaps.Selected.Edges = [ ] + Selected.Edges = [ ] }, removeEdge: function (edge) { - if (!Metamaps.Active.Map) return + if (!Active.Map) return - var authorized = Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') @@ -345,34 +346,34 @@ const Control = { if (edge.getData('displayIndex')) { delete edge.data.$displayIndex } - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + $(document).trigger(JIT.events.removeSynapse, [{ mappableid: mappableid }]) }, hideSelectedEdges: function () { var edge, - l = Metamaps.Selected.Edges.length, + l = Selected.Edges.length, i for (i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] Control.hideEdge(edge) } - Metamaps.Selected.Edges = [ ] + Selected.Edges = [ ] }, hideEdge: function (edge) { var from = edge.nodeFrom.id var to = edge.nodeTo.id edge.setData('alpha', 0, 'end') Control.deselectEdge(edge) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:alpha'], duration: 500 }) setTimeout(function () { - Metamaps.Visualize.mGraph.graph.removeAdjacence(from, to) + Visualize.mGraph.graph.removeAdjacence(from, to) }, 500) - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + Filter.checkSynapses() + Filter.checkMappers() }, updateSelectedPermissions: function (permission) { var edge, synapse, node, topic @@ -384,12 +385,12 @@ const Control = { sCount = 0 // change the permission of the selected synapses, if logged in user is the original creator - var l = Metamaps.Selected.Edges.length + var l = Selected.Edges.length for (var i = l - 1; i >= 0; i -= 1) { - edge = Metamaps.Selected.Edges[i] + edge = Selected.Edges[i] synapse = edge.getData('synapses')[0] - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (synapse.authorizePermissionChange(Active.Mapper)) { synapse.save({ permission: permission }) @@ -398,12 +399,12 @@ const Control = { } // change the permission of the selected topics, if logged in user is the original creator - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] topic = node.getData('topic') - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (topic.authorizePermissionChange(Active.Mapper)) { topic.save({ permission: permission }) @@ -428,12 +429,12 @@ const Control = { var nCount = 0 // change the permission of the selected topics, if logged in user is the original creator - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - node = Metamaps.Selected.Nodes[i] + node = Selected.Nodes[i] topic = node.getData('topic') - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + if (topic.authorizeToEdit(Active.Mapper)) { topic.save({ 'metacode_id': metacode_id }) @@ -445,7 +446,7 @@ const Control = { var message = nString + ' you can edit updated to ' + metacode.get('name') Metamaps.GlobalUI.notifyUser(message) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, } diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1348e9d2..49267d6d 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -1,6 +1,11 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, $ */ +import Mouse from './Mouse' +import Selected from './Selected' +import Synapse from './Synapse' +import Topic from './Topic' +import Visualize from './Visualize' + /* * Metamaps.Create.js * @@ -8,11 +13,6 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Backbone * - Metamaps.GlobalUI * - Metamaps.Metacodes - * - Metamaps.Mouse - * - Metamaps.Selected - * - Metamaps.Synapse - * - Metamaps.Topic - * - Metamaps.Visualize */ const Create = { @@ -193,7 +193,7 @@ const Create = { // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { - Metamaps.Topic.getTopicFromAutocomplete(datum.id) + Topic.getTopicFromAutocomplete(datum.id) }) // initialize metacode spinner and then hide it @@ -255,7 +255,7 @@ const Create = { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', prepare: function (query, settings) { var self = Create.newSynapse - if (Metamaps.Selected.Nodes.length < 2) { + if (Selected.Nodes.length < 2) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) return settings } else { @@ -307,16 +307,16 @@ const Create = { $('#synapse_desc').focusout(function () { if (Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally() + Synapse.createSynapseLocally() } }) $('#synapse_desc').bind('typeahead:select', function (event, datum, dataset) { if (datum.id) { // if they clicked on an existing synapse get it - Metamaps.Synapse.getSynapseFromAutocomplete(datum.id) + Synapse.getSynapseFromAutocomplete(datum.id) } else { Create.newSynapse.description = datum.value - Metamaps.Synapse.createSynapseLocally() + Synapse.createSynapseLocally() } }) }, @@ -338,8 +338,8 @@ const Create = { Create.newTopic.addSynapse = false Create.newSynapse.topic1id = 0 Create.newSynapse.topic2id = 0 - Metamaps.Mouse.synapseStartCoordinates = [] - Metamaps.Visualize.mGraph.plot() + Mouse.synapseStartCoordinates = [] + Visualize.mGraph.plot() }, } } diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index cc21f7e2..aed9964d 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,19 +1,20 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import Settings from './Settings' +import Visualize from './Visualize' + /* * Metamaps.Filter.js.erb * * Dependencies: - * - Metamaps.Active - * - Metamaps.Control * - Metamaps.Creators * - Metamaps.GlobalUI * - Metamaps.Mappers * - Metamaps.Metacodes - * - Metamaps.Settings * - Metamaps.Synapses * - Metamaps.Topics - * - Metamaps.Visualize */ const Filter = { filters: { @@ -216,7 +217,7 @@ const Filter = { }, checkMappers: function () { var self = Filter - var onMap = Metamaps.Active.Map ? true : false + var onMap = Active.Map ? true : false if (onMap) { self.updateFilters('Mappings', 'user_id', 'Mappers', 'mappers', 'mapper') } else { @@ -347,10 +348,10 @@ const Filter = { var passesMetacode, passesMapper, passesSynapse var onMap - if (Metamaps.Active.Map) { + if (Active.Map) { onMap = true } - else if (Metamaps.Active.Topic) { + else if (Active.Topic) { onMap = false } @@ -386,10 +387,10 @@ const Filter = { else console.log(topic) } else { if (n) { - Metamaps.Control.deselectNode(n, true) + Control.deselectNode(n, true) n.setData('alpha', opacityForFilter, 'end') n.eachAdjacency(function (e) { - Metamaps.Control.deselectEdge(e, true) + Control.deselectEdge(e, true) }) } else console.log(topic) @@ -442,12 +443,12 @@ const Filter = { if (visible.mappers.indexOf(user_id) == -1) passesMapper = false else passesMapper = true - var color = Metamaps.Settings.colors.synapses.normal + var color = Settings.colors.synapses.normal if (passesSynapse && passesMapper) { e.setData('alpha', 1, 'end') e.setData('color', color, 'end') } else { - Metamaps.Control.deselectEdge(e, true) + Control.deselectEdge(e, true) e.setData('alpha', opacityForFilter, 'end') } @@ -457,7 +458,7 @@ const Filter = { }) // run the animation - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['node-property:alpha', 'edge-property:alpha'], duration: 200 diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index e963ca32..bc0bab30 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,12 +1,13 @@ /* global Metamaps, $ */ +import Active from './Active' +import Map from './Map' + /* * Metamaps.Import.js.erb * * Dependencies: - * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.Map * - Metamaps.Mappings * - Metamaps.Metacodes * - Metamaps.Synapses @@ -256,15 +257,15 @@ const Import = { createTopicWithParameters: function (name, metacode_name, permission, desc, link, xloc, yloc, import_id, opts) { var self = Import - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null if (metacode === null) { metacode = Metamaps.Metacodes.where({ name: 'Wildcard' })[0] console.warn("Couldn't find metacode " + metacode_name + ' so used Wildcard instead.') } - var topic_permission = permission || Metamaps.Active.Map.get('permission') - var defer_to_map_id = permission === topic_permission ? Metamaps.Active.Map.get('id') : null + var topic_permission = permission || Active.Map.get('permission') + var defer_to_map_id = permission === topic_permission ? Active.Map.get('id') : null var topic = new Metamaps.Backbone.Topic({ name: name, metacode_id: metacode.id, @@ -272,7 +273,7 @@ const Import = { defer_to_map_id: defer_to_map_id, desc: desc || "", link: link || "", - calculated_permission: Metamaps.Active.Map.get('permission') + calculated_permission: Active.Map.get('permission') }) Metamaps.Topics.add(topic) diff --git a/frontend/src/Metamaps/Mobile.js b/frontend/src/Metamaps/Mobile.js index 9074f521..fddd90a4 100644 --- a/frontend/src/Metamaps/Mobile.js +++ b/frontend/src/Metamaps/Mobile.js @@ -1,12 +1,7 @@ -/* global Metamaps, $ */ +/* global $ */ -/* - * Metamaps.Mobile.js - * - * Dependencies: - * - Metamaps.Active - * - Metamaps.Map - */ +import Active from './Active' +import Map from './Map' const Mobile = { init: function () { @@ -30,8 +25,8 @@ const Mobile = { $('#mobile_menu').toggle() }, titleClick: function () { - if (Metamaps.Active.Map) { - Metamaps.Map.InfoBox.open() + if (Active.Map) { + Map.InfoBox.open() } } } diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 93ebb646..e0315486 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -1,14 +1,9 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import Mapper from './Mapper' +import Visualize from './Visualize' -/* - * Metamaps.SynapseCard.js - * - * Dependencies: - * - Metamaps.Active - * - Metamaps.Control - * - Metamaps.Mapper - * - Metamaps.Visualize - */ const SynapseCard = { openSynapseCard: null, showCard: function (edge, e) { @@ -20,7 +15,7 @@ const SynapseCard = { $('#edit_synapse').remove() // so label is missing while editing - Metamaps.Control.deselectEdge(edge) + Control.deselectEdge(edge) var index = edge.getData('displayIndex') ? edge.getData('displayIndex') : 0 var synapse = edge.getData('synapses')[index]; // for now, just get the first synapse @@ -30,9 +25,9 @@ const SynapseCard = { var edit_div = document.createElement('div') edit_div.innerHTML = '<div id="editSynUpperBar"></div><div id="editSynLowerBar"></div>' edit_div.setAttribute('id', 'edit_synapse') - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + if (synapse.authorizeToEdit(Active.Mapper)) { edit_div.className = 'permission canEdit' - edit_div.className += synapse.authorizePermissionChange(Metamaps.Active.Mapper) ? ' yourEdge' : '' + edit_div.className += synapse.authorizePermissionChange(Active.Mapper) ? ' yourEdge' : '' } else { edit_div.className = 'permission cannotEdit' } @@ -94,7 +89,7 @@ const SynapseCard = { // if edge data is blank or just whitespace, populate it with data_nil if ($('#edit_synapse_desc').html().trim() == '') { - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + if (synapse.authorizeToEdit(Active.Mapper)) { $('#edit_synapse_desc').html(data_nil) } else { $('#edit_synapse_desc').html('(no description)') @@ -109,8 +104,8 @@ const SynapseCard = { synapse.set('desc', desc) } synapse.trigger('saved') - Metamaps.Control.selectEdge(synapse.get('edge')) - Metamaps.Visualize.mGraph.plot() + Control.selectEdge(synapse.get('edge')) + Visualize.mGraph.plot() }) }, add_drop_down: function (edge, synapse) { @@ -152,7 +147,7 @@ const SynapseCard = { e.stopPropagation() var index = parseInt($(this).attr('data-synapse-index')) edge.setData('displayIndex', index) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() SynapseCard.showCard(edge, false) }) } @@ -167,7 +162,7 @@ const SynapseCard = { var setMapperImage = function (mapper) { $('#edgeUser img').attr('src', mapper.get('image')) } - Metamaps.Mapper.get(synapse.get('user_id'), setMapperImage) + Mapper.get(synapse.get('user_id'), setMapperImage) }, add_perms_form: function (synapse) { @@ -210,7 +205,7 @@ const SynapseCard = { $('#edit_synapse .permissionSelect').remove() } - if (synapse.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (synapse.authorizePermissionChange(Active.Mapper)) { $('#edit_synapse.yourEdge .mapPerm').click(openPermissionSelect) $('#edit_synapse').click(hidePermissionSelect) } @@ -257,7 +252,7 @@ const SynapseCard = { $('#edit_synapse_right').addClass('checked') } - if (synapse.authorizeToEdit(Metamaps.Active.Mapper)) { + if (synapse.authorizeToEdit(Active.Mapper)) { $('#edit_synapse_left, #edit_synapse_right').click(function () { $(this).toggleClass('checked') @@ -281,7 +276,7 @@ const SynapseCard = { node1_id: dir[0], node2_id: dir[1] }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }) } // if } // add_direction_form diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 0ebcc118..ab93e419 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,26 +1,25 @@ /* global Metamaps, $ */ +import Active from './Active' +import JIT from './JIT' +import Selected from './Selected' +import Settings from './Settings' +import Util from './Util' + /* * Metamaps.Topic.js.erb * * Dependencies: - * - Metamaps.Active - * - Metamaps.Backbone * - Metamaps.Backbone * - Metamaps.Create * - Metamaps.Creators - * - Metamaps.Famous * - Metamaps.Filter * - Metamaps.GlobalUI - * - Metamaps.JIT * - Metamaps.Mappings - * - Metamaps.Selected - * - Metamaps.Settings * - Metamaps.SynapseCard * - Metamaps.Synapses * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Util * - Metamaps.Visualize */ @@ -58,7 +57,7 @@ const Topic = { launch: function (id) { var bb = Metamaps.Backbone var start = function (data) { - Metamaps.Active.Topic = new bb.Topic(data.topic) + Active.Topic = new bb.Topic(data.topic) Metamaps.Creators = new bb.MapperCollection(data.creators) Metamaps.Topics = new bb.TopicCollection([data.topic].concat(data.relatives)) Metamaps.Synapses = new bb.SynapseCollection(data.synapses) @@ -69,13 +68,13 @@ const Topic = { // build and render the visualization Metamaps.Visualize.type = 'RGraph' - Metamaps.JIT.prepareVizData() + JIT.prepareVizData() // update filters Metamaps.Filter.reset() // reset selected arrays - Metamaps.Selected.reset() + Selected.reset() // these three update the actual filter box with the right list items Metamaps.Filter.checkMetacodes() @@ -83,7 +82,7 @@ const Topic = { Metamaps.Filter.checkMappers() // for mobile - $('#header_content').html(Metamaps.Active.Topic.get('name')) + $('#header_content').html(Active.Topic.get('name')) } $.ajax({ @@ -92,7 +91,7 @@ const Topic = { }) }, end: function () { - if (Metamaps.Active.Topic) { + if (Active.Topic) { $('.rightclickmenu').remove() Metamaps.TopicCard.hideCard() Metamaps.SynapseCard.hideCard() @@ -110,7 +109,7 @@ const Topic = { } }) Metamaps.Router.navigate('/topics/' + nodeid) - Metamaps.Active.Topic = Metamaps.Topics.get(nodeid) + Active.Topic = Metamaps.Topics.get(nodeid) } }, fetchRelatives: function (nodes, metacode_id) { @@ -141,7 +140,7 @@ const Topic = { topicColl.add(topic) var synapseColl = new Metamaps.Backbone.SynapseCollection(data.synapses) - var graph = Metamaps.JIT.convertModelsToJIT(topicColl, synapseColl)[0] + var graph = JIT.convertModelsToJIT(topicColl, synapseColl)[0] Metamaps.Visualize.mGraph.op.sum(graph, { type: 'fade', duration: 500, @@ -267,14 +266,14 @@ const Topic = { mappableid: mappingModel.get('mappable_id') } - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + $(document).trigger(JIT.events.newTopic, [newTopicData]) // call a success callback if provided if (opts.success) { opts.success(topicModel) } } var topicSuccessCallback = function (topicModel, response) { - if (Metamaps.Active.Map) { + if (Active.Map) { mapping.save({ mappable_id: topicModel.id }, { success: function (model, response) { mappingSuccessCallback(model, response, topicModel) @@ -290,7 +289,7 @@ const Topic = { } } - if (!Metamaps.Settings.sandbox && createNewInDB) { + if (!Settings.sandbox && createNewInDB) { if (topic.isNew()) { topic.save(null, { success: topicSuccessCallback, @@ -298,7 +297,7 @@ const Topic = { console.log('error saving topic to database') } }) - } else if (!topic.isNew() && Metamaps.Active.Map) { + } else if (!topic.isNew() && Active.Map) { mapping.save(null, { success: mappingSuccessCallback }) @@ -323,7 +322,7 @@ const Topic = { var topic = new Metamaps.Backbone.Topic({ name: Metamaps.Create.newTopic.name, metacode_id: metacode.id, - defer_to_map_id: Metamaps.Active.Map.id + defer_to_map_id: Active.Map.id }) Metamaps.Topics.add(topic) diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 5a7f1920..ebc79575 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,16 +1,17 @@ /* global Metamaps, $ */ +import Active from './Active' +import Mapper from './Mapper' +import Util from './Util' +import Visualize from './Visualize' + /* * Metamaps.TopicCard.js * * Dependencies: - * - Metamaps.Active * - Metamaps.GlobalUI - * - Metamaps.Mapper * - Metamaps.Metacodes * - Metamaps.Router - * - Metamaps.Util - * - Metamaps.Visualize */ const TopicCard = { openTopicCard: null, // stores the topic that's currently open @@ -43,7 +44,7 @@ const TopicCard = { var topic = node.getData('topic') self.openTopicCard = topic - self.authorizedToEdit = topic.authorizeToEdit(Metamaps.Active.Mapper) + 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() { @@ -96,7 +97,7 @@ const TopicCard = { var setMapperImage = function (mapper) { $('.contributorIcon').attr('src', mapper.get('image')) } - Metamaps.Mapper.get(topic.get('user_id'), setMapperImage) + Mapper.get(topic.get('user_id'), setMapperImage) // starting embed.ly var resetFunc = function () { @@ -179,7 +180,7 @@ const TopicCard = { topic.save({ metacode_id: metacode.id }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() $('.metacodeSelect').hide().removeClass('onRightEdge onBottomEdge') $('.metacodeTitle').hide() $('.showcard .icon').css('z-index', '1') @@ -265,7 +266,7 @@ const TopicCard = { // bind best_in_place ajax callbacks bipName.bind('ajax:success', function () { - var name = Metamaps.Util.decodeEntities($(this).html()) + var name = Util.decodeEntities($(this).html()) topic.set('name', name) topic.trigger('saved') }) @@ -313,7 +314,7 @@ const TopicCard = { } // ability to change permission var selectingPermission = false - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) { + if (topic.authorizePermissionChange(Active.Mapper)) { $('.showcard .yourTopic .mapPerm').click(openPermissionSelect) $('.showcard').click(hidePermissionSelect) } @@ -364,11 +365,11 @@ const TopicCard = { var topicForTemplate = self.buildObject(topic) var html = self.generateShowcardHTML.render(topicForTemplate) - if (topic.authorizeToEdit(Metamaps.Active.Mapper)) { + if (topic.authorizeToEdit(Active.Mapper)) { var perm = document.createElement('div') var string = 'permission canEdit' - if (topic.authorizePermissionChange(Metamaps.Active.Mapper)) string += ' yourTopic' + if (topic.authorizePermissionChange(Active.Mapper)) string += ' yourTopic' perm.className = string perm.innerHTML = html showCard.appendChild(perm) @@ -388,7 +389,7 @@ const TopicCard = { var nodeValues = {} - var authorized = topic.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = topic.authorizeToEdit(Active.Mapper) if (!authorized) { } else { diff --git a/frontend/src/Metamaps/Views.js b/frontend/src/Metamaps/Views.js index 90cd466d..aee0fdf0 100644 --- a/frontend/src/Metamaps/Views.js +++ b/frontend/src/Metamaps/Views.js @@ -1,12 +1,14 @@ /* global Metamaps, $ */ +import Active from './Active' +import ReactComponents from './ReactComponents' +import ReactDOM from 'react-dom' // TODO ensure this isn't a double import + /* * Metamaps.Views.js.erb * * Dependencies: * - Metamaps.Loading - * - Metamaps.Active - * - Metamaps.ReactComponents */ const Views = { @@ -33,7 +35,7 @@ const Views = { } var exploreObj = { - currentUser: Metamaps.Active.Mapper, + currentUser: Active.Mapper, section: self.collection.id, displayStyle: 'grid', maps: self.collection, @@ -42,7 +44,7 @@ const Views = { loadMore: self.loadMore } ReactDOM.render( - React.createElement(Metamaps.ReactComponents.Maps, exploreObj), + React.createElement(ReactComponents.Maps, exploreObj), document.getElementById('explore') ) diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 4aa65772..9e44e8e8 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,10 +1,12 @@ /* global Metamaps, $ */ + +import Active from './Active' +import JIT from './JIT' + /* * Metamaps.Visualize * * Dependencies: - * - Metamaps.Active - * - Metamaps.JIT * - Metamaps.Loading * - Metamaps.Metacodes * - Metamaps.Router @@ -34,7 +36,7 @@ const Visualize = { // prevent touch events on the canvas from default behaviour $('#infovis-canvas').bind('touchmove', function (event) { - // Metamaps.JIT.touchPanZoomHandler(event) + // JIT.touchPanZoomHandler(event) }) // prevent touch events on the canvas from default behaviour @@ -116,25 +118,25 @@ const Visualize = { // clear the previous canvas from #infovis $('#infovis').empty() - RGraphSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + RGraphSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) - $jit.RGraph.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) - $jit.RGraph.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + $jit.RGraph.Plot.NodeTypes.implement(JIT.ForceDirected.nodeSettings) + $jit.RGraph.Plot.EdgeTypes.implement(JIT.ForceDirected.edgeSettings) RGraphSettings.width = $(document).width() RGraphSettings.height = $(document).height() - RGraphSettings.background = Metamaps.JIT.RGraph.background - RGraphSettings.levelDistance = Metamaps.JIT.RGraph.levelDistance + RGraphSettings.background = JIT.RGraph.background + RGraphSettings.levelDistance = JIT.RGraph.levelDistance self.mGraph = new $jit.RGraph(RGraphSettings) } else if (self.type == 'ForceDirected' && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { // clear the previous canvas from #infovis $('#infovis').empty() - FDSettings = $.extend(true, {}, Metamaps.JIT.ForceDirected.graphSettings) + FDSettings = $.extend(true, {}, JIT.ForceDirected.graphSettings) - $jit.ForceDirected.Plot.NodeTypes.implement(Metamaps.JIT.ForceDirected.nodeSettings) - $jit.ForceDirected.Plot.EdgeTypes.implement(Metamaps.JIT.ForceDirected.edgeSettings) + $jit.ForceDirected.Plot.NodeTypes.implement(JIT.ForceDirected.nodeSettings) + $jit.ForceDirected.Plot.EdgeTypes.implement(JIT.ForceDirected.edgeSettings) FDSettings.width = $('body').width() FDSettings.height = $('body').height() @@ -145,14 +147,14 @@ const Visualize = { $('#infovis').empty() // init ForceDirected3D - self.mGraph = new $jit.ForceDirected3D(Metamaps.JIT.ForceDirected3D.graphSettings) + self.mGraph = new $jit.ForceDirected3D(JIT.ForceDirected3D.graphSettings) self.cameraPosition = self.mGraph.canvas.canvases[0].camera.position } else { self.mGraph.graph.empty() } - if (self.type == 'ForceDirected' && Metamaps.Active.Mapper) $.post('/maps/' + Metamaps.Active.Map.id + '/events/user_presence') + if (self.type == 'ForceDirected' && Active.Mapper) $.post('/maps/' + Active.Map.id + '/events/user_presence') function runAnimation () { Metamaps.Loading.hide() @@ -160,22 +162,22 @@ const Visualize = { if (!self.loadLater) { // load JSON data. var rootIndex = 0 - if (Metamaps.Active.Topic) { - var node = _.find(Metamaps.JIT.vizData, function (node) { - return node.id === Metamaps.Active.Topic.id + if (Active.Topic) { + var node = _.find(JIT.vizData, function (node) { + return node.id === Active.Topic.id }) - rootIndex = _.indexOf(Metamaps.JIT.vizData, node) + rootIndex = _.indexOf(JIT.vizData, node) } - self.mGraph.loadJSON(Metamaps.JIT.vizData, rootIndex) + self.mGraph.loadJSON(JIT.vizData, rootIndex) // compute positions and plot. self.computePositions() self.mGraph.busy = true if (self.type == 'RGraph') { - self.mGraph.fx.animate(Metamaps.JIT.RGraph.animate) + self.mGraph.fx.animate(JIT.RGraph.animate) } else if (self.type == 'ForceDirected') { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateSavedLayout) + self.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (self.type == 'ForceDirected3D') { - self.mGraph.animate(Metamaps.JIT.ForceDirected.animateFDLayout) + self.mGraph.animate(JIT.ForceDirected.animateFDLayout) } } } @@ -204,8 +206,8 @@ const Visualize = { // update the url now that the map is ready clearTimeout(Metamaps.Router.timeoutId) Metamaps.Router.timeoutId = setTimeout(function () { - var m = Metamaps.Active.Map - var t = Metamaps.Active.Topic + var m = Active.Map + var t = Active.Topic if (m && window.location.pathname !== '/maps/' + m.id) { Metamaps.Router.navigate('/maps/' + m.id) From 120c2c0b673d21b32e372820fce78b16b3d68c4b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 18:31:56 +0800 Subject: [PATCH 031/306] finish most except Backbone --- frontend/src/Metamaps/Account.js | 9 +- frontend/src/Metamaps/AutoLayout.js | 4 +- frontend/src/Metamaps/Backbone.js | 30 +- frontend/src/Metamaps/Control.js | 32 +- frontend/src/Metamaps/Create.js | 4 +- frontend/src/Metamaps/Debug.js | 6 +- frontend/src/Metamaps/Filter.js | 4 +- frontend/src/Metamaps/GlobalUI.js | 557 ++++++++++++++------------- frontend/src/Metamaps/Import.js | 9 +- frontend/src/Metamaps/JIT.js | 485 ++++++++++++----------- frontend/src/Metamaps/Listeners.js | 77 ++-- frontend/src/Metamaps/Map.js | 180 ++++----- frontend/src/Metamaps/Mapper.js | 6 +- frontend/src/Metamaps/PasteInput.js | 6 +- frontend/src/Metamaps/Realtime.js | 320 ++++++++------- frontend/src/Metamaps/Router.js | 158 ++++---- frontend/src/Metamaps/Synapse.js | 52 +-- frontend/src/Metamaps/SynapseCard.js | 2 +- frontend/src/Metamaps/Topic.js | 127 +++--- frontend/src/Metamaps/TopicCard.js | 8 +- frontend/src/Metamaps/Visualize.js | 14 +- frontend/src/Metamaps/index.js | 20 +- 22 files changed, 1077 insertions(+), 1033 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index f424019f..10311cbd 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,12 +1,11 @@ -/* uses window.Metamaps.Erb */ +/* + * Metamaps.Erb + */ const Account = { listenersInitialized: false, - init: function () { - var self = Metamaps.Account - }, initListeners: function () { - var self = Metamaps.Account + var self = Account $('#user_image').change(self.showImagePreview) self.listenersInitialized = true diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index 386b61ef..ee9dc33c 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -8,7 +8,7 @@ const AutoLayout = { timeToTurn: 0, getNextCoord: function () { - var self = Metamaps.AutoLayout + var self = AutoLayout var nextX = self.nextX var nextY = self.nextY @@ -55,7 +55,7 @@ const AutoLayout = { } }, resetSpiral: function () { - var self = Metamaps.AutoLayout + var self = AutoLayout self.nextX = 0 self.nextY = 0 self.nextXshift = 1 diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone.js index ce62c6be..9f18ef32 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone.js @@ -26,9 +26,9 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Visualize */ -Metamaps.Backbone = {} +const _Backbone = {} -Metamaps.Backbone.Map = Backbone.Model.extend({ +_Backbone.Map = Backbone.Model.extend({ urlRoot: '/maps', blacklist: ['created_at', 'updated_at', 'created_at_clean', 'updated_at_clean', 'user_name', 'contributor_count', 'topic_count', 'synapse_count', 'topics', 'synapses', 'mappings', 'mappers'], toJSON: function (options) { @@ -82,7 +82,7 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ return Metamaps.Mapper.get(this.get('user_id')) }, fetchContained: function () { - var bb = Metamaps.Backbone + var bb = _Backbone var that = this var start = function (data) { that.set('mappers', new bb.MapperCollection(data.mappers)) @@ -143,8 +143,8 @@ Metamaps.Backbone.Map = Backbone.Model.extend({ } } }) -Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Map, +_Backbone.MapsCollection = Backbone.Collection.extend({ + model: _Backbone.Map, initialize: function (models, options) { this.id = options.id this.sortBy = options.sortBy @@ -211,7 +211,7 @@ Metamaps.Backbone.MapsCollection = Backbone.Collection.extend({ } }) -Metamaps.Backbone.Message = Backbone.Model.extend({ +_Backbone.Message = Backbone.Model.extend({ urlRoot: '/messages', blacklist: ['created_at', 'updated_at'], toJSON: function (options) { @@ -227,12 +227,12 @@ Metamaps.Backbone.Message = Backbone.Model.extend({ */ } }) -Metamaps.Backbone.MessageCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Message, +_Backbone.MessageCollection = Backbone.Collection.extend({ + model: _Backbone.Message, url: '/messages' }) -Metamaps.Backbone.Mapper = Backbone.Model.extend({ +_Backbone.Mapper = Backbone.Model.extend({ urlRoot: '/users', blacklist: ['created_at', 'updated_at'], toJSON: function (options) { @@ -248,13 +248,13 @@ Metamaps.Backbone.Mapper = Backbone.Model.extend({ } }) -Metamaps.Backbone.MapperCollection = Backbone.Collection.extend({ - model: Metamaps.Backbone.Mapper, +_Backbone.MapperCollection = Backbone.Collection.extend({ + model: _Backbone.Mapper, url: '/users' }) -Metamaps.Backbone.init = function () { - var self = Metamaps.Backbone +_Backbone.init = function () { + var self = _Backbone self.Metacode = Backbone.Model.extend({ initialize: function () { @@ -694,6 +694,6 @@ Metamaps.Backbone.init = function () { } } self.attachCollectionEvents() -}; // end Metamaps.Backbone.init +}; // end _Backbone.init -export default Metamaps.Backbone +export default _Backbone diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 3eecb126..9e13e40c 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -2,6 +2,7 @@ import Active from './Active' import Filter from './Filter' +import GlobalUI from './GlobalUI' import JIT from './JIT' import Mouse from './Mouse' import Selected from './Selected' @@ -12,7 +13,6 @@ import Visualize from './Visualize' * Metamaps.Control.js * * Dependencies: - * - Metamaps.GlobalUI * - Metamaps.Mappings * - Metamaps.Metacodes * - Metamaps.Synapses @@ -57,7 +57,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -73,7 +73,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -89,7 +89,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -107,7 +107,7 @@ const Control = { }]) Control.hideNode(nodeid) } else { - Metamaps.GlobalUI.notifyUser('Only topics you created can be deleted') + GlobalUI.notifyUser('Only topics you created can be deleted') } }, removeSelectedNodes: function () { // refers to removing topics permanently from a map @@ -132,7 +132,7 @@ const Control = { authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -148,7 +148,7 @@ const Control = { var node = Visualize.mGraph.graph.getNode(nodeid) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -251,7 +251,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -266,7 +266,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -294,7 +294,7 @@ const Control = { mappableid: mappableid }]) } else { - Metamaps.GlobalUI.notifyUser('Only synapses you created can be deleted') + GlobalUI.notifyUser('Only synapses you created can be deleted') } }, removeSelectedEdges: function () { @@ -308,7 +308,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -324,7 +324,7 @@ const Control = { var authorized = Active.Map.authorizeToEdit(Active.Mapper) if (!authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') return } @@ -378,7 +378,7 @@ const Control = { updateSelectedPermissions: function (permission) { var edge, synapse, node, topic - Metamaps.GlobalUI.notifyUser('Working...') + GlobalUI.notifyUser('Working...') // variables to keep track of how many nodes and synapses you had the ability to change the permission of var nCount = 0, @@ -416,12 +416,12 @@ const Control = { var sString = sCount == 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') var message = nString + sString + ' you created updated to ' + permission - Metamaps.GlobalUI.notifyUser(message) + GlobalUI.notifyUser(message) }, updateSelectedMetacodes: function (metacode_id) { var node, topic - Metamaps.GlobalUI.notifyUser('Working...') + GlobalUI.notifyUser('Working...') var metacode = Metamaps.Metacodes.get(metacode_id) @@ -445,7 +445,7 @@ const Control = { var nString = nCount == 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') var message = nString + ' you can edit updated to ' + metacode.get('name') - Metamaps.GlobalUI.notifyUser(message) + GlobalUI.notifyUser(message) Visualize.mGraph.plot() }, } diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 49267d6d..c9252aba 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -5,13 +5,13 @@ import Selected from './Selected' import Synapse from './Synapse' import Topic from './Topic' import Visualize from './Visualize' +import GlobalUI from './GlobalUI' /* * Metamaps.Create.js * * Dependencies: * - Metamaps.Backbone - * - Metamaps.GlobalUI * - Metamaps.Metacodes */ @@ -101,7 +101,7 @@ const Create = { bringToFront: true }) - Metamaps.GlobalUI.closeLightbox() + GlobalUI.closeLightbox() $('#topic_name').focus() var mdata = { diff --git a/frontend/src/Metamaps/Debug.js b/frontend/src/Metamaps/Debug.js index e8e40e69..0fe5f769 100644 --- a/frontend/src/Metamaps/Debug.js +++ b/frontend/src/Metamaps/Debug.js @@ -1,6 +1,6 @@ -const Debug = () => { - console.debug(window.Metamaps) - console.debug(`Metamaps Version: ${window.Metamaps.VERSION}`) +const Debug = (arg = window.Metamaps) => { + console.debug(arg) + console.debug(`Metamaps Version: ${arg.VERSION}`) } export default Debug diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index aed9964d..38c4f369 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -2,6 +2,7 @@ import Active from './Active' import Control from './Control' +import GlobalUI from './GlobalUI' import Settings from './Settings' import Visualize from './Visualize' @@ -10,7 +11,6 @@ import Visualize from './Visualize' * * Dependencies: * - Metamaps.Creators - * - Metamaps.GlobalUI * - Metamaps.Mappers * - Metamaps.Metacodes * - Metamaps.Synapses @@ -56,7 +56,7 @@ const Filter = { open: function () { var self = Filter - Metamaps.GlobalUI.Account.close() + GlobalUI.Account.close() $('.sidebarFilterIcon div').addClass('hide') if (!self.isOpen && !self.changing) { diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index 5abf25ee..b24b31c7 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,317 +1,326 @@ -window.Metamaps = window.Metamaps || {}; +import Active from './Active' +import Create from './Create' +import Filter from './Filter' +import Router from './Router' + +/* + * Metamaps.Backbone + * Metamaps.Erb + * Metamaps.Maps + */ const GlobalUI = { - notifyTimeout: null, - lightbox: null, - init: function () { - var self = GlobalUI; + notifyTimeout: null, + lightbox: null, + init: function () { + var self = GlobalUI; - self.Search.init(); - self.CreateMap.init(); - self.Account.init(); - - if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) + self.Search.init(); + self.CreateMap.init(); + self.Account.init(); - //bind lightbox clicks - $('.openLightbox').click(function (event) { - self.openLightbox($(this).attr('data-open')); - event.preventDefault(); - return false; - }); + if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) - $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); + //bind lightbox clicks + $('.openLightbox').click(function (event) { + self.openLightbox($(this).attr('data-open')); + event.preventDefault(); + return false; + }); - // initialize global backbone models and collections - if (Metamaps.Active.Mapper) Metamaps.Active.Mapper = new Metamaps.Backbone.Mapper(Metamaps.Active.Mapper); + $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; - var mapperCollection = []; - var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; - if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models; - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; - } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; - Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); - Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); - Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); - // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); - Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); - Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); - }, - showDiv: function (selector) { - $(selector).show() - $(selector).animate({ - opacity: 1 - }, 200, 'easeOutCubic') - }, - hideDiv: function (selector) { - $(selector).animate({ - opacity: 0 - }, 200, 'easeInCubic', function () { $(this).hide() }) - }, - openLightbox: function (which) { - var self = GlobalUI; + // initialize global backbone models and collections + if (Active.Mapper) Active.Mapper = new Metamaps.Backbone.Mapper(Active.Mapper); - $('.lightboxContent').hide(); - $('#' + which).show(); - - self.lightbox = which; - - $('#lightbox_overlay').show(); - - var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; - // animate the content in from the bottom - $('#lightbox_main').animate({ - 'top': '50%', - 'margin-top': heightOfContent - }, 200, 'easeOutCubic'); - - // fade the black overlay in - $('#lightbox_screen').animate({ - 'opacity': '0.42' - }, 200); - - if (which == "switchMetacodes") { - Metamaps.Create.isSwitchingSet = true; - } - }, - - closeLightbox: function (event) { - var self = GlobalUI; - - if (event) event.preventDefault(); - - // animate the lightbox content offscreen - $('#lightbox_main').animate({ - 'top': '100%', - 'margin-top': '0' - }, 200, 'easeInCubic'); - - // fade the black overlay out - $('#lightbox_screen').animate({ - 'opacity': '0.0' - }, 200, function () { - $('#lightbox_overlay').hide(); - }); - - if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); - if (Metamaps.Create && Metamaps.Create.isSwitchingSet) { - Metamaps.Create.cancelMetacodeSetSwitch(); - } - self.lightbox = null; - }, - notifyUser: function (message, leaveOpen) { - var self = GlobalUI; - - $('#toast').html(message) - self.showDiv('#toast') - clearTimeout(self.notifyTimeOut); - if (!leaveOpen) { - self.notifyTimeOut = setTimeout(function () { - self.hideDiv('#toast') - }, 8000); - } - }, - clearNotify: function() { - var self = GlobalUI; - - clearTimeout(self.notifyTimeOut); - self.hideDiv('#toast') - }, - shareInvite: function(inviteLink) { - window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; + var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; + var mapperCollection = []; + var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; + if (Metamaps.Maps.Mapper) { + mapperCollection = Metamaps.Maps.Mapper.models; + mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; } + var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; + var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; + Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); + Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); + // 'Mapper' refers to another mapper + Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); + Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); + Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); + }, + showDiv: function (selector) { + $(selector).show() + $(selector).animate({ + opacity: 1 + }, 200, 'easeOutCubic') + }, + hideDiv: function (selector) { + $(selector).animate({ + opacity: 0 + }, 200, 'easeInCubic', function () { $(this).hide() }) + }, + openLightbox: function (which) { + var self = GlobalUI; + + $('.lightboxContent').hide(); + $('#' + which).show(); + + self.lightbox = which; + + $('#lightbox_overlay').show(); + + var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; + // animate the content in from the bottom + $('#lightbox_main').animate({ + 'top': '50%', + 'margin-top': heightOfContent + }, 200, 'easeOutCubic'); + + // fade the black overlay in + $('#lightbox_screen').animate({ + 'opacity': '0.42' + }, 200); + + if (which == "switchMetacodes") { + Create.isSwitchingSet = true; + } + }, + + closeLightbox: function (event) { + var self = GlobalUI; + + if (event) event.preventDefault(); + + // animate the lightbox content offscreen + $('#lightbox_main').animate({ + 'top': '100%', + 'margin-top': '0' + }, 200, 'easeInCubic'); + + // fade the black overlay out + $('#lightbox_screen').animate({ + 'opacity': '0.0' + }, 200, function () { + $('#lightbox_overlay').hide(); + }); + + if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); + if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); + if (Create && Create.isSwitchingSet) { + Create.cancelMetacodeSetSwitch(); + } + self.lightbox = null; + }, + notifyUser: function (message, leaveOpen) { + var self = GlobalUI; + + $('#toast').html(message) + self.showDiv('#toast') + clearTimeout(self.notifyTimeOut); + if (!leaveOpen) { + self.notifyTimeOut = setTimeout(function () { + self.hideDiv('#toast') + }, 8000); + } + }, + clearNotify: function() { + var self = GlobalUI; + + clearTimeout(self.notifyTimeOut); + self.hideDiv('#toast') + }, + shareInvite: function(inviteLink) { + window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + } } GlobalUI.CreateMap = { - newMap: null, - emptyMapForm: "", - emptyForkMapForm: "", - topicsToMap: [], - synapsesToMap: [], - init: function () { - var self = GlobalUI.CreateMap; + newMap: null, + emptyMapForm: "", + emptyForkMapForm: "", + topicsToMap: [], + synapsesToMap: [], + init: function () { + var self = GlobalUI.CreateMap; - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - self.bindFormEvents(); + self.bindFormEvents(); - self.emptyMapForm = $('#new_map').html(); + self.emptyMapForm = $('#new_map').html(); - }, - bindFormEvents: function () { - var self = GlobalUI.CreateMap; + }, + bindFormEvents: function () { + var self = GlobalUI.CreateMap; - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { - if (event.keyCode === 13) self.submit() - }) + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { + if (event.keyCode === 13) self.submit() + }) - $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault(); - GlobalUI.closeLightbox(); - }); - $('.new_map button.submitMap').unbind().bind('click', self.submit); + $('.new_map button.cancel').unbind().bind('click', function (event) { + event.preventDefault(); + GlobalUI.closeLightbox(); + }); + $('.new_map button.submitMap').unbind().bind('click', self.submit); - // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission); - }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function(){ - $(this).remove(); - }); - }, - generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; - stringStart += id; - stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; - var page = Metamaps.Active.Map ? 'map' : 'page'; - var stringEnd = "</a></div>"; - return stringStart + page + stringEnd; - }, - switchPermission: function () { - var self = GlobalUI.CreateMap; + // bind permission changer events on the createMap form + $('.permIcon').unbind().bind('click', self.switchPermission); + }, + closeSuccess: function () { + $('#mapCreatedSuccess').fadeOut(300, function(){ + $(this).remove(); + }); + }, + generateSuccessMessage: function (id) { + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; + stringStart += id; + stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; + var page = Active.Map ? 'map' : 'page'; + var stringEnd = "</a></div>"; + return stringStart + page + stringEnd; + }, + switchPermission: function () { + var self = GlobalUI.CreateMap; - self.newMap.set('permission', $(this).attr('data-permission')); - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); - $(this).find('.mapPermIcon').addClass('selected'); + self.newMap.set('permission', $(this).attr('data-permission')); + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); + $(this).find('.mapPermIcon').addClass('selected'); - var permText = $(this).find('.tip').html(); - $(this).parents('.new_map').find('.permText').html(permText); - }, - submit: function (event) { - if (event) event.preventDefault(); + var permText = $(this).find('.tip').html(); + $(this).parents('.new_map').find('.permText').html(permText); + }, + submit: function (event) { + if (event) event.preventDefault(); - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap; - if (GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap); - self.newMap.set('synapsesToMap', self.synapsesToMap); - } + if (GlobalUI.lightbox === 'forkmap') { + self.newMap.set('topicsToMap', self.topicsToMap); + self.newMap.set('synapsesToMap', self.synapsesToMap); + } - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); - self.newMap.set('name', $form.find('#map_name').val()); - self.newMap.set('desc', $form.find('#map_desc').val()); + self.newMap.set('name', $form.find('#map_name').val()); + self.newMap.set('desc', $form.find('#map_desc').val()); - if (self.newMap.get('name').length===0){ - self.throwMapNameError(); - return; - } + if (self.newMap.get('name').length===0){ + self.throwMapNameError(); + return; + } - self.newMap.save(null, { - success: self.success - // TODO add error message - }); + self.newMap.save(null, { + success: self.success + // TODO add error message + }); - GlobalUI.closeLightbox(); - GlobalUI.notifyUser('Working...'); - }, - throwMapNameError: function () { - var self = GlobalUI.CreateMap; + GlobalUI.closeLightbox(); + GlobalUI.notifyUser('Working...'); + }, + throwMapNameError: function () { + var self = GlobalUI.CreateMap; - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var $form = $(formId); - var message = $("<div class='feedback_message'>Please enter a map name...</div>"); + var message = $("<div class='feedback_message'>Please enter a map name...</div>"); - $form.find('#map_name').after(message); - setTimeout(function(){ - message.fadeOut('fast', function(){ - message.remove(); - }); - }, 5000); - }, - success: function (model) { - var self = GlobalUI.CreateMap; + $form.find('#map_name').after(message); + setTimeout(function(){ + message.fadeOut('fast', function(){ + message.remove(); + }); + }, 5000); + }, + success: function (model) { + var self = GlobalUI.CreateMap; - //push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model); + //push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model); - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; + var form = $(formId); - GlobalUI.clearNotify(); - $('#wrapper').append(self.generateSuccessMessage(model.id)); + GlobalUI.clearNotify(); + $('#wrapper').append(self.generateSuccessMessage(model.id)); - }, - reset: function (id) { - var self = GlobalUI.CreateMap; + }, + reset: function (id) { + var self = GlobalUI.CreateMap; - var form = $('#' + id); + var form = $('#' + id); - if (id === "fork_map") { - self.topicsToMap = []; - self.synapsesToMap = []; - form.html(self.emptyForkMapForm); - } - else { - form.html(self.emptyMapForm); - } + if (id === "fork_map") { + self.topicsToMap = []; + self.synapsesToMap = []; + form.html(self.emptyForkMapForm); + } + else { + form.html(self.emptyMapForm); + } - self.bindFormEvents(); - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.bindFormEvents(); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); - return false; - }, + return false; + }, } GlobalUI.Account = { - isOpen: false, - changing: false, - init: function () { - var self = GlobalUI.Account; + isOpen: false, + changing: false, + init: function () { + var self = GlobalUI.Account; - $('.sidebarAccountIcon').click(self.toggleBox); - $('.sidebarAccountBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); - }, - toggleBox: function (event) { - var self = GlobalUI.Account; + $('.sidebarAccountIcon').click(self.toggleBox); + $('.sidebarAccountBox').click(function(event){ + event.stopPropagation(); + }); + $('body').click(self.close); + }, + toggleBox: function (event) { + var self = GlobalUI.Account; - if (self.isOpen) self.close(); - else self.open(); + if (self.isOpen) self.close(); + else self.open(); - event.stopPropagation(); - }, - open: function () { - var self = GlobalUI.Account; + event.stopPropagation(); + }, + open: function () { + var self = GlobalUI.Account; - Metamaps.Filter.close(); - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); + Filter.close(); + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); - if (!self.isOpen && !self.changing) { - self.changing = true; - $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - $('.sidebarAccountBox #user_email').focus(); - }); - } - }, - close: function () { - var self = GlobalUI.Account; - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); - if (!self.changing) { - self.changing = true; - $('.sidebarAccountBox #user_email').blur(); - $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); - } + if (!self.isOpen && !self.changing) { + self.changing = true; + $('.sidebarAccountBox').fadeIn(200, function () { + self.changing = false; + self.isOpen = true; + $('.sidebarAccountBox #user_email').focus(); + }); } + }, + close: function () { + var self = GlobalUI.Account; + + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); + if (!self.changing) { + self.changing = true; + $('.sidebarAccountBox #user_email').blur(); + $('.sidebarAccountBox').fadeOut(200, function () { + self.changing = false; + self.isOpen = false; + }); + } + } } GlobalUI.Search = { @@ -425,8 +434,8 @@ GlobalUI.Search = { startTypeahead: function () { var self = GlobalUI.Search; - var mapheader = Metamaps.Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; - var topicheader = Metamaps.Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; + var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; + var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; var topics = { @@ -455,8 +464,8 @@ GlobalUI.Search = { url: '/search/topics', prepare: function(query, settings) { settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitTopicsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + if (Active.Mapper && self.limitTopicsToMe) { + settings.url += "&user=" + Active.Mapper.id.toString(); } return settings; }, @@ -488,8 +497,8 @@ GlobalUI.Search = { url: '/search/maps', prepare: function(query, settings) { settings.url += '?term=' + query; - if (Metamaps.Active.Mapper && self.limitMapsToMe) { - settings.url += "&user=" + Metamaps.Active.Mapper.id.toString(); + if (Active.Mapper && self.limitMapsToMe) { + settings.url += "&user=" + Active.Mapper.id.toString(); } return settings; }, @@ -578,11 +587,11 @@ GlobalUI.Search = { self.close(0, true); var win; if (datum.rtype == "topic") { - Metamaps.Router.topics(datum.id); + Router.topics(datum.id); } else if (datum.rtype == "map") { - Metamaps.Router.maps(datum.id); + Router.maps(datum.id); } else if (datum.rtype == "mapper") { - Metamaps.Router.explore("mapper", datum.id); + Router.explore("mapper", datum.id); } } }, diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index bc0bab30..d5a4b4e1 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,7 +1,10 @@ /* global Metamaps, $ */ import Active from './Active' +import GlobalUI from './GlobalUI' import Map from './Map' +import Synapse from './Synapse' +import Topic from './Topic' /* * Metamaps.Import.js.erb @@ -290,11 +293,11 @@ const Import = { Metamaps.Mappings.add(mapping) // this function also includes the creation of the topic in the database - Metamaps.Topic.renderTopic(mapping, topic, true, true, { + Topic.renderTopic(mapping, topic, true, true, { success: opts.success }) - Metamaps.GlobalUI.hideDiv('#instructions') + GlobalUI.hideDiv('#instructions') }, createSynapseWithParameters: function (desc, category, permission, @@ -322,7 +325,7 @@ const Import = { }) Metamaps.Mappings.add(mapping) - Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, true) + Synapse.renderSynapse(mapping, synapse, node1, node2, true) } } diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index ec8195de..50c48985 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,5 +1,30 @@ /* global Metamaps */ +import Active from './Active' +import Control from './Control' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import Map from './Map' +import Mouse from './Mouse' +import Realtime from './Realtime' +import Selected from './Selected' +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 Visualize from './Visualize' + +/* + * Metamaps.Erb + * Metamaps.Mappings + * Metamaps.Metacodes + * Metamaps.Synapses + * Metamaps.Topics + */ + let panningInt const JIT = { @@ -30,11 +55,11 @@ const JIT = { $('.zoomOut').click(self.zoomOut) var zoomExtents = function (event) { - self.zoomExtents(event, Metamaps.Visualize.mGraph.canvas) + self.zoomExtents(event, Visualize.mGraph.canvas) } $('.zoomExtents').click(zoomExtents) - $('.takeScreenshot').click(Metamaps.Map.exportImage) + $('.takeScreenshot').click(Map.exportImage) self.topicDescImage = new Image() self.topicDescImage.src = Metamaps.Erb['topic_description_signifier.png'] @@ -80,7 +105,7 @@ const JIT = { if (existingEdge) { // for when you're dealing with multiple relationships between the same two topics - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = s.getMapping() existingEdge.data['$mappingIDs'].push(mapping.id) } @@ -105,7 +130,7 @@ const JIT = { // reset/empty vizData self.vizData = [] - Metamaps.Visualize.loadLater = false + Visualize.loadLater = false var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) @@ -121,12 +146,12 @@ const JIT = { if (self.vizData.length == 0) { $('#instructions div').hide() $('#instructions div.addTopic').show() - Metamaps.GlobalUI.showDiv('#instructions') - Metamaps.Visualize.loadLater = true + GlobalUI.showDiv('#instructions') + Visualize.loadLater = true } - else Metamaps.GlobalUI.hideDiv('#instructions') + else GlobalUI.hideDiv('#instructions') - Metamaps.Visualize.render() + Visualize.render() }, // prepareVizData edgeRender: function (adj, canvas) { // get nodes cartesian coordinates @@ -151,7 +176,7 @@ const JIT = { // label placement on edges if (canvas.denySelected) { - var color = Metamaps.Settings.colors.synapses.normal + var color = Settings.colors.synapses.normal canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color } JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) @@ -191,7 +216,7 @@ const JIT = { if (!canvas.denySelected && desc != '' && showDesc) { // '&' to '&' - desc = Metamaps.Util.decodeEntities(desc) + desc = Util.decodeEntities(desc) // now adjust the label placement var ctx = canvas.getCtx() @@ -199,7 +224,7 @@ const JIT = { ctx.fillStyle = '#FFF' ctx.textBaseline = 'alphabetic' - var arrayOfLabelLines = Metamaps.Util.splitLine(desc, 30).split('\n') + var arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') var index, lineWidths = [] for (index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) @@ -258,7 +283,7 @@ const JIT = { transition: $jit.Trans.Quad.easeInOut, duration: 800, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false $(document).trigger(JIT.events.animationDone) } }, @@ -267,7 +292,7 @@ const JIT = { transition: $jit.Trans.Elastic.easeOut, duration: 800, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false } }, graphSettings: { @@ -306,7 +331,7 @@ const JIT = { }, Edge: { overridable: true, - color: Metamaps.Settings.colors.synapses.normal, + color: Settings.colors.synapses.normal, type: 'customEdge', lineWidth: 2, alpha: 1 @@ -317,7 +342,7 @@ const JIT = { size: 20, family: 'arial', textBaseline: 'alphabetic', - color: Metamaps.Settings.colors.labels.text + color: Settings.colors.labels.text }, // Add Tips Tips: { @@ -359,26 +384,26 @@ const JIT = { // remove the rightclickmenu $('.rightclickmenu').remove() - if (Metamaps.Mouse.boxStartCoordinates) { + if (Mouse.boxStartCoordinates) { if (e.ctrlKey) { - Metamaps.Visualize.mGraph.busy = false - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + Visualize.mGraph.busy = false + Mouse.boxEndCoordinates = eventInfo.getPos() - var bS = Metamaps.Mouse.boxStartCoordinates - var bE = Metamaps.Mouse.boxEndCoordinates + var bS = Mouse.boxStartCoordinates + var bE = Mouse.boxEndCoordinates if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { JIT.zoomToBox(e) return } else { - Metamaps.Mouse.boxStartCoordinates = null - Metamaps.Mouse.boxEndCoordinates = null + Mouse.boxStartCoordinates = null + Mouse.boxEndCoordinates = null } // console.log('called zoom to box') } if (e.shiftKey) { - Metamaps.Visualize.mGraph.busy = false - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + Visualize.mGraph.busy = false + Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) // console.log('called select with box') return @@ -404,9 +429,9 @@ const JIT = { // remove the rightclickmenu $('.rightclickmenu').remove() - if (Metamaps.Mouse.boxStartCoordinates) { - Metamaps.Visualize.mGraph.busy = false - Metamaps.Mouse.boxEndCoordinates = eventInfo.getPos() + if (Mouse.boxStartCoordinates) { + Visualize.mGraph.busy = false + Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) return } @@ -441,7 +466,7 @@ const JIT = { if (!canvas.denySelected && node.selected) { ctx.beginPath() ctx.arc(pos.x, pos.y, dim + 3, 0, 2 * Math.PI, false) - ctx.strokeStyle = Metamaps.Settings.colors.topics.selected + ctx.strokeStyle = Settings.colors.topics.selected ctx.lineWidth = 2 ctx.stroke() } @@ -482,8 +507,8 @@ const JIT = { 'contains': function (node, pos) { var npos = node.pos.getc(true), dim = node.getData('dim'), - arrayOfLabelLines = Metamaps.Util.splitLine(node.name, 30).split('\n'), - ctx = Metamaps.Visualize.mGraph.canvas.getCtx() + arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n'), + ctx = Visualize.mGraph.canvas.getCtx() var height = 25 * arrayOfLabelLines.length @@ -528,7 +553,7 @@ const JIT = { transition: $jit.Trans.Elastic.easeOut, duration: 2500, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false } }, graphSettings: { @@ -589,13 +614,13 @@ const JIT = { onMouseMove: function (node, eventInfo, e) { // if(this.i++ % 3) return var pos = eventInfo.getPos() - Metamaps.Visualize.cameraPosition.x += (pos.x - Metamaps.Visualize.cameraPosition.x) * 0.5 - Metamaps.Visualize.cameraPosition.y += (-pos.y - Metamaps.Visualize.cameraPosition.y) * 0.5 - Metamaps.Visualize.mGraph.plot() + Visualize.cameraPosition.x += (pos.x - Visualize.cameraPosition.x) * 0.5 + Visualize.cameraPosition.y += (-pos.y - Visualize.cameraPosition.y) * 0.5 + Visualize.mGraph.plot() }, onMouseWheel: function (delta) { - Metamaps.Visualize.cameraPosition.z += -delta * 20 - Metamaps.Visualize.mGraph.plot() + Visualize.cameraPosition.z += -delta * 20 + Visualize.mGraph.plot() }, onClick: function () {} }, @@ -616,7 +641,7 @@ const JIT = { modes: ['polar'], duration: 800, onComplete: function () { - Metamaps.Visualize.mGraph.busy = false + Visualize.mGraph.busy = false } }, // this will just be used to patch the ForceDirected graphsettings with the few things which actually differ @@ -636,10 +661,10 @@ const JIT = { // don't do anything if the edge is filtered // or if the canvas is animating - if (filtered || Metamaps.Visualize.mGraph.busy) return + if (filtered || Visualize.mGraph.busy) return $('canvas').css('cursor', 'pointer') - var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge) + var edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected if (edgeIsSelected == -1) { edge.setData('showDesc', true, 'current') @@ -648,16 +673,16 @@ const JIT = { edge.setDataset('end', { lineWidth: 4 }) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth'], duration: 100 }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, // onMouseEnter onMouseLeave: function (edge) { if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered $('canvas').css('cursor', 'default') - var edgeIsSelected = Metamaps.Selected.Edges.indexOf(edge) + var edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected if (edgeIsSelected == -1) { edge.setData('showDesc', false, 'current') @@ -666,65 +691,65 @@ const JIT = { edge.setDataset('end', { lineWidth: 2 }) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth'], duration: 100 }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() }, // onMouseLeave onMouseMoveHandler: function (node, eventInfo, e) { var self = JIT - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return var node = eventInfo.getNode() var edge = eventInfo.getEdge() // if we're on top of a node object, act like there aren't edges under it if (node != false) { - if (Metamaps.Mouse.edgeHoveringOver) { - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + if (Mouse.edgeHoveringOver) { + self.onMouseLeave(Mouse.edgeHoveringOver) } $('canvas').css('cursor', 'pointer') return } - if (edge == false && Metamaps.Mouse.edgeHoveringOver != false) { + if (edge == false && Mouse.edgeHoveringOver != false) { // mouse not on an edge, but we were on an edge previously - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) - } else if (edge != false && Metamaps.Mouse.edgeHoveringOver == false) { + self.onMouseLeave(Mouse.edgeHoveringOver) + } else if (edge != false && Mouse.edgeHoveringOver == false) { // mouse is on an edge, but there isn't a stored edge self.onMouseEnter(edge) - } else if (edge != false && Metamaps.Mouse.edgeHoveringOver != edge) { + } else if (edge != false && Mouse.edgeHoveringOver != edge) { // mouse is on an edge, but a different edge is stored - self.onMouseLeave(Metamaps.Mouse.edgeHoveringOver) + self.onMouseLeave(Mouse.edgeHoveringOver) self.onMouseEnter(edge) } // could be false - Metamaps.Mouse.edgeHoveringOver = edge + Mouse.edgeHoveringOver = edge if (!node && !edge) { $('canvas').css('cursor', 'default') } }, // onMouseMoveHandler enterKeyHandler: function () { - var creatingMap = Metamaps.GlobalUI.lightbox + var creatingMap = GlobalUI.lightbox if (creatingMap === 'newmap' || creatingMap === 'forkmap') { - Metamaps.GlobalUI.CreateMap.submit() + GlobalUI.CreateMap.submit() } // this is to submit new topic creation - else if (Metamaps.Create.newTopic.beingCreated) { - Metamaps.Topic.createTopicLocally() + else if (Create.newTopic.beingCreated) { + Topic.createTopicLocally() } // to submit new synapse creation - else if (Metamaps.Create.newSynapse.beingCreated) { - Metamaps.Synapse.createSynapseLocally() + else if (Create.newSynapse.beingCreated) { + Synapse.createSynapseLocally() } }, // enterKeyHandler escKeyHandler: function () { - Metamaps.Control.deselectAllEdges() - Metamaps.Control.deselectAllNodes() + Control.deselectAllEdges() + Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { var self = JIT @@ -734,7 +759,7 @@ const JIT = { var positionsToSend = {} var topic - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (node && !node.nodeFrom) { var pos = eventInfo.getPos() @@ -750,7 +775,7 @@ const JIT = { } else if (whatToDo == 'only-drag-this-one') { node.pos.setc(pos.x, pos.y) - if (Metamaps.Active.Map) { + if (Active.Map) { topic = node.getData('topic') // we use the topic ID not the node id // because we can't depend on the node id @@ -760,24 +785,24 @@ const JIT = { $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } else { - var len = Metamaps.Selected.Nodes.length + var len = Selected.Nodes.length // first define offset for each node var xOffset = [] var yOffset = [] for (var i = 0; i < len; i += 1) { - var n = Metamaps.Selected.Nodes[i] + var n = Selected.Nodes[i] xOffset[i] = n.pos.x - node.pos.x yOffset[i] = n.pos.y - node.pos.y } // for for (var i = 0; i < len; i += 1) { - var n = Metamaps.Selected.Nodes[i] + var n = Selected.Nodes[i] var x = pos.x + xOffset[i] var y = pos.y + yOffset[i] n.pos.setc(x, y) - if (Metamaps.Active.Map) { + if (Active.Map) { topic = n.getData('topic') // we use the topic ID not the node id // because we can't depend on the node id @@ -787,15 +812,15 @@ const JIT = { } } // for - if (Metamaps.Active.Map) { + if (Active.Map) { $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } // if if (whatToDo == 'deselect') { - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } // if it's a right click or holding down alt, start synapse creation ->third option is for firefox else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { @@ -803,48 +828,48 @@ const JIT = { JIT.tempNode = node JIT.tempInit = true - Metamaps.Create.newTopic.hide() - Metamaps.Create.newSynapse.hide() + Create.newTopic.hide() + Create.newSynapse.hide() // set the draw synapse start positions - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length if (l > 0) { for (var i = l - 1; i >= 0; i -= 1) { - var n = Metamaps.Selected.Nodes[i] - Metamaps.Mouse.synapseStartCoordinates.push({ + var n = Selected.Nodes[i] + Mouse.synapseStartCoordinates.push({ x: n.pos.getc().x, y: n.pos.getc().y }) } } else { - Metamaps.Mouse.synapseStartCoordinates = [{ + Mouse.synapseStartCoordinates = [{ x: JIT.tempNode.pos.getc().x, y: JIT.tempNode.pos.getc().y }] } - Metamaps.Mouse.synapseEndCoordinates = { + Mouse.synapseEndCoordinates = { x: pos.x, y: pos.y } } // let temp = eventInfo.getNode() - if (temp != false && temp.id != node.id && Metamaps.Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned + if (temp != false && temp.id != node.id && Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned JIT.tempNode2 = temp - Metamaps.Mouse.synapseEndCoordinates = { + Mouse.synapseEndCoordinates = { x: JIT.tempNode2.pos.getc().x, y: JIT.tempNode2.pos.getc().y } // before making the highlighted one bigger, make sure all the others are regular size - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current') }) temp.setData('dim', 35, 'current') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } else if (!temp) { JIT.tempNode2 = null - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { n.setData('dim', 25, 'current') }) // pop up node creation :) @@ -852,21 +877,21 @@ const JIT = { var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') - Metamaps.Create.newTopic.x = eventInfo.getPos().x - Metamaps.Create.newTopic.y = eventInfo.getPos().y - Metamaps.Visualize.mGraph.plot() + Create.newTopic.x = eventInfo.getPos().x + Create.newTopic.y = eventInfo.getPos().y + Visualize.mGraph.plot() - Metamaps.Mouse.synapseEndCoordinates = { + Mouse.synapseEndCoordinates = { x: pos.x, y: pos.y } } } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Metamaps.Active.Topic) { - Metamaps.GlobalUI.notifyUser('Cannot create in Topic view.') + else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Active.Topic) { + GlobalUI.notifyUser('Cannot create in Topic view.') } else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + GlobalUI.notifyUser('Cannot edit Public map.') } } }, // onDragMoveTopicHandler @@ -876,30 +901,30 @@ const JIT = { JIT.tempNode2 = null JIT.tempInit = false // reset the draw synapse positions to false - Metamaps.Mouse.synapseStartCoordinates = [] - Metamaps.Mouse.synapseEndCoordinates = null - Metamaps.Visualize.mGraph.plot() + Mouse.synapseStartCoordinates = [] + Mouse.synapseEndCoordinates = null + Visualize.mGraph.plot() }, // onDragCancelHandler onDragEndTopicHandler: function (node, eventInfo, e) { var midpoint = {}, pixelPos, mapping if (JIT.tempInit && JIT.tempNode2 == null) { // this means you want to add a new topic, and then a synapse - Metamaps.Create.newTopic.addSynapse = true - Metamaps.Create.newTopic.open() + Create.newTopic.addSynapse = true + Create.newTopic.open() } else if (JIT.tempInit && JIT.tempNode2 != null) { // this means you want to create a synapse between two existing topics - Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id - Metamaps.Create.newSynapse.topic2id = JIT.tempNode2.getData('topic').id + Create.newTopic.addSynapse = false + Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id + Create.newSynapse.topic2id = JIT.tempNode2.getData('topic').id JIT.tempNode2.setData('dim', 25, 'current') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() midpoint.x = JIT.tempNode.pos.getc().x + (JIT.tempNode2.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 midpoint.y = JIT.tempNode.pos.getc().y + (JIT.tempNode2.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 - pixelPos = Metamaps.Util.coordsToPixels(midpoint) + pixelPos = Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') - Metamaps.Create.newSynapse.open() + Create.newSynapse.open() JIT.tempNode = null JIT.tempNode2 = null JIT.tempInit = false @@ -908,17 +933,17 @@ const JIT = { // check whether to save mappings var checkWhetherToSave = function () { - var map = Metamaps.Active.Map + var map = Active.Map if (!map) return false - var mapper = Metamaps.Active.Mapper + var mapper = Active.Mapper // this case // covers when it is a public map owned by you // and also when it's a private map var activeMappersMap = map.authorizePermissionChange(mapper) var commonsMap = map.get('permission') === 'commons' - var realtimeOn = Metamaps.Realtime.status + var realtimeOn = Realtime.status // don't save if commons map, and you have realtime off, // even if you're map creator @@ -932,9 +957,9 @@ const JIT = { yloc: node.getPos().y }) // also save any other selected nodes that also got dragged along - var l = Metamaps.Selected.Nodes.length + var l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var n = Metamaps.Selected.Nodes[i] + var n = Selected.Nodes[i] if (n !== node) { mapping = n.getData('mapping') mapping.save({ @@ -948,61 +973,61 @@ const JIT = { }, // onDragEndTopicHandler canvasClickHandler: function (canvasLoc, e) { // grab the location and timestamp of the click - var storedTime = Metamaps.Mouse.lastCanvasClick + var storedTime = Mouse.lastCanvasClick var now = Date.now() // not compatible with IE8 FYI - Metamaps.Mouse.lastCanvasClick = now + Mouse.lastCanvasClick = now - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) - if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE && !Metamaps.Mouse.didPan) { - if (Metamaps.Active.Map && !authorized) { - Metamaps.GlobalUI.notifyUser('Cannot edit Public map.') + if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE && !Mouse.didPan) { + if (Active.Map && !authorized) { + GlobalUI.notifyUser('Cannot edit Public map.') return } - else if (Metamaps.Active.Topic) { - Metamaps.GlobalUI.notifyUser('Cannot create in Topic view.') + else if (Active.Topic) { + GlobalUI.notifyUser('Cannot create in Topic view.') return } // DOUBLE CLICK // pop up node creation :) - Metamaps.Create.newTopic.addSynapse = false - Metamaps.Create.newTopic.x = canvasLoc.x - Metamaps.Create.newTopic.y = canvasLoc.y + Create.newTopic.addSynapse = false + Create.newTopic.x = canvasLoc.x + Create.newTopic.y = canvasLoc.y $('#new_topic').css('left', e.clientX + 'px') $('#new_topic').css('top', e.clientY + 'px') - Metamaps.Create.newTopic.open() - } else if (!Metamaps.Mouse.didPan) { + Create.newTopic.open() + } else if (!Mouse.didPan) { // SINGLE CLICK, no pan - Metamaps.Filter.close() - Metamaps.TopicCard.hideCard() - Metamaps.SynapseCard.hideCard() - Metamaps.Create.newTopic.hide() + Filter.close() + TopicCard.hideCard() + SynapseCard.hideCard() + Create.newTopic.hide() $('.rightclickmenu').remove() // reset the draw synapse positions to false - Metamaps.Mouse.synapseStartCoordinates = [] - Metamaps.Mouse.synapseEndCoordinates = null + Mouse.synapseStartCoordinates = [] + Mouse.synapseEndCoordinates = null JIT.tempInit = false JIT.tempNode = null JIT.tempNode2 = null if (!e.ctrlKey && !e.shiftKey) { - Metamaps.Control.deselectAllEdges() - Metamaps.Control.deselectAllNodes() + Control.deselectAllEdges() + Control.deselectAllNodes() } } }, // canvasClickHandler nodeDoubleClickHandler: function (node, e) { - Metamaps.TopicCard.showCard(node) + TopicCard.showCard(node) }, // nodeDoubleClickHandler edgeDoubleClickHandler: function (adj, e) { - Metamaps.SynapseCard.showCard(adj, e) + SynapseCard.showCard(adj, e) }, // nodeDoubleClickHandler nodeWasDoubleClicked: function () { // grab the timestamp of the click - var storedTime = Metamaps.Mouse.lastNodeClick + var storedTime = Mouse.lastNodeClick var now = Date.now() // not compatible with IE8 FYI - Metamaps.Mouse.lastNodeClick = now + Mouse.lastNodeClick = now - if (now - storedTime < Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) { + if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE) { return true } else { return false @@ -1015,12 +1040,12 @@ const JIT = { // 3 others are selected only, no shift: drag only this one // 4 this node and others were selected, so drag them (just return false) // return value: deselect node again after? - if (Metamaps.Selected.Nodes.length == 0) { + if (Selected.Nodes.length == 0) { return 'only-drag-this-one' } - if (Metamaps.Selected.Nodes.indexOf(node) == -1) { + if (Selected.Nodes.indexOf(node) == -1) { if (e.shiftKey) { - Metamaps.Control.selectNode(node, e) + Control.selectNode(node, e) return 'nothing' } else { return 'only-drag-this-one' @@ -1040,18 +1065,18 @@ const JIT = { }, selectWithBox: function (e) { var self = this - var sX = Metamaps.Mouse.boxStartCoordinates.x, - sY = Metamaps.Mouse.boxStartCoordinates.y, - eX = Metamaps.Mouse.boxEndCoordinates.x, - eY = Metamaps.Mouse.boxEndCoordinates.y + var sX = Mouse.boxStartCoordinates.x, + sY = Mouse.boxStartCoordinates.y, + eX = Mouse.boxEndCoordinates.x, + eY = Mouse.boxEndCoordinates.y if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() } // select all nodes that are within the box - Metamaps.Visualize.mGraph.graph.eachNode(function(n) { + Visualize.mGraph.graph.eachNode(function(n) { var pos = self.getNodeXY(n) var x = pos.x, y = pos.y @@ -1064,12 +1089,12 @@ const JIT = { (sX < x && x < eX && sY > y && y > eY)) { if (e.shiftKey) { if (n.selected) { - Metamaps.Control.deselectNode(n) + Control.deselectNode(n) } else { - Metamaps.Control.selectNode(n, e) + Control.selectNode(n, e) } } else { - Metamaps.Control.selectNode(n, e) + Control.selectNode(n, e) } } }) @@ -1170,30 +1195,30 @@ const JIT = { if (selectTest) { // shiftKey = toggleSelect, otherwise if (e.shiftKey) { - if (Metamaps.Selected.Edges.indexOf(edge) != -1) { - Metamaps.Control.deselectEdge(edge) + if (Selected.Edges.indexOf(edge) != -1) { + Control.deselectEdge(edge) } else { - Metamaps.Control.selectEdge(edge) + Control.selectEdge(edge) } } else { - Metamaps.Control.selectEdge(edge) + Control.selectEdge(edge) } } }) - Metamaps.Mouse.boxStartCoordinates = false - Metamaps.Mouse.boxEndCoordinates = false - Metamaps.Visualize.mGraph.plot() + Mouse.boxStartCoordinates = false + Mouse.boxEndCoordinates = false + Visualize.mGraph.plot() }, // selectWithBox drawSelectBox: function (eventInfo, e) { - var ctx = Metamaps.Visualize.mGraph.canvas.getCtx() + var ctx = Visualize.mGraph.canvas.getCtx() - var startX = Metamaps.Mouse.boxStartCoordinates.x, - startY = Metamaps.Mouse.boxStartCoordinates.y, + var startX = Mouse.boxStartCoordinates.x, + startY = Mouse.boxStartCoordinates.y, currX = eventInfo.getPos().x, currY = eventInfo.getPos().y - Metamaps.Visualize.mGraph.canvas.clear() - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.canvas.clear() + Visualize.mGraph.plot() ctx.beginPath() ctx.moveTo(startX, startY) @@ -1205,7 +1230,7 @@ const JIT = { ctx.stroke() }, // drawSelectBox selectNodeOnClickHandler: function (node, e) { - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return var self = JIT @@ -1216,8 +1241,8 @@ const JIT = { } // if on a topic page, let alt+click center you on a new topic - if (Metamaps.Active.Topic && e.altKey) { - Metamaps.RGraph.centerOn(node.id) + if (Active.Topic && e.altKey) { + JIT.RGraph.centerOn(node.id) return } @@ -1232,24 +1257,24 @@ const JIT = { var nodeAlreadySelected = node.selected if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() } if (nodeAlreadySelected) { - Metamaps.Control.deselectNode(node) + Control.deselectNode(node) } else { - Metamaps.Control.selectNode(node, e) + Control.selectNode(node, e) } // trigger animation to final styles - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.animate({ modes: ['edge-property:lineWidth:color:alpha'], duration: 500 }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } - }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) + }, Mouse.DOUBLE_CLICK_TOLERANCE) } }, // selectNodeOnClickHandler selectNodeOnRightClickHandler: function (node, e) { @@ -1259,10 +1284,10 @@ const JIT = { e.preventDefault() e.stopPropagation() - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return // select the node - Metamaps.Control.selectNode(node, e) + Control.selectNode(node, e) // delete old right click menu $('.rightclickmenu').remove() @@ -1272,20 +1297,20 @@ const JIT = { // add the proper options to the menu var menustring = '<ul>' - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) var disabled = authorized ? '' : 'disabled' - if (Metamaps.Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' + if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' - if (Metamaps.Active.Topic) { + if (Active.Topic) { menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Alt+E</div></li>' } menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>' - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { var options = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ @@ -1299,8 +1324,8 @@ const JIT = { menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>' } - if (Metamaps.Active.Topic) { - if (!Metamaps.Active.Mapper) { + if (Active.Topic) { + if (!Active.Mapper) { menustring += '<li class="rc-spacer"></li>' } @@ -1358,30 +1383,30 @@ const JIT = { if (authorized) { $('.rc-delete').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.deleteSelected() + Control.deleteSelected() }) } // remove the selected things from the map - if (Metamaps.Active.Topic || authorized) { + if (Active.Topic || authorized) { $('.rc-remove').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.removeSelectedEdges() - Metamaps.Control.removeSelectedNodes() + Control.removeSelectedEdges() + Control.removeSelectedNodes() }) } // hide selected nodes and synapses until refresh $('.rc-hide').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.hideSelectedEdges() - Metamaps.Control.hideSelectedNodes() + Control.hideSelectedEdges() + Control.hideSelectedNodes() }) // when in radial, center on the topic you picked $('.rc-center').click(function () { $('.rightclickmenu').remove() - Metamaps.Topic.centerOn(node.id) + Topic.centerOn(node.id) }) // open the entity in a new tab @@ -1395,14 +1420,14 @@ const JIT = { $('.rc-permission li').click(function () { $('.rightclickmenu').remove() // $(this).text() will be 'commons' 'public' or 'private' - Metamaps.Control.updateSelectedPermissions($(this).text()) + 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() // - Metamaps.Control.updateSelectedMetacodes($(this).attr('data-id')) + Control.updateSelectedMetacodes($(this).attr('data-id')) }) // fetch relatives @@ -1416,7 +1441,7 @@ const JIT = { $('.rc-siblings .fetchAll').click(function () { $('.rightclickmenu').remove() // data-id is a metacode id - Metamaps.Topic.fetchRelatives(node) + Topic.fetchRelatives(node) }) }, // selectNodeOnRightClickHandler, populateRightClickSiblings: function (node) { @@ -1448,7 +1473,7 @@ const JIT = { $('.rc-siblings .getSiblings').click(function () { $('.rightclickmenu').remove() // data-id is a metacode id - Metamaps.Topic.fetchRelatives(node, $(this).attr('data-id')) + Topic.fetchRelatives(node, $(this).attr('data-id')) }) } @@ -1460,7 +1485,7 @@ const JIT = { }) }, selectEdgeOnClickHandler: function (adj, e) { - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return var self = JIT @@ -1478,22 +1503,22 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - var edgeAlreadySelected = Metamaps.Selected.Edges.indexOf(adj) !== -1 + var edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 if (!e.shiftKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() } if (edgeAlreadySelected) { - Metamaps.Control.deselectEdge(adj) + Control.deselectEdge(adj) } else { - Metamaps.Control.selectEdge(adj) + Control.selectEdge(adj) } - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } - }, Metamaps.Mouse.DOUBLE_CLICK_TOLERANCE) + }, Mouse.DOUBLE_CLICK_TOLERANCE) } }, // selectEdgeOnClickHandler selectEdgeOnRightClickHandler: function (adj, e) { @@ -1507,9 +1532,9 @@ const JIT = { e.preventDefault() e.stopPropagation() - if (Metamaps.Visualize.mGraph.busy) return + if (Visualize.mGraph.busy) return - Metamaps.Control.selectEdge(adj) + Control.selectEdge(adj) // delete old right click menu $('.rightclickmenu').remove() @@ -1520,18 +1545,18 @@ const JIT = { // add the proper options to the menu var menustring = '<ul>' - var authorized = Metamaps.Active.Map && Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper) + var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) var disabled = authorized ? '' : 'disabled' - if (Metamaps.Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' + if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Topic) menustring += '<li class="rc-remove"><div class="rc-icon"></div>Remove from view<div class="rc-keyboard">Ctrl+M</div></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-delete ' + disabled + '"><div class="rc-icon"></div>Delete<div class="rc-keyboard">Ctrl+D</div></li>' - if (Metamaps.Active.Map && Metamaps.Active.Mapper) menustring += '<li class="rc-spacer"></li>' + if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>' - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { var permOptions = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ @@ -1582,7 +1607,7 @@ const JIT = { if (authorized) { $('.rc-delete').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.deleteSelected() + Control.deleteSelected() }) } @@ -1590,30 +1615,30 @@ const JIT = { if (authorized) { $('.rc-remove').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.removeSelectedEdges() - Metamaps.Control.removeSelectedNodes() + Control.removeSelectedEdges() + Control.removeSelectedNodes() }) } // hide selected nodes and synapses until refresh $('.rc-hide').click(function () { $('.rightclickmenu').remove() - Metamaps.Control.hideSelectedEdges() - Metamaps.Control.hideSelectedNodes() + Control.hideSelectedEdges() + Control.hideSelectedNodes() }) // change the permission of all the selected nodes and synapses that you were the originator of $('.rc-permission li').click(function () { $('.rightclickmenu').remove() // $(this).text() will be 'commons' 'public' or 'private' - Metamaps.Control.updateSelectedPermissions($(this).text()) + Control.updateSelectedPermissions($(this).text()) }) }, // selectEdgeOnRightClickHandler SmoothPanning: function () { - var sx = Metamaps.Visualize.mGraph.canvas.scaleOffsetX, - sy = Metamaps.Visualize.mGraph.canvas.scaleOffsetY, - y_velocity = Metamaps.Mouse.changeInY, // initial y velocity - x_velocity = Metamaps.Mouse.changeInX, // initial x velocity + var sx = Visualize.mGraph.canvas.scaleOffsetX, + sy = Visualize.mGraph.canvas.scaleOffsetY, + y_velocity = Mouse.changeInY, // initial y velocity + x_velocity = Mouse.changeInX, // initial x velocity easing = 1 // frictional value easing = 1 @@ -1623,7 +1648,7 @@ const JIT = { }, 1) function myTimer () { - Metamaps.Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) + Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) $(document).trigger(JIT.events.pan) easing = easing * 0.75 @@ -1725,11 +1750,11 @@ const JIT = { } }, // renderEdgeArrows zoomIn: function (event) { - Metamaps.Visualize.mGraph.canvas.scale(1.25, 1.25) + Visualize.mGraph.canvas.scale(1.25, 1.25) $(document).trigger(JIT.events.zoom, [event]) }, zoomOut: function (event) { - Metamaps.Visualize.mGraph.canvas.scale(0.8, 0.8) + Visualize.mGraph.canvas.scale(0.8, 0.8) $(document).trigger(JIT.events.zoom, [event]) }, centerMap: function (canvas) { @@ -1743,12 +1768,12 @@ const JIT = { canvas.translate(-1 * offsetX, -1 * offsetY) }, zoomToBox: function (event) { - var sX = Metamaps.Mouse.boxStartCoordinates.x, - sY = Metamaps.Mouse.boxStartCoordinates.y, - eX = Metamaps.Mouse.boxEndCoordinates.x, - eY = Metamaps.Mouse.boxEndCoordinates.y + var sX = Mouse.boxStartCoordinates.x, + sY = Mouse.boxStartCoordinates.y, + eX = Mouse.boxEndCoordinates.x, + eY = Mouse.boxEndCoordinates.y - var canvas = Metamaps.Visualize.mGraph.canvas + var canvas = Visualize.mGraph.canvas JIT.centerMap(canvas) var height = $(document).height(), @@ -1778,9 +1803,9 @@ const JIT = { canvas.translate(-1 * cogX, -1 * cogY) $(document).trigger(JIT.events.zoom, [event]) - Metamaps.Mouse.boxStartCoordinates = false - Metamaps.Mouse.boxEndCoordinates = false - Metamaps.Visualize.mGraph.plot() + Mouse.boxStartCoordinates = false + Mouse.boxEndCoordinates = false + Visualize.mGraph.plot() }, zoomExtents: function (event, canvas, denySelected) { JIT.centerMap(canvas) @@ -1788,10 +1813,10 @@ const JIT = { width = canvas.getSize().width, maxX, minX, maxY, minY, counter = 0 - if (!denySelected && Metamaps.Selected.Nodes.length > 0) { - var nodes = Metamaps.Selected.Nodes + if (!denySelected && Selected.Nodes.length > 0) { + var nodes = Selected.Nodes } else { - var nodes = _.values(Metamaps.Visualize.mGraph.graph.nodes) + var nodes = _.values(Visualize.mGraph.graph.nodes) } if (nodes.length > 1) { @@ -1806,7 +1831,7 @@ const JIT = { minY = y } - var arrayOfLabelLines = Metamaps.Util.splitLine(n.name, 30).split('\n'), + var arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n'), dim = n.getData('dim'), ctx = canvas.getCtx() diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 1c56b679..78e881d4 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -1,57 +1,59 @@ -/* global Metamaps, $ */ +/* global $ */ + +import Active from './Active' +import Control from './Control' +import JIT from './JIT' +import Mobile from './Mobile' +import Realtime from './Realtime' +import Selected from './Selected' +import Topic from './Topic' +import Visualize from './Visualize' -/* - * Dependencies: - * - Metamaps.Active - * - Metamaps.Control - * - Metamaps.JIT - * - Metamaps.Visualize - */ const Listeners = { init: function () { var self = this $(document).on('keydown', function (e) { - if (!(Metamaps.Active.Map || Metamaps.Active.Topic)) return + if (!(Active.Map || Active.Topic)) return switch (e.which) { case 13: // if enter key is pressed - Metamaps.JIT.enterKeyHandler() + JIT.enterKeyHandler() e.preventDefault() break case 27: // if esc key is pressed - Metamaps.JIT.escKeyHandler() + JIT.escKeyHandler() break case 65: // if a or A is pressed if (e.ctrlKey) { - Metamaps.Control.deselectAllNodes() - Metamaps.Control.deselectAllEdges() + Control.deselectAllNodes() + Control.deselectAllEdges() e.preventDefault() - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { - Metamaps.Control.selectNode(n, e) + Visualize.mGraph.graph.eachNode(function (n) { + Control.selectNode(n, e) }) - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } break case 68: // if d or D is pressed if (e.ctrlKey) { e.preventDefault() - Metamaps.Control.deleteSelected() + Control.deleteSelected() } break case 69: // if e or E is pressed - if (e.ctrlKey && Metamaps.Active.Map) { + if (e.ctrlKey && Active.Map) { e.preventDefault() - Metamaps.JIT.zoomExtents(null, Metamaps.Visualize.mGraph.canvas) + JIT.zoomExtents(null, Visualize.mGraph.canvas) break } - if (e.altKey && Metamaps.Active.Topic) { + if (e.altKey && Active.Topic) { e.preventDefault() - if (Metamaps.Active.Topic) { - self.centerAndReveal(Metamaps.Selected.Nodes, { + if (Active.Topic) { + self.centerAndReveal(Selected.Nodes, { center: true, reveal: false }) @@ -62,30 +64,30 @@ const Listeners = { case 72: // if h or H is pressed if (e.ctrlKey) { e.preventDefault() - Metamaps.Control.hideSelectedNodes() - Metamaps.Control.hideSelectedEdges() + Control.hideSelectedNodes() + Control.hideSelectedEdges() } break case 77: // if m or M is pressed if (e.ctrlKey) { e.preventDefault() - Metamaps.Control.removeSelectedNodes() - Metamaps.Control.removeSelectedEdges() + Control.removeSelectedNodes() + Control.removeSelectedEdges() } break case 82: // if r or R is pressed - if (e.altKey && Metamaps.Active.Topic) { + if (e.altKey && Active.Topic) { e.preventDefault() - self.centerAndReveal(Metamaps.Selected.Nodes, { + self.centerAndReveal(Selected.Nodes, { center: false, reveal: true }) } break case 84: // if t or T is pressed - if (e.altKey && Metamaps.Active.Topic) { + if (e.altKey && Active.Topic) { e.preventDefault() - self.centerAndReveal(Metamaps.Selected.Nodes, { + self.centerAndReveal(Selected.Nodes, { center: true, reveal: true }) @@ -98,23 +100,22 @@ const Listeners = { }) $(window).resize(function () { - if (Metamaps.Visualize && Metamaps.Visualize.mGraph) Metamaps.Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) - if ((Metamaps.Active.Map || Metamaps.Active.Topic) && Metamaps.Famous && Metamaps.Famous.maps.surf) Metamaps.Famous.maps.reposition() - if (Metamaps.Active.Map && Metamaps.Realtime.inConversation) Metamaps.Realtime.positionVideos() - Metamaps.Mobile.resizeTitle() + if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() + Mobile.resizeTitle() }) }, centerAndReveal: function(nodes, opts) { if (nodes.length < 1) return var node = nodes[nodes.length - 1] if (opts.center && opts.reveal) { - Metamaps.Topic.centerOn(node.id, function() { - Metamaps.Topic.fetchRelatives(nodes) + Topic.centerOn(node.id, function() { + Topic.fetchRelatives(nodes) }) } else if (opts.center) { - Metamaps.Topic.centerOn(node.id) + Topic.centerOn(node.id) } else if (opts.reveal) { - Metamaps.Topic.fetchRelatives(nodes) + Topic.fetchRelatives(nodes) } } } diff --git a/frontend/src/Metamaps/Map.js b/frontend/src/Metamaps/Map.js index 690c2a6d..cd2c3d2e 100644 --- a/frontend/src/Metamaps/Map.js +++ b/frontend/src/Metamaps/Map.js @@ -1,30 +1,29 @@ -window.Metamaps = window.Metamaps || {} - /* global Metamaps, $ */ +import Active from './Active' +import AutoLayout from './AutoLayout' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Realtime from './Realtime' +import Selected from './Selected' +import SynapseCard from './SynapseCard' +import TopicCard from './TopicCard' +import Visualize from './Visualize' + /* * Metamaps.Map.js.erb * * Dependencies: - * - Metamaps.AutoLayout - * - Metamaps.Create - * - Metamaps.Erb - * - Metamaps.Filter - * - Metamaps.JIT - * - Metamaps.Loading - * - Metamaps.Maps - * - Metamaps.Realtime - * - Metamaps.Router - * - Metamaps.Selected - * - Metamaps.SynapseCard - * - Metamaps.TopicCard - * - Metamaps.Visualize - * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.GlobalUI + * - Metamaps.Erb + * - Metamaps.Loading * - Metamaps.Mappers * - Metamaps.Mappings + * - Metamaps.Maps * - Metamaps.Messages + * - Metamaps.Router * - Metamaps.Synapses * - Metamaps.Topics * @@ -33,6 +32,7 @@ window.Metamaps = window.Metamaps || {} * - Metamaps.Map.InfoBox */ +window.Metamaps = window.Metamaps || {} Metamaps.Map = { events: { editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' @@ -54,7 +54,7 @@ Metamaps.Map = { self.fork() }) - Metamaps.GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() + GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() self.updateStar() self.InfoBox.init() @@ -65,7 +65,7 @@ Metamaps.Map = { launch: function (id) { var bb = Metamaps.Backbone var start = function (data) { - Metamaps.Active.Map = new bb.Map(data.map) + Active.Map = new bb.Map(data.map) Metamaps.Mappers = new bb.MapperCollection(data.mappers) Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) Metamaps.Topics = new bb.TopicCollection(data.topics) @@ -75,8 +75,8 @@ Metamaps.Map = { Metamaps.Stars = data.stars Metamaps.Backbone.attachCollectionEvents() - var map = Metamaps.Active.Map - var mapper = Metamaps.Active.Mapper + var map = Active.Map + var mapper = Active.Mapper // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { @@ -95,24 +95,24 @@ Metamaps.Map = { $('#filter_by_mapper h3').html('MAPPERS') // build and render the visualization - Metamaps.Visualize.type = 'ForceDirected' - Metamaps.JIT.prepareVizData() + Visualize.type = 'ForceDirected' + JIT.prepareVizData() // update filters - Metamaps.Filter.reset() + Filter.reset() // reset selected arrays - Metamaps.Selected.reset() + Selected.reset() // set the proper mapinfobox content Metamaps.Map.InfoBox.load() // these three update the actual filter box with the right list items - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + Filter.checkMetacodes() + Filter.checkSynapses() + Filter.checkMappers() - Metamaps.Realtime.startActiveMap() + Realtime.startActiveMap() Metamaps.Loading.hide() // for mobile @@ -125,24 +125,24 @@ Metamaps.Map = { }) }, end: function () { - if (Metamaps.Active.Map) { + if (Active.Map) { $('.wrapper').removeClass('canEditMap commonsMap') - Metamaps.AutoLayout.resetSpiral() + AutoLayout.resetSpiral() $('.rightclickmenu').remove() - Metamaps.TopicCard.hideCard() - Metamaps.SynapseCard.hideCard() - Metamaps.Create.newTopic.hide(true) // true means force (and override pinned) - Metamaps.Create.newSynapse.hide() - Metamaps.Filter.close() + TopicCard.hideCard() + SynapseCard.hideCard() + Create.newTopic.hide(true) // true means force (and override pinned) + Create.newSynapse.hide() + Filter.close() Metamaps.Map.InfoBox.close() - Metamaps.Realtime.endActiveMap() + Realtime.endActiveMap() } }, updateStar: function () { - if (!Metamaps.Active.Mapper || !Metamaps.Stars) return + if (!Active.Mapper || !Metamaps.Stars) return // update the star/unstar icon - if (Metamaps.Stars.find(function (s) { return s.user_id === Metamaps.Active.Mapper.id })) { + if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { $('.starMap').addClass('starred') $('.starMap .tooltipsAbove').html('Unstar') } else { @@ -153,31 +153,31 @@ Metamaps.Map = { star: function () { var self = Metamaps.Map - if (!Metamaps.Active.Map) return - $.post('/maps/' + Metamaps.Active.Map.id + '/star') - Metamaps.Stars.push({ user_id: Metamaps.Active.Mapper.id, map_id: Metamaps.Active.Map.id }) - Metamaps.Maps.Starred.add(Metamaps.Active.Map) - Metamaps.GlobalUI.notifyUser('Map is now starred') + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/star') + Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) + Metamaps.Maps.Starred.add(Active.Map) + GlobalUI.notifyUser('Map is now starred') self.updateStar() }, unstar: function () { var self = Metamaps.Map - if (!Metamaps.Active.Map) return - $.post('/maps/' + Metamaps.Active.Map.id + '/unstar') - Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Metamaps.Active.Mapper.id }) - Metamaps.Maps.Starred.remove(Metamaps.Active.Map) + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/unstar') + Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) + Metamaps.Maps.Starred.remove(Active.Map) self.updateStar() }, fork: function () { - Metamaps.GlobalUI.openLightbox('forkmap') + GlobalUI.openLightbox('forkmap') var nodes_data = '', synapses_data = '' var nodes_array = [] var synapses_array = [] // collect the unfiltered topics - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { // if the opacity is less than 1 then it's filtered if (n.getData('alpha') === 1) { var id = n.getData('topic').id @@ -197,7 +197,7 @@ Metamaps.Map = { Metamaps.Synapses.each(function (synapse) { var desc = synapse.get('desc') - var descNotFiltered = Metamaps.Filter.visible.synapses.indexOf(desc) > -1 + var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 // make sure that both topics are being added, otherwise, it // doesn't make sense to add the synapse var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 @@ -210,32 +210,32 @@ Metamaps.Map = { synapses_data = synapses_array.join() nodes_data = nodes_data.slice(0, -1) - Metamaps.GlobalUI.CreateMap.topicsToMap = nodes_data - Metamaps.GlobalUI.CreateMap.synapsesToMap = synapses_data + GlobalUI.CreateMap.topicsToMap = nodes_data + GlobalUI.CreateMap.synapsesToMap = synapses_data }, leavePrivateMap: function () { - var map = Metamaps.Active.Map + var map = Active.Map Metamaps.Maps.Active.remove(map) Metamaps.Maps.Featured.remove(map) Metamaps.Router.home() - Metamaps.GlobalUI.notifyUser('Sorry! That map has been changed to Private.') + GlobalUI.notifyUser('Sorry! That map has been changed to Private.') }, cantEditNow: function () { - Metamaps.Realtime.turnOff(true); // true is for 'silence' - Metamaps.GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') - Metamaps.Active.Map.trigger('changeByOther') + Realtime.turnOff(true); // true is for 'silence' + GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') + Active.Map.trigger('changeByOther') }, canEditNow: function () { var confirmString = "You've been granted permission to edit this map. " confirmString += 'Do you want to reload and enable realtime collaboration?' var c = confirm(confirmString) if (c) { - Metamaps.Router.maps(Metamaps.Active.Map.id) + Metamaps.Router.maps(Active.Map.id) } }, editedByActiveMapper: function () { - if (Metamaps.Active.Mapper) { - Metamaps.Mappers.add(Metamaps.Active.Mapper) + if (Active.Mapper) { + Metamaps.Mappers.add(Active.Mapper) } }, exportImage: function () { @@ -282,14 +282,14 @@ Metamaps.Map = { // center it canvas.getCtx().translate(1880 / 2, 1260 / 2) - var mGraph = Metamaps.Visualize.mGraph + var mGraph = Visualize.mGraph var id = mGraph.root var root = mGraph.graph.getNode(id) var T = !!root.visited // pass true to avoid basing it on a selection - Metamaps.JIT.zoomExtents(null, canvas, true) + JIT.zoomExtents(null, canvas, true) var c = canvas.canvas, ctx = canvas.getCtx(), @@ -327,7 +327,7 @@ Metamaps.Map = { encoded_image: canvas.canvas.toDataURL() } - var map = Metamaps.Active.Map + var map = Active.Map var today = new Date() var dd = today.getDate() @@ -346,12 +346,12 @@ Metamaps.Map = { downloadMessage += 'Captured map screenshot! ' downloadMessage += "<a href='" + imageData.encoded_image + "' " downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" - Metamaps.GlobalUI.notifyUser(downloadMessage) + GlobalUI.notifyUser(downloadMessage) $.ajax({ type: 'POST', dataType: 'json', - url: '/maps/' + Metamaps.Active.Map.id + '/upload_screenshot', + url: '/maps/' + Active.Map.id + '/upload_screenshot', data: imageData, success: function (data) { console.log('successfully uploaded map screenshot') @@ -461,12 +461,12 @@ Metamaps.Map.InfoBox = { load: function () { var self = Metamaps.Map.InfoBox - var map = Metamaps.Active.Map + var map = Active.Map var obj = map.pick('permission', 'topic_count', 'synapse_count') - var isCreator = map.authorizePermissionChange(Metamaps.Active.Mapper) - var canEdit = map.authorizeToEdit(Metamaps.Active.Mapper) + var isCreator = map.authorizePermissionChange(Active.Mapper) + var canEdit = map.authorizeToEdit(Active.Mapper) var relevantPeople = map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var shareable = map.get('permission') !== 'private' @@ -519,8 +519,8 @@ Metamaps.Map.InfoBox = { $('.mapInfoName .best_in_place_name').unbind('ajax:success').bind('ajax:success', function () { var name = $(this).html() - Metamaps.Active.Map.set('name', name) - Metamaps.Active.Map.trigger('saved') + Active.Map.set('name', name) + Active.Map.trigger('saved') // mobile menu $('#header_content').html(name) $('.mapInfoBox').removeClass('mapRequestTitle') @@ -529,8 +529,8 @@ Metamaps.Map.InfoBox = { $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function () { var desc = $(this).html() - Metamaps.Active.Map.set('desc', desc) - Metamaps.Active.Map.trigger('saved') + Active.Map.set('desc', desc) + Active.Map.trigger('saved') }) $('.yourMap .mapPermission').unbind().click(self.onPermissionClick) @@ -558,7 +558,7 @@ Metamaps.Map.InfoBox = { addTypeahead: function () { var self = Metamaps.Map.InfoBox - if (!Metamaps.Active.Map) return + if (!Active.Map) return // for autocomplete var collaborators = { @@ -589,7 +589,7 @@ Metamaps.Map.InfoBox = { } // for adding map collaborators, who will have edit rights - if (Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id')) { + if (Active.Mapper && Active.Mapper.id === Active.Map.get('user_id')) { $('.collaboratorSearchField').typeahead( { highlight: false, @@ -606,23 +606,23 @@ Metamaps.Map.InfoBox = { var self = Metamaps.Map.InfoBox Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) - $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) self.updateNumbers() }, addCollaborator: function (newCollaboratorId) { var self = Metamaps.Map.InfoBox if (Metamaps.Collaborators.get(newCollaboratorId)) { - Metamaps.GlobalUI.notifyUser('That user already has access') + GlobalUI.notifyUser('That user already has access') return } function callback(mapper) { Metamaps.Collaborators.add(mapper) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) - $.post('/maps/' + Metamaps.Active.Map.id + '/access', { access: mapperIds }) + $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') - Metamaps.GlobalUI.notifyUser(name + ' will be notified by email') + GlobalUI.notifyUser(name + ' will be notified by email') self.updateNumbers() } @@ -642,13 +642,13 @@ Metamaps.Map.InfoBox = { }, createContributorList: function () { var self = Metamaps.Map.InfoBox - var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators - var activeMapperIsCreator = Metamaps.Active.Mapper && Metamaps.Active.Mapper.id === Metamaps.Active.Map.get('user_id') + var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id') var string = '' string += '<ul>' relevantPeople.each(function (m) { - var isCreator = Metamaps.Active.Map.get('user_id') === m.get('id') + var isCreator = Active.Map.get('user_id') === m.get('id') string += '<li><a href="/explore/mapper/' + m.get('id') + '">' + '<img class="rtUserImage" width="25" height="25" src="' + m.get('image') + '" />' + m.get('name') if (isCreator) string += ' (creator)' string += '</a>' @@ -664,11 +664,11 @@ Metamaps.Map.InfoBox = { return string }, updateNumbers: function () { - if (!Metamaps.Active.Map) return + if (!Active.Map) return var self = Metamaps.Map.InfoBox - var mapper = Metamaps.Active.Mapper - var relevantPeople = Metamaps.Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators + var mapper = Active.Mapper + var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var contributors_class = '' if (relevantPeople.length === 2) contributors_class = 'multiple mTwo' @@ -720,10 +720,10 @@ Metamaps.Map.InfoBox = { self.selectingPermission = false var permission = $(this).attr('class') - Metamaps.Active.Map.save({ + Active.Map.save({ permission: permission }) - Metamaps.Active.Map.updateMapWrapper() + Active.Map.updateMapWrapper() shareable = permission === 'private' ? '' : 'shareable' $('.mapPermission').removeClass('commons public private minimize').addClass(permission) $('.mapPermission .permissionSelect').remove() @@ -735,8 +735,8 @@ Metamaps.Map.InfoBox = { confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.' var doIt = confirm(confirmString) - var map = Metamaps.Active.Map - var mapper = Metamaps.Active.Mapper + var map = Active.Map + var mapper = Active.Mapper var authorized = map.authorizePermissionChange(mapper) if (doIt && authorized) { @@ -747,7 +747,7 @@ Metamaps.Map.InfoBox = { Metamaps.Maps.Shared.remove(map) map.destroy() Metamaps.Router.home() - Metamaps.GlobalUI.notifyUser('Map eliminated!') + GlobalUI.notifyUser('Map eliminated!') } else if (!authorized) { alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index ac93c34d..3858101d 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,4 +1,6 @@ -import Backbone from './Backbone' +/* + * Metamaps.Backbone + */ const Mapper = { // this function is to retrieve a mapper JSON object from the database @@ -9,7 +11,7 @@ const Mapper = { if (!response.ok) throw response return response.json() }).then(payload => { - callback(new Backbone.Mapper(payload)) + callback(new Metamaps.Backbone.Mapper(payload)) }) } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index ebe1d944..e0620329 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -2,6 +2,8 @@ import AutoLayout from './AutoLayout' import Import from './Import' +import TopicCard from './TopicCard' +import Util from './Util' const PasteInput = { // thanks to https://github.com/kevva/url-regex @@ -19,7 +21,7 @@ const PasteInput = { window.addEventListener("drop", function(e) { e = e || event; e.preventDefault(); - var coords = Metamaps.Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) + var coords = Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { var fileReader = new FileReader() var text = fileReader.readAsText(e.dataTransfer.files[0]) @@ -86,7 +88,7 @@ const PasteInput = { import_id, { success: function(topic) { - Metamaps.TopicCard.showCard(topic.get('node'), function() { + TopicCard.showCard(topic.get('node'), function() { $('#showcard #titleActivator').click() .find('textarea, input').focus() }) diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 35d00f06..1eef6408 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,27 +1,27 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Map from './Map' +import Mapper from './Mapper' +import Topic from './Topic' +import Util from './Util' +import Views from './Views' +import Visualize from './Visualize' + /* * Metamaps.Realtime.js * * Dependencies: - * - Metamaps.Active * - Metamaps.Backbone - * - Metamaps.Backbone - * - Metamaps.Control * - Metamaps.Erb - * - Metamaps.GlobalUI - * - Metamaps.JIT - * - Metamaps.Map - * - Metamaps.Mapper * - Metamaps.Mappers * - Metamaps.Mappings * - Metamaps.Messages * - Metamaps.Synapses - * - Metamaps.Topic * - Metamaps.Topics - * - Metamaps.Util - * - Metamaps.Views - * - Metamaps.Visualize */ const Realtime = { @@ -52,7 +52,7 @@ const Realtime = { self.disconnected = true }) - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { self.webrtc = new SimpleWebRTC({ connection: self.socket, localVideoEl: self.videoId, @@ -69,23 +69,23 @@ const Realtime = { video: true, audio: true }, - nick: Metamaps.Active.Mapper.id + nick: Active.Mapper.id }) var $video = $('<video></video>').attr('id', self.videoId) self.localVideo = { $video: $video, - view: new Metamaps.Views.videoView($video[0], $('body'), 'me', true, { + view: new Views.videoView($video[0], $('body'), 'me', true, { DOUBLE_CLICK_TOLERANCE: 200, - avatar: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '' + avatar: Active.Mapper ? Active.Mapper.get('image') : '' }) } - self.room = new Metamaps.Views.room({ + self.room = new Views.room({ webrtc: self.webrtc, socket: self.socket, - username: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('name') : '', - image: Metamaps.Active.Mapper ? Metamaps.Active.Mapper.get('image') : '', + username: Active.Mapper ? Active.Mapper.get('name') : '', + image: Active.Mapper ? Active.Mapper.get('image') : '', room: 'global', $video: self.localVideo.$video, myVideoView: self.localVideo.view, @@ -93,35 +93,35 @@ const Realtime = { }) self.room.videoAdded(self.handleVideoAdded) - if (!Metamaps.Active.Map) { + if (!Active.Map) { self.room.chat.$container.hide() } $('body').prepend(self.room.chat.$container) - } // if Metamaps.Active.Mapper + } // if Active.Mapper }, addJuntoListeners: function () { var self = Realtime - $(document).on(Metamaps.Views.chatView.events.openTray, function () { + $(document).on(Views.chatView.events.openTray, function () { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) - $(document).on(Metamaps.Views.chatView.events.closeTray, function () { + $(document).on(Views.chatView.events.closeTray, function () { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) - $(document).on(Metamaps.Views.chatView.events.videosOn, function () { + $(document).on(Views.chatView.events.videosOn, function () { $('#wrapper').removeClass('hideVideos') }) - $(document).on(Metamaps.Views.chatView.events.videosOff, function () { + $(document).on(Views.chatView.events.videosOff, function () { $('#wrapper').addClass('hideVideos') }) - $(document).on(Metamaps.Views.chatView.events.cursorsOn, function () { + $(document).on(Views.chatView.events.cursorsOn, function () { $('#wrapper').removeClass('hideCursors') }) - $(document).on(Metamaps.Views.chatView.events.cursorsOff, function () { + $(document).on(Views.chatView.events.cursorsOff, function () { $('#wrapper').addClass('hideCursors') }) }, @@ -187,8 +187,8 @@ const Realtime = { startActiveMap: function () { var self = Realtime - if (Metamaps.Active.Map && Metamaps.Active.Mapper) { - if (Metamaps.Active.Map.authorizeToEdit(Metamaps.Active.Mapper)) { + if (Active.Map && Active.Mapper) { + if (Active.Map.authorizeToEdit(Active.Mapper)) { self.turnOn() self.setupSocket() } else { @@ -219,15 +219,15 @@ const Realtime = { self.status = true $('.collabCompass').show() self.room.chat.$container.show() - self.room.room = 'map-' + Metamaps.Active.Map.id + self.room.room = 'map-' + Active.Map.id self.checkForACallToJoin() self.activeMapper = { - id: Metamaps.Active.Mapper.id, - name: Metamaps.Active.Mapper.get('name'), - username: Metamaps.Active.Mapper.get('name'), - image: Metamaps.Active.Mapper.get('image'), - color: Metamaps.Util.getPastelColor(), + id: Active.Mapper.id, + name: Active.Mapper.get('name'), + username: Active.Mapper.get('name'), + image: Active.Mapper.get('image'), + color: Util.getPastelColor(), self: true } self.localVideo.view.$container.find('.video-cutoff').css({ @@ -237,7 +237,7 @@ const Realtime = { }, checkForACallToJoin: function () { var self = Realtime - self.socket.emit('checkForCall', { room: self.room.room, mapid: Metamaps.Active.Map.id }) + self.socket.emit('checkForCall', { room: self.room.room, mapid: Active.Map.id }) }, promptToJoin: function () { var self = Realtime @@ -245,7 +245,7 @@ const Realtime = { var notifyText = "There's a conversation happening, want to join?" notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.GlobalUI.clearNotify()">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) self.room.conversationInProgress() }, conversationHasBegun: function () { @@ -255,7 +255,7 @@ const Realtime = { var notifyText = "There's a conversation starting, want to join?" notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.GlobalUI.clearNotify()">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) self.room.conversationInProgress() }, countOthersInConversation: function () { @@ -275,7 +275,7 @@ const Realtime = { if (self.inConversation) { var username = mapper.name var notifyText = username + ' joined the call' - Metamaps.GlobalUI.notifyUser(notifyText) + GlobalUI.notifyUser(notifyText) } mapper.inConversation = true @@ -290,7 +290,7 @@ const Realtime = { if (self.inConversation) { var username = mapper.name var notifyText = username + ' left the call' - Metamaps.GlobalUI.notifyUser(notifyText) + GlobalUI.notifyUser(notifyText) } mapper.inConversation = false @@ -332,7 +332,7 @@ const Realtime = { notifyText += username + ' is inviting you to a conversation. Join live?' notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.acceptCall(' + inviter + ')">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.Realtime.denyCall(' + inviter + ')">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) }, invitedToJoin: function (inviter) { var self = Realtime @@ -344,55 +344,55 @@ const Realtime = { var notifyText = username + ' is inviting you to the conversation. Join?' notifyText += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.joinCall()">Yes</button>' notifyText += ' <button type="button" class="toast-button button btn-no" onclick="Metamaps.Realtime.denyInvite(' + inviter + ')">No</button>' - Metamaps.GlobalUI.notifyUser(notifyText, true) + GlobalUI.notifyUser(notifyText, true) }, acceptCall: function (userid) { var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callAccepted', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + invited: Active.Mapper.id, inviter: userid }) - $.post('/maps/' + Metamaps.Active.Map.id + '/events/conversation') + $.post('/maps/' + Active.Map.id + '/events/conversation') self.joinCall() - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, denyCall: function (userid) { var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('callDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + invited: Active.Mapper.id, inviter: userid }) - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, denyInvite: function (userid) { var self = Realtime self.room.chat.sound.stop('sessioninvite') self.socket.emit('inviteDenied', { - mapid: Metamaps.Active.Map.id, - invited: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + invited: Active.Mapper.id, inviter: userid }) - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, inviteACall: function (userid) { var self = Realtime self.socket.emit('inviteACall', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + inviter: Active.Mapper.id, invited: userid }) self.room.chat.invitationPending(userid) - Metamaps.GlobalUI.clearNotify() + GlobalUI.clearNotify() }, inviteToJoin: function (userid) { var self = Realtime self.socket.emit('inviteToJoin', { - mapid: Metamaps.Active.Map.id, - inviter: Metamaps.Active.Mapper.id, + mapid: Active.Map.id, + inviter: Active.Mapper.id, invited: userid }) self.room.chat.invitationPending(userid) @@ -401,7 +401,7 @@ const Realtime = { var self = Realtime var username = self.mappersOnMap[userid].name - Metamaps.GlobalUI.notifyUser('Conversation starting...') + GlobalUI.notifyUser('Conversation starting...') self.joinCall() self.room.chat.invitationAnswered(userid) }, @@ -409,14 +409,14 @@ const Realtime = { var self = Realtime var username = self.mappersOnMap[userid].name - Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, inviteDenied: function (userid) { var self = Realtime var username = self.mappersOnMap[userid].name - Metamaps.GlobalUI.notifyUser(username + " didn't accept your invitation") + GlobalUI.notifyUser(username + " didn't accept your invitation") self.room.chat.invitationAnswered(userid) }, joinCall: function () { @@ -436,22 +436,22 @@ const Realtime = { }) self.inConversation = true self.socket.emit('mapperJoinedCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id + mapid: Active.Map.id, + id: Active.Mapper.id }) self.webrtc.startLocalVideo() - Metamaps.GlobalUI.clearNotify() - self.room.chat.mapperJoinedCall(Metamaps.Active.Mapper.id) + GlobalUI.clearNotify() + self.room.chat.mapperJoinedCall(Active.Mapper.id) }, leaveCall: function () { var self = Realtime self.socket.emit('mapperLeftCall', { - mapid: Metamaps.Active.Map.id, - id: Metamaps.Active.Mapper.id + mapid: Active.Map.id, + id: Active.Mapper.id }) - self.room.chat.mapperLeftCall(Metamaps.Active.Mapper.id) + self.room.chat.mapperLeftCall(Active.Mapper.id) self.room.leaveVideoOnly() self.inConversation = false self.localVideo.view.$container.hide() @@ -479,63 +479,63 @@ const Realtime = { setupSocket: function () { var self = Realtime var socket = Realtime.socket - var myId = Metamaps.Active.Mapper.id + var myId = Active.Mapper.id socket.emit('newMapperNotify', { userid: myId, - username: Metamaps.Active.Mapper.get('name'), - userimage: Metamaps.Active.Mapper.get('image'), - mapid: Metamaps.Active.Map.id + username: Active.Mapper.get('name'), + userimage: Active.Mapper.get('image'), + mapid: Active.Map.id }) - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToCall', self.invitedToCall) // new call - socket.on(myId + '-' + Metamaps.Active.Map.id + '-invitedToJoin', self.invitedToJoin) // call already in progress - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callAccepted', self.callAccepted) - socket.on(myId + '-' + Metamaps.Active.Map.id + '-callDenied', self.callDenied) - socket.on(myId + '-' + Metamaps.Active.Map.id + '-inviteDenied', self.inviteDenied) + socket.on(myId + '-' + Active.Map.id + '-invitedToCall', self.invitedToCall) // new call + socket.on(myId + '-' + Active.Map.id + '-invitedToJoin', self.invitedToJoin) // call already in progress + socket.on(myId + '-' + Active.Map.id + '-callAccepted', self.callAccepted) + socket.on(myId + '-' + Active.Map.id + '-callDenied', self.callDenied) + socket.on(myId + '-' + Active.Map.id + '-inviteDenied', self.inviteDenied) // receive word that there's a conversation in progress - socket.on('maps-' + Metamaps.Active.Map.id + '-callInProgress', self.promptToJoin) - socket.on('maps-' + Metamaps.Active.Map.id + '-callStarting', self.conversationHasBegun) + socket.on('maps-' + Active.Map.id + '-callInProgress', self.promptToJoin) + socket.on('maps-' + Active.Map.id + '-callStarting', self.conversationHasBegun) - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall) - socket.on('maps-' + Metamaps.Active.Map.id + '-mapperLeftCall', self.mapperLeftCall) + socket.on('maps-' + Active.Map.id + '-mapperJoinedCall', self.mapperJoinedCall) + socket.on('maps-' + Active.Map.id + '-mapperLeftCall', self.mapperLeftCall) // if you're the 'new guy' update your list with who's already online - socket.on(myId + '-' + Metamaps.Active.Map.id + '-UpdateMapperList', self.updateMapperList) + socket.on(myId + '-' + Active.Map.id + '-UpdateMapperList', self.updateMapperList) // receive word that there's a new mapper on the map - socket.on('maps-' + Metamaps.Active.Map.id + '-newmapper', self.newPeerOnMap) + socket.on('maps-' + Active.Map.id + '-newmapper', self.newPeerOnMap) // receive word that a mapper left the map - socket.on('maps-' + Metamaps.Active.Map.id + '-lostmapper', self.lostPeerOnMap) + socket.on('maps-' + Active.Map.id + '-lostmapper', self.lostPeerOnMap) // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-newrealtime', self.newCollaborator) + socket.on('maps-' + Active.Map.id + '-newrealtime', self.newCollaborator) // receive word that there's a mapper turned on realtime - socket.on('maps-' + Metamaps.Active.Map.id + '-lostrealtime', self.lostCollaborator) + socket.on('maps-' + Active.Map.id + '-lostrealtime', self.lostCollaborator) // - socket.on('maps-' + Metamaps.Active.Map.id + '-topicDrag', self.topicDrag) + socket.on('maps-' + Active.Map.id + '-topicDrag', self.topicDrag) // - socket.on('maps-' + Metamaps.Active.Map.id + '-newTopic', self.newTopic) + socket.on('maps-' + Active.Map.id + '-newTopic', self.newTopic) // - socket.on('maps-' + Metamaps.Active.Map.id + '-newMessage', self.newMessage) + socket.on('maps-' + Active.Map.id + '-newMessage', self.newMessage) // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeTopic', self.removeTopic) + socket.on('maps-' + Active.Map.id + '-removeTopic', self.removeTopic) // - socket.on('maps-' + Metamaps.Active.Map.id + '-newSynapse', self.newSynapse) + socket.on('maps-' + Active.Map.id + '-newSynapse', self.newSynapse) // - socket.on('maps-' + Metamaps.Active.Map.id + '-removeSynapse', self.removeSynapse) + socket.on('maps-' + Active.Map.id + '-removeSynapse', self.removeSynapse) // update mapper compass position - socket.on('maps-' + Metamaps.Active.Map.id + '-updatePeerCoords', self.updatePeerCoords) + socket.on('maps-' + Active.Map.id + '-updatePeerCoords', self.updatePeerCoords) // deletions socket.on('deleteTopicFromServer', self.removeTopic) @@ -551,7 +551,7 @@ const Realtime = { x: event.pageX, y: event.pageY } - var coords = Metamaps.Util.pixelsToCoords(pixels) + var coords = Util.pixelsToCoords(pixels) self.sendCoords(coords) } $(document).on('mousemove.map', sendCoords) @@ -562,54 +562,54 @@ const Realtime = { x: e.pageX, y: e.pageY } - var coords = Metamaps.Util.pixelsToCoords(pixels) + var coords = Util.pixelsToCoords(pixels) self.sendCoords(coords) } self.positionPeerIcons() } - $(document).on(Metamaps.JIT.events.zoom + '.map', zoom) + $(document).on(JIT.events.zoom + '.map', zoom) - $(document).on(Metamaps.JIT.events.pan + '.map', self.positionPeerIcons) + $(document).on(JIT.events.pan + '.map', self.positionPeerIcons) var sendTopicDrag = function (event, positions) { self.sendTopicDrag(positions) } - $(document).on(Metamaps.JIT.events.topicDrag + '.map', sendTopicDrag) + $(document).on(JIT.events.topicDrag + '.map', sendTopicDrag) var sendNewTopic = function (event, data) { self.sendNewTopic(data) } - $(document).on(Metamaps.JIT.events.newTopic + '.map', sendNewTopic) + $(document).on(JIT.events.newTopic + '.map', sendNewTopic) var sendDeleteTopic = function (event, data) { self.sendDeleteTopic(data) } - $(document).on(Metamaps.JIT.events.deleteTopic + '.map', sendDeleteTopic) + $(document).on(JIT.events.deleteTopic + '.map', sendDeleteTopic) var sendRemoveTopic = function (event, data) { self.sendRemoveTopic(data) } - $(document).on(Metamaps.JIT.events.removeTopic + '.map', sendRemoveTopic) + $(document).on(JIT.events.removeTopic + '.map', sendRemoveTopic) var sendNewSynapse = function (event, data) { self.sendNewSynapse(data) } - $(document).on(Metamaps.JIT.events.newSynapse + '.map', sendNewSynapse) + $(document).on(JIT.events.newSynapse + '.map', sendNewSynapse) var sendDeleteSynapse = function (event, data) { self.sendDeleteSynapse(data) } - $(document).on(Metamaps.JIT.events.deleteSynapse + '.map', sendDeleteSynapse) + $(document).on(JIT.events.deleteSynapse + '.map', sendDeleteSynapse) var sendRemoveSynapse = function (event, data) { self.sendRemoveSynapse(data) } - $(document).on(Metamaps.JIT.events.removeSynapse + '.map', sendRemoveSynapse) + $(document).on(JIT.events.removeSynapse + '.map', sendRemoveSynapse) var sendNewMessage = function (event, data) { self.sendNewMessage(data) } - $(document).on(Metamaps.Views.room.events.newMessage + '.map', sendNewMessage) + $(document).on(Views.room.events.newMessage + '.map', sendNewMessage) }, attachMapListener: function () { var self = Realtime @@ -623,9 +623,9 @@ const Realtime = { // send this new mapper back your details, and the awareness that you're online var update = { - username: Metamaps.Active.Mapper.get('name'), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id + username: Active.Mapper.get('name'), + userid: Active.Mapper.id, + mapid: Active.Map.id } socket.emit('notifyStartRealtime', update) }, @@ -635,9 +635,9 @@ const Realtime = { // send this new mapper back your details, and the awareness that you're online var update = { - username: Metamaps.Active.Mapper.get('name'), - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id + username: Active.Mapper.get('name'), + userid: Active.Mapper.id, + mapid: Active.Map.id } socket.emit('notifyStopRealtime', update) }, @@ -655,7 +655,7 @@ const Realtime = { name: data.username, username: data.username, image: data.userimage, - color: Metamaps.Util.getPastelColor(), + color: Util.getPastelColor(), realtime: data.userrealtime, inConversation: data.userinconversation, coords: { @@ -664,7 +664,7 @@ const Realtime = { } } - if (data.userid !== Metamaps.Active.Mapper.id) { + if (data.userid !== Active.Mapper.id) { self.room.chat.addParticipant(self.mappersOnMap[data.userid]) if (data.userinconversation) self.room.chat.mapperJoinedCall(data.userid) @@ -687,7 +687,7 @@ const Realtime = { name: data.username, username: data.username, image: data.userimage, - color: Metamaps.Util.getPastelColor(), + color: Util.getPastelColor(), realtime: true, coords: { x: 0, @@ -696,7 +696,7 @@ const Realtime = { } // create an item for them in the realtime box - if (data.userid !== Metamaps.Active.Mapper.id && self.status) { + if (data.userid !== Active.Mapper.id && self.status) { self.room.chat.sound.play('joinmap') self.room.chat.addParticipant(self.mappersOnMap[data.userid]) @@ -707,17 +707,17 @@ const Realtime = { if (firstOtherPerson) { notifyMessage += ' <button type="button" class="toast-button button" onclick="Metamaps.Realtime.inviteACall(' + data.userid + ')">Suggest A Video Call</button>' } - Metamaps.GlobalUI.notifyUser(notifyMessage) + GlobalUI.notifyUser(notifyMessage) // send this new mapper back your details, and the awareness that you've loaded the map var update = { userToNotify: data.userid, - username: Metamaps.Active.Mapper.get('name'), - userimage: Metamaps.Active.Mapper.get('image'), - userid: Metamaps.Active.Mapper.id, + username: Active.Mapper.get('name'), + userimage: Active.Mapper.get('image'), + userid: Active.Mapper.id, userrealtime: self.status, userinconversation: self.inConversation, - mapid: Metamaps.Active.Map.id + mapid: Active.Map.id } socket.emit('updateNewMapperList', update) } @@ -753,7 +753,7 @@ const Realtime = { $('#compass' + data.userid).remove() self.room.chat.removeParticipant(data.username) - Metamaps.GlobalUI.notifyUser(data.username + ' just left the map') + GlobalUI.notifyUser(data.username + ' just left the map') if ((self.inConversation && self.countOthersInConversation() === 0) || (!self.inConversation && self.countOthersInConversation() === 1)) { @@ -772,7 +772,7 @@ const Realtime = { // $('#mapper' + data.userid).removeClass('littleRtOff').addClass('littleRtOn') $('#compass' + data.userid).show() - Metamaps.GlobalUI.notifyUser(data.username + ' just turned on realtime') + GlobalUI.notifyUser(data.username + ' just turned on realtime') }, lostCollaborator: function (data) { var self = Realtime @@ -786,7 +786,7 @@ const Realtime = { // $('#mapper' + data.userid).removeClass('littleRtOn').addClass('littleRtOff') $('#compass' + data.userid).hide() - Metamaps.GlobalUI.notifyUser(data.username + ' just turned off realtime') + GlobalUI.notifyUser(data.username + ' just turned off realtime') }, updatePeerCoords: function (data) { var self = Realtime @@ -819,7 +819,7 @@ const Realtime = { var compassDiameter = 56 var compassArrowSize = 24 - var origPixels = Metamaps.Util.coordsToPixels(mapper.coords) + var origPixels = Util.coordsToPixels(mapper.coords) var pixels = self.limitPixelsToScreen(origPixels) $('#compass' + id).css({ left: pixels.x + 'px', @@ -867,14 +867,14 @@ const Realtime = { var self = Realtime var socket = Realtime.socket - var map = Metamaps.Active.Map - var mapper = Metamaps.Active.Mapper + var map = Active.Map + var mapper = Active.Mapper if (self.status && map.authorizeToEdit(mapper) && socket) { var update = { usercoords: coords, - userid: Metamaps.Active.Mapper.id, - mapid: Metamaps.Active.Map.id + userid: Active.Mapper.id, + mapid: Active.Map.id } socket.emit('updateMapperCoords', update) } @@ -883,8 +883,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map && self.status) { - positions.mapid = Metamaps.Active.Map.id + if (Active.Map && self.status) { + positions.mapid = Active.Map.id socket.emit('topicDrag', positions) } }, @@ -895,13 +895,13 @@ const Realtime = { var topic var node - if (Metamaps.Active.Map && self.status) { + if (Active.Map && self.status) { for (var key in positions) { topic = Metamaps.Topics.get(key) if (topic) node = topic.get('node') if (node) node.pos.setc(positions[key].x, positions[key].y) } // for - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } }, sendTopicChange: function (topic) { @@ -960,23 +960,23 @@ const Realtime = { socket.emit('mapChangeFromClient', data) }, mapChange: function (data) { - var map = Metamaps.Active.Map + var map = Active.Map var isActiveMap = map && data.mapId === map.id if (isActiveMap) { - var couldEditBefore = map.authorizeToEdit(Metamaps.Active.Mapper) + var couldEditBefore = map.authorizeToEdit(Active.Mapper) var idBefore = map.id map.fetch({ success: function (model, response) { var idNow = model.id - var canEditNow = model.authorizeToEdit(Metamaps.Active.Mapper) + var canEditNow = model.authorizeToEdit(Active.Mapper) if (idNow !== idBefore) { - Metamaps.Map.leavePrivateMap() // this means the map has been changed to private + Map.leavePrivateMap() // this means the map has been changed to private } else if (couldEditBefore && !canEditNow) { - Metamaps.Map.cantEditNow() + Map.cantEditNow() } else if (!couldEditBefore && canEditNow) { - Metamaps.Map.canEditNow() + Map.canEditNow() } else { model.fetchContained() model.trigger('changeByOther') @@ -991,7 +991,7 @@ const Realtime = { var socket = self.socket var message = data.attributes - message.mapid = Metamaps.Active.Map.id + message.mapid = Active.Map.id socket.emit('newMessage', message) }, newMessage: function (data) { @@ -1005,14 +1005,14 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map && self.status) { - data.mapperid = Metamaps.Active.Mapper.id - data.mapid = Metamaps.Active.Map.id + if (Active.Map && self.status) { + data.mapperid = Active.Mapper.id + data.mapid = Active.Map.id socket.emit('newTopic', data) } }, newTopic: function (data) { - var topic, mapping, mapper, mapperCallback, cancel + var topic, mapping, mapper, cancel var self = Realtime var socket = self.socket @@ -1021,7 +1021,7 @@ const Realtime = { function waitThenRenderTopic () { if (topic && mapping && mapper) { - Metamaps.Topic.renderTopic(mapping, topic, false, false) + Topic.renderTopic(mapping, topic, false, false) } else if (!cancel) { setTimeout(waitThenRenderTopic, 10) @@ -1030,11 +1030,10 @@ const Realtime = { mapper = Metamaps.Mappers.get(data.mapperid) if (mapper === undefined) { - mapperCallback = function (m) { + Mapper.get(data.mapperid, function(m) { Metamaps.Mappers.add(m) mapper = m - } - Metamaps.Mapper.get(data.mapperid, mapperCallback) + }) } $.ajax({ url: '/topics/' + data.mappableid + '.json', @@ -1064,7 +1063,7 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { + if (Active.Map) { socket.emit('deleteTopicFromClient', data) } }, @@ -1073,8 +1072,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapid = Active.Map.id socket.emit('removeTopic', data) } }, @@ -1088,7 +1087,7 @@ const Realtime = { if (topic) { var node = topic.get('node') var mapping = topic.getMapping() - Metamaps.Control.hideNode(node.id) + Control.hideNode(node.id) Metamaps.Topics.remove(topic) Metamaps.Mappings.remove(mapping) } @@ -1098,9 +1097,9 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapperid = Metamaps.Active.Mapper.id - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapperid = Active.Mapper.id + data.mapid = Active.Map.id socket.emit('newSynapse', data) } }, @@ -1119,7 +1118,7 @@ const Realtime = { topic2 = synapse.getTopic2() node2 = topic2.get('node') - Metamaps.Synapse.renderSynapse(mapping, synapse, node1, node2, false) + Synapse.renderSynapse(mapping, synapse, node1, node2, false) } else if (!cancel) { setTimeout(waitThenRenderSynapse, 10) @@ -1128,11 +1127,10 @@ const Realtime = { mapper = Metamaps.Mappers.get(data.mapperid) if (mapper === undefined) { - mapperCallback = function (m) { + Mapper.get(data.mapperid, function(m) { Metamaps.Mappers.add(m) mapper = m - } - Metamaps.Mapper.get(data.mapperid, mapperCallback) + }) } $.ajax({ url: '/synapses/' + data.mappableid + '.json', @@ -1161,8 +1159,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapid = Active.Map.id socket.emit('deleteSynapseFromClient', data) } }, @@ -1171,8 +1169,8 @@ const Realtime = { var self = Realtime var socket = self.socket - if (Metamaps.Active.Map) { - data.mapid = Metamaps.Active.Map.id + if (Active.Map) { + data.mapid = Active.Map.id socket.emit('removeSynapse', data) } }, @@ -1187,7 +1185,7 @@ const Realtime = { var edge = synapse.get('edge') var mapping = synapse.getMapping() if (edge.getData('mappings').length - 1 === 0) { - Metamaps.Control.hideEdge(edge) + Control.hideEdge(edge) } var index = _.indexOf(edge.getData('synapses'), synapse) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 8aacadd1..d5c07e12 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -1,19 +1,19 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, $ */ +import Active from './Active' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Map from './Map' +import Topic from './Topic' +import Views from './Views' +import Visualize from './Visualize' + /* * Metamaps.Router.js.erb * * Dependencies: - * - Metamaps.Active - * - Metamaps.GlobalUI - * - Metamaps.JIT * - Metamaps.Loading - * - Metamaps.Map * - Metamaps.Maps - * - Metamaps.Topic - * - Metamaps.Views - * - Metamaps.Visualize */ const _Router = Backbone.Router.extend({ @@ -24,53 +24,53 @@ const _Router = Backbone.Router.extend({ 'maps/:id': 'maps' // #maps/7 }, home: function () { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) - if (Metamaps.Active.Mapper) document.title = 'Explore Active Maps | Metamaps' + if (Active.Mapper) document.title = 'Explore Active Maps | Metamaps' else document.title = 'Home | Metamaps' - Metamaps.Router.currentSection = '' - Metamaps.Router.currentPage = '' + this.currentSection = '' + this.currentPage = '' $('.wrapper').removeClass('mapPage topicPage') - var classes = Metamaps.Active.Mapper ? 'homePage explorePage' : 'homePage' + var classes = Active.Mapper ? 'homePage explorePage' : 'homePage' $('.wrapper').addClass(classes) var navigate = function () { - Metamaps.Router.timeoutId = setTimeout(function () { - Metamaps.Router.navigate('') + this.timeoutId = setTimeout(function () { + this.navigate('') }, 300) } // all this only for the logged in home page - if (Metamaps.Active.Mapper) { + if (Active.Mapper) { $('.homeButton a').attr('href', '/') - Metamaps.GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.showDiv('#explore') + GlobalUI.showDiv('#explore') - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Views.exploreMaps.setCollection(Metamaps.Maps.Active) if (Metamaps.Maps.Active.length === 0) { Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { - Metamaps.Views.exploreMaps.render(navigate) + Views.exploreMaps.render(navigate) } } else { // logged out home page - Metamaps.GlobalUI.hideDiv('#explore') - Metamaps.GlobalUI.showDiv('#yield') - Metamaps.Router.timeoutId = setTimeout(navigate, 500) + GlobalUI.hideDiv('#explore') + GlobalUI.showDiv('#yield') + this.timeoutId = setTimeout(navigate, 500) } - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null + GlobalUI.hideDiv('#infovis') + GlobalUI.hideDiv('#instructions') + Map.end() + Topic.end() + Active.Map = null + Active.Topic = null }, explore: function (section, id) { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) // just capitalize the variable section // either 'featured', 'mapper', or 'active' @@ -90,12 +90,12 @@ const _Router = Backbone.Router.extend({ document.title = 'Explore My Maps | Metamaps' } - if (Metamaps.Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) + if (Active.Mapper && section != 'mapper') $('.homeButton a').attr('href', '/explore/' + section) $('.wrapper').removeClass('homePage mapPage topicPage') $('.wrapper').addClass('explorePage') - Metamaps.Router.currentSection = 'explore' - Metamaps.Router.currentPage = section + this.currentSection = 'explore' + this.currentPage = section // this will mean it's a mapper page being loaded if (id) { @@ -108,20 +108,20 @@ const _Router = Backbone.Router.extend({ Metamaps.Maps.Mapper.mapperId = id } - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) var navigate = function () { - var path = '/explore/' + Metamaps.Router.currentPage + var path = '/explore/' + this.currentPage // alter url if for mapper profile page - if (Metamaps.Router.currentPage === 'mapper') { + if (this.currentPage === 'mapper') { path += '/' + Metamaps.Maps.Mapper.mapperId } - Metamaps.Router.navigate(path) + this.navigate(path) } var navigateTimeout = function () { - Metamaps.Router.timeoutId = setTimeout(navigate, 300) + this.timeoutId = setTimeout(navigate, 300) } if (Metamaps.Maps[capitalize].length === 0) { Metamaps.Loading.show() @@ -130,77 +130,77 @@ const _Router = Backbone.Router.extend({ }, 300) // wait 300 milliseconds till the other animations are done to do the fetch } else { if (id) { - Metamaps.Views.exploreMaps.fetchUserThenRender(navigateTimeout) + Views.exploreMaps.fetchUserThenRender(navigateTimeout) } else { - Metamaps.Views.exploreMaps.render(navigateTimeout) + Views.exploreMaps.render(navigateTimeout) } } - Metamaps.GlobalUI.showDiv('#explore') - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#infovis') - Metamaps.GlobalUI.hideDiv('#instructions') - Metamaps.Map.end() - Metamaps.Topic.end() - Metamaps.Active.Map = null - Metamaps.Active.Topic = null + GlobalUI.showDiv('#explore') + GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#infovis') + GlobalUI.hideDiv('#instructions') + Map.end() + Topic.end() + Active.Map = null + Active.Topic = null }, maps: function (id) { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) document.title = 'Map ' + id + ' | Metamaps' - Metamaps.Router.currentSection = 'map' - Metamaps.Router.currentPage = id + this.currentSection = 'map' + this.currentPage = id $('.wrapper').removeClass('homePage explorePage topicPage') $('.wrapper').addClass('mapPage') // another class will be added to wrapper if you // can edit this map '.canEditMap' - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') + GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#explore') // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + if (Visualize.mGraph) { + Visualize.mGraph.graph.empty() + Visualize.mGraph.plot() + JIT.centerMap(Visualize.mGraph.canvas) } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Topic.end() - Metamaps.Active.Topic = null + GlobalUI.showDiv('#infovis') + Topic.end() + Active.Topic = null Metamaps.Loading.show() - Metamaps.Map.end() - Metamaps.Map.launch(id) + Map.end() + Map.launch(id) }, topics: function (id) { - clearTimeout(Metamaps.Router.timeoutId) + clearTimeout(this.timeoutId) document.title = 'Topic ' + id + ' | Metamaps' - Metamaps.Router.currentSection = 'topic' - Metamaps.Router.currentPage = id + this.currentSection = 'topic' + this.currentPage = id $('.wrapper').removeClass('homePage explorePage mapPage') $('.wrapper').addClass('topicPage') - Metamaps.GlobalUI.hideDiv('#yield') - Metamaps.GlobalUI.hideDiv('#explore') + GlobalUI.hideDiv('#yield') + GlobalUI.hideDiv('#explore') // clear the visualization, if there was one, before showing its div again - if (Metamaps.Visualize.mGraph) { - Metamaps.Visualize.mGraph.graph.empty() - Metamaps.Visualize.mGraph.plot() - Metamaps.JIT.centerMap(Metamaps.Visualize.mGraph.canvas) + if (Visualize.mGraph) { + Visualize.mGraph.graph.empty() + Visualize.mGraph.plot() + JIT.centerMap(Visualize.mGraph.canvas) } - Metamaps.GlobalUI.showDiv('#infovis') - Metamaps.Map.end() - Metamaps.Active.Map = null + GlobalUI.showDiv('#infovis') + Map.end() + Active.Map = null - Metamaps.Topic.end() - Metamaps.Topic.launch(id) + Topic.end() + Topic.launch(id) } }) @@ -227,9 +227,9 @@ Router.intercept = function (evt) { segments.splice(0, 1) // pop off the element created by the first / if (href.attr === '') { - Metamaps.Router.home() + Router.home() } else { - Metamaps.Router[segments[0]](segments[1], segments[2]) + Router[segments[0]](segments[1], segments[2]) } } } @@ -240,7 +240,7 @@ Router.init = function () { pushState: true, root: '/' }) - $(document).on('click', 'a[data-router="true"]', Metamaps.Router.intercept) + $(document).on('click', 'a[data-router="true"]', Router.intercept) } export default Router diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 5258de3b..b50e50e6 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -1,20 +1,22 @@ /* global Metamaps, $ */ +import Active from './Active' +import Control from './Control' +import Create from './Create' +import JIT from './JIT' +import Map from './Map' +import Selected from './Selected' +import Settings from './Settings' +import Visualize from './Visualize' + /* * Metamaps.Synapse.js.erb * * Dependencies: * - Metamaps.Backbone - * - Metamaps.Control - * - Metamaps.Create - * - Metamaps.JIT - * - Metamaps.Map * - Metamaps.Mappings - * - Metamaps.Selected - * - Metamaps.Settings * - Metamaps.Synapses * - Metamaps.Topics - * - Metamaps.Visualize */ const Synapse = { @@ -52,18 +54,18 @@ const Synapse = { * */ renderSynapse: function (mapping, synapse, node1, node2, createNewInDB) { - var self = Metamaps.Synapse + var self = Synapse var edgeOnViz var newedge = synapse.createEdge(mapping) - Metamaps.Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data) - edgeOnViz = Metamaps.Visualize.mGraph.graph.getAdjacence(node1.id, node2.id) + Visualize.mGraph.graph.addAdjacence(node1, node2, newedge.data) + edgeOnViz = Visualize.mGraph.graph.getAdjacence(node1.id, node2.id) synapse.set('edge', edgeOnViz) synapse.updateEdge() // links the synapse and the mapping to the edge - Metamaps.Control.selectEdge(edgeOnViz) + Control.selectEdge(edgeOnViz) var mappingSuccessCallback = function (mappingModel, response) { var newSynapseData = { @@ -71,17 +73,17 @@ const Synapse = { mappableid: mappingModel.get('mappable_id') } - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + $(document).trigger(JIT.events.newSynapse, [newSynapseData]) } var synapseSuccessCallback = function (synapseModel, response) { - if (Metamaps.Active.Map) { + if (Active.Map) { mapping.save({ mappable_id: synapseModel.id }, { success: mappingSuccessCallback }) } } - if (!Metamaps.Settings.sandbox && createNewInDB) { + if (!Settings.sandbox && createNewInDB) { if (synapse.isNew()) { synapse.save(null, { success: synapseSuccessCallback, @@ -89,7 +91,7 @@ const Synapse = { console.log('error saving synapse to database') } }) - } else if (!synapse.isNew() && Metamaps.Active.Map) { + } else if (!synapse.isNew() && Active.Map) { mapping.save(null, { success: mappingSuccessCallback }) @@ -105,27 +107,27 @@ const Synapse = { synapse, mapping - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) // for each node in this array we will create a synapse going to the position2 node. var synapsesToCreate = [] - topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id) + topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) node2 = topic2.get('node') - var len = Metamaps.Selected.Nodes.length + var len = Selected.Nodes.length if (len == 0) { - topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id) + topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) synapsesToCreate[0] = topic1.get('node') } else if (len > 0) { - synapsesToCreate = Metamaps.Selected.Nodes + synapsesToCreate = Selected.Nodes } for (var i = 0; i < synapsesToCreate.length; i++) { node1 = synapsesToCreate[i] topic1 = node1.getData('topic') synapse = new Metamaps.Backbone.Synapse({ - desc: Metamaps.Create.newSynapse.description, + desc: Create.newSynapse.description, node1_id: topic1.isNew() ? topic1.cid : topic1.id, node2_id: topic2.isNew() ? topic2.cid : topic2.id, }) @@ -141,7 +143,7 @@ const Synapse = { self.renderSynapse(mapping, synapse, node1, node2, true) } // for each in synapsesToCreate - Metamaps.Create.newSynapse.hide() + Create.newSynapse.hide() }, getSynapseFromAutocomplete: function (id) { var self = Synapse, @@ -158,11 +160,11 @@ const Synapse = { }) Metamaps.Mappings.add(mapping) - topic1 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic1id) + topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) node1 = topic1.get('node') - topic2 = Metamaps.Topics.get(Metamaps.Create.newSynapse.topic2id) + topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) node2 = topic2.get('node') - Metamaps.Create.newSynapse.hide() + Create.newSynapse.hide() self.renderSynapse(mapping, synapse, node1, node2, true) } diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index e0315486..28ff1e32 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global $ */ import Active from './Active' import Control from './Control' import Mapper from './Mapper' diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index ab93e419..412f7ef2 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,26 +1,29 @@ /* global Metamaps, $ */ import Active from './Active' +import AutoLayout from './AutoLayout' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' import JIT from './JIT' +import Map from './Map' +import Router from './Router' import Selected from './Selected' import Settings from './Settings' +import SynapseCard from './SynapseCard' +import TopicCard from './TopicCard' import Util from './Util' +import Visualize from './Visualize' /* * Metamaps.Topic.js.erb * * Dependencies: * - Metamaps.Backbone - * - Metamaps.Create * - Metamaps.Creators - * - Metamaps.Filter - * - Metamaps.GlobalUI * - Metamaps.Mappings - * - Metamaps.SynapseCard * - Metamaps.Synapses - * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Visualize */ const Topic = { @@ -67,19 +70,19 @@ const Topic = { $('#filter_by_mapper h3').html('CREATORS') // build and render the visualization - Metamaps.Visualize.type = 'RGraph' + Visualize.type = 'RGraph' JIT.prepareVizData() // update filters - Metamaps.Filter.reset() + Filter.reset() // reset selected arrays Selected.reset() // these three update the actual filter box with the right list items - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + Filter.checkMetacodes() + Filter.checkSynapses() + Filter.checkMappers() // for mobile $('#header_content').html(Active.Topic.get('name')) @@ -93,22 +96,22 @@ const Topic = { end: function () { if (Active.Topic) { $('.rightclickmenu').remove() - Metamaps.TopicCard.hideCard() - Metamaps.SynapseCard.hideCard() - Metamaps.Filter.close() + TopicCard.hideCard() + SynapseCard.hideCard() + Filter.close() } }, centerOn: function (nodeid, callback) { // don't clash with fetchRelatives - if (!Metamaps.Visualize.mGraph.busy) { - Metamaps.Visualize.mGraph.onClick(nodeid, { + if (!Visualize.mGraph.busy) { + Visualize.mGraph.onClick(nodeid, { hideLabels: false, duration: 1000, onComplete: function () { if (callback) callback() } }) - Metamaps.Router.navigate('/topics/' + nodeid) + Router.navigate('/topics/' + nodeid) Active.Topic = Metamaps.Topics.get(nodeid) } }, @@ -127,7 +130,7 @@ const Topic = { var successCallback; successCallback = function (data) { - if (Metamaps.Visualize.mGraph.busy) { + if (Visualize.mGraph.busy) { // don't clash with centerOn window.setTimeout(function() { successCallback(data) }, 100) return @@ -141,7 +144,7 @@ const Topic = { var synapseColl = new Metamaps.Backbone.SynapseCollection(data.synapses) var graph = JIT.convertModelsToJIT(topicColl, synapseColl)[0] - Metamaps.Visualize.mGraph.op.sum(graph, { + Visualize.mGraph.op.sum(graph, { type: 'fade', duration: 500, hideLabels: false @@ -149,7 +152,7 @@ const Topic = { var i, l, t, s - Metamaps.Visualize.mGraph.graph.eachNode(function (n) { + Visualize.mGraph.graph.eachNode(function (n) { t = Metamaps.Topics.get(n.id) t.set({ node: n }, { silent: true }) t.updateNode() @@ -186,7 +189,7 @@ const Topic = { // opts is additional options in a hash // TODO: move createNewInDB and permitCerateSYnapseAfter into opts renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts) { - var self = Metamaps.Topic + var self = Topic var nodeOnViz, tempPos @@ -194,37 +197,37 @@ const Topic = { var midpoint = {}, pixelPos - if (!$.isEmptyObject(Metamaps.Visualize.mGraph.graph.nodes)) { - Metamaps.Visualize.mGraph.graph.addNode(newnode) - nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id) + if (!$.isEmptyObject(Visualize.mGraph.graph.nodes)) { + Visualize.mGraph.graph.addNode(newnode) + nodeOnViz = Visualize.mGraph.graph.getNode(newnode.id) topic.set('node', nodeOnViz, {silent: true}) topic.updateNode() // links the topic and the mapping to the node nodeOnViz.setData('dim', 1, 'start') nodeOnViz.setData('dim', 25, 'end') - if (Metamaps.Visualize.type === 'RGraph') { + if (Visualize.type === 'RGraph') { tempPos = new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')) tempPos = tempPos.toPolar() nodeOnViz.setPos(tempPos, 'current') nodeOnViz.setPos(tempPos, 'start') nodeOnViz.setPos(tempPos, 'end') - } else if (Metamaps.Visualize.type === 'ForceDirected') { + } else if (Visualize.type === 'ForceDirected') { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'current') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'start') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') } - if (Metamaps.Create.newTopic.addSynapse && permitCreateSynapseAfter) { - Metamaps.Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id + if (Create.newTopic.addSynapse && permitCreateSynapseAfter) { + Create.newSynapse.topic1id = JIT.tempNode.getData('topic').id // position the form midpoint.x = JIT.tempNode.pos.getc().x + (nodeOnViz.pos.getc().x - JIT.tempNode.pos.getc().x) / 2 midpoint.y = JIT.tempNode.pos.getc().y + (nodeOnViz.pos.getc().y - JIT.tempNode.pos.getc().y) / 2 - pixelPos = Metamaps.Util.coordsToPixels(midpoint) + pixelPos = Util.coordsToPixels(midpoint) $('#new_synapse').css('left', pixelPos.x + 'px') $('#new_synapse').css('top', pixelPos.y + 'px') // show the form - Metamaps.Create.newSynapse.open() - Metamaps.Visualize.mGraph.fx.animate({ + Create.newSynapse.open() + Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, onComplete: function () { @@ -234,16 +237,16 @@ const Topic = { } }) } else { - Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.plotNode(nodeOnViz, Visualize.mGraph.canvas) + Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, onComplete: function () {} }) } } else { - Metamaps.Visualize.mGraph.loadJSON(newnode) - nodeOnViz = Metamaps.Visualize.mGraph.graph.getNode(newnode.id) + Visualize.mGraph.loadJSON(newnode) + nodeOnViz = Visualize.mGraph.graph.getNode(newnode.id) topic.set('node', nodeOnViz, {silent: true}) topic.updateNode() // links the topic and the mapping to the node @@ -252,8 +255,8 @@ const Topic = { nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'current') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'start') nodeOnViz.setPos(new $jit.Complex(mapping.get('xloc'), mapping.get('yloc')), 'end') - Metamaps.Visualize.mGraph.fx.plotNode(nodeOnViz, Metamaps.Visualize.mGraph.canvas) - Metamaps.Visualize.mGraph.fx.animate({ + Visualize.mGraph.fx.plotNode(nodeOnViz, Visualize.mGraph.canvas) + Visualize.mGraph.fx.animate({ modes: ['node-property:dim'], duration: 500, onComplete: function () {} @@ -284,8 +287,8 @@ const Topic = { }) } - if (Metamaps.Create.newTopic.addSynapse) { - Metamaps.Create.newSynapse.topic2id = topicModel.id + if (Create.newTopic.addSynapse) { + Create.newSynapse.topic2id = topicModel.id } } @@ -305,58 +308,58 @@ const Topic = { } }, createTopicLocally: function () { - var self = Metamaps.Topic + var self = Topic - if (Metamaps.Create.newTopic.name === '') { - Metamaps.GlobalUI.notifyUser('Please enter a topic title...') + if (Create.newTopic.name === '') { + GlobalUI.notifyUser('Please enter a topic title...') return } // hide the 'double-click to add a topic' message - Metamaps.GlobalUI.hideDiv('#instructions') + GlobalUI.hideDiv('#instructions') - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) - var metacode = Metamaps.Metacodes.get(Metamaps.Create.newTopic.metacode) + var metacode = Metamaps.Metacodes.get(Create.newTopic.metacode) var topic = new Metamaps.Backbone.Topic({ - name: Metamaps.Create.newTopic.name, + name: Create.newTopic.name, metacode_id: metacode.id, defer_to_map_id: Active.Map.id }) Metamaps.Topics.add(topic) - if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.AutoLayout.getNextCoord() + if (Create.newTopic.pinned) { + var nextCoords = AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, - yloc: nextCoords ? nextCoords.y : Metamaps.Create.newTopic.y, + xloc: nextCoords ? nextCoords.x : Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_id: topic.cid, mappable_type: 'Topic', }) Metamaps.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above - Metamaps.Create.newTopic.hide() + Create.newTopic.hide() self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database }, getTopicFromAutocomplete: function (id) { - var self = Metamaps.Topic + var self = Topic - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) - Metamaps.Create.newTopic.hide() + Create.newTopic.hide() var topic = self.get(id) - if (Metamaps.Create.newTopic.pinned) { - var nextCoords = Metamaps.AutoLayout.getNextCoord() + if (Create.newTopic.pinned) { + var nextCoords = AutoLayout.getNextCoord() } var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords ? nextCoords.x : Metamaps.Create.newTopic.x, - yloc: nextCoords ? nextCoords.y : Metamaps.Create.newTopic.y, + xloc: nextCoords ? nextCoords.x : Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Create.newTopic.y, mappable_type: 'Topic', mappable_id: topic.id, }) @@ -365,13 +368,13 @@ const Topic = { self.renderTopic(mapping, topic, true, true) }, getTopicFromSearch: function (event, id) { - var self = Metamaps.Topic + var self = Topic - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + $(document).trigger(Map.events.editedByActiveMapper) var topic = self.get(id) - var nextCoords = Metamaps.AutoLayout.getNextCoord() + var nextCoords = AutoLayout.getNextCoord() var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords.x, yloc: nextCoords.y, @@ -382,7 +385,7 @@ const Topic = { self.renderTopic(mapping, topic, true, true) - Metamaps.GlobalUI.notifyUser('Topic was added to your map!') + GlobalUI.notifyUser('Topic was added to your map!') event.stopPropagation() event.preventDefault() diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index ebc79575..7320d285 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,7 +1,9 @@ /* global Metamaps, $ */ import Active from './Active' +import GlobalUI from './GlobalUI' import Mapper from './Mapper' +import Router from './Router' import Util from './Util' import Visualize from './Visualize' @@ -9,9 +11,7 @@ import Visualize from './Visualize' * Metamaps.TopicCard.js * * Dependencies: - * - Metamaps.GlobalUI * - Metamaps.Metacodes - * - Metamaps.Router */ const TopicCard = { openTopicCard: null, // stores the topic that's currently open @@ -332,7 +332,7 @@ const TopicCard = { $('.showcard .hoverTip').removeClass('hide') }) - $('.mapCount .tip li a').click(Metamaps.Router.intercept) + $('.mapCount .tip li a').click(Router.intercept) var originalText = $('.showMore').html() $('.mapCount .tip .showMore').unbind().toggle( @@ -353,7 +353,7 @@ const TopicCard = { var self = TopicCard self.removeLink() - Metamaps.GlobalUI.notifyUser('Invalid link') + GlobalUI.notifyUser('Invalid link') }, populateShowCard: function (topic) { var self = TopicCard diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 9e44e8e8..678c7c64 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -2,6 +2,8 @@ import Active from './Active' import JIT from './JIT' +import Router from './Router' +import TopicCard from './TopicCard' /* * Metamaps.Visualize @@ -9,9 +11,7 @@ import JIT from './JIT' * Dependencies: * - Metamaps.Loading * - Metamaps.Metacodes - * - Metamaps.Router * - Metamaps.Synapses - * - Metamaps.TopicCard * - Metamaps.Topics */ @@ -42,7 +42,7 @@ const Visualize = { // prevent touch events on the canvas from default behaviour $('#infovis-canvas').bind('touchend touchcancel', function (event) { lastDist = 0 - if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) Metamaps.TopicCard.hideCurrentCard() + if (!self.mGraph.events.touchMoved && !Visualize.touchDragNode) TopicCard.hideCurrentCard() self.mGraph.events.touched = self.mGraph.events.touchMoved = false Visualize.touchDragNode = false }) @@ -204,16 +204,16 @@ const Visualize = { hold() // update the url now that the map is ready - clearTimeout(Metamaps.Router.timeoutId) - Metamaps.Router.timeoutId = setTimeout(function () { + clearTimeout(Router.timeoutId) + Router.timeoutId = setTimeout(function () { var m = Active.Map var t = Active.Topic if (m && window.location.pathname !== '/maps/' + m.id) { - Metamaps.Router.navigate('/maps/' + m.id) + Router.navigate('/maps/' + m.id) } else if (t && window.location.pathname !== '/topics/' + t.id) { - Metamaps.Router.navigate('/topics/' + t.id) + Router.navigate('/topics/' + t.id) } }, 800) } diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 37e93492..9fe8925b 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -83,22 +83,22 @@ document.addEventListener("DOMContentLoaded", function() { Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) if (Metamaps.currentPage === "mapper") { - Metamaps.Views.exploreMaps.fetchUserThenRender() + Views.exploreMaps.fetchUserThenRender() } else { - Metamaps.Views.exploreMaps.render() + Views.exploreMaps.render() } - Metamaps.GlobalUI.showDiv('#explore') + GlobalUI.showDiv('#explore') } - else if (Metamaps.currentSection === "" && Metamaps.Active.Mapper) { - Metamaps.Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Metamaps.Views.exploreMaps.render() - Metamaps.GlobalUI.showDiv('#explore') + else if (Metamaps.currentSection === "" && Active.Mapper) { + Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Views.exploreMaps.render() + GlobalUI.showDiv('#explore') } - else if (Metamaps.Active.Map || Metamaps.Active.Topic) { + else if (Active.Map || Active.Topic) { Metamaps.Loading.show() - Metamaps.JIT.prepareVizData() - Metamaps.GlobalUI.showDiv('#infovis') + JIT.prepareVizData() + GlobalUI.showDiv('#infovis') } }); From 59b471ac62a75da175a8a17bee11c9f4b22f8624 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 23:51:13 +0800 Subject: [PATCH 032/306] break Map into three files --- frontend/src/Metamaps/Map/CheatSheet.js | 27 ++ .../src/Metamaps/{Map.js => Map/InfoBox.js} | 446 ++---------------- frontend/src/Metamaps/Map/index.js | 365 ++++++++++++++ frontend/src/Metamaps/index.js | 4 +- 4 files changed, 425 insertions(+), 417 deletions(-) create mode 100644 frontend/src/Metamaps/Map/CheatSheet.js rename frontend/src/Metamaps/{Map.js => Map/InfoBox.js} (51%) create mode 100644 frontend/src/Metamaps/Map/index.js diff --git a/frontend/src/Metamaps/Map/CheatSheet.js b/frontend/src/Metamaps/Map/CheatSheet.js new file mode 100644 index 00000000..969ee159 --- /dev/null +++ b/frontend/src/Metamaps/Map/CheatSheet.js @@ -0,0 +1,27 @@ +const CheatSheet = { + init: function () { + // tab the cheatsheet + $('#cheatSheet').tabs() + $('#quickReference').tabs().addClass('ui-tabs-vertical ui-helper-clearfix') + $('#quickReference .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') + + // id = the id of a vimeo video + var switchVideo = function (element, id) { + $('.tutorialItem').removeClass('active') + $(element).addClass('active') + $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) + } + + $('#gettingStarted').click(function () { + // switchVideo(this,'88334167') + }) + $('#upYourSkillz').click(function () { + // switchVideo(this,'100118167') + }) + $('#advancedMapping').click(function () { + // switchVideo(this,'88334167') + }) + } +} + +export default CheatSheet diff --git a/frontend/src/Metamaps/Map.js b/frontend/src/Metamaps/Map/InfoBox.js similarity index 51% rename from frontend/src/Metamaps/Map.js rename to frontend/src/Metamaps/Map/InfoBox.js index cd2c3d2e..eaceba29 100644 --- a/frontend/src/Metamaps/Map.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,405 +1,19 @@ /* global Metamaps, $ */ import Active from './Active' -import AutoLayout from './AutoLayout' -import Create from './Create' -import Filter from './Filter' -import GlobalUI from './GlobalUI' -import JIT from './JIT' -import Realtime from './Realtime' -import Selected from './Selected' -import SynapseCard from './SynapseCard' -import TopicCard from './TopicCard' -import Visualize from './Visualize' +import GlobalUI from '../GlobalUI' +import Router from '../Router' /* - * Metamaps.Map.js.erb - * - * Dependencies: - * - Metamaps.Backbone - * - Metamaps.Erb - * - Metamaps.Loading - * - Metamaps.Mappers - * - Metamaps.Mappings - * - Metamaps.Maps - * - Metamaps.Messages - * - Metamaps.Router - * - Metamaps.Synapses - * - Metamaps.Topics - * - * Major sub-modules: - * - Metamaps.Map.CheatSheet - * - Metamaps.Map.InfoBox + * Metamaps.Collaborators + * Metamaps.Erb + * Metamaps.Mappers + * Metamaps.Maps + * Metamaps.Synapses + * Metamaps.Topics */ -window.Metamaps = window.Metamaps || {} -Metamaps.Map = { - events: { - editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' - }, - init: function () { - var self = Metamaps.Map - - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#center-container').bind('contextmenu', function (e) { - return false - }) - - $('.starMap').click(function () { - if ($(this).is('.starred')) self.unstar() - else self.star() - }) - - $('.sidebarFork').click(function () { - self.fork() - }) - - GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() - - self.updateStar() - self.InfoBox.init() - self.CheatSheet.init() - - $(document).on(Metamaps.Map.events.editedByActiveMapper, self.editedByActiveMapper) - }, - launch: function (id) { - var bb = Metamaps.Backbone - var start = function (data) { - Active.Map = new bb.Map(data.map) - Metamaps.Mappers = new bb.MapperCollection(data.mappers) - Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) - Metamaps.Topics = new bb.TopicCollection(data.topics) - Metamaps.Synapses = new bb.SynapseCollection(data.synapses) - Metamaps.Mappings = new bb.MappingCollection(data.mappings) - Metamaps.Messages = data.messages - Metamaps.Stars = data.stars - Metamaps.Backbone.attachCollectionEvents() - - var map = Active.Map - var mapper = Active.Mapper - - // add class to .wrapper for specifying whether you can edit the map - if (map.authorizeToEdit(mapper)) { - $('.wrapper').addClass('canEditMap') - } - - // add class to .wrapper for specifying if the map can - // be collaborated on - if (map.get('permission') === 'commons') { - $('.wrapper').addClass('commonsMap') - } - - Metamaps.Map.updateStar() - - // set filter mapper H3 text - $('#filter_by_mapper h3').html('MAPPERS') - - // build and render the visualization - Visualize.type = 'ForceDirected' - JIT.prepareVizData() - - // update filters - Filter.reset() - - // reset selected arrays - Selected.reset() - - // set the proper mapinfobox content - Metamaps.Map.InfoBox.load() - - // these three update the actual filter box with the right list items - Filter.checkMetacodes() - Filter.checkSynapses() - Filter.checkMappers() - - Realtime.startActiveMap() - Metamaps.Loading.hide() - - // for mobile - $('#header_content').html(map.get('name')) - } - - $.ajax({ - url: '/maps/' + id + '/contains.json', - success: start - }) - }, - end: function () { - if (Active.Map) { - $('.wrapper').removeClass('canEditMap commonsMap') - AutoLayout.resetSpiral() - - $('.rightclickmenu').remove() - TopicCard.hideCard() - SynapseCard.hideCard() - Create.newTopic.hide(true) // true means force (and override pinned) - Create.newSynapse.hide() - Filter.close() - Metamaps.Map.InfoBox.close() - Realtime.endActiveMap() - } - }, - updateStar: function () { - if (!Active.Mapper || !Metamaps.Stars) return - // update the star/unstar icon - if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { - $('.starMap').addClass('starred') - $('.starMap .tooltipsAbove').html('Unstar') - } else { - $('.starMap').removeClass('starred') - $('.starMap .tooltipsAbove').html('Star') - } - }, - star: function () { - var self = Metamaps.Map - - if (!Active.Map) return - $.post('/maps/' + Active.Map.id + '/star') - Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) - Metamaps.Maps.Starred.add(Active.Map) - GlobalUI.notifyUser('Map is now starred') - self.updateStar() - }, - unstar: function () { - var self = Metamaps.Map - - if (!Active.Map) return - $.post('/maps/' + Active.Map.id + '/unstar') - Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) - Metamaps.Maps.Starred.remove(Active.Map) - self.updateStar() - }, - fork: function () { - GlobalUI.openLightbox('forkmap') - - var nodes_data = '', - synapses_data = '' - var nodes_array = [] - var synapses_array = [] - // collect the unfiltered topics - Visualize.mGraph.graph.eachNode(function (n) { - // if the opacity is less than 1 then it's filtered - if (n.getData('alpha') === 1) { - var id = n.getData('topic').id - nodes_array.push(id) - var x, y - if (n.pos.x && n.pos.y) { - x = n.pos.x - y = n.pos.y - } else { - var x = Math.cos(n.pos.theta) * n.pos.rho - var y = Math.sin(n.pos.theta) * n.pos.rho - } - nodes_data += id + '/' + x + '/' + y + ',' - } - }) - // collect the unfiltered synapses - Metamaps.Synapses.each(function (synapse) { - var desc = synapse.get('desc') - - var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 - // make sure that both topics are being added, otherwise, it - // doesn't make sense to add the synapse - var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 - topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 - if (descNotFiltered && topicsNotFiltered) { - synapses_array.push(synapse.id) - } - }) - - synapses_data = synapses_array.join() - nodes_data = nodes_data.slice(0, -1) - - GlobalUI.CreateMap.topicsToMap = nodes_data - GlobalUI.CreateMap.synapsesToMap = synapses_data - }, - leavePrivateMap: function () { - var map = Active.Map - Metamaps.Maps.Active.remove(map) - Metamaps.Maps.Featured.remove(map) - Metamaps.Router.home() - GlobalUI.notifyUser('Sorry! That map has been changed to Private.') - }, - cantEditNow: function () { - Realtime.turnOff(true); // true is for 'silence' - GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') - Active.Map.trigger('changeByOther') - }, - canEditNow: function () { - var confirmString = "You've been granted permission to edit this map. " - confirmString += 'Do you want to reload and enable realtime collaboration?' - var c = confirm(confirmString) - if (c) { - Metamaps.Router.maps(Active.Map.id) - } - }, - editedByActiveMapper: function () { - if (Active.Mapper) { - Metamaps.Mappers.add(Active.Mapper) - } - }, - exportImage: function () { - var canvas = {} - - canvas.canvas = document.createElement('canvas') - canvas.canvas.width = 1880 // 960 - canvas.canvas.height = 1260 // 630 - - canvas.scaleOffsetX = 1 - canvas.scaleOffsetY = 1 - canvas.translateOffsetY = 0 - canvas.translateOffsetX = 0 - canvas.denySelected = true - - canvas.getSize = function () { - if (this.size) return this.size - var canvas = this.canvas - return this.size = { - width: canvas.width, - height: canvas.height - } - } - canvas.scale = function (x, y) { - var px = this.scaleOffsetX * x, - py = this.scaleOffsetY * y - var dx = this.translateOffsetX * (x - 1) / px, - dy = this.translateOffsetY * (y - 1) / py - this.scaleOffsetX = px - this.scaleOffsetY = py - this.getCtx().scale(x, y) - this.translate(dx, dy) - } - canvas.translate = function (x, y) { - var sx = this.scaleOffsetX, - sy = this.scaleOffsetY - this.translateOffsetX += x * sx - this.translateOffsetY += y * sy - this.getCtx().translate(x, y) - } - canvas.getCtx = function () { - return this.canvas.getContext('2d') - } - // center it - canvas.getCtx().translate(1880 / 2, 1260 / 2) - - var mGraph = Visualize.mGraph - - var id = mGraph.root - var root = mGraph.graph.getNode(id) - var T = !!root.visited - - // pass true to avoid basing it on a selection - JIT.zoomExtents(null, canvas, true) - - var c = canvas.canvas, - ctx = canvas.getCtx(), - scale = canvas.scaleOffsetX - - // draw a grey background - ctx.fillStyle = '#d8d9da' - var xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale), - yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) - ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale) - - // draw the graph - mGraph.graph.eachNode(function (node) { - var nodeAlpha = node.getData('alpha') - node.eachAdjacency(function (adj) { - var nodeTo = adj.nodeTo - if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { - mGraph.fx.plotLine(adj, canvas) - } - }) - if (node.drawn) { - mGraph.fx.plotNode(node, canvas) - } - if (!mGraph.labelsHidden) { - if (node.drawn && nodeAlpha >= 0.95) { - mGraph.labels.plotLabel(canvas, node) - } else { - mGraph.labels.hideLabel(node, false) - } - } - node.visited = !T - }) - - var imageData = { - encoded_image: canvas.canvas.toDataURL() - } - - var map = Active.Map - - var today = new Date() - var dd = today.getDate() - var mm = today.getMonth() + 1; // January is 0! - var yyyy = today.getFullYear() - if (dd < 10) { - dd = '0' + dd - } - if (mm < 10) { - mm = '0' + mm - } - today = mm + '/' + dd + '/' + yyyy - - var mapName = map.get('name').split(' ').join([separator = '-']) - var downloadMessage = '' - downloadMessage += 'Captured map screenshot! ' - downloadMessage += "<a href='" + imageData.encoded_image + "' " - downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" - GlobalUI.notifyUser(downloadMessage) - - $.ajax({ - type: 'POST', - dataType: 'json', - url: '/maps/' + Active.Map.id + '/upload_screenshot', - data: imageData, - success: function (data) { - console.log('successfully uploaded map screenshot') - }, - error: function () { - console.log('failed to save map screenshot') - } - }) - } -} - -/* - * - * CHEATSHEET - * - */ -Metamaps.Map.CheatSheet = { - init: function () { - // tab the cheatsheet - $('#cheatSheet').tabs() - $('#quickReference').tabs().addClass('ui-tabs-vertical ui-helper-clearfix') - $('#quickReference .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') - - // id = the id of a vimeo video - var switchVideo = function (element, id) { - $('.tutorialItem').removeClass('active') - $(element).addClass('active') - $('#tutorialVideo').attr('src', '//player.vimeo.com/video/' + id) - } - - $('#gettingStarted').click(function () { - // switchVideo(this,'88334167') - }) - $('#upYourSkillz').click(function () { - // switchVideo(this,'100118167') - }) - $('#advancedMapping').click(function () { - // switchVideo(this,'88334167') - }) - } -}; // end Metamaps.Map.CheatSheet - -/* - * - * INFOBOX - * - */ -Metamaps.Map.InfoBox = { +const InfoBox = { isOpen: false, changing: false, selectingPermission: false, @@ -407,7 +21,7 @@ Metamaps.Map.InfoBox = { nameHTML: '<span class="best_in_place best_in_place_name" id="best_in_place_map_{{id}}_name" data-url="/maps/{{id}}" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName">{{name}}</span>', descHTML: '<span class="best_in_place best_in_place_desc" id="best_in_place_map_{{id}}_desc" data-url="/maps/{{id}}" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc">{{desc}}</span>', init: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoIcon').click(self.toggleBox) $('.mapInfoBox').click(function (event) { @@ -426,7 +40,7 @@ Metamaps.Map.InfoBox = { } }, toggleBox: function (event) { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (self.isOpen) self.close() else self.open() @@ -434,7 +48,7 @@ Metamaps.Map.InfoBox = { event.stopPropagation() }, open: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoIcon div').addClass('hide') if (!self.isOpen && !self.changing) { self.changing = true @@ -445,7 +59,7 @@ Metamaps.Map.InfoBox = { } }, close: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoIcon div').removeClass('hide') if (!self.changing) { @@ -459,7 +73,7 @@ Metamaps.Map.InfoBox = { } }, load: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox var map = Active.Map @@ -494,7 +108,7 @@ Metamaps.Map.InfoBox = { self.attachEventListeners() }, attachEventListeners: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox $('.mapInfoBox.canEdit .best_in_place').best_in_place() @@ -547,7 +161,7 @@ Metamaps.Map.InfoBox = { $('.mapContributors .tip').unbind().click(function (event) { event.stopPropagation() }) - $('.mapContributors .tip li a').click(Metamaps.Router.intercept) + $('.mapContributors .tip li a').click(Router.intercept) $('.mapInfoBox').unbind('.hideTip').bind('click.hideTip', function () { $('.mapContributors .tip').hide() @@ -556,7 +170,7 @@ Metamaps.Map.InfoBox = { self.addTypeahead() }, addTypeahead: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (!Active.Map) return @@ -603,14 +217,14 @@ Metamaps.Map.InfoBox = { } }, removeCollaborator: function (collaboratorId) { - var self = Metamaps.Map.InfoBox + var self = InfoBox Metamaps.Collaborators.remove(Metamaps.Collaborators.get(collaboratorId)) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) self.updateNumbers() }, addCollaborator: function (newCollaboratorId) { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (Metamaps.Collaborators.get(newCollaboratorId)) { GlobalUI.notifyUser('That user already has access') @@ -629,7 +243,7 @@ Metamaps.Map.InfoBox = { $.getJSON('/users/' + newCollaboratorId + '.json', callback) }, handleResultClick: function (event, item) { - var self = Metamaps.Map.InfoBox + var self = InfoBox self.addCollaborator(item.id) $('.collaboratorSearchField').typeahead('val', '') @@ -641,7 +255,7 @@ Metamaps.Map.InfoBox = { $('.mapInfoBox .mapPermission').removeClass('commons public private').addClass(perm) }, createContributorList: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators var activeMapperIsCreator = Active.Mapper && Active.Mapper.id === Active.Map.get('user_id') var string = '' @@ -666,7 +280,7 @@ Metamaps.Map.InfoBox = { updateNumbers: function () { if (!Active.Map) return - var self = Metamaps.Map.InfoBox + var self = InfoBox var mapper = Active.Mapper var relevantPeople = Active.Map.get('permission') === 'commons' ? Metamaps.Mappers : Metamaps.Collaborators @@ -689,10 +303,10 @@ Metamaps.Map.InfoBox = { $('.mapTopics').text(Metamaps.Topics.length) $('.mapSynapses').text(Metamaps.Synapses.length) - $('.mapEditedAt').html('<span>Last edited: </span>' + Metamaps.Util.nowDateFormatted()) + $('.mapEditedAt').html('<span>Last edited: </span>' + Util.nowDateFormatted()) }, onPermissionClick: function (event) { - var self = Metamaps.Map.InfoBox + var self = InfoBox if (!self.selectingPermission) { self.selectingPermission = true @@ -709,14 +323,14 @@ Metamaps.Map.InfoBox = { } }, hidePermissionSelect: function () { - var self = Metamaps.Map.InfoBox + var self = InfoBox self.selectingPermission = false $('.mapPermission').removeClass('minimize') // this line flips the pull up arrow to a drop down arrow $('.mapPermission .permissionSelect').remove() }, selectPermission: function (event) { - var self = Metamaps.Map.InfoBox + var self = InfoBox self.selectingPermission = false var permission = $(this).attr('class') @@ -740,19 +354,19 @@ Metamaps.Map.InfoBox = { var authorized = map.authorizePermissionChange(mapper) if (doIt && authorized) { - Metamaps.Map.InfoBox.close() + InfoBox.close() Metamaps.Maps.Active.remove(map) Metamaps.Maps.Featured.remove(map) Metamaps.Maps.Mine.remove(map) Metamaps.Maps.Shared.remove(map) map.destroy() - Metamaps.Router.home() + Router.home() GlobalUI.notifyUser('Map eliminated!') } else if (!authorized) { alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") } } -}; // end Metamaps.Map.InfoBox +} -export default Metamaps.Map +export default InfoBox diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js new file mode 100644 index 00000000..84ee8b39 --- /dev/null +++ b/frontend/src/Metamaps/Map/index.js @@ -0,0 +1,365 @@ +/* global Metamaps, $ */ + +import Active from './Active' +import AutoLayout from './AutoLayout' +import Create from './Create' +import Filter from './Filter' +import GlobalUI from './GlobalUI' +import JIT from './JIT' +import Realtime from './Realtime' +import Router from './Router' +import Selected from './Selected' +import SynapseCard from './SynapseCard' +import TopicCard from './TopicCard' +import Visualize from './Visualize' + +import CheatSheet from './CheatSheet' +import InfoBox from './InfoBox' + +/* + * Metamaps.Map.js.erb + * + * Dependencies: + * - Metamaps.Backbone + * - Metamaps.Erb + * - Metamaps.Loading + * - Metamaps.Mappers + * - Metamaps.Mappings + * - Metamaps.Maps + * - Metamaps.Messages + * - Metamaps.Synapses + * - Metamaps.Topics + */ + +const Map = { + events: { + editedByActiveMapper: 'Metamaps:Map:events:editedByActiveMapper' + }, + init: function () { + var self = Map + + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#center-container').bind('contextmenu', function (e) { + return false + }) + + $('.starMap').click(function () { + if ($(this).is('.starred')) self.unstar() + else self.star() + }) + + $('.sidebarFork').click(function () { + self.fork() + }) + + GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() + + self.updateStar() + self.InfoBox.init() + CheatSheet.init() + + $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) + }, + launch: function (id) { + var bb = Metamaps.Backbone + var start = function (data) { + Active.Map = new bb.Map(data.map) + Metamaps.Mappers = new bb.MapperCollection(data.mappers) + Metamaps.Collaborators = new bb.MapperCollection(data.collaborators) + Metamaps.Topics = new bb.TopicCollection(data.topics) + Metamaps.Synapses = new bb.SynapseCollection(data.synapses) + Metamaps.Mappings = new bb.MappingCollection(data.mappings) + Metamaps.Messages = data.messages + Metamaps.Stars = data.stars + Metamaps.Backbone.attachCollectionEvents() + + var map = Active.Map + var mapper = Active.Mapper + + // add class to .wrapper for specifying whether you can edit the map + if (map.authorizeToEdit(mapper)) { + $('.wrapper').addClass('canEditMap') + } + + // add class to .wrapper for specifying if the map can + // be collaborated on + if (map.get('permission') === 'commons') { + $('.wrapper').addClass('commonsMap') + } + + Map.updateStar() + + // set filter mapper H3 text + $('#filter_by_mapper h3').html('MAPPERS') + + // build and render the visualization + Visualize.type = 'ForceDirected' + JIT.prepareVizData() + + // update filters + Filter.reset() + + // reset selected arrays + Selected.reset() + + // set the proper mapinfobox content + Map.InfoBox.load() + + // these three update the actual filter box with the right list items + Filter.checkMetacodes() + Filter.checkSynapses() + Filter.checkMappers() + + Realtime.startActiveMap() + Metamaps.Loading.hide() + + // for mobile + $('#header_content').html(map.get('name')) + } + + $.ajax({ + url: '/maps/' + id + '/contains.json', + success: start + }) + }, + end: function () { + if (Active.Map) { + $('.wrapper').removeClass('canEditMap commonsMap') + AutoLayout.resetSpiral() + + $('.rightclickmenu').remove() + TopicCard.hideCard() + SynapseCard.hideCard() + Create.newTopic.hide(true) // true means force (and override pinned) + Create.newSynapse.hide() + Filter.close() + Map.InfoBox.close() + Realtime.endActiveMap() + } + }, + updateStar: function () { + if (!Active.Mapper || !Metamaps.Stars) return + // update the star/unstar icon + if (Metamaps.Stars.find(function (s) { return s.user_id === Active.Mapper.id })) { + $('.starMap').addClass('starred') + $('.starMap .tooltipsAbove').html('Unstar') + } else { + $('.starMap').removeClass('starred') + $('.starMap .tooltipsAbove').html('Star') + } + }, + star: function () { + var self = Map + + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/star') + Metamaps.Stars.push({ user_id: Active.Mapper.id, map_id: Active.Map.id }) + Metamaps.Maps.Starred.add(Active.Map) + GlobalUI.notifyUser('Map is now starred') + self.updateStar() + }, + unstar: function () { + var self = Map + + if (!Active.Map) return + $.post('/maps/' + Active.Map.id + '/unstar') + Metamaps.Stars = Metamaps.Stars.filter(function (s) { return s.user_id != Active.Mapper.id }) + Metamaps.Maps.Starred.remove(Active.Map) + self.updateStar() + }, + fork: function () { + GlobalUI.openLightbox('forkmap') + + var nodes_data = '', + synapses_data = '' + var nodes_array = [] + var synapses_array = [] + // collect the unfiltered topics + Visualize.mGraph.graph.eachNode(function (n) { + // if the opacity is less than 1 then it's filtered + if (n.getData('alpha') === 1) { + var id = n.getData('topic').id + nodes_array.push(id) + var x, y + if (n.pos.x && n.pos.y) { + x = n.pos.x + y = n.pos.y + } else { + var x = Math.cos(n.pos.theta) * n.pos.rho + var y = Math.sin(n.pos.theta) * n.pos.rho + } + nodes_data += id + '/' + x + '/' + y + ',' + } + }) + // collect the unfiltered synapses + Metamaps.Synapses.each(function (synapse) { + var desc = synapse.get('desc') + + var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 + // make sure that both topics are being added, otherwise, it + // doesn't make sense to add the synapse + var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 + topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 + if (descNotFiltered && topicsNotFiltered) { + synapses_array.push(synapse.id) + } + }) + + synapses_data = synapses_array.join() + nodes_data = nodes_data.slice(0, -1) + + GlobalUI.CreateMap.topicsToMap = nodes_data + GlobalUI.CreateMap.synapsesToMap = synapses_data + }, + leavePrivateMap: function () { + var map = Active.Map + Metamaps.Maps.Active.remove(map) + Metamaps.Maps.Featured.remove(map) + Router.home() + GlobalUI.notifyUser('Sorry! That map has been changed to Private.') + }, + cantEditNow: function () { + Realtime.turnOff(true); // true is for 'silence' + GlobalUI.notifyUser('Map was changed to Public. Editing is disabled.') + Active.Map.trigger('changeByOther') + }, + canEditNow: function () { + var confirmString = "You've been granted permission to edit this map. " + confirmString += 'Do you want to reload and enable realtime collaboration?' + var c = confirm(confirmString) + if (c) { + Router.maps(Active.Map.id) + } + }, + editedByActiveMapper: function () { + if (Active.Mapper) { + Metamaps.Mappers.add(Active.Mapper) + } + }, + exportImage: function () { + var canvas = {} + + canvas.canvas = document.createElement('canvas') + canvas.canvas.width = 1880 // 960 + canvas.canvas.height = 1260 // 630 + + canvas.scaleOffsetX = 1 + canvas.scaleOffsetY = 1 + canvas.translateOffsetY = 0 + canvas.translateOffsetX = 0 + canvas.denySelected = true + + canvas.getSize = function () { + if (this.size) return this.size + var canvas = this.canvas + return this.size = { + width: canvas.width, + height: canvas.height + } + } + canvas.scale = function (x, y) { + var px = this.scaleOffsetX * x, + py = this.scaleOffsetY * y + var dx = this.translateOffsetX * (x - 1) / px, + dy = this.translateOffsetY * (y - 1) / py + this.scaleOffsetX = px + this.scaleOffsetY = py + this.getCtx().scale(x, y) + this.translate(dx, dy) + } + canvas.translate = function (x, y) { + var sx = this.scaleOffsetX, + sy = this.scaleOffsetY + this.translateOffsetX += x * sx + this.translateOffsetY += y * sy + this.getCtx().translate(x, y) + } + canvas.getCtx = function () { + return this.canvas.getContext('2d') + } + // center it + canvas.getCtx().translate(1880 / 2, 1260 / 2) + + var mGraph = Visualize.mGraph + + var id = mGraph.root + var root = mGraph.graph.getNode(id) + var T = !!root.visited + + // pass true to avoid basing it on a selection + JIT.zoomExtents(null, canvas, true) + + var c = canvas.canvas, + ctx = canvas.getCtx(), + scale = canvas.scaleOffsetX + + // draw a grey background + ctx.fillStyle = '#d8d9da' + var xPoint = (-(c.width / scale) / 2) - (canvas.translateOffsetX / scale), + yPoint = (-(c.height / scale) / 2) - (canvas.translateOffsetY / scale) + ctx.fillRect(xPoint, yPoint, c.width / scale, c.height / scale) + + // draw the graph + mGraph.graph.eachNode(function (node) { + var nodeAlpha = node.getData('alpha') + node.eachAdjacency(function (adj) { + var nodeTo = adj.nodeTo + if (!!nodeTo.visited === T && node.drawn && nodeTo.drawn) { + mGraph.fx.plotLine(adj, canvas) + } + }) + if (node.drawn) { + mGraph.fx.plotNode(node, canvas) + } + if (!mGraph.labelsHidden) { + if (node.drawn && nodeAlpha >= 0.95) { + mGraph.labels.plotLabel(canvas, node) + } else { + mGraph.labels.hideLabel(node, false) + } + } + node.visited = !T + }) + + var imageData = { + encoded_image: canvas.canvas.toDataURL() + } + + var map = Active.Map + + var today = new Date() + var dd = today.getDate() + var mm = today.getMonth() + 1; // January is 0! + var yyyy = today.getFullYear() + if (dd < 10) { + dd = '0' + dd + } + if (mm < 10) { + mm = '0' + mm + } + today = mm + '/' + dd + '/' + yyyy + + var mapName = map.get('name').split(' ').join([separator = '-']) + var downloadMessage = '' + downloadMessage += 'Captured map screenshot! ' + downloadMessage += "<a href='" + imageData.encoded_image + "' " + downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" + GlobalUI.notifyUser(downloadMessage) + + $.ajax({ + type: 'POST', + dataType: 'json', + url: '/maps/' + Active.Map.id + '/upload_screenshot', + data: imageData, + success: function (data) { + console.log('successfully uploaded map screenshot') + }, + error: function () { + console.log('failed to save map screenshot') + } + }) + } +} + +export CheatSheet, InfoBox +export default Map diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 9fe8925b..7b431d1f 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -13,7 +13,7 @@ import GlobalUI from './GlobalUI' import Import from './Import' import JIT from './JIT' import Listeners from './Listeners' -import Map from './Map' +import Map, { CheatSheet, InfoBox } from './Map' import Mapper from './Mapper' import Mobile from './Mobile' import Mouse from './Mouse' @@ -46,6 +46,8 @@ Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners Metamaps.Map = Map +Metamaps.Map.CheatSheet = CheatSheet +Metamaps.Map.InfoBox = InfoBox Metamaps.Maps = {} Metamaps.Mapper = Mapper Metamaps.Mobile = Mobile From fe3012136da1a674d87cd0cb0bb48d73dd33c08c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 22 Sep 2016 23:51:33 +0800 Subject: [PATCH 033/306] import _ --- frontend/src/Metamaps/Backbone.js | 3 ++- frontend/src/Metamaps/Control.js | 2 ++ frontend/src/Metamaps/Filter.js | 2 ++ frontend/src/Metamaps/JIT.js | 2 ++ frontend/src/Metamaps/Organize.js | 2 ++ frontend/src/Metamaps/Realtime.js | 2 ++ frontend/src/Metamaps/Visualize.js | 2 ++ 7 files changed, 14 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone.js index 9f18ef32..bc303df4 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone.js @@ -1,6 +1,7 @@ -window.Metamaps = window.Metamaps || {} /* global Metamaps, Backbone, _, $ */ +import _ from 'lodash' + /* * Metamaps.Backbone.js.erb * diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 9e13e40c..2c14cfca 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import Filter from './Filter' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index 38c4f369..f67c6ec8 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import Control from './Control' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 50c48985..0fe5a224 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,5 +1,7 @@ /* global Metamaps */ +import _ from 'lodash' + import Active from './Active' import Control from './Control' import Create from './Create' diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index ee29c2b8..c05f870e 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,5 +1,7 @@ /* global $ */ +import _ from 'lodash' + import Visualize from './Visualize' import JIT from './JIT' diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 1eef6408..80143f25 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import Control from './Control' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 678c7c64..047cb81d 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import _ from 'lodash' + import Active from './Active' import JIT from './JIT' import Router from './Router' From 30894a313fbde92faabcc5ed37124cf995a34967 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 00:07:30 +0800 Subject: [PATCH 034/306] move views to their own frontend folder --- .../javascripts/src/Metamaps.Erb.js.erb | 2 + .../javascripts/src/views/chatView.js.erb | 343 ------------------ app/assets/javascripts/src/views/room.js | 195 ---------- app/assets/javascripts/src/views/videoView.js | 207 ----------- frontend/src/Metamaps/Realtime.js | 18 +- frontend/src/Metamaps/Router.js | 10 +- frontend/src/Metamaps/Views.js | 91 ----- frontend/src/Metamaps/Views/ChatView.js | 337 +++++++++++++++++ frontend/src/Metamaps/Views/ExploreMaps.js | 86 +++++ frontend/src/Metamaps/Views/Room.js | 198 ++++++++++ frontend/src/Metamaps/Views/VideoView.js | 202 +++++++++++ frontend/src/Metamaps/Views/index.js | 6 + frontend/src/Metamaps/index.js | 12 +- 13 files changed, 851 insertions(+), 856 deletions(-) delete mode 100644 app/assets/javascripts/src/views/chatView.js.erb delete mode 100644 app/assets/javascripts/src/views/room.js delete mode 100644 app/assets/javascripts/src/views/videoView.js delete mode 100644 frontend/src/Metamaps/Views.js create mode 100644 frontend/src/Metamaps/Views/ChatView.js create mode 100644 frontend/src/Metamaps/Views/ExploreMaps.js create mode 100644 frontend/src/Metamaps/Views/Room.js create mode 100644 frontend/src/Metamaps/Views/VideoView.js create mode 100644 frontend/src/Metamaps/Views/index.js diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index 90eba5e5..60b64e46 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -14,5 +14,7 @@ Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' +Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.VERSION = '<%= METAMAPS_VERSION %>' diff --git a/app/assets/javascripts/src/views/chatView.js.erb b/app/assets/javascripts/src/views/chatView.js.erb deleted file mode 100644 index 7a1e7f8e..00000000 --- a/app/assets/javascripts/src/views/chatView.js.erb +++ /dev/null @@ -1,343 +0,0 @@ -Metamaps.Views = Metamaps.Views || {}; - -Metamaps.Views.chatView = (function () { - var - chatView, - linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); - - var Private = { - messageHTML: "<div class='chat-message'>" + - "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + - "<div class='chat-message-text'>{{ message }}</div>" + - "<div class='chat-message-time'>{{ timestamp }}</div>" + - "<div class='clearfloat'></div>" + - "</div>", - participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + - "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + - "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + - "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + - "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + - "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + - "<div class='clearfloat'></div>" + - "</div>", - templates: function() { - _.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - this.messageTemplate = _.template(Private.messageHTML); - - this.participantTemplate = _.template(Private.participantHTML); - }, - createElements: function() { - this.$unread = $('<div class="chat-unread"></div>'); - this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>'); - this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>'); - this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>'); - this.$videoToggle = $('<div class="video-toggle"></div>'); - this.$cursorToggle = $('<div class="cursor-toggle"></div>'); - this.$participants = $('<div class="participants"></div>'); - this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>'); - this.$chatHeader = $('<div class="chat-header">CHAT</div>'); - this.$soundToggle = $('<div class="sound-toggle"></div>'); - this.$messages = $('<div class="chat-messages"></div>'); - this.$container = $('<div class="chat-box"></div>'); - }, - attachElements: function() { - this.$button.append(this.$unread); - - this.$juntoHeader.append(this.$videoToggle); - this.$juntoHeader.append(this.$cursorToggle); - - this.$chatHeader.append(this.$soundToggle); - - this.$participants.append(this.$conversationInProgress); - - this.$container.append(this.$juntoHeader); - this.$container.append(this.$participants); - this.$container.append(this.$chatHeader); - this.$container.append(this.$button); - this.$container.append(this.$messages); - this.$container.append(this.$messageInput); - }, - addEventListeners: function() { - var self = this; - - this.participants.on('add', function (participant) { - Private.addParticipant.call(self, participant); - }); - - this.participants.on('remove', function (participant) { - Private.removeParticipant.call(self, participant); - }); - - this.$button.on('click', function () { - Handlers.buttonClick.call(self); - }); - this.$videoToggle.on('click', function () { - Handlers.videoToggleClick.call(self); - }); - this.$cursorToggle.on('click', function () { - Handlers.cursorToggleClick.call(self); - }); - this.$soundToggle.on('click', function () { - Handlers.soundToggleClick.call(self); - }); - this.$messageInput.on('keyup', function (event) { - Handlers.keyUp.call(self, event); - }); - this.$messageInput.on('focus', function () { - Handlers.inputFocus.call(self); - }); - this.$messageInput.on('blur', function () { - Handlers.inputBlur.call(self); - }); - }, - initializeSounds: function() { - this.sound = new Howl({ - urls: ["<%= asset_path 'sounds/MM_sounds.mp3' %>", "<%= asset_path 'sounds/MM_sounds.ogg' %>"], - sprite: { - joinmap: [0, 561], - leavemap: [1000, 592], - receivechat: [2000, 318], - sendchat: [3000, 296], - sessioninvite: [4000, 5393, true] - } - }); - }, - incrementUnread: function() { - this.unreadMessages++; - this.$unread.html(this.unreadMessages); - this.$unread.show(); - }, - addMessage: function(message, isInitial, wasMe) { - - if (!this.isOpen && !isInitial) Private.incrementUnread.call(this); - - function addZero(i) { - if (i < 10) { - i = "0" + i; - } - return i; - } - var m = _.clone(message.attributes); - - var today = new Date(); - m.timestamp = new Date(m.created_at); - - var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate(); - date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes()); - m.timestamp = date; - m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove - m.message = linker.link(m.message); - var $html = $(this.messageTemplate(m)); - this.$messages.append($html); - if (!isInitial) this.scrollMessages(200); - - if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat'); - }, - initialMessages: function() { - var messages = this.messages.models; - for (var i = 0; i < messages.length; i++) { - Private.addMessage.call(this, messages[i], true); - } - }, - handleInputMessage: function() { - var message = { - message: this.$messageInput.val(), - }; - this.$messageInput.val(''); - $(document).trigger(chatView.events.message + '-' + this.room, [message]); - }, - addParticipant: function(participant) { - var p = _.clone(participant.attributes); - if (p.self) { - p.selfClass = 'is-self'; - p.selfName = '(me)'; - } else { - p.selfClass = ''; - p.selfName = ''; - } - var html = this.participantTemplate(p); - this.$participants.append(html); - }, - removeParticipant: function(participant) { - this.$container.find('.participant-' + participant.get('id')).remove(); - } - }; - - var Handlers = { - buttonClick: function() { - if (this.isOpen) this.close(); - else if (!this.isOpen) this.open(); - }, - videoToggleClick: function() { - this.$videoToggle.toggleClass('active'); - this.videosShowing = !this.videosShowing; - $(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff); - }, - cursorToggleClick: function() { - this.$cursorToggle.toggleClass('active'); - this.cursorsShowing = !this.cursorsShowing; - $(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff); - }, - soundToggleClick: function() { - this.alertSound = !this.alertSound; - this.$soundToggle.toggleClass('active'); - }, - keyUp: function(event) { - switch(event.which) { - case 13: // enter - Private.handleInputMessage.call(this); - break; - } - }, - inputFocus: function() { - $(document).trigger(chatView.events.inputFocus); - }, - inputBlur: function() { - $(document).trigger(chatView.events.inputBlur); - } - }; - - chatView = function(messages, mapper, room) { - var self = this; - - this.room = room; - this.mapper = mapper; - this.messages = messages; // backbone collection - - this.isOpen = false; - this.alertSound = true; // whether to play sounds on arrival of new messages or not - this.cursorsShowing = true; - this.videosShowing = true; - this.unreadMessages = 0; - this.participants = new Backbone.Collection(); - - Private.templates.call(this); - Private.createElements.call(this); - Private.attachElements.call(this); - Private.addEventListeners.call(this); - Private.initialMessages.call(this); - Private.initializeSounds.call(this); - this.$container.css({ - right: '-300px' - }); - }; - - chatView.prototype.conversationInProgress = function (participating) { - this.$conversationInProgress.show(); - this.$participants.addClass('is-live'); - if (participating) this.$participants.addClass('is-participating'); - this.$button.addClass('active'); - - // hide invite to call buttons - } - - chatView.prototype.conversationEnded = function () { - this.$conversationInProgress.hide(); - this.$participants.removeClass('is-live'); - this.$participants.removeClass('is-participating'); - this.$button.removeClass('active'); - this.$participants.find('.participant').removeClass('active'); - this.$participants.find('.participant').removeClass('pending'); - } - - chatView.prototype.leaveConversation = function () { - this.$participants.removeClass('is-participating'); - } - - chatView.prototype.mapperJoinedCall = function (id) { - this.$participants.find('.participant-' + id).addClass('active'); - } - - chatView.prototype.mapperLeftCall = function (id) { - this.$participants.find('.participant-' + id).removeClass('active'); - } - - chatView.prototype.invitationPending = function (id) { - this.$participants.find('.participant-' + id).addClass('pending'); - } - - chatView.prototype.invitationAnswered = function (id) { - this.$participants.find('.participant-' + id).removeClass('pending'); - } - - chatView.prototype.addParticipant = function (participant) { - this.participants.add(participant); - } - - chatView.prototype.removeParticipant = function (username) { - var p = this.participants.find(function (p) { return p.get('username') === username; }); - if (p) { - this.participants.remove(p); - } - } - - chatView.prototype.removeParticipants = function () { - this.participants.remove(this.participants.models); - } - - chatView.prototype.open = function () { - this.$container.css({ - right: '0' - }); - this.$messageInput.focus(); - this.isOpen = true; - this.unreadMessages = 0; - this.$unread.hide(); - this.scrollMessages(0); - $(document).trigger(chatView.events.openTray); - } - - chatView.prototype.addMessage = function(message, isInitial, wasMe) { - this.messages.add(message); - Private.addMessage.call(this, message, isInitial, wasMe); - } - - chatView.prototype.scrollMessages = function(duration) { - duration = duration || 0; - - this.$messages.animate({ - scrollTop: this.$messages[0].scrollHeight - }, duration); - } - - chatView.prototype.clearMessages = function () { - this.unreadMessages = 0; - this.$unread.hide(); - this.$messages.empty(); - } - - chatView.prototype.close = function () { - this.$container.css({ - right: '-300px' - }); - this.$messageInput.blur(); - this.isOpen = false; - $(document).trigger(chatView.events.closeTray); - } - - chatView.prototype.remove = function () { - this.$button.off(); - this.$container.remove(); - } - - /** - * @class - * @static - */ - chatView.events = { - message: 'ChatView:message', - openTray: 'ChatView:openTray', - closeTray: 'ChatView:closeTray', - inputFocus: 'ChatView:inputFocus', - inputBlur: 'ChatView:inputBlur', - cursorsOff: 'ChatView:cursorsOff', - cursorsOn: 'ChatView:cursorsOn', - videosOff: 'ChatView:videosOff', - videosOn: 'ChatView:videosOn' - }; - - return chatView; - -})(); diff --git a/app/assets/javascripts/src/views/room.js b/app/assets/javascripts/src/views/room.js deleted file mode 100644 index 4595c3cb..00000000 --- a/app/assets/javascripts/src/views/room.js +++ /dev/null @@ -1,195 +0,0 @@ -Metamaps.Views = Metamaps.Views || {}; - -Metamaps.Views.room = (function () { - - var ChatView = Metamaps.Views.chatView; - var VideoView = Metamaps.Views.videoView; - - var room = function(opts) { - var self = this; - - this.isActiveRoom = false; - this.socket = opts.socket; - this.webrtc = opts.webrtc; - //this.roomRef = opts.firebase; - this.room = opts.room; - this.config = opts.config; - this.peopleCount = 0; - - this.$myVideo = opts.$video; - this.myVideo = opts.myVideoView; - - this.messages = new Backbone.Collection(); - this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image }); - this.chat = new ChatView(this.messages, this.currentMapper, this.room); - - this.videos = {}; - - this.init(); - }; - - room.prototype.join = function(cb) { - this.isActiveRoom = true; - this.webrtc.joinRoom(this.room, cb); - this.chat.conversationInProgress(true); // true indicates participation - } - - room.prototype.conversationInProgress = function() { - this.chat.conversationInProgress(false); // false indicates not participating - } - - room.prototype.conversationEnding = function() { - this.chat.conversationEnded(); - } - - room.prototype.leaveVideoOnly = function() { - this.chat.leaveConversation(); // the conversation will carry on without you - for (var id in this.videos) { - this.removeVideo(id); - } - this.isActiveRoom = false; - this.webrtc.leaveRoom(); - } - - room.prototype.leave = function() { - for (var id in this.videos) { - this.removeVideo(id); - } - this.isActiveRoom = false; - this.webrtc.leaveRoom(); - this.chat.conversationEnded(); - this.chat.removeParticipants(); - this.chat.clearMessages(); - this.messages.reset(); - } - - room.prototype.setPeopleCount = function(count) { - this.peopleCount = count; - } - - room.prototype.init = function () { - var self = this; - - $(document).on(VideoView.events.audioControlClick, function (event, videoView) { - if (!videoView.audioStatus) self.webrtc.mute(); - else if (videoView.audioStatus) self.webrtc.unmute(); - }); - $(document).on(VideoView.events.videoControlClick, function (event, videoView) { - if (!videoView.videoStatus) self.webrtc.pauseVideo(); - else if (videoView.videoStatus) self.webrtc.resumeVideo(); - }); - - this.webrtc.webrtc.off('peerStreamAdded'); - this.webrtc.webrtc.off('peerStreamRemoved'); - this.webrtc.on('peerStreamAdded', function (peer) { - var mapper = Metamaps.Realtime.mappersOnMap[peer.nick]; - peer.avatar = mapper.image; - peer.username = mapper.name; - if (self.isActiveRoom) { - self.addVideo(peer); - } - }); - - this.webrtc.on('peerStreamRemoved', function (peer) { - if (self.isActiveRoom) { - self.removeVideo(peer); - } - }); - - this.webrtc.on('mute', function (data) { - var v = self.videos[data.id]; - if (!v) return; - - if (data.name === 'audio') { - v.audioStatus = false; - } - else if (data.name === 'video') { - v.videoStatus = false; - v.$avatar.show(); - } - if (!v.audioStatus && !v.videoStatus) v.$container.hide(); - }); - this.webrtc.on('unmute', function (data) { - var v = self.videos[data.id]; - if (!v) return; - - if (data.name === 'audio') { - v.audioStatus = true; - } - else if (data.name === 'video') { - v.videoStatus = true; - v.$avatar.hide(); - } - v.$container.show(); - }); - - var sendChatMessage = function (event, data) { - self.sendChatMessage(data); - }; - $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage); - } - - room.prototype.videoAdded = function (callback) { - this._videoAdded = callback; - } - - room.prototype.addVideo = function (peer) { - var - id = this.webrtc.getDomId(peer), - video = attachMediaStream(peer.stream); - - var - v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }); - - this.videos[peer.id] = v; - if (this._videoAdded) this._videoAdded(v, peer.nick); - } - - room.prototype.removeVideo = function (peer) { - var id = typeof peer == 'string' ? peer : peer.id; - if (this.videos[id]) { - this.videos[id].remove(); - delete this.videos[id]; - } - } - - room.prototype.sendChatMessage = function (data) { - var self = this; - //this.roomRef.child('messages').push(data); - if (self.chat.alertSound) self.chat.sound.play('sendchat'); - var m = new Metamaps.Backbone.Message({ - message: data.message, - resource_id: Metamaps.Active.Map.id, - resource_type: "Map" - }); - m.save(null, { - success: function (model, response) { - self.addMessages(new Metamaps.Backbone.MessageCollection(model), false, true); - $(document).trigger(room.events.newMessage, [model]); - }, - error: function (model, response) { - console.log('error!', response); - } - }); - } - - // they should be instantiated as backbone models before they get - // passed to this function - room.prototype.addMessages = function (messages, isInitial, wasMe) { - var self = this; - - messages.models.forEach(function (message) { - self.chat.addMessage(message, isInitial, wasMe); - }); - } - - /** - * @class - * @static - */ - room.events = { - newMessage: "Room:newMessage" - }; - - return room; -})(); diff --git a/app/assets/javascripts/src/views/videoView.js b/app/assets/javascripts/src/views/videoView.js deleted file mode 100644 index b9d39c06..00000000 --- a/app/assets/javascripts/src/views/videoView.js +++ /dev/null @@ -1,207 +0,0 @@ -Metamaps.Views = Metamaps.Views || {}; - -Metamaps.Views.videoView = (function () { - - var videoView; - - var Private = { - addControls: function() { - var self = this; - - this.$audioControl = $('<div class="video-audio"></div>'); - this.$videoControl = $('<div class="video-video"></div>'); - - this.$audioControl.on('click', function () { - Handlers.audioControlClick.call(self); - }); - - this.$videoControl.on('click', function () { - Handlers.videoControlClick.call(self); - }); - - this.$container.append(this.$audioControl); - this.$container.append(this.$videoControl); - }, - cancelClick: function() { - this.mouseIsDown = false; - - if (this.hasMoved) { - - } - - $(document).trigger(videoView.events.dragEnd); - } - }; - - var Handlers = { - mousedown: function(event) { - this.mouseIsDown = true; - this.hasMoved = false; - this.mouseMoveStart = { - x: event.pageX, - y: event.pageY - }; - this.posStart = { - x: parseInt(this.$container.css('left'), '10'), - y: parseInt(this.$container.css('top'), '10') - } - - $(document).trigger(videoView.events.mousedown); - }, - mouseup: function(event) { - $(document).trigger(videoView.events.mouseup, [this]); - - var storedTime = this.lastClick; - var now = Date.now(); - this.lastClick = now; - - if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) { - $(document).trigger(videoView.events.doubleClick, [this]); - } - }, - mousemove: function(event) { - var - diffX, - diffY, - newX, - newY; - - if (this.$parent && this.mouseIsDown) { - this.manuallyPositioned = true; - this.hasMoved = true; - diffX = event.pageX - this.mouseMoveStart.x; - diffY = this.mouseMoveStart.y - event.pageY; - newX = this.posStart.x + diffX; - newY = this.posStart.y - diffY; - this.$container.css({ - top: newY, - left: newX - }); - } - }, - audioControlClick: function() { - if (this.audioStatus) { - this.audioOff(); - } else { - this.audioOn(); - } - $(document).trigger(videoView.events.audioControlClick, [this]); - }, - videoControlClick: function() { - if (this.videoStatus) { - this.videoOff(); - } else { - this.videoOn(); - } - $(document).trigger(videoView.events.videoControlClick, [this]); - }, - }; - - var videoView = function(video, $parent, id, isMyself, config) { - var self = this; - - this.$parent = $parent; // mapView - - this.video = video; - this.id = id; - - this.config = config; - - this.mouseIsDown = false; - this.mouseDownOffset = { x: 0, y: 0 }; - this.lastClick = null; - this.hasMoved = false; - - this.audioStatus = true; - this.videoStatus = true; - - this.$container = $('<div></div>'); - this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : '')); - this.$container.attr('id', 'container_' + id); - - - var $vidContainer = $('<div></div>'); - $vidContainer.addClass('video-cutoff'); - $vidContainer.append(this.video); - - this.avatar = config.avatar; - this.$avatar = $('<img draggable="false" class="collaborator-video-avatar" src="' + config.avatar + '" width="150" height="150" />'); - $vidContainer.append(this.$avatar); - - this.$container.append($vidContainer); - - this.$container.on('mousedown', function (event) { - Handlers.mousedown.call(self, event); - }); - - if (isMyself) { - Private.addControls.call(this); - } - - // suppress contextmenu - this.video.oncontextmenu = function () { return false; }; - - if (this.$parent) this.setParent(this.$parent); - }; - - videoView.prototype.setParent = function($parent) { - var self = this; - this.$parent = $parent; - this.$parent.off('.video' + this.id); - this.$parent.on('mouseup.video' + this.id, function (event) { - Handlers.mouseup.call(self, event); - Private.cancelClick.call(self); - }); - this.$parent.on('mousemove.video' + this.id, function (event) { - Handlers.mousemove.call(self, event); - }); - } - - videoView.prototype.setAvatar = function (src) { - this.$avatar.attr('src', src); - this.avatar = src; - } - - videoView.prototype.remove = function () { - this.$container.off(); - if (this.$parent) this.$parent.off('.video' + this.id); - this.$container.remove(); - } - - videoView.prototype.videoOff = function () { - this.$videoControl.addClass('active'); - this.$avatar.show(); - this.videoStatus = false; - } - - videoView.prototype.videoOn = function () { - this.$videoControl.removeClass('active'); - this.$avatar.hide(); - this.videoStatus = true; - } - - videoView.prototype.audioOff = function () { - this.$audioControl.addClass('active'); - this.audioStatus = false; - } - - videoView.prototype.audioOn = function () { - this.$audioControl.removeClass('active'); - this.audioStatus = true; - } - - /** - * @class - * @static - */ - videoView.events = { - mousedown: "VideoView:mousedown", - mouseup: "VideoView:mouseup", - doubleClick: "VideoView:doubleClick", - dragEnd: "VideoView:dragEnd", - audioControlClick: "VideoView:audioControlClick", - videoControlClick: "VideoView:videoControlClick", - }; - - return videoView; -})(); diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 80143f25..355e73f8 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -77,13 +77,13 @@ const Realtime = { var $video = $('<video></video>').attr('id', self.videoId) self.localVideo = { $video: $video, - view: new Views.videoView($video[0], $('body'), 'me', true, { + view: new Views.VideoView($video[0], $('body'), 'me', true, { DOUBLE_CLICK_TOLERANCE: 200, avatar: Active.Mapper ? Active.Mapper.get('image') : '' }) } - self.room = new Views.room({ + self.room = new Views.Room({ webrtc: self.webrtc, socket: self.socket, username: Active.Mapper ? Active.Mapper.get('name') : '', @@ -104,26 +104,26 @@ const Realtime = { addJuntoListeners: function () { var self = Realtime - $(document).on(Views.chatView.events.openTray, function () { + $(document).on(Views.ChatView.events.openTray, function () { $('.main').addClass('compressed') self.chatOpen = true self.positionPeerIcons() }) - $(document).on(Views.chatView.events.closeTray, function () { + $(document).on(Views.ChatView.events.closeTray, function () { $('.main').removeClass('compressed') self.chatOpen = false self.positionPeerIcons() }) - $(document).on(Views.chatView.events.videosOn, function () { + $(document).on(Views.ChatView.events.videosOn, function () { $('#wrapper').removeClass('hideVideos') }) - $(document).on(Views.chatView.events.videosOff, function () { + $(document).on(Views.ChatView.events.videosOff, function () { $('#wrapper').addClass('hideVideos') }) - $(document).on(Views.chatView.events.cursorsOn, function () { + $(document).on(Views.ChatView.events.cursorsOn, function () { $('#wrapper').removeClass('hideCursors') }) - $(document).on(Views.chatView.events.cursorsOff, function () { + $(document).on(Views.ChatView.events.cursorsOff, function () { $('#wrapper').addClass('hideCursors') }) }, @@ -611,7 +611,7 @@ const Realtime = { var sendNewMessage = function (event, data) { self.sendNewMessage(data) } - $(document).on(Views.room.events.newMessage + '.map', sendNewMessage) + $(document).on(Views.Room.events.newMessage + '.map', sendNewMessage) }, attachMapListener: function () { var self = Realtime diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index d5c07e12..6760edcc 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -49,11 +49,11 @@ const _Router = Backbone.Router.extend({ GlobalUI.showDiv('#explore') - Views.exploreMaps.setCollection(Metamaps.Maps.Active) + Views.ExploreMaps.setCollection(Metamaps.Maps.Active) if (Metamaps.Maps.Active.length === 0) { Metamaps.Maps.Active.getMaps(navigate) // this will trigger an explore maps render } else { - Views.exploreMaps.render(navigate) + Views.ExploreMaps.render(navigate) } } else { // logged out home page @@ -108,7 +108,7 @@ const _Router = Backbone.Router.extend({ Metamaps.Maps.Mapper.mapperId = id } - Views.exploreMaps.setCollection(Metamaps.Maps[capitalize]) + Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) var navigate = function () { var path = '/explore/' + this.currentPage @@ -130,9 +130,9 @@ const _Router = Backbone.Router.extend({ }, 300) // wait 300 milliseconds till the other animations are done to do the fetch } else { if (id) { - Views.exploreMaps.fetchUserThenRender(navigateTimeout) + Views.ExploreMaps.fetchUserThenRender(navigateTimeout) } else { - Views.exploreMaps.render(navigateTimeout) + Views.ExploreMaps.render(navigateTimeout) } } diff --git a/frontend/src/Metamaps/Views.js b/frontend/src/Metamaps/Views.js deleted file mode 100644 index aee0fdf0..00000000 --- a/frontend/src/Metamaps/Views.js +++ /dev/null @@ -1,91 +0,0 @@ -/* global Metamaps, $ */ - -import Active from './Active' -import ReactComponents from './ReactComponents' -import ReactDOM from 'react-dom' // TODO ensure this isn't a double import - -/* - * Metamaps.Views.js.erb - * - * Dependencies: - * - Metamaps.Loading - */ - -const Views = { - exploreMaps: { - setCollection: function (collection) { - var self = Views.exploreMaps - - if (self.collection) { - self.collection.off('add', self.render) - self.collection.off('successOnFetch', self.handleSuccess) - self.collection.off('errorOnFetch', self.handleError) - } - self.collection = collection - self.collection.on('add', self.render) - self.collection.on('successOnFetch', self.handleSuccess) - self.collection.on('errorOnFetch', self.handleError) - }, - render: function (mapperObj, cb) { - var self = Views.exploreMaps - - if (typeof mapperObj === 'function') { - cb = mapperObj - mapperObj = null - } - - var exploreObj = { - currentUser: Active.Mapper, - section: self.collection.id, - displayStyle: 'grid', - maps: self.collection, - moreToLoad: self.collection.page != 'loadedAll', - user: mapperObj, - loadMore: self.loadMore - } - ReactDOM.render( - React.createElement(ReactComponents.Maps, exploreObj), - document.getElementById('explore') - ) - - if (cb) cb() - Metamaps.Loading.hide() - }, - loadMore: function () { - var self = Views.exploreMaps - - if (self.collection.page != "loadedAll") { - self.collection.getMaps() - } - else self.render() - }, - handleSuccess: function (cb) { - var self = Views.exploreMaps - - if (self.collection && self.collection.id === 'mapper') { - self.fetchUserThenRender(cb) - } else { - self.render(cb) - } - }, - handleError: function () { - console.log('error loading maps!') // TODO - }, - fetchUserThenRender: function (cb) { - var self = Views.exploreMaps - - // first load the mapper object and then call the render function - $.ajax({ - url: '/users/' + self.collection.mapperId + '/details.json', - success: function (response) { - self.render(response, cb) - }, - error: function () { - self.render(cb) - } - }) - } - } -} - -export default Views diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js new file mode 100644 index 00000000..5d8f5f65 --- /dev/null +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -0,0 +1,337 @@ +/* global Autolinker, $ */ +var linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); + +var Private = { + messageHTML: "<div class='chat-message'>" + + "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + + "<div class='chat-message-text'>{{ message }}</div>" + + "<div class='chat-message-time'>{{ timestamp }}</div>" + + "<div class='clearfloat'></div>" + + "</div>", + participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + + "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + + "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + + "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + + "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + + "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + + "<div class='clearfloat'></div>" + + "</div>", + templates: function() { + _.templateSettings = { + interpolate: /\{\{(.+?)\}\}/g + }; + this.messageTemplate = _.template(Private.messageHTML); + + this.participantTemplate = _.template(Private.participantHTML); + }, + createElements: function() { + this.$unread = $('<div class="chat-unread"></div>'); + this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>'); + this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>'); + this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>'); + this.$videoToggle = $('<div class="video-toggle"></div>'); + this.$cursorToggle = $('<div class="cursor-toggle"></div>'); + this.$participants = $('<div class="participants"></div>'); + this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>'); + this.$chatHeader = $('<div class="chat-header">CHAT</div>'); + this.$soundToggle = $('<div class="sound-toggle"></div>'); + this.$messages = $('<div class="chat-messages"></div>'); + this.$container = $('<div class="chat-box"></div>'); + }, + attachElements: function() { + this.$button.append(this.$unread); + + this.$juntoHeader.append(this.$videoToggle); + this.$juntoHeader.append(this.$cursorToggle); + + this.$chatHeader.append(this.$soundToggle); + + this.$participants.append(this.$conversationInProgress); + + this.$container.append(this.$juntoHeader); + this.$container.append(this.$participants); + this.$container.append(this.$chatHeader); + this.$container.append(this.$button); + this.$container.append(this.$messages); + this.$container.append(this.$messageInput); + }, + addEventListeners: function() { + var self = this; + + this.participants.on('add', function (participant) { + Private.addParticipant.call(self, participant); + }); + + this.participants.on('remove', function (participant) { + Private.removeParticipant.call(self, participant); + }); + + this.$button.on('click', function () { + Handlers.buttonClick.call(self); + }); + this.$videoToggle.on('click', function () { + Handlers.videoToggleClick.call(self); + }); + this.$cursorToggle.on('click', function () { + Handlers.cursorToggleClick.call(self); + }); + this.$soundToggle.on('click', function () { + Handlers.soundToggleClick.call(self); + }); + this.$messageInput.on('keyup', function (event) { + Handlers.keyUp.call(self, event); + }); + this.$messageInput.on('focus', function () { + Handlers.inputFocus.call(self); + }); + this.$messageInput.on('blur', function () { + Handlers.inputBlur.call(self); + }); + }, + initializeSounds: function() { + this.sound = new Howl({ + urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg'], + sprite: { + joinmap: [0, 561], + leavemap: [1000, 592], + receivechat: [2000, 318], + sendchat: [3000, 296], + sessioninvite: [4000, 5393, true] + } + }); + }, + incrementUnread: function() { + this.unreadMessages++; + this.$unread.html(this.unreadMessages); + this.$unread.show(); + }, + addMessage: function(message, isInitial, wasMe) { + + if (!this.isOpen && !isInitial) Private.incrementUnread.call(this); + + function addZero(i) { + if (i < 10) { + i = "0" + i; + } + return i; + } + var m = _.clone(message.attributes); + + var today = new Date(); + m.timestamp = new Date(m.created_at); + + var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate(); + date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes()); + m.timestamp = date; + m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove + m.message = linker.link(m.message); + var $html = $(this.messageTemplate(m)); + this.$messages.append($html); + if (!isInitial) this.scrollMessages(200); + + if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat'); + }, + initialMessages: function() { + var messages = this.messages.models; + for (var i = 0; i < messages.length; i++) { + Private.addMessage.call(this, messages[i], true); + } + }, + handleInputMessage: function() { + var message = { + message: this.$messageInput.val(), + }; + this.$messageInput.val(''); + $(document).trigger(chatView.events.message + '-' + this.room, [message]); + }, + addParticipant: function(participant) { + var p = _.clone(participant.attributes); + if (p.self) { + p.selfClass = 'is-self'; + p.selfName = '(me)'; + } else { + p.selfClass = ''; + p.selfName = ''; + } + var html = this.participantTemplate(p); + this.$participants.append(html); + }, + removeParticipant: function(participant) { + this.$container.find('.participant-' + participant.get('id')).remove(); + } +}; + +var Handlers = { + buttonClick: function() { + if (this.isOpen) this.close(); + else if (!this.isOpen) this.open(); + }, + videoToggleClick: function() { + this.$videoToggle.toggleClass('active'); + this.videosShowing = !this.videosShowing; + $(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff); + }, + cursorToggleClick: function() { + this.$cursorToggle.toggleClass('active'); + this.cursorsShowing = !this.cursorsShowing; + $(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff); + }, + soundToggleClick: function() { + this.alertSound = !this.alertSound; + this.$soundToggle.toggleClass('active'); + }, + keyUp: function(event) { + switch(event.which) { + case 13: // enter + Private.handleInputMessage.call(this); + break; + } + }, + inputFocus: function() { + $(document).trigger(chatView.events.inputFocus); + }, + inputBlur: function() { + $(document).trigger(chatView.events.inputBlur); + } +}; + +const ChatView = function(messages, mapper, room) { + var self = this; + + this.room = room; + this.mapper = mapper; + this.messages = messages; // backbone collection + + this.isOpen = false; + this.alertSound = true; // whether to play sounds on arrival of new messages or not + this.cursorsShowing = true; + this.videosShowing = true; + this.unreadMessages = 0; + this.participants = new Backbone.Collection(); + + Private.templates.call(this); + Private.createElements.call(this); + Private.attachElements.call(this); + Private.addEventListeners.call(this); + Private.initialMessages.call(this); + Private.initializeSounds.call(this); + this.$container.css({ + right: '-300px' + }); +}; + +ChatView.prototype.conversationInProgress = function (participating) { + this.$conversationInProgress.show(); + this.$participants.addClass('is-live'); + if (participating) this.$participants.addClass('is-participating'); + this.$button.addClass('active'); + + // hide invite to call buttons +} + +ChatView.prototype.conversationEnded = function () { + this.$conversationInProgress.hide(); + this.$participants.removeClass('is-live'); + this.$participants.removeClass('is-participating'); + this.$button.removeClass('active'); + this.$participants.find('.participant').removeClass('active'); + this.$participants.find('.participant').removeClass('pending'); +} + +ChatView.prototype.leaveConversation = function () { + this.$participants.removeClass('is-participating'); +} + +ChatView.prototype.mapperJoinedCall = function (id) { + this.$participants.find('.participant-' + id).addClass('active'); +} + +ChatView.prototype.mapperLeftCall = function (id) { + this.$participants.find('.participant-' + id).removeClass('active'); +} + +ChatView.prototype.invitationPending = function (id) { + this.$participants.find('.participant-' + id).addClass('pending'); +} + +ChatView.prototype.invitationAnswered = function (id) { + this.$participants.find('.participant-' + id).removeClass('pending'); +} + +ChatView.prototype.addParticipant = function (participant) { + this.participants.add(participant); +} + +ChatView.prototype.removeParticipant = function (username) { + var p = this.participants.find(function (p) { return p.get('username') === username; }); + if (p) { + this.participants.remove(p); + } +} + +ChatView.prototype.removeParticipants = function () { + this.participants.remove(this.participants.models); +} + +ChatView.prototype.open = function () { + this.$container.css({ + right: '0' + }); + this.$messageInput.focus(); + this.isOpen = true; + this.unreadMessages = 0; + this.$unread.hide(); + this.scrollMessages(0); + $(document).trigger(ChatView.events.openTray); +} + +ChatView.prototype.addMessage = function(message, isInitial, wasMe) { + this.messages.add(message); + Private.addMessage.call(this, message, isInitial, wasMe); +} + +ChatView.prototype.scrollMessages = function(duration) { + duration = duration || 0; + + this.$messages.animate({ + scrollTop: this.$messages[0].scrollHeight + }, duration); +} + +ChatView.prototype.clearMessages = function () { + this.unreadMessages = 0; + this.$unread.hide(); + this.$messages.empty(); +} + +ChatView.prototype.close = function () { + this.$container.css({ + right: '-300px' + }); + this.$messageInput.blur(); + this.isOpen = false; + $(document).trigger(ChatView.events.closeTray); +} + +ChatView.prototype.remove = function () { + this.$button.off(); + this.$container.remove(); +} + +/** + * @class + * @static + */ +ChatView.events = { + message: 'ChatView:message', + openTray: 'ChatView:openTray', + closeTray: 'ChatView:closeTray', + inputFocus: 'ChatView:inputFocus', + inputBlur: 'ChatView:inputBlur', + cursorsOff: 'ChatView:cursorsOff', + cursorsOn: 'ChatView:cursorsOn', + videosOff: 'ChatView:videosOff', + videosOn: 'ChatView:videosOn' +}; + +export default ChatView diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js new file mode 100644 index 00000000..4ffbf9fb --- /dev/null +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -0,0 +1,86 @@ +/* global Metamaps, $ */ + +import Active from './Active' +import ReactComponents from './ReactComponents' +import ReactDOM from 'react-dom' // TODO ensure this isn't a double import + +/* + * - Metamaps.Loading + */ + +const ExploreMaps = { + setCollection: function (collection) { + var self = ExploreMaps + + if (self.collection) { + self.collection.off('add', self.render) + self.collection.off('successOnFetch', self.handleSuccess) + self.collection.off('errorOnFetch', self.handleError) + } + self.collection = collection + self.collection.on('add', self.render) + self.collection.on('successOnFetch', self.handleSuccess) + self.collection.on('errorOnFetch', self.handleError) + }, + render: function (mapperObj, cb) { + var self = ExploreMaps + + if (typeof mapperObj === 'function') { + cb = mapperObj + mapperObj = null + } + + var exploreObj = { + currentUser: Active.Mapper, + section: self.collection.id, + displayStyle: 'grid', + maps: self.collection, + moreToLoad: self.collection.page != 'loadedAll', + user: mapperObj, + loadMore: self.loadMore + } + ReactDOM.render( + React.createElement(ReactComponents.Maps, exploreObj), + document.getElementById('explore') + ) + + if (cb) cb() + Metamaps.Loading.hide() + }, + loadMore: function () { + var self = ExploreMaps + + if (self.collection.page != "loadedAll") { + self.collection.getMaps() + } + else self.render() + }, + handleSuccess: function (cb) { + var self = ExploreMaps + + if (self.collection && self.collection.id === 'mapper') { + self.fetchUserThenRender(cb) + } else { + self.render(cb) + } + }, + handleError: function () { + console.log('error loading maps!') // TODO + }, + fetchUserThenRender: function (cb) { + var self = ExploreMaps + + // first load the mapper object and then call the render function + $.ajax({ + url: '/users/' + self.collection.mapperId + '/details.json', + success: function (response) { + self.render(response, cb) + }, + error: function () { + self.render(cb) + } + }) + } +} + +export default ExploreMaps diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js new file mode 100644 index 00000000..014df61b --- /dev/null +++ b/frontend/src/Metamaps/Views/Room.js @@ -0,0 +1,198 @@ +/* global Metamaps, $ */ +import Active from '../Active' +import Realtime from '../Realtime' + +import ChatView from './ChatView' +import VideoView from './VideoView' + +/* + * Metamaps.Backbone + */ + +const Room = function(opts) { + var self = this + + this.isActiveRoom = false + this.socket = opts.socket + this.webrtc = opts.webrtc + //this.roomRef = opts.firebase + this.room = opts.room + this.config = opts.config + this.peopleCount = 0 + + this.$myVideo = opts.$video + this.myVideo = opts.myVideoView + + this.messages = new Backbone.Collection() + this.currentMapper = new Backbone.Model({ name: opts.username, image: opts.image }) + this.chat = new ChatView(this.messages, this.currentMapper, this.room) + + this.videos = {} + + this.init() +} + +Room.prototype.join = function(cb) { + this.isActiveRoom = true + this.webrtc.joinRoom(this.room, cb) + this.chat.conversationInProgress(true) // true indicates participation +} + +Room.prototype.conversationInProgress = function() { + this.chat.conversationInProgress(false) // false indicates not participating +} + +Room.prototype.conversationEnding = function() { + this.chat.conversationEnded() +} + +Room.prototype.leaveVideoOnly = function() { + this.chat.leaveConversation() // the conversation will carry on without you + for (var id in this.videos) { + this.removeVideo(id) + } + this.isActiveRoom = false + this.webrtc.leaveRoom() +} + +Room.prototype.leave = function() { + for (var id in this.videos) { + this.removeVideo(id) + } + this.isActiveRoom = false + this.webrtc.leaveRoom() + this.chat.conversationEnded() + this.chat.removeParticipants() + this.chat.clearMessages() + this.messages.reset() +} + +Room.prototype.setPeopleCount = function(count) { + this.peopleCount = count +} + +Room.prototype.init = function () { + var self = this + + $(document).on(VideoView.events.audioControlClick, function (event, videoView) { + if (!videoView.audioStatus) self.webrtc.mute() + else if (videoView.audioStatus) self.webrtc.unmute() + }) + $(document).on(VideoView.events.videoControlClick, function (event, videoView) { + if (!videoView.videoStatus) self.webrtc.pauseVideo() + else if (videoView.videoStatus) self.webrtc.resumeVideo() + }) + + this.webrtc.webrtc.off('peerStreamAdded') + this.webrtc.webrtc.off('peerStreamRemoved') + this.webrtc.on('peerStreamAdded', function (peer) { + var mapper = Realtime.mappersOnMap[peer.nick] + peer.avatar = mapper.image + peer.username = mapper.name + if (self.isActiveRoom) { + self.addVideo(peer) + } + }) + + this.webrtc.on('peerStreamRemoved', function (peer) { + if (self.isActiveRoom) { + self.removeVideo(peer) + } + }) + + this.webrtc.on('mute', function (data) { + var v = self.videos[data.id] + if (!v) return + + if (data.name === 'audio') { + v.audioStatus = false + } + else if (data.name === 'video') { + v.videoStatus = false + v.$avatar.show() + } + if (!v.audioStatus && !v.videoStatus) v.$container.hide() + }) + this.webrtc.on('unmute', function (data) { + var v = self.videos[data.id] + if (!v) return + + if (data.name === 'audio') { + v.audioStatus = true + } + else if (data.name === 'video') { + v.videoStatus = true + v.$avatar.hide() + } + v.$container.show() + }) + + var sendChatMessage = function (event, data) { + self.sendChatMessage(data) + } + $(document).on(ChatView.events.message + '-' + this.room, sendChatMessage) + } + + Room.prototype.videoAdded = function (callback) { + this._videoAdded = callback + } + + Room.prototype.addVideo = function (peer) { + var + id = this.webrtc.getDomId(peer), + video = attachMediaStream(peer.stream) + + var + v = new VideoView(video, null, id, false, { DOUBLE_CLICK_TOLERANCE: 200, avatar: peer.avatar, username: peer.username }) + + this.videos[peer.id] = v + if (this._videoAdded) this._videoAdded(v, peer.nick) + } + + Room.prototype.removeVideo = function (peer) { + var id = typeof peer == 'string' ? peer : peer.id + if (this.videos[id]) { + this.videos[id].remove() + delete this.videos[id] + } + } + + Room.prototype.sendChatMessage = function (data) { + var self = this + //this.roomRef.child('messages').push(data) + if (self.chat.alertSound) self.chat.sound.play('sendchat') + var m = new Metamaps.Backbone.Message({ + message: data.message, + resource_id: Active.Map.id, + resource_type: "Map" + }) + m.save(null, { + success: function (model, response) { + self.addMessages(new Metamaps.Backbone.MessageCollection(model), false, true) + $(document).trigger(Room.events.newMessage, [model]) + }, + error: function (model, response) { + console.log('error!', response) + } + }) + } + + // they should be instantiated as backbone models before they get + // passed to this function + Room.prototype.addMessages = function (messages, isInitial, wasMe) { + var self = this + + messages.models.forEach(function (message) { + self.chat.addMessage(message, isInitial, wasMe) + }) + } + +/** + * @class + * @static + */ +Room.events = { + newMessage: "Room:newMessage" +} + +export default Room diff --git a/frontend/src/Metamaps/Views/VideoView.js b/frontend/src/Metamaps/Views/VideoView.js new file mode 100644 index 00000000..401ece54 --- /dev/null +++ b/frontend/src/Metamaps/Views/VideoView.js @@ -0,0 +1,202 @@ +/* global $ */ + +var Private = { + addControls: function() { + var self = this; + + this.$audioControl = $('<div class="video-audio"></div>'); + this.$videoControl = $('<div class="video-video"></div>'); + + this.$audioControl.on('click', function () { + Handlers.audioControlClick.call(self); + }); + + this.$videoControl.on('click', function () { + Handlers.videoControlClick.call(self); + }); + + this.$container.append(this.$audioControl); + this.$container.append(this.$videoControl); + }, + cancelClick: function() { + this.mouseIsDown = false; + + if (this.hasMoved) { + + } + + $(document).trigger(VideoView.events.dragEnd); + } +}; + +var Handlers = { + mousedown: function(event) { + this.mouseIsDown = true; + this.hasMoved = false; + this.mouseMoveStart = { + x: event.pageX, + y: event.pageY + }; + this.posStart = { + x: parseInt(this.$container.css('left'), '10'), + y: parseInt(this.$container.css('top'), '10') + } + + $(document).trigger(VideoView.events.mousedown); + }, + mouseup: function(event) { + $(document).trigger(VideoView.events.mouseup, [this]); + + var storedTime = this.lastClick; + var now = Date.now(); + this.lastClick = now; + + if (now - storedTime < this.config.DOUBLE_CLICK_TOLERANCE) { + $(document).trigger(VideoView.events.doubleClick, [this]); + } + }, + mousemove: function(event) { + var + diffX, + diffY, + newX, + newY; + + if (this.$parent && this.mouseIsDown) { + this.manuallyPositioned = true; + this.hasMoved = true; + diffX = event.pageX - this.mouseMoveStart.x; + diffY = this.mouseMoveStart.y - event.pageY; + newX = this.posStart.x + diffX; + newY = this.posStart.y - diffY; + this.$container.css({ + top: newY, + left: newX + }); + } + }, + audioControlClick: function() { + if (this.audioStatus) { + this.audioOff(); + } else { + this.audioOn(); + } + $(document).trigger(VideoView.events.audioControlClick, [this]); + }, + videoControlClick: function() { + if (this.videoStatus) { + this.videoOff(); + } else { + this.videoOn(); + } + $(document).trigger(VideoView.events.videoControlClick, [this]); + }, +}; + +var VideoView = function(video, $parent, id, isMyself, config) { + var self = this; + + this.$parent = $parent; // mapView + + this.video = video; + this.id = id; + + this.config = config; + + this.mouseIsDown = false; + this.mouseDownOffset = { x: 0, y: 0 }; + this.lastClick = null; + this.hasMoved = false; + + this.audioStatus = true; + this.videoStatus = true; + + this.$container = $('<div></div>'); + this.$container.addClass('collaborator-video' + (isMyself ? ' my-video' : '')); + this.$container.attr('id', 'container_' + id); + + + var $vidContainer = $('<div></div>'); + $vidContainer.addClass('video-cutoff'); + $vidContainer.append(this.video); + + this.avatar = config.avatar; + this.$avatar = $('<img draggable="false" class="collaborator-video-avatar" src="' + config.avatar + '" width="150" height="150" />'); + $vidContainer.append(this.$avatar); + + this.$container.append($vidContainer); + + this.$container.on('mousedown', function (event) { + Handlers.mousedown.call(self, event); + }); + + if (isMyself) { + Private.addControls.call(this); + } + + // suppress contextmenu + this.video.oncontextmenu = function () { return false; }; + + if (this.$parent) this.setParent(this.$parent); +}; + +VideoView.prototype.setParent = function($parent) { + var self = this; + this.$parent = $parent; + this.$parent.off('.video' + this.id); + this.$parent.on('mouseup.video' + this.id, function (event) { + Handlers.mouseup.call(self, event); + Private.cancelClick.call(self); + }); + this.$parent.on('mousemove.video' + this.id, function (event) { + Handlers.mousemove.call(self, event); + }); +} + +VideoView.prototype.setAvatar = function (src) { + this.$avatar.attr('src', src); + this.avatar = src; +} + +VideoView.prototype.remove = function () { + this.$container.off(); + if (this.$parent) this.$parent.off('.video' + this.id); + this.$container.remove(); +} + +VideoView.prototype.videoOff = function () { + this.$videoControl.addClass('active'); + this.$avatar.show(); + this.videoStatus = false; +} + +VideoView.prototype.videoOn = function () { + this.$videoControl.removeClass('active'); + this.$avatar.hide(); + this.videoStatus = true; +} + +VideoView.prototype.audioOff = function () { + this.$audioControl.addClass('active'); + this.audioStatus = false; +} + +VideoView.prototype.audioOn = function () { + this.$audioControl.removeClass('active'); + this.audioStatus = true; +} + +/** + * @class + * @static + */ +VideoView.events = { + mousedown: "VideoView:mousedown", + mouseup: "VideoView:mouseup", + doubleClick: "VideoView:doubleClick", + dragEnd: "VideoView:dragEnd", + audioControlClick: "VideoView:audioControlClick", + videoControlClick: "VideoView:videoControlClick", +}; + +export default VideoView diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js new file mode 100644 index 00000000..ca0e751a --- /dev/null +++ b/frontend/src/Metamaps/Views/index.js @@ -0,0 +1,6 @@ +import ExploreMaps from './ExploreMaps' +import ChatView from './ChatView' +import VideoView from './VideoView' +import Room from './Room' + +export ExploreMaps, ChatView, VideoView, Room diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 7b431d1f..45283c89 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -28,7 +28,7 @@ import SynapseCard from './SynapseCard' import Topic from './Topic' import TopicCard from './TopicCard' import Util from './Util' -import Views from './Views' +import * as Views from './Views' import Visualize from './Visualize' import ReactComponents from './ReactComponents' @@ -83,18 +83,18 @@ document.addEventListener("DOMContentLoaded", function() { if (Metamaps.currentSection === "explore") { const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - Metamaps.Views.exploreMaps.setCollection( Metamaps.Maps[capitalize] ) + Metamaps.Views.ExploreMaps.setCollection( Metamaps.Maps[capitalize] ) if (Metamaps.currentPage === "mapper") { - Views.exploreMaps.fetchUserThenRender() + Views.ExploreMaps.fetchUserThenRender() } else { - Views.exploreMaps.render() + Views.ExploreMaps.render() } GlobalUI.showDiv('#explore') } else if (Metamaps.currentSection === "" && Active.Mapper) { - Views.exploreMaps.setCollection(Metamaps.Maps.Active) - Views.exploreMaps.render() + Views.ExploreMaps.setCollection(Metamaps.Maps.Active) + Views.ExploreMaps.render() GlobalUI.showDiv('#explore') } else if (Active.Map || Active.Topic) { From a996734c793aebd75eea8031e4a328f3e5a5a2b5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 00:16:15 +0800 Subject: [PATCH 035/306] remove Backbone from window --- .../src/Metamaps/{Backbone.js => Backbone/index.js} | 4 +++- frontend/src/Metamaps/Router.js | 6 +++++- frontend/src/Metamaps/Views/ChatView.js | 5 +++++ frontend/src/Metamaps/Views/Room.js | 8 +++++++- frontend/src/index.js | 11 ++--------- 5 files changed, 22 insertions(+), 12 deletions(-) rename frontend/src/Metamaps/{Backbone.js => Backbone/index.js} (99%) diff --git a/frontend/src/Metamaps/Backbone.js b/frontend/src/Metamaps/Backbone/index.js similarity index 99% rename from frontend/src/Metamaps/Backbone.js rename to frontend/src/Metamaps/Backbone/index.js index bc303df4..7e6878b3 100644 --- a/frontend/src/Metamaps/Backbone.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -1,6 +1,8 @@ -/* global Metamaps, Backbone, _, $ */ +/* global Metamaps, Backbone, $ */ import _ from 'lodash' +import Backbone from 'backbone' +Backbone.$ = window.$ /* * Metamaps.Backbone.js.erb diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 6760edcc..0ad88efd 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -1,4 +1,8 @@ -/* global Metamaps, Backbone, $ */ +/* global Metamaps, $ */ + +import Backbone from 'backbone' +//TODO is this line good or bad? +//Backbone.$ = window.$ import Active from './Active' import GlobalUI from './GlobalUI' diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 5d8f5f65..d1efdf74 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,4 +1,9 @@ /* global Autolinker, $ */ + +import Backbone from 'backbone' +// TODO is this line good or bad +// Backbone.$ = window.$ + var linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); var Private = { diff --git a/frontend/src/Metamaps/Views/Room.js b/frontend/src/Metamaps/Views/Room.js index 014df61b..5b70ee7c 100644 --- a/frontend/src/Metamaps/Views/Room.js +++ b/frontend/src/Metamaps/Views/Room.js @@ -1,4 +1,9 @@ /* global Metamaps, $ */ + +import Backbone from 'backbone' +// TODO is this line good or bad +// Backbone.$ = window.$ + import Active from '../Active' import Realtime from '../Realtime' @@ -6,7 +11,8 @@ import ChatView from './ChatView' import VideoView from './VideoView' /* - * Metamaps.Backbone + * Dependencies: + * Metamaps.Backbone */ const Room = function(opts) { diff --git a/frontend/src/index.js b/frontend/src/index.js index e5705512..176ac329 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,15 +1,8 @@ -import React from 'react' +// create global references to some utility libraries import ReactDOM from 'react-dom' -import Backbone from 'backbone' import _ from 'underscore' - -import Metamaps from './Metamaps' - -// create global references to some libraries -window.React = React window.ReactDOM = ReactDOM -Backbone.$ = window.$ // jquery from rails -window.Backbone = Backbone window._ = _ +import Metamaps from './Metamaps' window.Metamaps = Metamaps From 30fc9438331620b9b27a0edf897581ccbf51e19a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 00:20:05 +0800 Subject: [PATCH 036/306] clean up backbone file imports --- frontend/src/Metamaps/Backbone/index.js | 141 ++++++++++++------------ 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index 7e6878b3..2c7ae530 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -4,29 +4,30 @@ import _ from 'lodash' import Backbone from 'backbone' Backbone.$ = window.$ +import Active from '../Active' +import Filter from '../Filter' +import JIT from '../JIT' +import Map, { InfoBox } from '../Map' +import Mapper from '../Mapper' +import Realtime from '../Realtime' +import Synapse from '../Synapse' +import SynapseCard from '../SynapseCard' +import Topic from '../Topic' +import TopicCard from '../TopicCard' +import Visualize from '../Visualize' + /* * Metamaps.Backbone.js.erb * * Dependencies: - * - Metamaps.Active * - Metamaps.Collaborators * - Metamaps.Creators - * - Metamaps.Filter - * - Metamaps.JIT * - Metamaps.Loading - * - Metamaps.Map - * - Metamaps.Mapper * - Metamaps.Mappers * - Metamaps.Mappings * - Metamaps.Metacodes - * - Metamaps.Realtime - * - Metamaps.Synapse - * - Metamaps.SynapseCard * - Metamaps.Synapses - * - Metamaps.Topic - * - Metamaps.TopicCard * - Metamaps.Topics - * - Metamaps.Visualize */ const _Backbone = {} @@ -62,7 +63,7 @@ _Backbone.Map = Backbone.Model.extend({ this.on('saved', this.savedEvent) }, savedEvent: function () { - Metamaps.Realtime.sendMapChange(this) + Realtime.sendMapChange(this) }, authorizeToEdit: function (mapper) { if (mapper && ( @@ -82,7 +83,7 @@ _Backbone.Map = Backbone.Model.extend({ } }, getUser: function () { - return Metamaps.Mapper.get(this.get('user_id')) + return Mapper.get(this.get('user_id')) }, fetchContained: function () { var bb = _Backbone @@ -126,10 +127,10 @@ _Backbone.Map = Backbone.Model.extend({ return this.get('mappers') }, updateView: function () { - var map = Metamaps.Active.Map + var map = Active.Map var isActiveMap = this.id === map.id if (isActiveMap) { - Metamaps.Map.InfoBox.updateNameDescPerm(this.get('name'), this.get('desc'), this.get('permission')) + InfoBox.updateNameDescPerm(this.get('name'), this.get('desc'), this.get('permission')) this.updateMapWrapper() // mobile menu $('#header_content').html(this.get('name')) @@ -137,9 +138,9 @@ _Backbone.Map = Backbone.Model.extend({ } }, updateMapWrapper: function () { - var map = Metamaps.Active.Map + var map = Active.Map var isActiveMap = this.id === map.id - var authorized = map && map.authorizeToEdit(Metamaps.Active.Mapper) ? 'canEditMap' : '' + var authorized = map && map.authorizeToEdit(Active.Mapper) ? 'canEditMap' : '' var commonsMap = map && map.get('permission') === 'commons' ? 'commonsMap' : '' if (isActiveMap) { $('.wrapper').removeClass('canEditMap commonsMap').addClass(authorized + ' ' + commonsMap) @@ -324,10 +325,10 @@ _Backbone.init = function () { initialize: function () { if (this.isNew()) { this.set({ - 'user_id': Metamaps.Active.Mapper.id, + 'user_id': Active.Mapper.id, 'desc': this.get('desc') || '', 'link': this.get('link') || '', - 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons' + 'permission': Active.Map ? Active.Map.get('permission') : 'commons' }) } @@ -339,7 +340,7 @@ _Backbone.init = function () { mappableid: this.id } - $(document).trigger(Metamaps.JIT.events.removeTopic, [removeTopicData]) + $(document).trigger(JIT.events.removeTopic, [removeTopicData]) }) this.on('noLongerPrivate', function () { var newTopicData = { @@ -347,10 +348,10 @@ _Backbone.init = function () { mappableid: this.id } - $(document).trigger(Metamaps.JIT.events.newTopic, [newTopicData]) + $(document).trigger(JIT.events.newTopic, [newTopicData]) }) - this.on('change:metacode_id', Metamaps.Filter.checkMetacodes, this) + this.on('change:metacode_id', Filter.checkMetacodes, this) }, authorizeToEdit: function (mapper) { if (mapper && @@ -371,10 +372,10 @@ _Backbone.init = function () { return Metamaps.Metacodes.get(this.get('metacode_id')) }, getMapping: function () { - if (!Metamaps.Active.Map) return false + if (!Active.Map) return false return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, + map_id: Active.Map.id, mappable_type: 'Topic', mappable_id: this.isNew() ? this.cid : this.id }) @@ -387,7 +388,7 @@ _Backbone.init = function () { name: this.get('name') } - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = this.getMapping() node.data = { $mapping: null, @@ -402,7 +403,7 @@ _Backbone.init = function () { var node = this.get('node') node.setData('topic', this) - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = this.getMapping() node.setData('mapping', mapping) } @@ -410,38 +411,38 @@ _Backbone.init = function () { return node }, savedEvent: function () { - Metamaps.Realtime.sendTopicChange(this) + Realtime.sendTopicChange(this) }, updateViews: function () { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node) + if (onPageWithTopicCard && this == TopicCard.openTopicCard) { + TopicCard.showCard(node) } // update the node on the map if (onPageWithTopicCard && node) { node.name = this.get('name') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } }, updateCardView: function () { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update topic card, if this topic is the one open there - if (onPageWithTopicCard && this == Metamaps.TopicCard.openTopicCard) { - Metamaps.TopicCard.showCard(node) + if (onPageWithTopicCard && this == TopicCard.openTopicCard) { + TopicCard.showCard(node) } }, updateNodeView: function () { - var onPageWithTopicCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithTopicCard = Active.Map || Active.Topic var node = this.get('node') // update the node on the map if (onPageWithTopicCard && node) { node.name = this.get('name') - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } } }) @@ -489,8 +490,8 @@ _Backbone.init = function () { initialize: function () { if (this.isNew()) { this.set({ - 'user_id': Metamaps.Active.Mapper.id, - 'permission': Metamaps.Active.Map ? Metamaps.Active.Map.get('permission') : 'commons', + 'user_id': Active.Mapper.id, + 'permission': Active.Map ? Active.Map.get('permission') : 'commons', 'category': 'from-to' }) } @@ -504,15 +505,15 @@ _Backbone.init = function () { mappableid: this.id } - $(document).trigger(Metamaps.JIT.events.newSynapse, [newSynapseData]) + $(document).trigger(JIT.events.newSynapse, [newSynapseData]) }) this.on('nowPrivate', function () { - $(document).trigger(Metamaps.JIT.events.removeSynapse, [{ + $(document).trigger(JIT.events.removeSynapse, [{ mappableid: this.id }]) }) - this.on('change:desc', Metamaps.Filter.checkSynapses, this) + this.on('change:desc', Filter.checkSynapses, this) }, prepareLiForFilter: function () { var li = '' @@ -546,10 +547,10 @@ _Backbone.init = function () { ] : false }, getMapping: function () { - if (!Metamaps.Active.Map) return false + if (!Active.Map) return false return Metamaps.Mappings.findWhere({ - map_id: Metamaps.Active.Map.id, + map_id: Active.Map.id, mappable_type: 'Synapse', mappable_id: this.isNew() ? this.cid : this.id }) @@ -567,7 +568,7 @@ _Backbone.init = function () { } } - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = providedMapping || this.getMapping() mappingID = mapping.isNew() ? mapping.cid : mapping.id edge.data.$mappings = [] @@ -581,7 +582,7 @@ _Backbone.init = function () { var edge = this.get('edge') edge.getData('synapses').push(this) - if (Metamaps.Active.Map) { + if (Active.Map) { mapping = this.getMapping() edge.getData('mappings').push(mapping) } @@ -589,28 +590,28 @@ _Backbone.init = function () { return edge }, savedEvent: function () { - Metamaps.Realtime.sendSynapseChange(this) + Realtime.sendSynapseChange(this) }, updateViews: function () { this.updateCardView() this.updateEdgeView() }, updateCardView: function () { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithSynapseCard = Active.Map || Active.Topic var edge = this.get('edge') // update synapse card, if this synapse is the one open there - if (onPageWithSynapseCard && edge == Metamaps.SynapseCard.openSynapseCard) { - Metamaps.SynapseCard.showCard(edge) + if (onPageWithSynapseCard && edge == SynapseCard.openSynapseCard) { + SynapseCard.showCard(edge) } }, updateEdgeView: function () { - var onPageWithSynapseCard = Metamaps.Active.Map || Metamaps.Active.Topic + var onPageWithSynapseCard = Active.Map || Active.Topic var edge = this.get('edge') // update the edge on the map if (onPageWithSynapseCard && edge) { - Metamaps.Visualize.mGraph.plot() + Visualize.mGraph.plot() } } }) @@ -629,20 +630,20 @@ _Backbone.init = function () { initialize: function () { if (this.isNew()) { this.set({ - 'user_id': Metamaps.Active.Mapper.id, - 'map_id': Metamaps.Active.Map ? Metamaps.Active.Map.id : null + 'user_id': Active.Mapper.id, + 'map_id': Active.Map ? Active.Map.id : null }) } }, getMap: function () { - return Metamaps.Map.get(this.get('map_id')) + return Map.get(this.get('map_id')) }, getTopic: function () { - if (this.get('mappable_type') === 'Topic') return Metamaps.Topic.get(this.get('mappable_id')) + if (this.get('mappable_type') === 'Topic') return Topic.get(this.get('mappable_id')) else return false }, getSynapse: function () { - if (this.get('mappable_type') === 'Synapse') return Metamaps.Synapse.get(this.get('mappable_id')) + if (this.get('mappable_type') === 'Synapse') return Synapse.get(this.get('mappable_id')) else return false } }) @@ -665,34 +666,34 @@ _Backbone.init = function () { // this is for topic view Metamaps.Creators = Metamaps.Creators ? new self.MapperCollection(Metamaps.Creators) : new self.MapperCollection() - if (Metamaps.Active.Map) { + if (Active.Map) { Metamaps.Mappings = Metamaps.Mappings ? new self.MappingCollection(Metamaps.Mappings) : new self.MappingCollection() - Metamaps.Active.Map = new self.Map(Metamaps.Active.Map) + Active.Map = new self.Map(Active.Map) } - if (Metamaps.Active.Topic) Metamaps.Active.Topic = new self.Topic(Metamaps.Active.Topic) + if (Active.Topic) Active.Topic = new self.Topic(Active.Topic) // attach collection event listeners self.attachCollectionEvents = function () { Metamaps.Topics.on('add remove', function (topic) { - Metamaps.Map.InfoBox.updateNumbers() - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkMappers() + InfoBox.updateNumbers() + Filter.checkMetacodes() + Filter.checkMappers() }) Metamaps.Synapses.on('add remove', function (synapse) { - Metamaps.Map.InfoBox.updateNumbers() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMappers() + InfoBox.updateNumbers() + Filter.checkSynapses() + Filter.checkMappers() }) - if (Metamaps.Active.Map) { + if (Active.Map) { Metamaps.Mappings.on('add remove', function (mapping) { - Metamaps.Map.InfoBox.updateNumbers() - Metamaps.Filter.checkSynapses() - Metamaps.Filter.checkMetacodes() - Metamaps.Filter.checkMappers() + InfoBox.updateNumbers() + Filter.checkSynapses() + Filter.checkMetacodes() + Filter.checkMappers() }) } } From 73e7c38873c21551f718478eedf8a40a81a831fc Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 08:05:26 +0800 Subject: [PATCH 037/306] syntax fixes --- frontend/src/Metamaps/Map/InfoBox.js | 2 +- frontend/src/Metamaps/Map/index.js | 26 +++++++++++----------- frontend/src/Metamaps/Views/ChatView.js | 2 +- frontend/src/Metamaps/Views/ExploreMaps.js | 5 +++-- frontend/src/Metamaps/Views/index.js | 2 +- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index eaceba29..a2cc5de2 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,6 +1,6 @@ /* global Metamaps, $ */ -import Active from './Active' +import Active from '../Active' import GlobalUI from '../GlobalUI' import Router from '../Router' diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 84ee8b39..3dd1c531 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,17 +1,17 @@ /* global Metamaps, $ */ -import Active from './Active' -import AutoLayout from './AutoLayout' -import Create from './Create' -import Filter from './Filter' -import GlobalUI from './GlobalUI' -import JIT from './JIT' -import Realtime from './Realtime' -import Router from './Router' -import Selected from './Selected' -import SynapseCard from './SynapseCard' -import TopicCard from './TopicCard' -import Visualize from './Visualize' +import Active from '../Active' +import AutoLayout from '../AutoLayout' +import Create from '../Create' +import Filter from '../Filter' +import GlobalUI from '../GlobalUI' +import JIT from '../JIT' +import Realtime from '../Realtime' +import Router from '../Router' +import Selected from '../Selected' +import SynapseCard from '../SynapseCard' +import TopicCard from '../TopicCard' +import Visualize from '../Visualize' import CheatSheet from './CheatSheet' import InfoBox from './InfoBox' @@ -361,5 +361,5 @@ const Map = { } } -export CheatSheet, InfoBox +export { CheatSheet, InfoBox } export default Map diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index d1efdf74..9f800c4e 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -95,7 +95,7 @@ var Private = { }, initializeSounds: function() { this.sound = new Howl({ - urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg'], + urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], sprite: { joinmap: [0, 561], leavemap: [1000, 592], diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 4ffbf9fb..d8ba5360 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -1,9 +1,10 @@ /* global Metamaps, $ */ -import Active from './Active' -import ReactComponents from './ReactComponents' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import +import Active from '../Active' +import ReactComponents from '../ReactComponents' + /* * - Metamaps.Loading */ diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index ca0e751a..9663ba98 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -3,4 +3,4 @@ import ChatView from './ChatView' import VideoView from './VideoView' import Room from './Room' -export ExploreMaps, ChatView, VideoView, Room +export { ExploreMaps, ChatView, VideoView, Room } From f59a5775ae6136a2bb274170204176da018e325e Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 22 Sep 2016 20:16:18 -0400 Subject: [PATCH 038/306] tweaks to import/exports --- app/assets/javascripts/application.js | 3 --- frontend/src/Metamaps/Views/ExploreMaps.js | 1 + frontend/src/Metamaps/Views/index.js | 3 ++- frontend/src/Metamaps/index.js | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 03dac4fb..68f2179b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -17,7 +17,4 @@ //= require ./src/JIT //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle -//= require ./src/views/chatView -//= require ./src/views/videoView -//= require ./src/views/room //= require ./src/check-canvas-support diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index d8ba5360..155e8453 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -1,5 +1,6 @@ /* global Metamaps, $ */ +import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' diff --git a/frontend/src/Metamaps/Views/index.js b/frontend/src/Metamaps/Views/index.js index 9663ba98..d13482d0 100644 --- a/frontend/src/Metamaps/Views/index.js +++ b/frontend/src/Metamaps/Views/index.js @@ -3,4 +3,5 @@ import ChatView from './ChatView' import VideoView from './VideoView' import Room from './Room' -export { ExploreMaps, ChatView, VideoView, Room } +const Views = { ExploreMaps, ChatView, VideoView, Room } +export default Views diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 45283c89..5d15559c 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -28,7 +28,7 @@ import SynapseCard from './SynapseCard' import Topic from './Topic' import TopicCard from './TopicCard' import Util from './Util' -import * as Views from './Views' +import Views from './Views' import Visualize from './Visualize' import ReactComponents from './ReactComponents' From 499593fc82f7c8338ad2c9dbead7511829f8aef4 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 22 Sep 2016 21:40:49 -0400 Subject: [PATCH 039/306] fixing references --- frontend/src/Metamaps/Router.js | 20 +++++++++++--------- frontend/src/Metamaps/Views/ChatView.js | 10 +++++----- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 0ad88efd..c5f1c9a7 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -21,6 +21,9 @@ import Visualize from './Visualize' */ const _Router = Backbone.Router.extend({ + currentPage: '', + currentSection: '', + timeoutId: undefined, routes: { '': 'home', // #home 'explore/:section': 'explore', // #explore/active @@ -28,6 +31,7 @@ const _Router = Backbone.Router.extend({ 'maps/:id': 'maps' // #maps/7 }, home: function () { + let self = this clearTimeout(this.timeoutId) if (Active.Mapper) document.title = 'Explore Active Maps | Metamaps' @@ -41,8 +45,8 @@ const _Router = Backbone.Router.extend({ $('.wrapper').addClass(classes) var navigate = function () { - this.timeoutId = setTimeout(function () { - this.navigate('') + self.timeoutId = setTimeout(function () { + self.navigate('') }, 300) } @@ -74,6 +78,7 @@ const _Router = Backbone.Router.extend({ Active.Topic = null }, explore: function (section, id) { + var self = this clearTimeout(this.timeoutId) // just capitalize the variable section @@ -115,17 +120,17 @@ const _Router = Backbone.Router.extend({ Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) var navigate = function () { - var path = '/explore/' + this.currentPage + var path = '/explore/' + self.currentPage // alter url if for mapper profile page - if (this.currentPage === 'mapper') { + if (self.currentPage === 'mapper') { path += '/' + Metamaps.Maps.Mapper.mapperId } - this.navigate(path) + self.navigate(path) } var navigateTimeout = function () { - this.timeoutId = setTimeout(navigate, 300) + self.timeoutId = setTimeout(navigate, 300) } if (Metamaps.Maps[capitalize].length === 0) { Metamaps.Loading.show() @@ -209,9 +214,6 @@ const _Router = Backbone.Router.extend({ }) const Router = new _Router() -Router.currentPage = '' -Router.currentSection = undefined -Router.timeoutId = undefined Router.intercept = function (evt) { var segments diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 9f800c4e..cdcda4e5 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -147,7 +147,7 @@ var Private = { message: this.$messageInput.val(), }; this.$messageInput.val(''); - $(document).trigger(chatView.events.message + '-' + this.room, [message]); + $(document).trigger(ChatView.events.message + '-' + this.room, [message]); }, addParticipant: function(participant) { var p = _.clone(participant.attributes); @@ -174,12 +174,12 @@ var Handlers = { videoToggleClick: function() { this.$videoToggle.toggleClass('active'); this.videosShowing = !this.videosShowing; - $(document).trigger(this.videosShowing ? chatView.events.videosOn : chatView.events.videosOff); + $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff); }, cursorToggleClick: function() { this.$cursorToggle.toggleClass('active'); this.cursorsShowing = !this.cursorsShowing; - $(document).trigger(this.cursorsShowing ? chatView.events.cursorsOn : chatView.events.cursorsOff); + $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff); }, soundToggleClick: function() { this.alertSound = !this.alertSound; @@ -193,10 +193,10 @@ var Handlers = { } }, inputFocus: function() { - $(document).trigger(chatView.events.inputFocus); + $(document).trigger(ChatView.events.inputFocus); }, inputBlur: function() { - $(document).trigger(chatView.events.inputBlur); + $(document).trigger(ChatView.events.inputBlur); } }; From 07e4ac386530694ff2b185bb9231dd3893ad76c3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 10:37:59 +0800 Subject: [PATCH 040/306] attempt to get npm testing working; fail --- .travis.yml | 2 +- app/assets/javascripts/lib/Autolinker.js | 2756 ----------------- .../javascripts/src/{JIT.js.erb => JIT.js} | 2 +- frontend/src/Metamaps/JIT.js | 4 +- frontend/src/Metamaps/Views/ChatView.js | 5 +- frontend/test/Metamaps.Import.spec.js | 6 +- package.json | 2 + 7 files changed, 15 insertions(+), 2762 deletions(-) delete mode 100644 app/assets/javascripts/lib/Autolinker.js rename app/assets/javascripts/src/{JIT.js.erb => JIT.js} (99%) diff --git a/.travis.yml b/.travis.yml index 28559996..99c917c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,4 +18,4 @@ before_script: - nvm use stable - npm install script: - - bundle exec rspec && npm test && bundle exec brakeman -q -z + - bundle exec rspec && bundle exec brakeman -q -z || npm test diff --git a/app/assets/javascripts/lib/Autolinker.js b/app/assets/javascripts/lib/Autolinker.js deleted file mode 100644 index 6f363d4c..00000000 --- a/app/assets/javascripts/lib/Autolinker.js +++ /dev/null @@ -1,2756 +0,0 @@ -(function (root, factory) { - if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module unless amdModuleId is set - define([], function () { - return (root['Autolinker'] = factory()); - }); - } else if (typeof exports === 'object') { - // Node. Does not work with strict CommonJS, but - // only CommonJS-like environments that support module.exports, - // like Node. - module.exports = factory(); - } else { - root['Autolinker'] = factory(); - } -}(this, function () { - -/*! - * Autolinker.js - * 0.17.1 - * - * Copyright(c) 2015 Gregory Jacobs <greg@greg-jacobs.com> - * MIT Licensed. http://www.opensource.org/licenses/mit-license.php - * - * https://github.com/gregjacobs/Autolinker.js - */ -/** - * @class Autolinker - * @extends Object - * - * Utility class used to process a given string of text, and wrap the matches in - * the appropriate anchor (<a>) tags to turn them into links. - * - * Any of the configuration options may be provided in an Object (map) provided - * to the Autolinker constructor, which will configure how the {@link #link link()} - * method will process the links. - * - * For example: - * - * var autolinker = new Autolinker( { - * newWindow : false, - * truncate : 30 - * } ); - * - * var html = autolinker.link( "Joe went to www.yahoo.com" ); - * // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' - * - * - * The {@link #static-link static link()} method may also be used to inline options into a single call, which may - * be more convenient for one-off uses. For example: - * - * var html = Autolinker.link( "Joe went to www.yahoo.com", { - * newWindow : false, - * truncate : 30 - * } ); - * // produces: 'Joe went to <a href="http://www.yahoo.com">yahoo.com</a>' - * - * - * ## Custom Replacements of Links - * - * If the configuration options do not provide enough flexibility, a {@link #replaceFn} - * may be provided to fully customize the output of Autolinker. This function is - * called once for each URL/Email/Phone#/Twitter Handle/Hashtag match that is - * encountered. - * - * For example: - * - * var input = "..."; // string with URLs, Email Addresses, Phone #s, Twitter Handles, and Hashtags - * - * var linkedText = Autolinker.link( input, { - * replaceFn : function( autolinker, match ) { - * console.log( "href = ", match.getAnchorHref() ); - * console.log( "text = ", match.getAnchorText() ); - * - * switch( match.getType() ) { - * case 'url' : - * console.log( "url: ", match.getUrl() ); - * - * if( match.getUrl().indexOf( 'mysite.com' ) === -1 ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an `Autolinker.HtmlTag` instance, which provides mutator methods for easy changes - * tag.setAttr( 'rel', 'nofollow' ); - * tag.addClass( 'external-link' ); - * - * return tag; - * - * } else { - * return true; // let Autolinker perform its normal anchor tag replacement - * } - * - * case 'email' : - * var email = match.getEmail(); - * console.log( "email: ", email ); - * - * if( email === "my@own.address" ) { - * return false; // don't auto-link this particular email address; leave as-is - * } else { - * return; // no return value will have Autolinker perform its normal anchor tag replacement (same as returning `true`) - * } - * - * case 'phone' : - * var phoneNumber = match.getPhoneNumber(); - * console.log( phoneNumber ); - * - * return '<a href="http://newplace.to.link.phone.numbers.to/">' + phoneNumber + '</a>'; - * - * case 'twitter' : - * var twitterHandle = match.getTwitterHandle(); - * console.log( twitterHandle ); - * - * return '<a href="http://newplace.to.link.twitter.handles.to/">' + twitterHandle + '</a>'; - * - * case 'hashtag' : - * var hashtag = match.getHashtag(); - * console.log( hashtag ); - * - * return '<a href="http://newplace.to.link.hashtag.handles.to/">' + hashtag + '</a>'; - * } - * } - * } ); - * - * - * The function may return the following values: - * - * - `true` (Boolean): Allow Autolinker to replace the match as it normally would. - * - `false` (Boolean): Do not replace the current match at all - leave as-is. - * - Any String: If a string is returned from the function, the string will be used directly as the replacement HTML for - * the match. - * - An {@link Autolinker.HtmlTag} instance, which can be used to build/modify an HTML tag before writing out its HTML text. - * - * @constructor - * @param {Object} [config] The configuration options for the Autolinker instance, specified in an Object (map). - */ -var Autolinker = function( cfg ) { - Autolinker.Util.assign( this, cfg ); // assign the properties of `cfg` onto the Autolinker instance. Prototype properties will be used for missing configs. - - // Validate the value of the `hashtag` cfg. - var hashtag = this.hashtag; - if( hashtag !== false && hashtag !== 'twitter' && hashtag !== 'facebook' ) { - throw new Error( "invalid `hashtag` cfg - see docs" ); - } -}; - -Autolinker.prototype = { - constructor : Autolinker, // fix constructor property - - /** - * @cfg {Boolean} urls - * - * `true` if miscellaneous URLs should be automatically linked, `false` if they should not be. - */ - urls : true, - - /** - * @cfg {Boolean} email - * - * `true` if email addresses should be automatically linked, `false` if they should not be. - */ - email : true, - - /** - * @cfg {Boolean} twitter - * - * `true` if Twitter handles ("@example") should be automatically linked, `false` if they should not be. - */ - twitter : true, - - /** - * @cfg {Boolean} phone - * - * `true` if Phone numbers ("(555)555-5555") should be automatically linked, `false` if they should not be. - */ - phone: true, - - /** - * @cfg {Boolean/String} hashtag - * - * A string for the service name to have hashtags (ex: "#myHashtag") - * auto-linked to. The currently-supported values are: - * - * - 'twitter' - * - 'facebook' - * - * Pass `false` to skip auto-linking of hashtags. - */ - hashtag : false, - - /** - * @cfg {Boolean} newWindow - * - * `true` if the links should open in a new window, `false` otherwise. - */ - newWindow : true, - - /** - * @cfg {Boolean} stripPrefix - * - * `true` if 'http://' or 'https://' and/or the 'www.' should be stripped - * from the beginning of URL links' text, `false` otherwise. - */ - stripPrefix : true, - - /** - * @cfg {Number} truncate - * - * A number for how many characters long matched text should be truncated to inside the text of - * a link. If the matched text is over this number of characters, it will be truncated to this length by - * adding a two period ellipsis ('..') to the end of the string. - * - * For example: A url like 'http://www.yahoo.com/some/long/path/to/a/file' truncated to 25 characters might look - * something like this: 'yahoo.com/some/long/pat..' - */ - truncate : undefined, - - /** - * @cfg {String} className - * - * A CSS class name to add to the generated links. This class will be added to all links, as well as this class - * plus match suffixes for styling url/email/phone/twitter/hashtag links differently. - * - * For example, if this config is provided as "myLink", then: - * - * - URL links will have the CSS classes: "myLink myLink-url" - * - Email links will have the CSS classes: "myLink myLink-email", and - * - Twitter links will have the CSS classes: "myLink myLink-twitter" - * - Phone links will have the CSS classes: "myLink myLink-phone" - * - Hashtag links will have the CSS classes: "myLink myLink-hashtag" - */ - className : "", - - /** - * @cfg {Function} replaceFn - * - * A function to individually process each match found in the input string. - * - * See the class's description for usage. - * - * This function is called with the following parameters: - * - * @cfg {Autolinker} replaceFn.autolinker The Autolinker instance, which may be used to retrieve child objects from (such - * as the instance's {@link #getTagBuilder tag builder}). - * @cfg {Autolinker.match.Match} replaceFn.match The Match instance which can be used to retrieve information about the - * match that the `replaceFn` is currently processing. See {@link Autolinker.match.Match} subclasses for details. - */ - - - /** - * @private - * @property {Autolinker.htmlParser.HtmlParser} htmlParser - * - * The HtmlParser instance used to skip over HTML tags, while finding text nodes to process. This is lazily instantiated - * in the {@link #getHtmlParser} method. - */ - htmlParser : undefined, - - /** - * @private - * @property {Autolinker.matchParser.MatchParser} matchParser - * - * The MatchParser instance used to find matches in the text nodes of an input string passed to - * {@link #link}. This is lazily instantiated in the {@link #getMatchParser} method. - */ - matchParser : undefined, - - /** - * @private - * @property {Autolinker.AnchorTagBuilder} tagBuilder - * - * The AnchorTagBuilder instance used to build match replacement anchor tags. Note: this is lazily instantiated - * in the {@link #getTagBuilder} method. - */ - tagBuilder : undefined, - - /** - * Automatically links URLs, Email addresses, Phone numbers, Twitter - * handles, and Hashtags found in the given chunk of HTML. Does not link - * URLs found within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to - * <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * This method finds the text around any HTML elements in the input - * `textOrHtml`, which will be the text that is processed. Any original HTML - * elements will be left as-is, as well as the text that is already wrapped - * in anchor (<a>) tags. - * - * @param {String} textOrHtml The HTML or text to autolink matches within - * (depending on if the {@link #urls}, {@link #email}, {@link #phone}, - * {@link #twitter}, and {@link #hashtag} options are enabled). - * @return {String} The HTML, with matches automatically linked. - */ - link : function( textOrHtml ) { - var htmlParser = this.getHtmlParser(), - htmlNodes = htmlParser.parse( textOrHtml ), - anchorTagStackCount = 0, // used to only process text around anchor tags, and any inner text/html they may have - resultHtml = []; - - for( var i = 0, len = htmlNodes.length; i < len; i++ ) { - var node = htmlNodes[ i ], - nodeType = node.getType(), - nodeText = node.getText(); - - if( nodeType === 'element' ) { - // Process HTML nodes in the input `textOrHtml` - if( node.getTagName() === 'a' ) { - if( !node.isClosing() ) { // it's the start <a> tag - anchorTagStackCount++; - } else { // it's the end </a> tag - anchorTagStackCount = Math.max( anchorTagStackCount - 1, 0 ); // attempt to handle extraneous </a> tags by making sure the stack count never goes below 0 - } - } - resultHtml.push( nodeText ); // now add the text of the tag itself verbatim - - } else if( nodeType === 'entity' || nodeType === 'comment' ) { - resultHtml.push( nodeText ); // append HTML entity nodes (such as ' ') or HTML comments (such as '<!-- Comment -->') verbatim - - } else { - // Process text nodes in the input `textOrHtml` - if( anchorTagStackCount === 0 ) { - // If we're not within an <a> tag, process the text node to linkify - var linkifiedStr = this.linkifyStr( nodeText ); - resultHtml.push( linkifiedStr ); - - } else { - // `text` is within an <a> tag, simply append the text - we do not want to autolink anything - // already within an <a>...</a> tag - resultHtml.push( nodeText ); - } - } - } - - return resultHtml.join( "" ); - }, - - /** - * Process the text that lies in between HTML tags, performing the anchor - * tag replacements for the matches, and returns the string with the - * replacements made. - * - * This method does the actual wrapping of matches with anchor tags. - * - * @private - * @param {String} str The string of text to auto-link. - * @return {String} The text with anchor tags auto-filled. - */ - linkifyStr : function( str ) { - return this.getMatchParser().replace( str, this.createMatchReturnVal, this ); - }, - - - /** - * Creates the return string value for a given match in the input string, - * for the {@link #linkifyStr} method. - * - * This method handles the {@link #replaceFn}, if one was provided. - * - * @private - * @param {Autolinker.match.Match} match The Match object that represents the match. - * @return {String} The string that the `match` should be replaced with. This is usually the anchor tag string, but - * may be the `matchStr` itself if the match is not to be replaced. - */ - createMatchReturnVal : function( match ) { - // Handle a custom `replaceFn` being provided - var replaceFnResult; - if( this.replaceFn ) { - replaceFnResult = this.replaceFn.call( this, this, match ); // Autolinker instance is the context, and the first arg - } - - if( typeof replaceFnResult === 'string' ) { - return replaceFnResult; // `replaceFn` returned a string, use that - - } else if( replaceFnResult === false ) { - return match.getMatchedText(); // no replacement for the match - - } else if( replaceFnResult instanceof Autolinker.HtmlTag ) { - return replaceFnResult.toAnchorString(); - - } else { // replaceFnResult === true, or no/unknown return value from function - // Perform Autolinker's default anchor tag generation - var tagBuilder = this.getTagBuilder(), - anchorTag = tagBuilder.build( match ); // returns an Autolinker.HtmlTag instance - - return anchorTag.toAnchorString(); - } - }, - - - /** - * Lazily instantiates and returns the {@link #htmlParser} instance for this Autolinker instance. - * - * @protected - * @return {Autolinker.htmlParser.HtmlParser} - */ - getHtmlParser : function() { - var htmlParser = this.htmlParser; - - if( !htmlParser ) { - htmlParser = this.htmlParser = new Autolinker.htmlParser.HtmlParser(); - } - - return htmlParser; - }, - - - /** - * Lazily instantiates and returns the {@link #matchParser} instance for this Autolinker instance. - * - * @protected - * @return {Autolinker.matchParser.MatchParser} - */ - getMatchParser : function() { - var matchParser = this.matchParser; - - if( !matchParser ) { - matchParser = this.matchParser = new Autolinker.matchParser.MatchParser( { - urls : this.urls, - email : this.email, - twitter : this.twitter, - phone : this.phone, - hashtag : this.hashtag, - stripPrefix : this.stripPrefix - } ); - } - - return matchParser; - }, - - - /** - * Returns the {@link #tagBuilder} instance for this Autolinker instance, lazily instantiating it - * if it does not yet exist. - * - * This method may be used in a {@link #replaceFn} to generate the {@link Autolinker.HtmlTag HtmlTag} instance that - * Autolinker would normally generate, and then allow for modifications before returning it. For example: - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a> - * - * @return {Autolinker.AnchorTagBuilder} - */ - getTagBuilder : function() { - var tagBuilder = this.tagBuilder; - - if( !tagBuilder ) { - tagBuilder = this.tagBuilder = new Autolinker.AnchorTagBuilder( { - newWindow : this.newWindow, - truncate : this.truncate, - className : this.className - } ); - } - - return tagBuilder; - } - -}; - - -/** - * Automatically links URLs, Email addresses, Phone Numbers, Twitter handles, - * and Hashtags found in the given chunk of HTML. Does not link URLs found - * within HTML tags. - * - * For instance, if given the text: `You should go to http://www.yahoo.com`, - * then the result will be `You should go to <a href="http://www.yahoo.com">http://www.yahoo.com</a>` - * - * Example: - * - * var linkedText = Autolinker.link( "Go to google.com", { newWindow: false } ); - * // Produces: "Go to <a href="http://google.com">google.com</a>" - * - * @static - * @param {String} textOrHtml The HTML or text to find matches within (depending - * on if the {@link #urls}, {@link #email}, {@link #phone}, {@link #twitter}, - * and {@link #hashtag} options are enabled). - * @param {Object} [options] Any of the configuration options for the Autolinker - * class, specified in an Object (map). See the class description for an - * example call. - * @return {String} The HTML text, with matches automatically linked. - */ -Autolinker.link = function( textOrHtml, options ) { - var autolinker = new Autolinker( options ); - return autolinker.link( textOrHtml ); -}; - - -// Autolinker Namespaces -Autolinker.match = {}; -Autolinker.htmlParser = {}; -Autolinker.matchParser = {}; - -/*global Autolinker */ -/*jshint eqnull:true, boss:true */ -/** - * @class Autolinker.Util - * @singleton - * - * A few utility methods for Autolinker. - */ -Autolinker.Util = { - - /** - * @property {Function} abstractMethod - * - * A function object which represents an abstract method. - */ - abstractMethod : function() { throw "abstract"; }, - - - /** - * @private - * @property {RegExp} trimRegex - * - * The regular expression used to trim the leading and trailing whitespace - * from a string. - */ - trimRegex : /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - - /** - * Assigns (shallow copies) the properties of `src` onto `dest`. - * - * @param {Object} dest The destination object. - * @param {Object} src The source object. - * @return {Object} The destination object (`dest`) - */ - assign : function( dest, src ) { - for( var prop in src ) { - if( src.hasOwnProperty( prop ) ) { - dest[ prop ] = src[ prop ]; - } - } - - return dest; - }, - - - /** - * Extends `superclass` to create a new subclass, adding the `protoProps` to the new subclass's prototype. - * - * @param {Function} superclass The constructor function for the superclass. - * @param {Object} protoProps The methods/properties to add to the subclass's prototype. This may contain the - * special property `constructor`, which will be used as the new subclass's constructor function. - * @return {Function} The new subclass function. - */ - extend : function( superclass, protoProps ) { - var superclassProto = superclass.prototype; - - var F = function() {}; - F.prototype = superclassProto; - - var subclass; - if( protoProps.hasOwnProperty( 'constructor' ) ) { - subclass = protoProps.constructor; - } else { - subclass = function() { superclassProto.constructor.apply( this, arguments ); }; - } - - var subclassProto = subclass.prototype = new F(); // set up prototype chain - subclassProto.constructor = subclass; // fix constructor property - subclassProto.superclass = superclassProto; - - delete protoProps.constructor; // don't re-assign constructor property to the prototype, since a new function may have been created (`subclass`), which is now already there - Autolinker.Util.assign( subclassProto, protoProps ); - - return subclass; - }, - - - /** - * Truncates the `str` at `len - ellipsisChars.length`, and adds the `ellipsisChars` to the - * end of the string (by default, two periods: '..'). If the `str` length does not exceed - * `len`, the string will be returned unchanged. - * - * @param {String} str The string to truncate and add an ellipsis to. - * @param {Number} truncateLen The length to truncate the string at. - * @param {String} [ellipsisChars=..] The ellipsis character(s) to add to the end of `str` - * when truncated. Defaults to '..' - */ - ellipsis : function( str, truncateLen, ellipsisChars ) { - if( str.length > truncateLen ) { - ellipsisChars = ( ellipsisChars == null ) ? '..' : ellipsisChars; - str = str.substring( 0, truncateLen - ellipsisChars.length ) + ellipsisChars; - } - return str; - }, - - - /** - * Supports `Array.prototype.indexOf()` functionality for old IE (IE8 and below). - * - * @param {Array} arr The array to find an element of. - * @param {*} element The element to find in the array, and return the index of. - * @return {Number} The index of the `element`, or -1 if it was not found. - */ - indexOf : function( arr, element ) { - if( Array.prototype.indexOf ) { - return arr.indexOf( element ); - - } else { - for( var i = 0, len = arr.length; i < len; i++ ) { - if( arr[ i ] === element ) return i; - } - return -1; - } - }, - - - - /** - * Performs the functionality of what modern browsers do when `String.prototype.split()` is called - * with a regular expression that contains capturing parenthesis. - * - * For example: - * - * // Modern browsers: - * "a,b,c".split( /(,)/ ); // --> [ 'a', ',', 'b', ',', 'c' ] - * - * // Old IE (including IE8): - * "a,b,c".split( /(,)/ ); // --> [ 'a', 'b', 'c' ] - * - * This method emulates the functionality of modern browsers for the old IE case. - * - * @param {String} str The string to split. - * @param {RegExp} splitRegex The regular expression to split the input `str` on. The splitting - * character(s) will be spliced into the array, as in the "modern browsers" example in the - * description of this method. - * Note #1: the supplied regular expression **must** have the 'g' flag specified. - * Note #2: for simplicity's sake, the regular expression does not need - * to contain capturing parenthesis - it will be assumed that any match has them. - * @return {String[]} The split array of strings, with the splitting character(s) included. - */ - splitAndCapture : function( str, splitRegex ) { - if( !splitRegex.global ) throw new Error( "`splitRegex` must have the 'g' flag set" ); - - var result = [], - lastIdx = 0, - match; - - while( match = splitRegex.exec( str ) ) { - result.push( str.substring( lastIdx, match.index ) ); - result.push( match[ 0 ] ); // push the splitting char(s) - - lastIdx = match.index + match[ 0 ].length; - } - result.push( str.substring( lastIdx ) ); - - return result; - }, - - - /** - * Trims the leading and trailing whitespace from a string. - * - * @param {String} str The string to trim. - * @return {String} - */ - trim : function( str ) { - return str.replace( this.trimRegex, '' ); - } - -}; -/*global Autolinker */ -/*jshint boss:true */ -/** - * @class Autolinker.HtmlTag - * @extends Object - * - * Represents an HTML tag, which can be used to easily build/modify HTML tags programmatically. - * - * Autolinker uses this abstraction to create HTML tags, and then write them out as strings. You may also use - * this class in your code, especially within a {@link Autolinker#replaceFn replaceFn}. - * - * ## Examples - * - * Example instantiation: - * - * var tag = new Autolinker.HtmlTag( { - * tagName : 'a', - * attrs : { 'href': 'http://google.com', 'class': 'external-link' }, - * innerHtml : 'Google' - * } ); - * - * tag.toAnchorString(); // <a href="http://google.com" class="external-link">Google</a> - * - * // Individual accessor methods - * tag.getTagName(); // 'a' - * tag.getAttr( 'href' ); // 'http://google.com' - * tag.hasClass( 'external-link' ); // true - * - * - * Using mutator methods (which may be used in combination with instantiation config properties): - * - * var tag = new Autolinker.HtmlTag(); - * tag.setTagName( 'a' ); - * tag.setAttr( 'href', 'http://google.com' ); - * tag.addClass( 'external-link' ); - * tag.setInnerHtml( 'Google' ); - * - * tag.getTagName(); // 'a' - * tag.getAttr( 'href' ); // 'http://google.com' - * tag.hasClass( 'external-link' ); // true - * - * tag.toAnchorString(); // <a href="http://google.com" class="external-link">Google</a> - * - * - * ## Example use within a {@link Autolinker#replaceFn replaceFn} - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance, configured with the Match's href and anchor text - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a> - * - * - * ## Example use with a new tag for the replacement - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = new Autolinker.HtmlTag( { - * tagName : 'button', - * attrs : { 'title': 'Load URL: ' + match.getAnchorHref() }, - * innerHtml : 'Load URL: ' + match.getAnchorText() - * } ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <button title="Load URL: http://google.com">Load URL: google.com</button> - */ -Autolinker.HtmlTag = Autolinker.Util.extend( Object, { - - /** - * @cfg {String} tagName - * - * The tag name. Ex: 'a', 'button', etc. - * - * Not required at instantiation time, but should be set using {@link #setTagName} before {@link #toAnchorString} - * is executed. - */ - - /** - * @cfg {Object.<String, String>} attrs - * - * An key/value Object (map) of attributes to create the tag with. The keys are the attribute names, and the - * values are the attribute values. - */ - - /** - * @cfg {String} innerHtml - * - * The inner HTML for the tag. - * - * Note the camel case name on `innerHtml`. Acronyms are camelCased in this utility (such as not to run into the acronym - * naming inconsistency that the DOM developers created with `XMLHttpRequest`). You may alternatively use {@link #innerHTML} - * if you prefer, but this one is recommended. - */ - - /** - * @cfg {String} innerHTML - * - * Alias of {@link #innerHtml}, accepted for consistency with the browser DOM api, but prefer the camelCased version - * for acronym names. - */ - - - /** - * @protected - * @property {RegExp} whitespaceRegex - * - * Regular expression used to match whitespace in a string of CSS classes. - */ - whitespaceRegex : /\s+/, - - - /** - * @constructor - * @param {Object} [cfg] The configuration properties for this class, in an Object (map) - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - - this.innerHtml = this.innerHtml || this.innerHTML; // accept either the camelCased form or the fully capitalized acronym - }, - - - /** - * Sets the tag name that will be used to generate the tag with. - * - * @param {String} tagName - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setTagName : function( tagName ) { - this.tagName = tagName; - return this; - }, - - - /** - * Retrieves the tag name. - * - * @return {String} - */ - getTagName : function() { - return this.tagName || ""; - }, - - - /** - * Sets an attribute on the HtmlTag. - * - * @param {String} attrName The attribute name to set. - * @param {String} attrValue The attribute value to set. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setAttr : function( attrName, attrValue ) { - var tagAttrs = this.getAttrs(); - tagAttrs[ attrName ] = attrValue; - - return this; - }, - - - /** - * Retrieves an attribute from the HtmlTag. If the attribute does not exist, returns `undefined`. - * - * @param {String} name The attribute name to retrieve. - * @return {String} The attribute's value, or `undefined` if it does not exist on the HtmlTag. - */ - getAttr : function( attrName ) { - return this.getAttrs()[ attrName ]; - }, - - - /** - * Sets one or more attributes on the HtmlTag. - * - * @param {Object.<String, String>} attrs A key/value Object (map) of the attributes to set. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setAttrs : function( attrs ) { - var tagAttrs = this.getAttrs(); - Autolinker.Util.assign( tagAttrs, attrs ); - - return this; - }, - - - /** - * Retrieves the attributes Object (map) for the HtmlTag. - * - * @return {Object.<String, String>} A key/value object of the attributes for the HtmlTag. - */ - getAttrs : function() { - return this.attrs || ( this.attrs = {} ); - }, - - - /** - * Sets the provided `cssClass`, overwriting any current CSS classes on the HtmlTag. - * - * @param {String} cssClass One or more space-separated CSS classes to set (overwrite). - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setClass : function( cssClass ) { - return this.setAttr( 'class', cssClass ); - }, - - - /** - * Convenience method to add one or more CSS classes to the HtmlTag. Will not add duplicate CSS classes. - * - * @param {String} cssClass One or more space-separated CSS classes to add. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - addClass : function( cssClass ) { - var classAttr = this.getClass(), - whitespaceRegex = this.whitespaceRegex, - indexOf = Autolinker.Util.indexOf, // to support IE8 and below - classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), - newClasses = cssClass.split( whitespaceRegex ), - newClass; - - while( newClass = newClasses.shift() ) { - if( indexOf( classes, newClass ) === -1 ) { - classes.push( newClass ); - } - } - - this.getAttrs()[ 'class' ] = classes.join( " " ); - return this; - }, - - - /** - * Convenience method to remove one or more CSS classes from the HtmlTag. - * - * @param {String} cssClass One or more space-separated CSS classes to remove. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - removeClass : function( cssClass ) { - var classAttr = this.getClass(), - whitespaceRegex = this.whitespaceRegex, - indexOf = Autolinker.Util.indexOf, // to support IE8 and below - classes = ( !classAttr ) ? [] : classAttr.split( whitespaceRegex ), - removeClasses = cssClass.split( whitespaceRegex ), - removeClass; - - while( classes.length && ( removeClass = removeClasses.shift() ) ) { - var idx = indexOf( classes, removeClass ); - if( idx !== -1 ) { - classes.splice( idx, 1 ); - } - } - - this.getAttrs()[ 'class' ] = classes.join( " " ); - return this; - }, - - - /** - * Convenience method to retrieve the CSS class(es) for the HtmlTag, which will each be separated by spaces when - * there are multiple. - * - * @return {String} - */ - getClass : function() { - return this.getAttrs()[ 'class' ] || ""; - }, - - - /** - * Convenience method to check if the tag has a CSS class or not. - * - * @param {String} cssClass The CSS class to check for. - * @return {Boolean} `true` if the HtmlTag has the CSS class, `false` otherwise. - */ - hasClass : function( cssClass ) { - return ( ' ' + this.getClass() + ' ' ).indexOf( ' ' + cssClass + ' ' ) !== -1; - }, - - - /** - * Sets the inner HTML for the tag. - * - * @param {String} html The inner HTML to set. - * @return {Autolinker.HtmlTag} This HtmlTag instance, so that method calls may be chained. - */ - setInnerHtml : function( html ) { - this.innerHtml = html; - - return this; - }, - - - /** - * Retrieves the inner HTML for the tag. - * - * @return {String} - */ - getInnerHtml : function() { - return this.innerHtml || ""; - }, - - - /** - * Override of superclass method used to generate the HTML string for the tag. - * - * @return {String} - */ - toAnchorString : function() { - var tagName = this.getTagName(), - attrsStr = this.buildAttrsStr(); - - attrsStr = ( attrsStr ) ? ' ' + attrsStr : ''; // prepend a space if there are actually attributes - - return [ '<', tagName, attrsStr, '>', this.getInnerHtml(), '</', tagName, '>' ].join( "" ); - }, - - - /** - * Support method for {@link #toAnchorString}, returns the string space-separated key="value" pairs, used to populate - * the stringified HtmlTag. - * - * @protected - * @return {String} Example return: `attr1="value1" attr2="value2"` - */ - buildAttrsStr : function() { - if( !this.attrs ) return ""; // no `attrs` Object (map) has been set, return empty string - - var attrs = this.getAttrs(), - attrsArr = []; - - for( var prop in attrs ) { - if( attrs.hasOwnProperty( prop ) ) { - attrsArr.push( prop + '="' + attrs[ prop ] + '"' ); - } - } - return attrsArr.join( " " ); - } - -} ); - -/*global Autolinker */ -/*jshint sub:true */ -/** - * @protected - * @class Autolinker.AnchorTagBuilder - * @extends Object - * - * Builds anchor (<a>) tags for the Autolinker utility when a match is found. - * - * Normally this class is instantiated, configured, and used internally by an {@link Autolinker} instance, but may - * actually be retrieved in a {@link Autolinker#replaceFn replaceFn} to create {@link Autolinker.HtmlTag HtmlTag} instances - * which may be modified before returning from the {@link Autolinker#replaceFn replaceFn}. For example: - * - * var html = Autolinker.link( "Test google.com", { - * replaceFn : function( autolinker, match ) { - * var tag = autolinker.getTagBuilder().build( match ); // returns an {@link Autolinker.HtmlTag} instance - * tag.setAttr( 'rel', 'nofollow' ); - * - * return tag; - * } - * } ); - * - * // generated html: - * // Test <a href="http://google.com" target="_blank" rel="nofollow">google.com</a> - */ -Autolinker.AnchorTagBuilder = Autolinker.Util.extend( Object, { - - /** - * @cfg {Boolean} newWindow - * @inheritdoc Autolinker#newWindow - */ - - /** - * @cfg {Number} truncate - * @inheritdoc Autolinker#truncate - */ - - /** - * @cfg {String} className - * @inheritdoc Autolinker#className - */ - - - /** - * @constructor - * @param {Object} [cfg] The configuration options for the AnchorTagBuilder instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - }, - - - /** - * Generates the actual anchor (<a>) tag to use in place of the - * matched text, via its `match` object. - * - * @param {Autolinker.match.Match} match The Match instance to generate an - * anchor tag from. - * @return {Autolinker.HtmlTag} The HtmlTag instance for the anchor tag. - */ - build : function( match ) { - var tag = new Autolinker.HtmlTag( { - tagName : 'a', - attrs : this.createAttrs( match.getType(), match.getAnchorHref() ), - innerHtml : this.processAnchorText( match.getAnchorText() ) - } ); - - return tag; - }, - - - /** - * Creates the Object (map) of the HTML attributes for the anchor (<a>) - * tag being generated. - * - * @protected - * @param {"url"/"email"/"phone"/"twitter"/"hashtag"} matchType The type of - * match that an anchor tag is being generated for. - * @param {String} href The href for the anchor tag. - * @return {Object} A key/value Object (map) of the anchor tag's attributes. - */ - createAttrs : function( matchType, anchorHref ) { - var attrs = { - 'href' : anchorHref // we'll always have the `href` attribute - }; - - var cssClass = this.createCssClass( matchType ); - if( cssClass ) { - attrs[ 'class' ] = cssClass; - } - if( this.newWindow ) { - attrs[ 'target' ] = "_blank"; - } - - return attrs; - }, - - - /** - * Creates the CSS class that will be used for a given anchor tag, based on - * the `matchType` and the {@link #className} config. - * - * @private - * @param {"url"/"email"/"phone"/"twitter"/"hashtag"} matchType The type of - * match that an anchor tag is being generated for. - * @return {String} The CSS class string for the link. Example return: - * "myLink myLink-url". If no {@link #className} was configured, returns - * an empty string. - */ - createCssClass : function( matchType ) { - var className = this.className; - - if( !className ) - return ""; - else - return className + " " + className + "-" + matchType; // ex: "myLink myLink-url", "myLink myLink-email", "myLink myLink-phone", "myLink myLink-twitter", or "myLink myLink-hashtag" - }, - - - /** - * Processes the `anchorText` by truncating the text according to the - * {@link #truncate} config. - * - * @private - * @param {String} anchorText The anchor tag's text (i.e. what will be - * displayed). - * @return {String} The processed `anchorText`. - */ - processAnchorText : function( anchorText ) { - anchorText = this.doTruncate( anchorText ); - - return anchorText; - }, - - - /** - * Performs the truncation of the `anchorText`, if the `anchorText` is - * longer than the {@link #truncate} option. Truncates the text to 2 - * characters fewer than the {@link #truncate} option, and adds ".." to the - * end. - * - * @private - * @param {String} text The anchor tag's text (i.e. what will be displayed). - * @return {String} The truncated anchor text. - */ - doTruncate : function( anchorText ) { - return Autolinker.Util.ellipsis( anchorText, this.truncate || Number.POSITIVE_INFINITY ); - } - -} ); -/*global Autolinker */ -/** - * @private - * @class Autolinker.htmlParser.HtmlParser - * @extends Object - * - * An HTML parser implementation which simply walks an HTML string and returns an array of - * {@link Autolinker.htmlParser.HtmlNode HtmlNodes} that represent the basic HTML structure of the input string. - * - * Autolinker uses this to only link URLs/emails/Twitter handles within text nodes, effectively ignoring / "walking - * around" HTML tags. - */ -Autolinker.htmlParser.HtmlParser = Autolinker.Util.extend( Object, { - - /** - * @private - * @property {RegExp} htmlRegex - * - * The regular expression used to pull out HTML tags from a string. Handles namespaced HTML tags and - * attribute names, as specified by http://www.w3.org/TR/html-markup/syntax.html. - * - * Capturing groups: - * - * 1. The "!DOCTYPE" tag name, if a tag is a <!DOCTYPE> tag. - * 2. If it is an end tag, this group will have the '/'. - * 3. If it is a comment tag, this group will hold the comment text (i.e. - * the text inside the `<!--` and `-->`. - * 4. The tag name for all tags (other than the <!DOCTYPE> tag) - */ - htmlRegex : (function() { - var commentTagRegex = /!--([\s\S]+?)--/, - tagNameRegex = /[0-9a-zA-Z][0-9a-zA-Z:]*/, - attrNameRegex = /[^\s\0"'>\/=\x01-\x1F\x7F]+/, // the unicode range accounts for excluding control chars, and the delete char - attrValueRegex = /(?:"[^"]*?"|'[^']*?'|[^'"=<>`\s]+)/, // double quoted, single quoted, or unquoted attribute values - nameEqualsValueRegex = attrNameRegex.source + '(?:\\s*=\\s*' + attrValueRegex.source + ')?'; // optional '=[value]' - - return new RegExp( [ - // for <!DOCTYPE> tag. Ex: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">) - '(?:', - '<(!DOCTYPE)', // *** Capturing Group 1 - If it's a doctype tag - - // Zero or more attributes following the tag name - '(?:', - '\\s+', // one or more whitespace chars before an attribute - - // Either: - // A. attr="value", or - // B. "value" alone (To cover example doctype tag: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">) - '(?:', nameEqualsValueRegex, '|', attrValueRegex.source + ')', - ')*', - '>', - ')', - - '|', - - // All other HTML tags (i.e. tags that are not <!DOCTYPE>) - '(?:', - '<(/)?', // Beginning of a tag or comment. Either '<' for a start tag, or '</' for an end tag. - // *** Capturing Group 2: The slash or an empty string. Slash ('/') for end tag, empty string for start or self-closing tag. - - '(?:', - commentTagRegex.source, // *** Capturing Group 3 - A Comment Tag's Text - - '|', - - '(?:', - - // *** Capturing Group 4 - The tag name - '(' + tagNameRegex.source + ')', - - // Zero or more attributes following the tag name - '(?:', - '\\s+', // one or more whitespace chars before an attribute - nameEqualsValueRegex, // attr="value" (with optional ="value" part) - ')*', - - '\\s*/?', // any trailing spaces and optional '/' before the closing '>' - - ')', - ')', - '>', - ')' - ].join( "" ), 'gi' ); - } )(), - - /** - * @private - * @property {RegExp} htmlCharacterEntitiesRegex - * - * The regular expression that matches common HTML character entities. - * - * Ignoring & as it could be part of a query string -- handling it separately. - */ - htmlCharacterEntitiesRegex: /( | |<|<|>|>|"|"|')/gi, - - - /** - * Parses an HTML string and returns a simple array of {@link Autolinker.htmlParser.HtmlNode HtmlNodes} - * to represent the HTML structure of the input string. - * - * @param {String} html The HTML to parse. - * @return {Autolinker.htmlParser.HtmlNode[]} - */ - parse : function( html ) { - var htmlRegex = this.htmlRegex, - currentResult, - lastIndex = 0, - textAndEntityNodes, - nodes = []; // will be the result of the method - - while( ( currentResult = htmlRegex.exec( html ) ) !== null ) { - var tagText = currentResult[ 0 ], - commentText = currentResult[ 3 ], // if we've matched a comment - tagName = currentResult[ 1 ] || currentResult[ 4 ], // The <!DOCTYPE> tag (ex: "!DOCTYPE"), or another tag (ex: "a" or "img") - isClosingTag = !!currentResult[ 2 ], - inBetweenTagsText = html.substring( lastIndex, currentResult.index ); - - // Push TextNodes and EntityNodes for any text found between tags - if( inBetweenTagsText ) { - textAndEntityNodes = this.parseTextAndEntityNodes( inBetweenTagsText ); - nodes.push.apply( nodes, textAndEntityNodes ); - } - - // Push the CommentNode or ElementNode - if( commentText ) { - nodes.push( this.createCommentNode( tagText, commentText ) ); - } else { - nodes.push( this.createElementNode( tagText, tagName, isClosingTag ) ); - } - - lastIndex = currentResult.index + tagText.length; - } - - // Process any remaining text after the last HTML element. Will process all of the text if there were no HTML elements. - if( lastIndex < html.length ) { - var text = html.substring( lastIndex ); - - // Push TextNodes and EntityNodes for any text found between tags - if( text ) { - textAndEntityNodes = this.parseTextAndEntityNodes( text ); - nodes.push.apply( nodes, textAndEntityNodes ); - } - } - - return nodes; - }, - - - /** - * Parses text and HTML entity nodes from a given string. The input string - * should not have any HTML tags (elements) within it. - * - * @private - * @param {String} text The text to parse. - * @return {Autolinker.htmlParser.HtmlNode[]} An array of HtmlNodes to - * represent the {@link Autolinker.htmlParser.TextNode TextNodes} and - * {@link Autolinker.htmlParser.EntityNode EntityNodes} found. - */ - parseTextAndEntityNodes : function( text ) { - var nodes = [], - textAndEntityTokens = Autolinker.Util.splitAndCapture( text, this.htmlCharacterEntitiesRegex ); // split at HTML entities, but include the HTML entities in the results array - - // Every even numbered token is a TextNode, and every odd numbered token is an EntityNode - // For example: an input `text` of "Test "this" today" would turn into the - // `textAndEntityTokens`: [ 'Test ', '"', 'this', '"', ' today' ] - for( var i = 0, len = textAndEntityTokens.length; i < len; i += 2 ) { - var textToken = textAndEntityTokens[ i ], - entityToken = textAndEntityTokens[ i + 1 ]; - - if( textToken ) nodes.push( this.createTextNode( textToken ) ); - if( entityToken ) nodes.push( this.createEntityNode( entityToken ) ); - } - return nodes; - }, - - - /** - * Factory method to create an {@link Autolinker.htmlParser.CommentNode CommentNode}. - * - * @private - * @param {String} tagText The full text of the tag (comment) that was - * matched, including its <!-- and -->. - * @param {String} comment The full text of the comment that was matched. - */ - createCommentNode : function( tagText, commentText ) { - return new Autolinker.htmlParser.CommentNode( { - text: tagText, - comment: Autolinker.Util.trim( commentText ) - } ); - }, - - - /** - * Factory method to create an {@link Autolinker.htmlParser.ElementNode ElementNode}. - * - * @private - * @param {String} tagText The full text of the tag (element) that was - * matched, including its attributes. - * @param {String} tagName The name of the tag. Ex: An <img> tag would - * be passed to this method as "img". - * @param {Boolean} isClosingTag `true` if it's a closing tag, false - * otherwise. - * @return {Autolinker.htmlParser.ElementNode} - */ - createElementNode : function( tagText, tagName, isClosingTag ) { - return new Autolinker.htmlParser.ElementNode( { - text : tagText, - tagName : tagName.toLowerCase(), - closing : isClosingTag - } ); - }, - - - /** - * Factory method to create a {@link Autolinker.htmlParser.EntityNode EntityNode}. - * - * @private - * @param {String} text The text that was matched for the HTML entity (such - * as '&nbsp;'). - * @return {Autolinker.htmlParser.EntityNode} - */ - createEntityNode : function( text ) { - return new Autolinker.htmlParser.EntityNode( { text: text } ); - }, - - - /** - * Factory method to create a {@link Autolinker.htmlParser.TextNode TextNode}. - * - * @private - * @param {String} text The text that was matched. - * @return {Autolinker.htmlParser.TextNode} - */ - createTextNode : function( text ) { - return new Autolinker.htmlParser.TextNode( { text: text } ); - } - -} ); -/*global Autolinker */ -/** - * @abstract - * @class Autolinker.htmlParser.HtmlNode - * - * Represents an HTML node found in an input string. An HTML node is one of the following: - * - * 1. An {@link Autolinker.htmlParser.ElementNode ElementNode}, which represents HTML tags. - * 2. A {@link Autolinker.htmlParser.TextNode TextNode}, which represents text outside or within HTML tags. - * 3. A {@link Autolinker.htmlParser.EntityNode EntityNode}, which represents one of the known HTML - * entities that Autolinker looks for. This includes common ones such as &quot; and &nbsp; - */ -Autolinker.htmlParser.HtmlNode = Autolinker.Util.extend( Object, { - - /** - * @cfg {String} text (required) - * - * The original text that was matched for the HtmlNode. - * - * - In the case of an {@link Autolinker.htmlParser.ElementNode ElementNode}, this will be the tag's - * text. - * - In the case of a {@link Autolinker.htmlParser.TextNode TextNode}, this will be the text itself. - * - In the case of a {@link Autolinker.htmlParser.EntityNode EntityNode}, this will be the text of - * the HTML entity. - */ - text : "", - - - /** - * @constructor - * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - }, - - - /** - * Returns a string name for the type of node that this class represents. - * - * @abstract - * @return {String} - */ - getType : Autolinker.Util.abstractMethod, - - - /** - * Retrieves the {@link #text} for the HtmlNode. - * - * @return {String} - */ - getText : function() { - return this.text; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.CommentNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents an HTML comment node that has been parsed by the - * {@link Autolinker.htmlParser.HtmlParser}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more - * details. - */ -Autolinker.htmlParser.CommentNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * @cfg {String} comment (required) - * - * The text inside the comment tag. This text is stripped of any leading or - * trailing whitespace. - */ - comment : '', - - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'comment'; - }, - - - /** - * Returns the comment inside the comment tag. - * - * @return {String} - */ - getComment : function() { - return this.comment; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.ElementNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents an HTML element node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. - */ -Autolinker.htmlParser.ElementNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * @cfg {String} tagName (required) - * - * The name of the tag that was matched. - */ - tagName : '', - - /** - * @cfg {Boolean} closing (required) - * - * `true` if the element (tag) is a closing tag, `false` if its an opening tag. - */ - closing : false, - - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'element'; - }, - - - /** - * Returns the HTML element's (tag's) name. Ex: for an <img> tag, returns "img". - * - * @return {String} - */ - getTagName : function() { - return this.tagName; - }, - - - /** - * Determines if the HTML element (tag) is a closing tag. Ex: <div> returns - * `false`, while </div> returns `true`. - * - * @return {Boolean} - */ - isClosing : function() { - return this.closing; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.EntityNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents a known HTML entity node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. - * Ex: '&nbsp;', or '&#160;' (which will be retrievable from the {@link #getText} method. - * - * Note that this class will only be returned from the HtmlParser for the set of checked HTML entity nodes - * defined by the {@link Autolinker.htmlParser.HtmlParser#htmlCharacterEntitiesRegex}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. - */ -Autolinker.htmlParser.EntityNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'entity'; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.htmlParser.TextNode - * @extends Autolinker.htmlParser.HtmlNode - * - * Represents a text node that has been parsed by the {@link Autolinker.htmlParser.HtmlParser}. - * - * See this class's superclass ({@link Autolinker.htmlParser.HtmlNode}) for more details. - */ -Autolinker.htmlParser.TextNode = Autolinker.Util.extend( Autolinker.htmlParser.HtmlNode, { - - /** - * Returns a string name for the type of node that this class represents. - * - * @return {String} - */ - getType : function() { - return 'text'; - } - -} ); -/*global Autolinker */ -/** - * @private - * @class Autolinker.matchParser.MatchParser - * @extends Object - * - * Used by Autolinker to parse potential matches, given an input string of text. - * - * The MatchParser is fed a non-HTML string in order to search for matches. - * Autolinker first uses the {@link Autolinker.htmlParser.HtmlParser} to "walk - * around" HTML tags, and then the text around the HTML tags is passed into the - * MatchParser in order to find the actual matches. - */ -Autolinker.matchParser.MatchParser = Autolinker.Util.extend( Object, { - - /** - * @cfg {Boolean} urls - * @inheritdoc Autolinker#urls - */ - urls : true, - - /** - * @cfg {Boolean} email - * @inheritdoc Autolinker#email - */ - email : true, - - /** - * @cfg {Boolean} twitter - * @inheritdoc Autolinker#twitter - */ - twitter : true, - - /** - * @cfg {Boolean} phone - * @inheritdoc Autolinker#phone - */ - phone: true, - - /** - * @cfg {Boolean/String} hashtag - * @inheritdoc Autolinker#hashtag - */ - hashtag : false, - - /** - * @cfg {Boolean} stripPrefix - * @inheritdoc Autolinker#stripPrefix - */ - stripPrefix : true, - - - /** - * @private - * @property {RegExp} matcherRegex - * - * The regular expression that matches URLs, email addresses, phone #s, - * Twitter handles, and Hashtags. - * - * This regular expression has the following capturing groups: - * - * 1. Group that is used to determine if there is a Twitter handle match - * (i.e. \@someTwitterUser). Simply check for its existence to determine - * if there is a Twitter handle match. The next couple of capturing - * groups give information about the Twitter handle match. - * 2. The whitespace character before the \@sign in a Twitter handle. This - * is needed because there are no lookbehinds in JS regular expressions, - * and can be used to reconstruct the original string in a replace(). - * 3. The Twitter handle itself in a Twitter match. If the match is - * '@someTwitterUser', the handle is 'someTwitterUser'. - * 4. Group that matches an email address. Used to determine if the match - * is an email address, as well as holding the full address. Ex: - * 'me@my.com' - * 5. Group that matches a URL in the input text. Ex: 'http://google.com', - * 'www.google.com', or just 'google.com'. This also includes a path, - * url parameters, or hash anchors. Ex: google.com/path/to/file?q1=1&q2=2#myAnchor - * 6. Group that matches a protocol URL (i.e. 'http://google.com'). This is - * used to match protocol URLs with just a single word, like 'http://localhost', - * where we won't double check that the domain name has at least one '.' - * in it. - * 7. A protocol-relative ('//') match for the case of a 'www.' prefixed - * URL. Will be an empty string if it is not a protocol-relative match. - * We need to know the character before the '//' in order to determine - * if it is a valid match or the // was in a string we don't want to - * auto-link. - * 8. A protocol-relative ('//') match for the case of a known TLD prefixed - * URL. Will be an empty string if it is not a protocol-relative match. - * See #6 for more info. - * 9. Group that is used to determine if there is a phone number match. The - * next 3 groups give segments of the phone number. - * 10. Group that is used to determine if there is a Hashtag match - * (i.e. \#someHashtag). Simply check for its existence to determine if - * there is a Hashtag match. The next couple of capturing groups give - * information about the Hashtag match. - * 11. The whitespace character before the #sign in a Hashtag handle. This - * is needed because there are no look-behinds in JS regular - * expressions, and can be used to reconstruct the original string in a - * replace(). - * 12. The Hashtag itself in a Hashtag match. If the match is - * '#someHashtag', the hashtag is 'someHashtag'. - */ - matcherRegex : (function() { - var twitterRegex = /(^|[^\w])@(\w{1,15})/, // For matching a twitter handle. Ex: @gregory_jacobs - - hashtagRegex = /(^|[^\w])#(\w{1,15})/, // For matching a Hashtag. Ex: #games - - emailRegex = /(?:[\-;:&=\+\$,\w\.]+@)/, // something@ for email addresses (a.k.a. local-part) - phoneRegex = /(?:\+?\d{1,3}[-\s.])?\(?\d{3}\)?[-\s.]?\d{3}[-\s.]\d{4}/, // ex: (123) 456-7890, 123 456 7890, 123-456-7890, etc. - protocolRegex = /(?:[A-Za-z][-.+A-Za-z0-9]+:(?![A-Za-z][-.+A-Za-z0-9]+:\/\/)(?!\d+\/?)(?:\/\/)?)/, // match protocol, allow in format "http://" or "mailto:". However, do not match the first part of something like 'link:http://www.google.com' (i.e. don't match "link:"). Also, make sure we don't interpret 'google.com:8000' as if 'google.com' was a protocol here (i.e. ignore a trailing port number in this regex) - wwwRegex = /(?:www\.)/, // starting with 'www.' - domainNameRegex = /[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/, // anything looking at all like a domain, non-unicode domains, not ending in a period - tldRegex = /\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/, // match our known top level domains (TLDs) - - // Allow optional path, query string, and hash anchor, not ending in the following characters: "?!:,.;" - // http://blog.codinghorror.com/the-problem-with-urls/ - urlSuffixRegex = /[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]?!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]]/; - - return new RegExp( [ - '(', // *** Capturing group $1, which can be used to check for a twitter handle match. Use group $3 for the actual twitter handle though. $2 may be used to reconstruct the original string in a replace() - // *** Capturing group $2, which matches the whitespace character before the '@' sign (needed because of no lookbehinds), and - // *** Capturing group $3, which matches the actual twitter handle - twitterRegex.source, - ')', - - '|', - - '(', // *** Capturing group $4, which is used to determine an email match - emailRegex.source, - domainNameRegex.source, - tldRegex.source, - ')', - - '|', - - '(', // *** Capturing group $5, which is used to match a URL - '(?:', // parens to cover match for protocol (optional), and domain - '(', // *** Capturing group $6, for a protocol-prefixed url (ex: http://google.com) - protocolRegex.source, - domainNameRegex.source, - ')', - - '|', - - '(?:', // non-capturing paren for a 'www.' prefixed url (ex: www.google.com) - '(.?//)?', // *** Capturing group $7 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character - wwwRegex.source, - domainNameRegex.source, - ')', - - '|', - - '(?:', // non-capturing paren for known a TLD url (ex: google.com) - '(.?//)?', // *** Capturing group $8 for an optional protocol-relative URL. Must be at the beginning of the string or start with a non-word character - domainNameRegex.source, - tldRegex.source, - ')', - ')', - - '(?:' + urlSuffixRegex.source + ')?', // match for path, query string, and/or hash anchor - optional - ')', - - '|', - - // this setup does not scale well for open extension :( Need to rethink design of autolinker... - // *** Capturing group $9, which matches a (USA for now) phone number - '(', - phoneRegex.source, - ')', - - '|', - - '(', // *** Capturing group $10, which can be used to check for a Hashtag match. Use group $12 for the actual Hashtag though. $11 may be used to reconstruct the original string in a replace() - // *** Capturing group $11, which matches the whitespace character before the '#' sign (needed because of no lookbehinds), and - // *** Capturing group $12, which matches the actual Hashtag - hashtagRegex.source, - ')' - ].join( "" ), 'gi' ); - } )(), - - /** - * @private - * @property {RegExp} charBeforeProtocolRelMatchRegex - * - * The regular expression used to retrieve the character before a - * protocol-relative URL match. - * - * This is used in conjunction with the {@link #matcherRegex}, which needs - * to grab the character before a protocol-relative '//' due to the lack of - * a negative look-behind in JavaScript regular expressions. The character - * before the match is stripped from the URL. - */ - charBeforeProtocolRelMatchRegex : /^(.)?\/\//, - - /** - * @private - * @property {Autolinker.MatchValidator} matchValidator - * - * The MatchValidator object, used to filter out any false positives from - * the {@link #matcherRegex}. See {@link Autolinker.MatchValidator} for details. - */ - - - /** - * @constructor - * @param {Object} [cfg] The configuration options for the AnchorTagBuilder - * instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - - this.matchValidator = new Autolinker.MatchValidator(); - }, - - - /** - * Parses the input `text` to search for matches, and calls the `replaceFn` - * to allow replacements of the matches. Returns the `text` with matches - * replaced. - * - * @param {String} text The text to search and repace matches in. - * @param {Function} replaceFn The iterator function to handle the - * replacements. The function takes a single argument, a {@link Autolinker.match.Match} - * object, and should return the text that should make the replacement. - * @param {Object} [contextObj=window] The context object ("scope") to run - * the `replaceFn` in. - * @return {String} - */ - replace : function( text, replaceFn, contextObj ) { - var me = this; // for closure - - return text.replace( this.matcherRegex, function( matchStr, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ) { - var matchDescObj = me.processCandidateMatch( matchStr, $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 ); // "match description" object - - // Return out with no changes for match types that are disabled (url, - // email, phone, etc.), or for matches that are invalid (false - // positives from the matcherRegex, which can't use look-behinds - // since they are unavailable in JS). - if( !matchDescObj ) { - return matchStr; - - } else { - // Generate replacement text for the match from the `replaceFn` - var replaceStr = replaceFn.call( contextObj, matchDescObj.match ); - return matchDescObj.prefixStr + replaceStr + matchDescObj.suffixStr; - } - } ); - }, - - - /** - * Processes a candidate match from the {@link #matcherRegex}. - * - * Not all matches found by the regex are actual URL/Email/Phone/Twitter/Hashtag - * matches, as determined by the {@link #matchValidator}. In this case, the - * method returns `null`. Otherwise, a valid Object with `prefixStr`, - * `match`, and `suffixStr` is returned. - * - * @private - * @param {String} matchStr The full match that was found by the - * {@link #matcherRegex}. - * @param {String} twitterMatch The matched text of a Twitter handle, if the - * match is a Twitter match. - * @param {String} twitterHandlePrefixWhitespaceChar The whitespace char - * before the @ sign in a Twitter handle match. This is needed because of - * no lookbehinds in JS regexes, and is need to re-include the character - * for the anchor tag replacement. - * @param {String} twitterHandle The actual Twitter user (i.e the word after - * the @ sign in a Twitter match). - * @param {String} emailAddressMatch The matched email address for an email - * address match. - * @param {String} urlMatch The matched URL string for a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to match something like - * 'http://localhost', where we won't double check that the domain name - * has at least one '.' in it. - * @param {String} wwwProtocolRelativeMatch The '//' for a protocol-relative - * match from a 'www' url, with the character that comes before the '//'. - * @param {String} tldProtocolRelativeMatch The '//' for a protocol-relative - * match from a TLD (top level domain) match, with the character that - * comes before the '//'. - * @param {String} phoneMatch The matched text of a phone number - * @param {String} hashtagMatch The matched text of a Twitter - * Hashtag, if the match is a Hashtag match. - * @param {String} hashtagPrefixWhitespaceChar The whitespace char - * before the # sign in a Hashtag match. This is needed because of no - * lookbehinds in JS regexes, and is need to re-include the character for - * the anchor tag replacement. - * @param {String} hashtag The actual Hashtag (i.e the word - * after the # sign in a Hashtag match). - * - * @return {Object} A "match description object". This will be `null` if the - * match was invalid, or if a match type is disabled. Otherwise, this will - * be an Object (map) with the following properties: - * @return {String} return.prefixStr The char(s) that should be prepended to - * the replacement string. These are char(s) that were needed to be - * included from the regex match that were ignored by processing code, and - * should be re-inserted into the replacement stream. - * @return {String} return.suffixStr The char(s) that should be appended to - * the replacement string. These are char(s) that were needed to be - * included from the regex match that were ignored by processing code, and - * should be re-inserted into the replacement stream. - * @return {Autolinker.match.Match} return.match The Match object that - * represents the match that was found. - */ - processCandidateMatch : function( - matchStr, twitterMatch, twitterHandlePrefixWhitespaceChar, twitterHandle, - emailAddressMatch, urlMatch, protocolUrlMatch, wwwProtocolRelativeMatch, - tldProtocolRelativeMatch, phoneMatch, hashtagMatch, - hashtagPrefixWhitespaceChar, hashtag - ) { - // Note: The `matchStr` variable wil be fixed up to remove characters that are no longer needed (which will - // be added to `prefixStr` and `suffixStr`). - - var protocolRelativeMatch = wwwProtocolRelativeMatch || tldProtocolRelativeMatch, - match, // Will be an Autolinker.match.Match object - - prefixStr = "", // A string to use to prefix the anchor tag that is created. This is needed for the Twitter and Hashtag matches. - suffixStr = ""; // A string to suffix the anchor tag that is created. This is used if there is a trailing parenthesis that should not be auto-linked. - - // Return out with `null` for match types that are disabled (url, email, - // twitter, hashtag), or for matches that are invalid (false positives - // from the matcherRegex, which can't use look-behinds since they are - // unavailable in JS). - if( - ( urlMatch && !this.urls ) || - ( emailAddressMatch && !this.email ) || - ( phoneMatch && !this.phone ) || - ( twitterMatch && !this.twitter ) || - ( hashtagMatch && !this.hashtag ) || - !this.matchValidator.isValidMatch( urlMatch, protocolUrlMatch, protocolRelativeMatch ) - ) { - return null; - } - - // Handle a closing parenthesis at the end of the match, and exclude it - // if there is not a matching open parenthesis - // in the match itself. - if( this.matchHasUnbalancedClosingParen( matchStr ) ) { - matchStr = matchStr.substr( 0, matchStr.length - 1 ); // remove the trailing ")" - suffixStr = ")"; // this will be added after the generated <a> tag - } - - if( emailAddressMatch ) { - match = new Autolinker.match.Email( { matchedText: matchStr, email: emailAddressMatch } ); - - } else if( twitterMatch ) { - // fix up the `matchStr` if there was a preceding whitespace char, - // which was needed to determine the match itself (since there are - // no look-behinds in JS regexes) - if( twitterHandlePrefixWhitespaceChar ) { - prefixStr = twitterHandlePrefixWhitespaceChar; - matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match - } - match = new Autolinker.match.Twitter( { matchedText: matchStr, twitterHandle: twitterHandle } ); - - } else if( phoneMatch ) { - // remove non-numeric values from phone number string - var cleanNumber = matchStr.replace( /\D/g, '' ); - match = new Autolinker.match.Phone( { matchedText: matchStr, number: cleanNumber } ); - - } else if( hashtagMatch ) { - // fix up the `matchStr` if there was a preceding whitespace char, - // which was needed to determine the match itself (since there are - // no look-behinds in JS regexes) - if( hashtagPrefixWhitespaceChar ) { - prefixStr = hashtagPrefixWhitespaceChar; - matchStr = matchStr.slice( 1 ); // remove the prefixed whitespace char from the match - } - match = new Autolinker.match.Hashtag( { matchedText: matchStr, serviceName: this.hashtag, hashtag: hashtag } ); - - } else { // url match - // If it's a protocol-relative '//' match, remove the character - // before the '//' (which the matcherRegex needed to match due to - // the lack of a negative look-behind in JavaScript regular - // expressions) - if( protocolRelativeMatch ) { - var charBeforeMatch = protocolRelativeMatch.match( this.charBeforeProtocolRelMatchRegex )[ 1 ] || ""; - - if( charBeforeMatch ) { // fix up the `matchStr` if there was a preceding char before a protocol-relative match, which was needed to determine the match itself (since there are no look-behinds in JS regexes) - prefixStr = charBeforeMatch; - matchStr = matchStr.slice( 1 ); // remove the prefixed char from the match - } - } - - match = new Autolinker.match.Url( { - matchedText : matchStr, - url : matchStr, - protocolUrlMatch : !!protocolUrlMatch, - protocolRelativeMatch : !!protocolRelativeMatch, - stripPrefix : this.stripPrefix - } ); - } - - return { - prefixStr : prefixStr, - suffixStr : suffixStr, - match : match - }; - }, - - - /** - * Determines if a match found has an unmatched closing parenthesis. If so, - * this parenthesis will be removed from the match itself, and appended - * after the generated anchor tag in {@link #processCandidateMatch}. - * - * A match may have an extra closing parenthesis at the end of the match - * because the regular expression must include parenthesis for URLs such as - * "wikipedia.com/something_(disambiguation)", which should be auto-linked. - * - * However, an extra parenthesis *will* be included when the URL itself is - * wrapped in parenthesis, such as in the case of "(wikipedia.com/something_(disambiguation))". - * In this case, the last closing parenthesis should *not* be part of the - * URL itself, and this method will return `true`. - * - * @private - * @param {String} matchStr The full match string from the {@link #matcherRegex}. - * @return {Boolean} `true` if there is an unbalanced closing parenthesis at - * the end of the `matchStr`, `false` otherwise. - */ - matchHasUnbalancedClosingParen : function( matchStr ) { - var lastChar = matchStr.charAt( matchStr.length - 1 ); - - if( lastChar === ')' ) { - var openParensMatch = matchStr.match( /\(/g ), - closeParensMatch = matchStr.match( /\)/g ), - numOpenParens = ( openParensMatch && openParensMatch.length ) || 0, - numCloseParens = ( closeParensMatch && closeParensMatch.length ) || 0; - - if( numOpenParens < numCloseParens ) { - return true; - } - } - - return false; - } - -} ); -/*global Autolinker */ -/*jshint scripturl:true */ -/** - * @private - * @class Autolinker.MatchValidator - * @extends Object - * - * Used by Autolinker to filter out false positives from the - * {@link Autolinker.matchParser.MatchParser#matcherRegex}. - * - * Due to the limitations of regular expressions (including the missing feature - * of look-behinds in JS regular expressions), we cannot always determine the - * validity of a given match. This class applies a bit of additional logic to - * filter out any false positives that have been matched by the - * {@link Autolinker.matchParser.MatchParser#matcherRegex}. - */ -Autolinker.MatchValidator = Autolinker.Util.extend( Object, { - - /** - * @private - * @property {RegExp} invalidProtocolRelMatchRegex - * - * The regular expression used to check a potential protocol-relative URL - * match, coming from the {@link Autolinker.matchParser.MatchParser#matcherRegex}. - * A protocol-relative URL is, for example, "//yahoo.com" - * - * This regular expression checks to see if there is a word character before - * the '//' match in order to determine if we should actually autolink a - * protocol-relative URL. This is needed because there is no negative - * look-behind in JavaScript regular expressions. - * - * For instance, we want to autolink something like "Go to: //google.com", - * but we don't want to autolink something like "abc//google.com" - */ - invalidProtocolRelMatchRegex : /^[\w]\/\//, - - /** - * Regex to test for a full protocol, with the two trailing slashes. Ex: 'http://' - * - * @private - * @property {RegExp} hasFullProtocolRegex - */ - hasFullProtocolRegex : /^[A-Za-z][-.+A-Za-z0-9]+:\/\//, - - /** - * Regex to find the URI scheme, such as 'mailto:'. - * - * This is used to filter out 'javascript:' and 'vbscript:' schemes. - * - * @private - * @property {RegExp} uriSchemeRegex - */ - uriSchemeRegex : /^[A-Za-z][-.+A-Za-z0-9]+:/, - - /** - * Regex to determine if at least one word char exists after the protocol (i.e. after the ':') - * - * @private - * @property {RegExp} hasWordCharAfterProtocolRegex - */ - hasWordCharAfterProtocolRegex : /:[^\s]*?[A-Za-z]/, - - - /** - * Determines if a given match found by the {@link Autolinker.matchParser.MatchParser} - * is valid. Will return `false` for: - * - * 1) URL matches which do not have at least have one period ('.') in the - * domain name (effectively skipping over matches like "abc:def"). - * However, URL matches with a protocol will be allowed (ex: 'http://localhost') - * 2) URL matches which do not have at least one word character in the - * domain name (effectively skipping over matches like "git:1.0"). - * 3) A protocol-relative url match (a URL beginning with '//') whose - * previous character is a word character (effectively skipping over - * strings like "abc//google.com") - * - * Otherwise, returns `true`. - * - * @param {String} urlMatch The matched URL, if there was one. Will be an - * empty string if the match is not a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to match something like - * 'http://localhost', where we won't double check that the domain name - * has at least one '.' in it. - * @param {String} protocolRelativeMatch The protocol-relative string for a - * URL match (i.e. '//'), possibly with a preceding character (ex, a - * space, such as: ' //', or a letter, such as: 'a//'). The match is - * invalid if there is a word character preceding the '//'. - * @return {Boolean} `true` if the match given is valid and should be - * processed, or `false` if the match is invalid and/or should just not be - * processed. - */ - isValidMatch : function( urlMatch, protocolUrlMatch, protocolRelativeMatch ) { - if( - ( protocolUrlMatch && !this.isValidUriScheme( protocolUrlMatch ) ) || - this.urlMatchDoesNotHaveProtocolOrDot( urlMatch, protocolUrlMatch ) || // At least one period ('.') must exist in the URL match for us to consider it an actual URL, *unless* it was a full protocol match (like 'http://localhost') - this.urlMatchDoesNotHaveAtLeastOneWordChar( urlMatch, protocolUrlMatch ) || // At least one letter character must exist in the domain name after a protocol match. Ex: skip over something like "git:1.0" - this.isInvalidProtocolRelativeMatch( protocolRelativeMatch ) // A protocol-relative match which has a word character in front of it (so we can skip something like "abc//google.com") - ) { - return false; - } - - return true; - }, - - - /** - * Determines if the URI scheme is a valid scheme to be autolinked. Returns - * `false` if the scheme is 'javascript:' or 'vbscript:' - * - * @private - * @param {String} uriSchemeMatch The match URL string for a full URI scheme - * match. Ex: 'http://yahoo.com' or 'mailto:a@a.com'. - * @return {Boolean} `true` if the scheme is a valid one, `false` otherwise. - */ - isValidUriScheme : function( uriSchemeMatch ) { - var uriScheme = uriSchemeMatch.match( this.uriSchemeRegex )[ 0 ].toLowerCase(); - - return ( uriScheme !== 'javascript:' && uriScheme !== 'vbscript:' ); - }, - - - /** - * Determines if a URL match does not have either: - * - * a) a full protocol (i.e. 'http://'), or - * b) at least one dot ('.') in the domain name (for a non-full-protocol - * match). - * - * Either situation is considered an invalid URL (ex: 'git:d' does not have - * either the '://' part, or at least one dot in the domain name. If the - * match was 'git:abc.com', we would consider this valid.) - * - * @private - * @param {String} urlMatch The matched URL, if there was one. Will be an - * empty string if the match is not a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to match something like - * 'http://localhost', where we won't double check that the domain name - * has at least one '.' in it. - * @return {Boolean} `true` if the URL match does not have a full protocol, - * or at least one dot ('.') in a non-full-protocol match. - */ - urlMatchDoesNotHaveProtocolOrDot : function( urlMatch, protocolUrlMatch ) { - return ( !!urlMatch && ( !protocolUrlMatch || !this.hasFullProtocolRegex.test( protocolUrlMatch ) ) && urlMatch.indexOf( '.' ) === -1 ); - }, - - - /** - * Determines if a URL match does not have at least one word character after - * the protocol (i.e. in the domain name). - * - * At least one letter character must exist in the domain name after a - * protocol match. Ex: skip over something like "git:1.0" - * - * @private - * @param {String} urlMatch The matched URL, if there was one. Will be an - * empty string if the match is not a URL match. - * @param {String} protocolUrlMatch The match URL string for a protocol - * match. Ex: 'http://yahoo.com'. This is used to know whether or not we - * have a protocol in the URL string, in order to check for a word - * character after the protocol separator (':'). - * @return {Boolean} `true` if the URL match does not have at least one word - * character in it after the protocol, `false` otherwise. - */ - urlMatchDoesNotHaveAtLeastOneWordChar : function( urlMatch, protocolUrlMatch ) { - if( urlMatch && protocolUrlMatch ) { - return !this.hasWordCharAfterProtocolRegex.test( urlMatch ); - } else { - return false; - } - }, - - - /** - * Determines if a protocol-relative match is an invalid one. This method - * returns `true` if there is a `protocolRelativeMatch`, and that match - * contains a word character before the '//' (i.e. it must contain - * whitespace or nothing before the '//' in order to be considered valid). - * - * @private - * @param {String} protocolRelativeMatch The protocol-relative string for a - * URL match (i.e. '//'), possibly with a preceding character (ex, a - * space, such as: ' //', or a letter, such as: 'a//'). The match is - * invalid if there is a word character preceding the '//'. - * @return {Boolean} `true` if it is an invalid protocol-relative match, - * `false` otherwise. - */ - isInvalidProtocolRelativeMatch : function( protocolRelativeMatch ) { - return ( !!protocolRelativeMatch && this.invalidProtocolRelMatchRegex.test( protocolRelativeMatch ) ); - } - -} ); -/*global Autolinker */ -/** - * @abstract - * @class Autolinker.match.Match - * - * Represents a match found in an input string which should be Autolinked. A Match object is what is provided in a - * {@link Autolinker#replaceFn replaceFn}, and may be used to query for details about the match. - * - * For example: - * - * var input = "..."; // string with URLs, Email Addresses, and Twitter Handles - * - * var linkedText = Autolinker.link( input, { - * replaceFn : function( autolinker, match ) { - * console.log( "href = ", match.getAnchorHref() ); - * console.log( "text = ", match.getAnchorText() ); - * - * switch( match.getType() ) { - * case 'url' : - * console.log( "url: ", match.getUrl() ); - * - * case 'email' : - * console.log( "email: ", match.getEmail() ); - * - * case 'twitter' : - * console.log( "twitter: ", match.getTwitterHandle() ); - * } - * } - * } ); - * - * See the {@link Autolinker} class for more details on using the {@link Autolinker#replaceFn replaceFn}. - */ -Autolinker.match.Match = Autolinker.Util.extend( Object, { - - /** - * @cfg {String} matchedText (required) - * - * The original text that was matched. - */ - - - /** - * @constructor - * @param {Object} cfg The configuration properties for the Match instance, specified in an Object (map). - */ - constructor : function( cfg ) { - Autolinker.Util.assign( this, cfg ); - }, - - - /** - * Returns a string name for the type of match that this class represents. - * - * @abstract - * @return {String} - */ - getType : Autolinker.Util.abstractMethod, - - - /** - * Returns the original text that was matched. - * - * @return {String} - */ - getMatchedText : function() { - return this.matchedText; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @abstract - * @return {String} - */ - getAnchorHref : Autolinker.Util.abstractMethod, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @abstract - * @return {String} - */ - getAnchorText : Autolinker.Util.abstractMethod - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Email - * @extends Autolinker.match.Match - * - * Represents a Email match found in an input string which should be Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more details. - */ -Autolinker.match.Email = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} email (required) - * - * The email address that was matched. - */ - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'email'; - }, - - - /** - * Returns the email address that was matched. - * - * @return {String} - */ - getEmail : function() { - return this.email; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - return 'mailto:' + this.email; - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return this.email; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Hashtag - * @extends Autolinker.match.Match - * - * Represents a Hashtag match found in an input string which should be - * Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more - * details. - */ -Autolinker.match.Hashtag = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} serviceName (required) - * - * The service to point hashtag matches to. See {@link Autolinker#hashtag} - * for available values. - */ - - /** - * @cfg {String} hashtag (required) - * - * The Hashtag that was matched, without the '#'. - */ - - - /** - * Returns the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'hashtag'; - }, - - - /** - * Returns the matched hashtag. - * - * @return {String} - */ - getHashtag : function() { - return this.hashtag; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - var serviceName = this.serviceName, - hashtag = this.hashtag; - - switch( serviceName ) { - case 'twitter' : - return 'https://twitter.com/hashtag/' + hashtag; - case 'facebook' : - return 'https://www.facebook.com/hashtag/' + hashtag; - - default : // Shouldn't happen because Autolinker's constructor should block any invalid values, but just in case. - throw new Error( 'Unknown service name to point hashtag to: ', serviceName ); - } - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return '#' + this.hashtag; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Phone - * @extends Autolinker.match.Match - * - * Represents a Phone number match found in an input string which should be - * Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more - * details. - */ -Autolinker.match.Phone = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} number (required) - * - * The phone number that was matched. - */ - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'phone'; - }, - - - /** - * Returns the phone number that was matched. - * - * @return {String} - */ - getNumber: function() { - return this.number; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - return 'tel:' + this.number; - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return this.matchedText; - } - -} ); - -/*global Autolinker */ -/** - * @class Autolinker.match.Twitter - * @extends Autolinker.match.Match - * - * Represents a Twitter match found in an input string which should be Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more details. - */ -Autolinker.match.Twitter = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} twitterHandle (required) - * - * The Twitter handle that was matched. - */ - - - /** - * Returns the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'twitter'; - }, - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getTwitterHandle : function() { - return this.twitterHandle; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - return 'https://twitter.com/' + this.twitterHandle; - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - return '@' + this.twitterHandle; - } - -} ); -/*global Autolinker */ -/** - * @class Autolinker.match.Url - * @extends Autolinker.match.Match - * - * Represents a Url match found in an input string which should be Autolinked. - * - * See this class's superclass ({@link Autolinker.match.Match}) for more details. - */ -Autolinker.match.Url = Autolinker.Util.extend( Autolinker.match.Match, { - - /** - * @cfg {String} url (required) - * - * The url that was matched. - */ - - /** - * @cfg {Boolean} protocolUrlMatch (required) - * - * `true` if the URL is a match which already has a protocol (i.e. 'http://'), `false` if the match was from a 'www' or - * known TLD match. - */ - - /** - * @cfg {Boolean} protocolRelativeMatch (required) - * - * `true` if the URL is a protocol-relative match. A protocol-relative match is a URL that starts with '//', - * and will be either http:// or https:// based on the protocol that the site is loaded under. - */ - - /** - * @cfg {Boolean} stripPrefix (required) - * @inheritdoc Autolinker#stripPrefix - */ - - - /** - * @private - * @property {RegExp} urlPrefixRegex - * - * A regular expression used to remove the 'http://' or 'https://' and/or the 'www.' from URLs. - */ - urlPrefixRegex: /^(https?:\/\/)?(www\.)?/i, - - /** - * @private - * @property {RegExp} protocolRelativeRegex - * - * The regular expression used to remove the protocol-relative '//' from the {@link #url} string, for purposes - * of {@link #getAnchorText}. A protocol-relative URL is, for example, "//yahoo.com" - */ - protocolRelativeRegex : /^\/\//, - - /** - * @private - * @property {Boolean} protocolPrepended - * - * Will be set to `true` if the 'http://' protocol has been prepended to the {@link #url} (because the - * {@link #url} did not have a protocol) - */ - protocolPrepended : false, - - - /** - * Returns a string name for the type of match that this class represents. - * - * @return {String} - */ - getType : function() { - return 'url'; - }, - - - /** - * Returns the url that was matched, assuming the protocol to be 'http://' if the original - * match was missing a protocol. - * - * @return {String} - */ - getUrl : function() { - var url = this.url; - - // if the url string doesn't begin with a protocol, assume 'http://' - if( !this.protocolRelativeMatch && !this.protocolUrlMatch && !this.protocolPrepended ) { - url = this.url = 'http://' + url; - - this.protocolPrepended = true; - } - - return url; - }, - - - /** - * Returns the anchor href that should be generated for the match. - * - * @return {String} - */ - getAnchorHref : function() { - var url = this.getUrl(); - - return url.replace( /&/g, '&' ); // any &'s in the URL should be converted back to '&' if they were displayed as & in the source html - }, - - - /** - * Returns the anchor text that should be generated for the match. - * - * @return {String} - */ - getAnchorText : function() { - var anchorText = this.getUrl(); - - if( this.protocolRelativeMatch ) { - // Strip off any protocol-relative '//' from the anchor text - anchorText = this.stripProtocolRelativePrefix( anchorText ); - } - if( this.stripPrefix ) { - anchorText = this.stripUrlPrefix( anchorText ); - } - anchorText = this.removeTrailingSlash( anchorText ); // remove trailing slash, if there is one - - return anchorText; - }, - - - // --------------------------------------- - - // Utility Functionality - - /** - * Strips the URL prefix (such as "http://" or "https://") from the given text. - * - * @private - * @param {String} text The text of the anchor that is being generated, for which to strip off the - * url prefix (such as stripping off "http://") - * @return {String} The `anchorText`, with the prefix stripped. - */ - stripUrlPrefix : function( text ) { - return text.replace( this.urlPrefixRegex, '' ); - }, - - - /** - * Strips any protocol-relative '//' from the anchor text. - * - * @private - * @param {String} text The text of the anchor that is being generated, for which to strip off the - * protocol-relative prefix (such as stripping off "//") - * @return {String} The `anchorText`, with the protocol-relative prefix stripped. - */ - stripProtocolRelativePrefix : function( text ) { - return text.replace( this.protocolRelativeRegex, '' ); - }, - - - /** - * Removes any trailing slash from the given `anchorText`, in preparation for the text to be displayed. - * - * @private - * @param {String} anchorText The text of the anchor that is being generated, for which to remove any trailing - * slash ('/') that may exist. - * @return {String} The `anchorText`, with the trailing slash removed. - */ - removeTrailingSlash : function( anchorText ) { - if( anchorText.charAt( anchorText.length - 1 ) === '/' ) { - anchorText = anchorText.slice( 0, -1 ); - } - return anchorText; - } - -} ); -return Autolinker; - -})); diff --git a/app/assets/javascripts/src/JIT.js.erb b/app/assets/javascripts/src/JIT.js similarity index 99% rename from app/assets/javascripts/src/JIT.js.erb rename to app/assets/javascripts/src/JIT.js index 2cb202dc..4754871e 100644 --- a/app/assets/javascripts/src/JIT.js.erb +++ b/app/assets/javascripts/src/JIT.js @@ -3232,7 +3232,7 @@ var Canvas; ctx = base.getCtx(), scale = base.scaleOffsetX; //var pattern = new Image(); - //pattern.src = "<%= asset_path('cubes.png') %>"; + //pattern.src = Metamaps.Erb['cubes.png'] //var ptrn = ctx.createPattern(pattern, 'repeat'); //ctx.fillStyle = ptrn; ctx.fillStyle = Metamaps.Settings.colors.background; diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 0fe5a224..ae9e9293 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,6 @@ -/* global Metamaps */ +/* global Metamaps, $jit */ + +const $jit = $jit || {} import _ from 'lodash' diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index cdcda4e5..a49aaa4d 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,10 +1,11 @@ -/* global Autolinker, $ */ +/* global $ */ import Backbone from 'backbone' +import Autolinker from 'autolinker' // TODO is this line good or bad // Backbone.$ = window.$ -var linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); +const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); var Private = { messageHTML: "<div class='chat-message'>" + diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index 68946bea..ae482c46 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -1,7 +1,11 @@ /* global describe, it */ import chai from 'chai' -import Import from '../src/Metamaps/Import' + +// JIT needs window.$jit +require('../../app/assets/javascripts/src/JIT.js') + +const Import = require('../src/Metamaps/Import') const { expect } = chai diff --git a/package.json b/package.json index 925323ac..d15d248e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ }, "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { + "autolinker": "^0.17.1", "babel-cli": "^6.11.4", "babel-loader": "^6.2.4", "babel-plugin-transform-class-properties": "^6.11.5", @@ -27,6 +28,7 @@ "chai": "^3.5.0", "jquery": "1.12.1", "mocha": "^3.0.2", + "mocha-jsdom": "^1.1.0", "node-uuid": "1.2.0", "react": "^15.3.0", "react-dom": "^15.3.0", From 700119cc9efdfd887a4fefe758c2e3eb931a9341 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 22 Sep 2016 23:04:46 -0400 Subject: [PATCH 041/306] opts can be undefined and throw error --- Gemfile.lock | 3 --- frontend/src/Metamaps/TopicCard.js | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c2fd0d28..79638ae7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -332,8 +332,5 @@ DEPENDENCIES uglifier uservoice-ruby -RUBY VERSION - ruby 2.3.0p0 - BUNDLED WITH 1.12.5 diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 7320d285..b92d7edd 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -40,7 +40,7 @@ const TopicCard = { */ showCard: function (node, opts) { var self = TopicCard - + if (!opts) opts = {} var topic = node.getData('topic') self.openTopicCard = topic From bda740491c532a907cba06750c4d50a2b3e9756f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 11:47:40 +0800 Subject: [PATCH 042/306] moved JIT to npm. tests pass. whoop whoop --- app/assets/javascripts/application.js | 1 - frontend/src/Metamaps/JIT.js | 14 +++++++++----- frontend/src/Metamaps/Organize.js | 2 ++ frontend/src/Metamaps/Topic.js | 3 +++ frontend/src/Metamaps/Visualize.js | 2 ++ .../src => frontend/src/patched}/JIT.js | 12 ++++++------ frontend/test/Metamaps.Import.spec.js | 5 +---- 7 files changed, 23 insertions(+), 16 deletions(-) rename {app/assets/javascripts/src => frontend/src/patched}/JIT.js (99%) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 68f2179b..df086157 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -14,7 +14,6 @@ //= require jquery-ui //= require jquery_ujs //= require_directory ./lib -//= require ./src/JIT //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle //= require ./src/check-canvas-support diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index ae9e9293..7145bf9c 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,9 +1,9 @@ /* global Metamaps, $jit */ -const $jit = $jit || {} - import _ from 'lodash' +import $jit from '../patched/JIT' + import Active from './Active' import Control from './Control' import Create from './Create' @@ -21,6 +21,7 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' + /* * Metamaps.Erb * Metamaps.Mappings @@ -284,7 +285,8 @@ const JIT = { ForceDirected: { animateSavedLayout: { modes: ['linear'], - transition: $jit.Trans.Quad.easeInOut, + // TODO fix tests so we don't need _.get + transition: _.get($jit, 'Trans.Quad.easeInOut'), duration: 800, onComplete: function () { Visualize.mGraph.busy = false @@ -293,7 +295,8 @@ const JIT = { }, animateFDLayout: { modes: ['linear'], - transition: $jit.Trans.Elastic.easeOut, + // TODO fix tests so we don't need _.get + transition: _.get($jit, 'Trans.Elastic.easeOut'), duration: 800, onComplete: function () { Visualize.mGraph.busy = false @@ -554,7 +557,8 @@ const JIT = { ForceDirected3D: { animate: { modes: ['linear'], - transition: $jit.Trans.Elastic.easeOut, + // TODO fix tests so we don't need _.get + transition: _.get($jit, 'Trans.Elastic.easeOut'), duration: 2500, onComplete: function () { Visualize.mGraph.busy = false diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index c05f870e..ed005d39 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -2,6 +2,8 @@ import _ from 'lodash' +import $jit from '../patched/JIT' + import Visualize from './Visualize' import JIT from './JIT' diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 412f7ef2..3e8743b6 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import $jit from '../patched/JIT' + import Active from './Active' import AutoLayout from './AutoLayout' import Create from './Create' @@ -15,6 +17,7 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' + /* * Metamaps.Topic.js.erb * diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 047cb81d..df5bab99 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -2,6 +2,8 @@ import _ from 'lodash' +import $jit from '../patched/JIT' + import Active from './Active' import JIT from './JIT' import Router from './Router' diff --git a/app/assets/javascripts/src/JIT.js b/frontend/src/patched/JIT.js similarity index 99% rename from app/assets/javascripts/src/JIT.js rename to frontend/src/patched/JIT.js index 4754871e..7814ecbe 100644 --- a/app/assets/javascripts/src/JIT.js +++ b/frontend/src/patched/JIT.js @@ -20,7 +20,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - (function () { /* File: Core.js @@ -34,7 +33,11 @@ THE SOFTWARE. This variable is the *only* global variable defined in the Toolkit. There are also other interesting properties attached to this variable described below. */ -window.$jit = function(w) { +// START METAMAPS CODE +const $jit = function(w) { +// ORIGINAL: +// window.$jit = function(w) { +// END METAMAPS CODE w = w || window; for(var k in $jit) { if($jit[k].$extend) { @@ -11312,7 +11315,4 @@ $jit.ForceDirected3D.$extend = true; })($jit.ForceDirected3D); - - - - })(); +export default $jit diff --git a/frontend/test/Metamaps.Import.spec.js b/frontend/test/Metamaps.Import.spec.js index ae482c46..c8ee33b1 100644 --- a/frontend/test/Metamaps.Import.spec.js +++ b/frontend/test/Metamaps.Import.spec.js @@ -2,10 +2,7 @@ import chai from 'chai' -// JIT needs window.$jit -require('../../app/assets/javascripts/src/JIT.js') - -const Import = require('../src/Metamaps/Import') +import Import from '../src/Metamaps/Import' const { expect } = chai From e65a5e2d1c6dc05f3703b9cf2bd7d6fa6a09d0a6 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 12:00:47 +0800 Subject: [PATCH 043/306] whoops, reenable travis npm test --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 99c917c7..d607a7a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,4 +18,4 @@ before_script: - nvm use stable - npm install script: - - bundle exec rspec && bundle exec brakeman -q -z || npm test + - bundle exec rspec && bundle exec brakeman -q -z && npm test From 6f91ce5ff52fb622a154e9fb6c2cfbab15ee4df4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 14:12:27 +0800 Subject: [PATCH 044/306] fix a few more errors --- frontend/src/Metamaps/JIT.js | 4 ++-- frontend/src/Metamaps/Map/InfoBox.js | 1 + frontend/src/Metamaps/Topic.js | 2 +- frontend/src/Metamaps/TopicCard.js | 24 ++++++++---------------- frontend/src/patched/JIT.js | 24 +++++++++++++++++++----- 5 files changed, 31 insertions(+), 24 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 7145bf9c..5eccbc6c 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -99,11 +99,11 @@ const JIT = { synapsesToRemove.push(s) } else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { - existingEdge = _.findWhere(edges, { + existingEdge = _.find(edges, { nodeFrom: edge.nodeFrom, nodeTo: edge.nodeTo }) || - _.findWhere(edges, { + _.find(edges, { nodeFrom: edge.nodeTo, nodeTo: edge.nodeFrom }) diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index a2cc5de2..ec5c1405 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -3,6 +3,7 @@ import Active from '../Active' import GlobalUI from '../GlobalUI' import Router from '../Router' +import Util from '../Util' /* * Metamaps.Collaborators diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 3e8743b6..c2f3ff29 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -191,7 +191,7 @@ const Topic = { // opts is additional options in a hash // TODO: move createNewInDB and permitCerateSYnapseAfter into opts - renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts) { + renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts = {}) { var self = Topic var nodeOnViz, tempPos diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index b92d7edd..dad58565 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -187,17 +187,9 @@ const TopicCard = { } var openMetacodeSelect = function (event) { - var windowWidth - var showcardLeft var TOPICCARD_WIDTH = 300 var METACODESELECT_WIDTH = 404 - var distanceFromEdge - var MAX_METACODELIST_HEIGHT = 270 - var windowHeight - var showcardTop - var topicTitleHeight - var distanceFromBottom if (!selectingMetacode) { selectingMetacode = true @@ -206,9 +198,9 @@ const TopicCard = { // select is accessible onscreen, when opened // while topic card is close to the right // edge of the screen - windowWidth = $(window).width() - showcardLeft = parseInt($('.showcard').css('left')) - distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH) + var windowWidth = $(window).width() + var showcardLeft = parseInt($('.showcard').css('left')) + var distanceFromEdge = windowWidth - (showcardLeft + TOPICCARD_WIDTH) if (distanceFromEdge < METACODESELECT_WIDTH) { $('.metacodeSelect').addClass('onRightEdge') } @@ -217,11 +209,11 @@ const TopicCard = { // select is accessible onscreen, when opened // while topic card is close to the bottom // edge of the screen - windowHeight = $(window).height() - showcardTop = parseInt($('.showcard').css('top')) - topicTitleHeight = $('.showcard .title').height() + parseInt($('.showcard .title').css('padding-top')) + parseInt($('.showcard .title').css('padding-bottom')) - heightOfSetList = $('.showcard .metacodeSelect').height() - distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) + 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 heightOfSetList = $('.showcard .metacodeSelect').height() + var distanceFromBottom = windowHeight - (showcardTop + topicTitleHeight) if (distanceFromBottom < MAX_METACODELIST_HEIGHT) { $('.metacodeSelect').addClass('onBottomEdge') } diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index 7814ecbe..af7311be 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -3125,9 +3125,15 @@ var Canvas; }; }, translateToCenter: function(ps) { - var size = this.getSize(), - width = ps? (size.width - ps.width - this.translateOffsetX*2) : size.width; - height = ps? (size.height - ps.height - this.translateOffsetY*2) : size.height; + // START METAMAPS CODE + var size = this.getSize(); + var width = ps ? (size.width - ps.width - this.translateOffsetX*2) : size.width; + var height = ps ? (size.height - ps.height - this.translateOffsetY*2) : size.height; + // ORIGINAL CODE + // var size = this.getSize(), + // width = ps? (size.width - ps.width - this.translateOffsetX*2) : size.width; + // height = ps? (size.height - ps.height - this.translateOffsetY*2) : size.height; + // END METAMAPS CODE var ctx = this.getCtx(); ps && ctx.scale(1/this.scaleOffsetX, 1/this.scaleOffsetY); ctx.translate(width/2, height/2); @@ -5637,7 +5643,11 @@ Graph.Op = { break; case 'fade:seq': case 'fade': case 'fade:con': - that = this; + // START METAMAPS CODE + var that = this; + // ORIGINAL CODE: + // that = this; + // END METAMAPS CODE graph = viz.construct(json); //set alpha to 0 for nodes to add. @@ -5773,7 +5783,11 @@ Graph.Op = { break; case 'fade:seq': case 'fade': case 'fade:con': - that = this; + // START METAMAPS CODE + var that = this; + // ORIGINAL CODE: + // that = this; + // END METAMAPS CODE graph = viz.construct(json); //preprocessing for nodes to delete. //get node property modes to interpolate From df84bd9e1d115c64a20d934e03a76d351aa806b4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 14:39:15 +0800 Subject: [PATCH 045/306] fix @maps serialization bug if @maps is empty, it returns {"maps":[]}, instead of [] like we expect on the frontend. This commit fixes this issue --- app/controllers/maps_controller.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 7c4a74a7..22e75819 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -19,7 +19,7 @@ class MapsController < ApplicationController redirect_to(root_url) && return if authenticated? respond_with(@maps, @user) end - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -33,7 +33,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -51,7 +51,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -69,7 +69,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -88,7 +88,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -101,7 +101,7 @@ class MapsController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end From ce1ad3e24bdd3a0c88ad356510280d58e7a046f4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 15:28:31 +0800 Subject: [PATCH 046/306] update gems --- Gemfile.lock | 107 ++++++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 52 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 79638ae7..7e2590c1 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,50 +1,50 @@ GEM remote: https://rubygems.org/ specs: - actioncable (5.0.0) - actionpack (= 5.0.0) + actioncable (5.0.0.1) + actionpack (= 5.0.0.1) nio4r (~> 1.2) websocket-driver (~> 0.6.1) - actionmailer (5.0.0) - actionpack (= 5.0.0) - actionview (= 5.0.0) - activejob (= 5.0.0) + actionmailer (5.0.0.1) + actionpack (= 5.0.0.1) + actionview (= 5.0.0.1) + activejob (= 5.0.0.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.0) - actionview (= 5.0.0) - activesupport (= 5.0.0) + actionpack (5.0.0.1) + actionview (= 5.0.0.1) + activesupport (= 5.0.0.1) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.0) - activesupport (= 5.0.0) + actionview (5.0.0.1) + activesupport (= 5.0.0.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - active_model_serializers (0.10.1) + active_model_serializers (0.10.2) actionpack (>= 4.1, < 6) activemodel (>= 4.1, < 6) jsonapi (~> 0.1.1.beta2) railties (>= 4.1, < 6) - activejob (5.0.0) - activesupport (= 5.0.0) + activejob (5.0.0.1) + activesupport (= 5.0.0.1) globalid (>= 0.3.6) - activemodel (5.0.0) - activesupport (= 5.0.0) - activerecord (5.0.0) - activemodel (= 5.0.0) - activesupport (= 5.0.0) + activemodel (5.0.0.1) + activesupport (= 5.0.0.1) + activerecord (5.0.0.1) + activemodel (= 5.0.0.1) + activesupport (= 5.0.0.1) arel (~> 7.0) - activesupport (5.0.0) + activesupport (5.0.0.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) tzinfo (~> 1.1) addressable (2.3.8) - arel (7.1.1) + arel (7.1.2) ast (2.3.0) aws-sdk (1.66.0) aws-sdk-v1 (= 1.66.0) @@ -61,7 +61,7 @@ GEM rack (>= 0.9.0) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) - brakeman (3.3.3) + brakeman (3.4.0) builder (3.2.2) byebug (9.0.5) climate_control (0.0.3) @@ -120,7 +120,7 @@ GEM jbuilder (2.6.0) activesupport (>= 3.0.0, < 5.1) multi_json (~> 1.2) - jquery-rails (4.1.1) + jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) thor (>= 0.14, < 2.0) @@ -153,15 +153,15 @@ GEM pkg-config (~> 1.1.7) oauth (0.5.1) orm_adapter (0.5.0) - paperclip (4.3.6) + paperclip (4.3.7) activemodel (>= 3.2.0) activesupport (>= 3.2.0) cocaine (~> 0.5.5) mime-types mimemagic (= 0.3.0) - parser (2.3.1.2) + parser (2.3.1.4) ast (~> 2.2) - pg (0.18.4) + pg (0.19.0) pkg-config (1.1.7) powerpack (0.1.1) pry (0.10.4) @@ -175,22 +175,22 @@ GEM pry (>= 0.9.10) pundit (1.1.0) activesupport (>= 3.0.0) - pundit_extra (0.2.0) + pundit_extra (0.3.0) rack (2.0.1) rack-cors (0.4.0) rack-test (0.6.3) rack (>= 1.0) - rails (5.0.0) - actioncable (= 5.0.0) - actionmailer (= 5.0.0) - actionpack (= 5.0.0) - actionview (= 5.0.0) - activejob (= 5.0.0) - activemodel (= 5.0.0) - activerecord (= 5.0.0) - activesupport (= 5.0.0) + rails (5.0.0.1) + actioncable (= 5.0.0.1) + actionmailer (= 5.0.0.1) + actionpack (= 5.0.0.1) + actionview (= 5.0.0.1) + activejob (= 5.0.0.1) + activemodel (= 5.0.0.1) + activerecord (= 5.0.0.1) + activesupport (= 5.0.0.1) bundler (>= 1.3.0, < 2.0) - railties (= 5.0.0) + railties (= 5.0.0.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.1) activesupport (>= 4.2.0, < 6.0) @@ -204,18 +204,18 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.0.0) - actionpack (= 5.0.0) - activesupport (= 5.0.0) + railties (5.0.0.1) + actionpack (= 5.0.0.1) + activesupport (= 5.0.0.1) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (2.1.0) - rake (11.2.2) + rake (11.3.0) redis (3.3.1) - responders (2.2.0) + responders (2.3.0) railties (>= 4.2.0, < 5.1) - rspec-core (3.5.2) + rspec-core (3.5.3) rspec-support (~> 3.5.0) rspec-expectations (3.5.0) diff-lcs (>= 1.2.0, < 2.0) @@ -223,7 +223,7 @@ GEM rspec-mocks (3.5.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.5.0) - rspec-rails (3.5.1) + rspec-rails (3.5.2) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) @@ -232,7 +232,7 @@ GEM rspec-mocks (~> 3.5.0) rspec-support (~> 3.5.0) rspec-support (3.5.0) - rubocop (0.42.0) + rubocop (0.43.0) parser (>= 2.3.1.1, < 3.0) powerpack (~> 0.1) rainbow (>= 1.99.1, < 3.0) @@ -240,7 +240,7 @@ GEM unicode-display_width (~> 1.0, >= 1.0.1) ruby-progressbar (1.8.1) sass (3.4.22) - sass-rails (5.0.5) + sass-rails (5.0.6) railties (>= 4.0.0, < 6) sass (~> 3.1) sprockets (>= 2.8, < 4.0) @@ -248,19 +248,19 @@ GEM tilt (>= 1.1, < 3) shoulda-matchers (3.1.1) activesupport (>= 4.0.0) - simplecov (0.11.2) + simplecov (0.12.0) docile (~> 1.1.0) - json (~> 1.8) + json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.0) slack-notifier (1.5.1) slop (3.6.0) snorlax (0.1.6) rails (> 4.1) - sprockets (3.6.2) + sprockets (3.7.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) - sprockets-rails (3.1.1) + sprockets-rails (3.2.0) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -270,9 +270,9 @@ GEM tunemygc (1.0.68) tzinfo (1.2.2) thread_safe (~> 0.1) - uglifier (3.0.1) + uglifier (3.0.2) execjs (>= 0.3.0, < 3) - unicode-display_width (1.1.0) + unicode-display_width (1.1.1) uservoice-ruby (0.0.11) ezcrypto (>= 0.7.2) json (>= 1.7.5) @@ -332,5 +332,8 @@ DEPENDENCIES uglifier uservoice-ruby +RUBY VERSION + ruby 2.3.0p0 + BUNDLED WITH 1.12.5 From 117b7910bf8465feb444a1399cbe15e51fa5cb74 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:40:30 +0800 Subject: [PATCH 047/306] test --- spec/controllers/maps_controller_spec.rb | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 278ec559..a10c20d1 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -8,6 +8,21 @@ RSpec.describe MapsController, type: :controller do sign_in create(:user) end + describe 'GET #activemaps' do + context 'always returns an array' do + it 'with 0 records' do + Map.delete_all + get :activemaps, format: :json + expect(JSON.parse(response.body)).to eq [] + end + it 'with 1 record' do + map = create(:map) + get :activemaps, format: :json + expect(JSON.parse(response.body).class).to be Array + end + end + end + describe 'POST #create' do context 'with valid params' do it 'creates a new Map' do From a7338f8960d0b9fe7296d38d196cff007b5e263e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:49:26 +0800 Subject: [PATCH 048/306] safer git dating --- config/initializers/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/version.rb b/config/initializers/version.rb index 94f54a37..c378cb6f 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,2 @@ METAMAPS_VERSION = "2 build `git log -1 --pretty=%H`".freeze -METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad' --date=format:'%b %d, %Y'`.freeze +METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1,2,4).join(' ').freeze From bb5ba4861d421dffb21b1aeef44269993cea6a53 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:36:47 +0800 Subject: [PATCH 049/306] [WIP] code climate config file (#654) code climate config file --- .codeclimate.yml | 32 ++++++++++++++++++++++++++++++++ .eslintignore | 3 +++ .eslintrc.js | 9 +++++++++ package.json | 7 +++++++ 4 files changed, 51 insertions(+) create mode 100644 .codeclimate.yml create mode 100644 .eslintignore create mode 100644 .eslintrc.js diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..9156645c --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,32 @@ +--- +engines: + brakeman: + enabled: true + bundler-audit: + enabled: true + duplication: + enabled: true + config: + languages: + - ruby + - javascript + eslint: + enabled: true + fixme: + enabled: true + rubocop: + enabled: true +ratings: + paths: + - 'Gemfile.lock' + - '**.erb' + - '**.rb' + - '**.js' + - '**.jsx' +exclude_paths: +- app/assets/images/ +- app/assets/javascripts/lib/ +- frontend/src/patched/ +- db/ +- script/ +- spec/ diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..86a563fd --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +**/*{.,-}min.js +frontend/src/patched/* +app/assets/javascripts/lib/* diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 00000000..bc65fe94 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,9 @@ +module.exports = { + "sourceType": "module", + "parser": "babel-eslint", + "extends": "standard", + "installedESLint": true, + "plugins": [ + "standard" + ] +}; diff --git a/package.json b/package.json index d15d248e..bb32377d 100644 --- a/package.json +++ b/package.json @@ -36,5 +36,12 @@ "socket.io": "0.9.12", "underscore": "^1.4.4", "webpack": "^1.13.1" + }, + "devDependencies": { + "babel-eslint": "^6.1.2", + "eslint": "^3.5.0", + "eslint-config-standard": "^6.0.1", + "eslint-plugin-promise": "^2.0.1", + "eslint-plugin-standard": "^2.0.0" } } From 04a302736856d15ab3503e1902c7c5d6796fb803 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:43:54 +0800 Subject: [PATCH 050/306] code climate linked to travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d607a7a5..37186702 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,3 +19,6 @@ before_script: - npm install script: - bundle exec rspec && bundle exec brakeman -q -z && npm test +addons: + code_climate: + repo_token: 479d3bf56798fbc7fff3fc8151a5ed09e8ac368fd5af332c437b9e07dbebb44e From 8255653d24ac5bcbe5a079d244e44b22e9515075 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:51:34 +0800 Subject: [PATCH 051/306] disable duplication checking in code climate for now --- .codeclimate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 9156645c..d3c19ad6 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -5,7 +5,7 @@ engines: bundler-audit: enabled: true duplication: - enabled: true + enabled: false config: languages: - ruby From c76657ecb4dd46ee289cfdbafc2d883bdb0a9204 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:54:05 +0800 Subject: [PATCH 052/306] fix restful controller style issuse --- app/controllers/api/v2/restful_controller.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index e73f21b8..de86dafd 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -147,10 +147,9 @@ module Api search_column = -> (column) { table[column].matches(safe_query) } condition = searchable_columns.reduce(nil) do |prev, column| - next search_column.(column) if prev.nil? - search_column.(column).or(prev) + next search_column.call(column) if prev.nil? + search_column.call(column).or(prev) end - puts collection.where(condition).to_sql collection.where(condition) end From b8ae2c4b6a9c7b358cf1e8510aad26054c68a02b Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 23 Sep 2016 15:45:11 -0400 Subject: [PATCH 053/306] Update Router.js --- frontend/src/Metamaps/Router.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index c5f1c9a7..073c1d1b 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -157,8 +157,6 @@ const _Router = Backbone.Router.extend({ maps: function (id) { clearTimeout(this.timeoutId) - document.title = 'Map ' + id + ' | Metamaps' - this.currentSection = 'map' this.currentPage = id From f41ece6f1c5a7e59c560518a1b57d81a31df0398 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 23 Sep 2016 15:47:37 -0400 Subject: [PATCH 054/306] Update index.js --- frontend/src/Metamaps/Map/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 3dd1c531..5de9e061 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -76,6 +76,8 @@ const Map = { var map = Active.Map var mapper = Active.Mapper + document.title = map.attributes.name + ' | Metamaps' + // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { $('.wrapper').addClass('canEditMap') From afa0cc96b9eea6b717ab21e2dd102aa8f400e477 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 23 Sep 2016 16:06:28 -0400 Subject: [PATCH 055/306] Update index.js --- frontend/src/Metamaps/Map/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 5de9e061..944a387b 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -76,7 +76,7 @@ const Map = { var map = Active.Map var mapper = Active.Mapper - document.title = map.attributes.name + ' | Metamaps' + document.title = map.get('name') + ' | Metamaps' // add class to .wrapper for specifying whether you can edit the map if (map.authorizeToEdit(mapper)) { From 0ace202ace342b389dba3e81181799bf7fdc2882 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 11:00:46 +0800 Subject: [PATCH 056/306] automatic rubocop updates --- Gemfile | 1 + Rakefile | 1 + Vagrantfile | 3 ++- .../api/v1/deprecated_controller.rb | 3 ++- app/controllers/api/v1/mappings_controller.rb | 1 + app/controllers/api/v1/maps_controller.rb | 1 + app/controllers/api/v1/synapses_controller.rb | 1 + app/controllers/api/v1/tokens_controller.rb | 1 + app/controllers/api/v1/topics_controller.rb | 1 + app/controllers/api/v2/mappings_controller.rb | 1 + app/controllers/api/v2/maps_controller.rb | 1 + app/controllers/api/v2/restful_controller.rb | 9 +++++---- app/controllers/api/v2/sessions_controller.rb | 3 ++- app/controllers/api/v2/synapses_controller.rb | 1 + app/controllers/api/v2/tokens_controller.rb | 1 + app/controllers/api/v2/topics_controller.rb | 1 + app/controllers/application_controller.rb | 8 ++++---- app/controllers/main_controller.rb | 19 ++++++++++--------- app/controllers/mappings_controller.rb | 1 + app/controllers/maps_controller.rb | 13 +++++-------- app/controllers/messages_controller.rb | 1 + app/controllers/metacode_sets_controller.rb | 1 + app/controllers/synapses_controller.rb | 1 + app/controllers/topics_controller.rb | 1 + app/controllers/users/passwords_controller.rb | 3 ++- .../users/registrations_controller.rb | 1 + app/controllers/users_controller.rb | 1 + app/helpers/application_helper.rb | 1 + app/helpers/content_helper.rb | 1 + app/helpers/devise_helper.rb | 1 + app/helpers/in_metacode_sets_helper.rb | 1 + app/helpers/main_helper.rb | 1 + app/helpers/mapping_helper.rb | 1 + app/helpers/maps_helper.rb | 3 ++- app/helpers/metacode_sets_helper.rb | 1 + app/helpers/metacodes_helper.rb | 1 + app/helpers/synapses_helper.rb | 1 + app/helpers/topics_helper.rb | 5 +++-- app/helpers/users_helper.rb | 1 + app/mailers/application_mailer.rb | 1 + app/mailers/map_mailer.rb | 1 + app/models/application_record.rb | 1 + app/models/concerns/routing.rb | 1 + app/models/event.rb | 1 + .../events/conversation_started_on_map.rb | 1 + app/models/events/new_mapping.rb | 1 + app/models/events/user_present_on_map.rb | 1 + app/models/in_metacode_set.rb | 1 + app/models/map.rb | 3 ++- app/models/mapping.rb | 1 + app/models/message.rb | 1 + app/models/metacode.rb | 1 + app/models/metacode_set.rb | 1 + app/models/permitted_params.rb | 1 + app/models/star.rb | 1 + app/models/synapse.rb | 6 ++---- app/models/token.rb | 1 + app/models/topic.rb | 6 ++---- app/models/user.rb | 7 ++++--- app/models/user_map.rb | 1 + app/models/user_preference.rb | 3 ++- app/models/webhook.rb | 1 + app/models/webhooks/slack/base.rb | 1 + .../slack/conversation_started_on_map.rb | 1 + .../webhooks/slack/synapse_added_to_map.rb | 1 + .../webhooks/slack/topic_added_to_map.rb | 1 + .../webhooks/slack/user_present_on_map.rb | 1 + app/policies/application_policy.rb | 3 ++- app/policies/main_policy.rb | 1 + app/policies/map_policy.rb | 1 + app/policies/mapping_policy.rb | 1 + app/policies/message_policy.rb | 1 + app/policies/synapse_policy.rb | 1 + app/policies/token_policy.rb | 1 + app/policies/topic_policy.rb | 1 + .../api/v2/application_serializer.rb | 1 + app/serializers/api/v2/event_serializer.rb | 1 + app/serializers/api/v2/map_serializer.rb | 15 ++++++++------- app/serializers/api/v2/mapping_serializer.rb | 11 ++++++----- app/serializers/api/v2/metacode_serializer.rb | 9 +++++---- app/serializers/api/v2/synapse_serializer.rb | 13 +++++++------ app/serializers/api/v2/token_serializer.rb | 7 ++++--- app/serializers/api/v2/topic_serializer.rb | 15 ++++++++------- app/serializers/api/v2/user_serializer.rb | 9 +++++---- app/serializers/api/v2/webhook_serializer.rb | 1 + app/services/map_export_service.rb | 1 + app/services/perm.rb | 1 + app/services/webhook_service.rb | 1 + config.ru | 1 + config/application.rb | 1 + config/boot.rb | 1 + config/environment.rb | 1 + config/environments/development.rb | 1 + config/environments/production.rb | 1 + config/environments/test.rb | 1 + config/initializers/access_codes.rb | 1 + .../initializers/active_model_serializers.rb | 1 + .../application_controller_renderer.rb | 1 + config/initializers/assets.rb | 3 ++- config/initializers/backtrace_silencers.rb | 1 + config/initializers/cookies_serializer.rb | 1 + config/initializers/cors.rb | 1 + config/initializers/devise.rb | 1 + config/initializers/doorkeeper.rb | 1 + config/initializers/exception_notification.rb | 1 + .../initializers/filter_parameter_logging.rb | 1 + config/initializers/inflections.rb | 1 + config/initializers/kaminari_config.rb | 1 + config/initializers/mime_types.rb | 1 + config/initializers/new_framework_defaults.rb | 1 + config/initializers/paperclip.rb | 1 + config/initializers/secret_token.rb | 1 + config/initializers/session_store.rb | 1 + config/initializers/uservoice.rb | 1 + config/initializers/version.rb | 5 +++-- config/initializers/wrap_parameters.rb | 1 + config/puma.rb | 7 ++++--- config/routes.rb | 1 + config/spring.rb | 1 + lib/tasks/extensions.rake | 5 +++-- lib/tasks/heroku.rake | 3 ++- lib/tasks/perms.rake | 1 + script/rails | 1 + spec/api/v2/mappings_api_spec.rb | 1 + spec/api/v2/maps_api_spec.rb | 1 + spec/api/v2/synapses_api_spec.rb | 1 + spec/api/v2/tokens_api_spec.rb | 1 + spec/api/v2/topics_api_spec.rb | 1 + spec/controllers/mappings_controller_spec.rb | 1 + spec/controllers/maps_controller_spec.rb | 1 + spec/controllers/metacodes_controller_spec.rb | 1 + spec/controllers/synapses_controller_spec.rb | 1 + spec/controllers/topics_controller_spec.rb | 1 + spec/factories/mappings.rb | 1 + spec/factories/maps.rb | 1 + spec/factories/metacodes.rb | 1 + spec/factories/synapses.rb | 3 ++- spec/factories/tokens.rb | 1 + spec/factories/topics.rb | 1 + spec/factories/users.rb | 1 + spec/mailers/previews/map_mailer_preview.rb | 1 + spec/models/map_spec.rb | 1 + spec/models/mapping_spec.rb | 1 + spec/models/metacode_spec.rb | 1 + spec/models/synapse_spec.rb | 1 + spec/models/token_spec.rb | 1 + spec/models/topic_spec.rb | 1 + spec/policies/map_policy_spec.rb | 1 + spec/policies/mapping_policy_spec.rb | 1 + spec/policies/synapse_policy.rb | 1 + spec/policies/topic_policy_spec.rb | 1 + spec/rails_helper.rb | 1 + spec/spec_helper.rb | 1 + spec/support/controller_helpers.rb | 1 + spec/support/factory_girl.rb | 1 + spec/support/pundit.rb | 1 + spec/support/schema_matcher.rb | 1 + spec/support/simplecov.rb | 2 +- 158 files changed, 239 insertions(+), 93 deletions(-) diff --git a/Gemfile b/Gemfile index 4c58772c..b4a0967b 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true source 'https://rubygems.org' ruby '2.3.0' diff --git a/Rakefile b/Rakefile index 30cf58f5..74eb42c7 100644 --- a/Rakefile +++ b/Rakefile @@ -1,4 +1,5 @@ #!/usr/bin/env rake +# frozen_string_literal: true # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. diff --git a/Vagrantfile b/Vagrantfile index 52e040cf..0fa3e8da 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,3 +1,4 @@ +# frozen_string_literal: true # -*- mode: ruby -*- # vi: set ft=ruby : @@ -31,7 +32,7 @@ sudo -u postgres psql -c "ALTER USER postgres WITH PASSWORD '3112';" SCRIPT -VAGRANTFILE_API_VERSION = '2'.freeze +VAGRANTFILE_API_VERSION = '2' Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| config.vm.box = 'trusty64' diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index ed68b897..6f6b5f15 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true module Api module V1 class DeprecatedController < ApplicationController def method_missing - render json: { error: "/api/v1 is deprecated! Please use /api/v2 instead." } + render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } end end end diff --git a/app/controllers/api/v1/mappings_controller.rb b/app/controllers/api/v1/mappings_controller.rb index 35c7d6bd..8ba6e704 100644 --- a/app/controllers/api/v1/mappings_controller.rb +++ b/app/controllers/api/v1/mappings_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class MappingsController < DeprecatedController diff --git a/app/controllers/api/v1/maps_controller.rb b/app/controllers/api/v1/maps_controller.rb index 056810f1..0ff6f472 100644 --- a/app/controllers/api/v1/maps_controller.rb +++ b/app/controllers/api/v1/maps_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class MapsController < DeprecatedController diff --git a/app/controllers/api/v1/synapses_controller.rb b/app/controllers/api/v1/synapses_controller.rb index e2111e95..32522e52 100644 --- a/app/controllers/api/v1/synapses_controller.rb +++ b/app/controllers/api/v1/synapses_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class SynapsesController < DeprecatedController diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb index c96b1065..9df2094a 100644 --- a/app/controllers/api/v1/tokens_controller.rb +++ b/app/controllers/api/v1/tokens_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class TokensController < DeprecatedController diff --git a/app/controllers/api/v1/topics_controller.rb b/app/controllers/api/v1/topics_controller.rb index e974fff3..d316bfa8 100644 --- a/app/controllers/api/v1/topics_controller.rb +++ b/app/controllers/api/v1/topics_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V1 class TopicsController < DeprecatedController diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb index 7f0d9513..86aba865 100644 --- a/app/controllers/api/v2/mappings_controller.rb +++ b/app/controllers/api/v2/mappings_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class MappingsController < RestfulController diff --git a/app/controllers/api/v2/maps_controller.rb b/app/controllers/api/v2/maps_controller.rb index fd54fa7b..0bcd9bee 100644 --- a/app/controllers/api/v2/maps_controller.rb +++ b/app/controllers/api/v2/maps_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class MapsController < RestfulController diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index de86dafd..d7a1856e 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class RestfulController < ActionController::Base @@ -101,9 +102,9 @@ module Api next_page = current_page < total_pages ? current_page + 1 : 0 base_url = request.base_url + request.path - nxt = request.query_parameters.merge(page: next_page).map{|x| x.join('=')}.join('&') - prev = request.query_parameters.merge(page: prev_page).map{|x| x.join('=')}.join('&') - last = request.query_parameters.merge(page: total_pages).map{|x| x.join('=')}.join('&') + nxt = request.query_parameters.merge(page: next_page).map { |x| x.join('=') }.join('&') + prev = request.query_parameters.merge(page: prev_page).map { |x| x.join('=') }.join('&') + last = request.query_parameters.merge(page: total_pages).map { |x| x.join('=') }.join('&') response.headers['Link'] = [ %(<#{base_url}?#{nxt}>; rel="next"), %(<#{base_url}?#{prev}>; rel="prev"), @@ -163,7 +164,7 @@ module Api builder = builder.order(sort => direction) end end - return builder + builder end def visible_records diff --git a/app/controllers/api/v2/sessions_controller.rb b/app/controllers/api/v2/sessions_controller.rb index 3aefa214..2aa93669 100644 --- a/app/controllers/api/v2/sessions_controller.rb +++ b/app/controllers/api/v2/sessions_controller.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true module Api module V2 class SessionsController < ApplicationController def create @user = User.find_by(email: params[:email]) - if @user && @user.valid_password(params[:password]) + if @user&.valid_password(params[:password]) sign_in(@user) render json: @user else diff --git a/app/controllers/api/v2/synapses_controller.rb b/app/controllers/api/v2/synapses_controller.rb index 6572997d..6484699e 100644 --- a/app/controllers/api/v2/synapses_controller.rb +++ b/app/controllers/api/v2/synapses_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class SynapsesController < RestfulController diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index 6eeb102b..d1a6b255 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class TokensController < RestfulController diff --git a/app/controllers/api/v2/topics_controller.rb b/app/controllers/api/v2/topics_controller.rb index 74fa7105..22e534ce 100644 --- a/app/controllers/api/v2/topics_controller.rb +++ b/app/controllers/api/v2/topics_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class TopicsController < RestfulController diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 58c996c5..7735c681 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationController < ActionController::Base include ApplicationHelper include Pundit @@ -60,10 +61,9 @@ class ApplicationController < ActionController::Base end def require_admin - unless authenticated? && admin? - redirect_to root_url, notice: 'You need to be an admin for that.' - return false - end + return true if authenticated? && admin? + redirect_to root_url, notice: 'You need to be an admin for that.' + false end def user diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 01304328..0d6af64b 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MainController < ApplicationController include TopicsHelper include MapsHelper @@ -12,13 +13,13 @@ class MainController < ApplicationController def home @maps = policy_scope(Map).order('updated_at DESC').page(1).per(20) respond_to do |format| - format.html { - if !authenticated? - render 'main/home' - else - render 'maps/activemaps' - end - } + format.html do + if !authenticated? + render 'main/home' + else + render 'maps/activemaps' + end + end end end @@ -163,8 +164,8 @@ class MainController < ApplicationController @synapses = [] end - #limit to 5 results - @synapses = @synapses.to_a.slice(0,5) + # limit to 5 results + @synapses = @synapses.to_a.slice(0, 5) render json: autocomplete_synapse_array_json(@synapses) end diff --git a/app/controllers/mappings_controller.rb b/app/controllers/mappings_controller.rb index 3d162c0f..de2c8ea1 100644 --- a/app/controllers/mappings_controller.rb +++ b/app/controllers/mappings_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MappingsController < ApplicationController before_action :require_user, only: [:create, :update, :destroy] after_action :verify_authorized, except: :index diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 7c4a74a7..8e7b4136 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] @@ -107,14 +108,14 @@ class MapsController < ApplicationController # GET maps/new def new - @map = Map.new(name: "Untitled Map", permission: "public", arranged: true) + @map = Map.new(name: 'Untitled Map', permission: 'public', arranged: true) authorize @map respond_to do |format| format.html do @map.user = current_user @map.save - redirect_to(map_path(@map) + '?new') + redirect_to(map_path(@map) + '?new') end end end @@ -305,9 +306,7 @@ class MapsController < ApplicationController @map = Map.find(params[:id]) authorize @map star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - if not star - star = Star.create(map_id: @map.id, user_id: current_user.id) - end + star = Star.create(map_id: @map.id, user_id: current_user.id) unless star respond_to do |format| format.json do @@ -321,9 +320,7 @@ class MapsController < ApplicationController @map = Map.find(params[:id]) authorize @map star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - if star - star.delete - end + star&.delete respond_to do |format| format.json do diff --git a/app/controllers/messages_controller.rb b/app/controllers/messages_controller.rb index ec59a2a4..dd3544e9 100644 --- a/app/controllers/messages_controller.rb +++ b/app/controllers/messages_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MessagesController < ApplicationController before_action :require_user, except: [:show] after_action :verify_authorized diff --git a/app/controllers/metacode_sets_controller.rb b/app/controllers/metacode_sets_controller.rb index a57c557f..8fec58cc 100644 --- a/app/controllers/metacode_sets_controller.rb +++ b/app/controllers/metacode_sets_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MetacodeSetsController < ApplicationController before_action :require_admin diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index ddb3e5ab..8fc31688 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class SynapsesController < ApplicationController include TopicsHelper diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 30ac57fd..1b966ca2 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class TopicsController < ApplicationController include TopicsHelper diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index ee7b8667..bffe3ab6 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Users::PasswordsController < Devise::PasswordsController protected @@ -5,7 +6,7 @@ class Users::PasswordsController < Devise::PasswordsController signed_in_root_path(resource) end - def after_sending_reset_password_instructions_path_for(resource_name) + def after_sending_reset_password_instructions_path_for(_resource_name) new_user_session_path if is_navigational_format? end end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 8895cfd2..21cd9666 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :configure_account_update_params, only: [:update] diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0ea95211..a9fff9de 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UsersController < ApplicationController before_action :require_user, only: [:edit, :update, :updatemetacodes] diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 555a32d2..57b24106 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ApplicationHelper def get_metacodeset @m = current_user.settings.metacodes diff --git a/app/helpers/content_helper.rb b/app/helpers/content_helper.rb index 4a8820f0..4fb2fe84 100644 --- a/app/helpers/content_helper.rb +++ b/app/helpers/content_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ContentHelper def resource_name :user diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index 5081cba0..4b34effc 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module DeviseHelper def devise_error_messages! resource.errors.to_a[0] diff --git a/app/helpers/in_metacode_sets_helper.rb b/app/helpers/in_metacode_sets_helper.rb index 52a47cdc..0c45fd98 100644 --- a/app/helpers/in_metacode_sets_helper.rb +++ b/app/helpers/in_metacode_sets_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module InMetacodeSetsHelper end diff --git a/app/helpers/main_helper.rb b/app/helpers/main_helper.rb index 826effed..33378f43 100644 --- a/app/helpers/main_helper.rb +++ b/app/helpers/main_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MainHelper end diff --git a/app/helpers/mapping_helper.rb b/app/helpers/mapping_helper.rb index 7055739f..9dd903ca 100644 --- a/app/helpers/mapping_helper.rb +++ b/app/helpers/mapping_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MappingHelper end diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb index 3f60fe4d..8ca7b047 100644 --- a/app/helpers/maps_helper.rb +++ b/app/helpers/maps_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module MapsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_map_array_json(maps) @@ -16,7 +17,7 @@ module MapsHelper contributorTip = '' firstContributorImage = 'https://s3.amazonaws.com/metamaps-assets/site/user.png' - if m.contributors.count > 0 + if m.contributors.count.positive? firstContributorImage = m.contributors[0].image.url(:thirtytwo) m.contributors.each_with_index do |c, _index| userImage = c.image.url(:thirtytwo) diff --git a/app/helpers/metacode_sets_helper.rb b/app/helpers/metacode_sets_helper.rb index 668ceb5c..9a6e09c7 100644 --- a/app/helpers/metacode_sets_helper.rb +++ b/app/helpers/metacode_sets_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MetacodeSetsHelper end diff --git a/app/helpers/metacodes_helper.rb b/app/helpers/metacodes_helper.rb index c896d26d..d00f1ef5 100644 --- a/app/helpers/metacodes_helper.rb +++ b/app/helpers/metacodes_helper.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true module MetacodesHelper end diff --git a/app/helpers/synapses_helper.rb b/app/helpers/synapses_helper.rb index 470292e5..471f0e05 100644 --- a/app/helpers/synapses_helper.rb +++ b/app/helpers/synapses_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module SynapsesHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_synapse_generic_json(unique) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index bb589e9a..32697db5 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module TopicsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_array_json(topics) @@ -7,7 +8,7 @@ module TopicsHelper topic['id'] = t.id topic['label'] = t.name topic['value'] = t.name - topic['description'] = t.desc ? t.desc.truncate(70) : '' # make this return matched results + topic['description'] = t.desc ? t.desc&.truncate(70) # make this return matched results topic['type'] = t.metacode.name topic['typeImageURL'] = t.metacode.icon topic['permission'] = t.permission @@ -34,7 +35,7 @@ module TopicsHelper # add the node to the array array.push(node) - return array if count == 0 + return array if count.zero? count -= 1 diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 8144dfd8..379cf20a 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module UsersHelper # build custom json autocomplete for typeahead def autocomplete_user_array_json(users) diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb index d934e218..59a2175a 100644 --- a/app/mailers/application_mailer.rb +++ b/app/mailers/application_mailer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationMailer < ActionMailer::Base default from: 'team@metamaps.cc' layout 'mailer' diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb index 94e8ebd5..e70d0b82 100644 --- a/app/mailers/map_mailer.rb +++ b/app/mailers/map_mailer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapMailer < ApplicationMailer default from: 'team@metamaps.cc' diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 10a4cba8..767a072b 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationRecord < ActiveRecord::Base self.abstract_class = true end diff --git a/app/models/concerns/routing.rb b/app/models/concerns/routing.rb index 2f8467bf..ddbfad6f 100644 --- a/app/models/concerns/routing.rb +++ b/app/models/concerns/routing.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Routing extend ActiveSupport::Concern include Rails.application.routes.url_helpers diff --git a/app/models/event.rb b/app/models/event.rb index 90407314..02c6d698 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Event < ApplicationRecord KINDS = %w(user_present_on_map conversation_started_on_map topic_added_to_map synapse_added_to_map).freeze diff --git a/app/models/events/conversation_started_on_map.rb b/app/models/events/conversation_started_on_map.rb index 4ca922be..20fb89c1 100644 --- a/app/models/events/conversation_started_on_map.rb +++ b/app/models/events/conversation_started_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Events::ConversationStartedOnMap < Event # after_create :notify_users! diff --git a/app/models/events/new_mapping.rb b/app/models/events/new_mapping.rb index d7b91576..889c69bc 100644 --- a/app/models/events/new_mapping.rb +++ b/app/models/events/new_mapping.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Events::NewMapping < Event # after_create :notify_users! diff --git a/app/models/events/user_present_on_map.rb b/app/models/events/user_present_on_map.rb index 45726002..38d524ef 100644 --- a/app/models/events/user_present_on_map.rb +++ b/app/models/events/user_present_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Events::UserPresentOnMap < Event # after_create :notify_users! diff --git a/app/models/in_metacode_set.rb b/app/models/in_metacode_set.rb index de1f2514..78dc1c29 100644 --- a/app/models/in_metacode_set.rb +++ b/app/models/in_metacode_set.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class InMetacodeSet < ApplicationRecord belongs_to :metacode, class_name: 'Metacode', foreign_key: 'metacode_id' belongs_to :metacode_set, class_name: 'MetacodeSet', foreign_key: 'metacode_set_id' diff --git a/app/models/map.rb b/app/models/map.rb index f59eb790..9c30479f 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Map < ApplicationRecord belongs_to :user @@ -19,7 +20,7 @@ class Map < ApplicationRecord thumb: ['188x126#', :png] #:full => ['940x630#', :png] }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } diff --git a/app/models/mapping.rb b/app/models/mapping.rb index eba7a6d2..f7219008 100644 --- a/app/models/mapping.rb +++ b/app/models/mapping.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Mapping < ApplicationRecord scope :topicmapping, -> { where(mappable_type: :Topic) } scope :synapsemapping, -> { where(mappable_type: :Synapse) } diff --git a/app/models/message.rb b/app/models/message.rb index 348c5d4e..682b7e51 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Message < ApplicationRecord belongs_to :user belongs_to :resource, polymorphic: true diff --git a/app/models/metacode.rb b/app/models/metacode.rb index 9b05bee5..c97c4fb5 100644 --- a/app/models/metacode.rb +++ b/app/models/metacode.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Metacode < ApplicationRecord has_many :in_metacode_sets has_many :metacode_sets, through: :in_metacode_sets diff --git a/app/models/metacode_set.rb b/app/models/metacode_set.rb index c52811fd..72bcc719 100644 --- a/app/models/metacode_set.rb +++ b/app/models/metacode_set.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MetacodeSet < ApplicationRecord belongs_to :user has_many :in_metacode_sets diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 0ccea1c8..d0696985 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class PermittedParams < Struct.new(:params) %w(map synapse topic mapping token).each do |kind| define_method(kind) do diff --git a/app/models/star.rb b/app/models/star.rb index 52a77044..dcaaa559 100644 --- a/app/models/star.rb +++ b/app/models/star.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Star < ActiveRecord::Base belongs_to :user belongs_to :map diff --git a/app/models/synapse.rb b/app/models/synapse.rb index afd40a25..c7161469 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Synapse < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' @@ -44,10 +45,7 @@ class Synapse < ApplicationRecord # :nocov: def calculated_permission if defer_to_map - defer_to_map.permission - else - permission - end + defer_to_map&.permission end # :nocov: diff --git a/app/models/token.rb b/app/models/token.rb index 9103aebc..9cd93043 100644 --- a/app/models/token.rb +++ b/app/models/token.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Token < ApplicationRecord belongs_to :user diff --git a/app/models/topic.rb b/app/models/topic.rb index c250338b..09d61897 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Topic < ApplicationRecord include TopicsHelper @@ -74,10 +75,7 @@ class Topic < ApplicationRecord def calculated_permission if defer_to_map - defer_to_map.permission - else - permission - end + defer_to_map&.permission end def as_json(_options = {}) diff --git a/app/models/user.rb b/app/models/user.rb index 876e10cd..4da66e57 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'open-uri' class User < ApplicationRecord @@ -41,7 +42,7 @@ class User < ApplicationRecord default_url: 'https://s3.amazonaws.com/metamaps-assets/site/user.png' # Validate the attached image is image/jpg, image/png, etc - validates_attachment_content_type :image, content_type: %r(\Aimage/.*\Z) + validates_attachment_content_type :image, content_type: %r{\Aimage/.*\Z} # override default as_json def as_json(_options = {}) @@ -79,8 +80,8 @@ class User < ApplicationRecord end end - def starred_map?(map) - return self.stars.where(map_id: map.id).exists? + def starred_map?(map) + stars.where(map_id: map.id).exists? end def settings diff --git a/app/models/user_map.rb b/app/models/user_map.rb index c48cfb96..dc268047 100644 --- a/app/models/user_map.rb +++ b/app/models/user_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UserMap < ApplicationRecord belongs_to :map belongs_to :user diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb index 3aadbdb3..29ca3948 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class UserPreference attr_accessor :metacodes @@ -9,7 +10,7 @@ class UserPreference array.push(metacode.id.to_s) if metacode rescue ActiveRecord::StatementInvalid if m == 'Action' - Rails.logger.warn("TODO: remove this travis workaround in user_preference.rb") + Rails.logger.warn('TODO: remove this travis workaround in user_preference.rb') end end end diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 6389398e..65057411 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhook < ApplicationRecord belongs_to :hookable, polymorphic: true diff --git a/app/models/webhooks/slack/base.rb b/app/models/webhooks/slack/base.rb index 97cf1f04..960775dd 100644 --- a/app/models/webhooks/slack/base.rb +++ b/app/models/webhooks/slack/base.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Webhooks::Slack::Base = Struct.new(:event) do include Routing diff --git a/app/models/webhooks/slack/conversation_started_on_map.rb b/app/models/webhooks/slack/conversation_started_on_map.rb index 5fa325d0..daf2270e 100644 --- a/app/models/webhooks/slack/conversation_started_on_map.rb +++ b/app/models/webhooks/slack/conversation_started_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::ConversationStartedOnMap < Webhooks::Slack::Base def text "There is a live conversation starting on map *#{event.map.name}*. #{view_map_on_metamaps('Join in!')}" diff --git a/app/models/webhooks/slack/synapse_added_to_map.rb b/app/models/webhooks/slack/synapse_added_to_map.rb index 5dc636e1..5157afa7 100644 --- a/app/models/webhooks/slack/synapse_added_to_map.rb +++ b/app/models/webhooks/slack/synapse_added_to_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::SynapseAddedToMap < Webhooks::Slack::Base def text "\"*#{eventable.mappable.topic1.name}* #{eventable.mappable.desc || '->'} *#{eventable.mappable.topic2.name}*\" was added as a connection to the map *#{view_map_on_metamaps}*" diff --git a/app/models/webhooks/slack/topic_added_to_map.rb b/app/models/webhooks/slack/topic_added_to_map.rb index 07a20759..d3a19760 100644 --- a/app/models/webhooks/slack/topic_added_to_map.rb +++ b/app/models/webhooks/slack/topic_added_to_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::TopicAddedToMap < Webhooks::Slack::Base def text "New #{eventable.mappable.metacode.name} topic *#{eventable.mappable.name}* was added to the map *#{view_map_on_metamaps}*" diff --git a/app/models/webhooks/slack/user_present_on_map.rb b/app/models/webhooks/slack/user_present_on_map.rb index 666d5121..c3185e48 100644 --- a/app/models/webhooks/slack/user_present_on_map.rb +++ b/app/models/webhooks/slack/user_present_on_map.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Webhooks::Slack::UserPresentOnMap < Webhooks::Slack::Base def text "Mapper *#{event.user.name}* has joined the map *#{event.map.name}*. #{view_map_on_metamaps('Map with them')}" diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index a9835c98..348ef5f2 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ApplicationPolicy attr_reader :user, :record @@ -39,7 +40,7 @@ class ApplicationPolicy # explicitly say they want to (E.g. seeing/editing/deleting private # maps - they should be able to, but not by accident) def admin_override - user && user.admin + user&.admin end def scope diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb index 77ab373c..e0ffc30b 100644 --- a/app/policies/main_policy.rb +++ b/app/policies/main_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MainPolicy < ApplicationPolicy def initialize(user, _record) @user = user diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 0a2b33ce..3894520c 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index 1cd99783..efcb798b 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MappingPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb index 8df6e916..f35a2895 100644 --- a/app/policies/message_policy.rb +++ b/app/policies/message_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MessagePolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index 310b3947..f9557e70 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/token_policy.rb b/app/policies/token_policy.rb index e150fec9..cd9a5ab7 100644 --- a/app/policies/token_policy.rb +++ b/app/policies/token_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class TokenPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 7bca6770..2484ee54 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index f943646c..55a9b8a0 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class ApplicationSerializer < ActiveModel::Serializer diff --git a/app/serializers/api/v2/event_serializer.rb b/app/serializers/api/v2/event_serializer.rb index 644598cf..c875056a 100644 --- a/app/serializers/api/v2/event_serializer.rb +++ b/app/serializers/api/v2/event_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class EventSerializer < ApplicationSerializer diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb index 438f97ee..0a0be2c0 100644 --- a/app/serializers/api/v2/map_serializer.rb +++ b/app/serializers/api/v2/map_serializer.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true module Api module V2 class MapSerializer < ApplicationSerializer attributes :id, - :name, - :desc, - :permission, - :screenshot, - :created_at, - :updated_at + :name, + :desc, + :permission, + :screenshot, + :created_at, + :updated_at def self.embeddable { @@ -20,7 +21,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/mapping_serializer.rb b/app/serializers/api/v2/mapping_serializer.rb index dc36421e..19e7318e 100644 --- a/app/serializers/api/v2/mapping_serializer.rb +++ b/app/serializers/api/v2/mapping_serializer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module Api module V2 class MappingSerializer < ApplicationSerializer attributes :id, - :created_at, - :updated_at, - :mappable_id, - :mappable_type + :created_at, + :updated_at, + :mappable_id, + :mappable_type attribute :xloc, if: -> { object.mappable_type == 'Topic' } attribute :yloc, if: -> { object.mappable_type == 'Topic' } @@ -17,7 +18,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/metacode_serializer.rb b/app/serializers/api/v2/metacode_serializer.rb index 4f4daa35..16013e33 100644 --- a/app/serializers/api/v2/metacode_serializer.rb +++ b/app/serializers/api/v2/metacode_serializer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module Api module V2 class MetacodeSerializer < ApplicationSerializer attributes :id, - :name, - :manual_icon, - :color, - :aws_icon + :name, + :manual_icon, + :color, + :aws_icon end end end diff --git a/app/serializers/api/v2/synapse_serializer.rb b/app/serializers/api/v2/synapse_serializer.rb index 9ef86660..f647022c 100644 --- a/app/serializers/api/v2/synapse_serializer.rb +++ b/app/serializers/api/v2/synapse_serializer.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true module Api module V2 class SynapseSerializer < ApplicationSerializer attributes :id, - :desc, - :category, - :permission, - :created_at, - :updated_at + :desc, + :category, + :permission, + :created_at, + :updated_at def self.embeddable { @@ -16,7 +17,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/token_serializer.rb b/app/serializers/api/v2/token_serializer.rb index 18d15d15..8f86757b 100644 --- a/app/serializers/api/v2/token_serializer.rb +++ b/app/serializers/api/v2/token_serializer.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true module Api module V2 class TokenSerializer < ApplicationSerializer attributes :id, - :token, - :description, - :created_at + :token, + :description, + :created_at end end end diff --git a/app/serializers/api/v2/topic_serializer.rb b/app/serializers/api/v2/topic_serializer.rb index 48d1d6de..8da46a2a 100644 --- a/app/serializers/api/v2/topic_serializer.rb +++ b/app/serializers/api/v2/topic_serializer.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true module Api module V2 class TopicSerializer < ApplicationSerializer attributes :id, - :name, - :desc, - :link, - :permission, - :created_at, - :updated_at + :name, + :desc, + :link, + :permission, + :created_at, + :updated_at def self.embeddable { @@ -16,7 +17,7 @@ module Api } end - self.class_eval do + class_eval do embed_dat end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index fdfffae0..e97bc420 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true module Api module V2 class UserSerializer < ApplicationSerializer attributes :id, - :name, - :avatar, - :is_admin, - :generation + :name, + :avatar, + :is_admin, + :generation def avatar object.image.url(:sixtyfour) diff --git a/app/serializers/api/v2/webhook_serializer.rb b/app/serializers/api/v2/webhook_serializer.rb index 59d60283..3221e450 100644 --- a/app/serializers/api/v2/webhook_serializer.rb +++ b/app/serializers/api/v2/webhook_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module Api module V2 class WebhookSerializer < ApplicationSerializer diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index c52b0802..bd256140 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MapExportService < Struct.new(:user, :map) def json # marshal_dump turns OpenStruct into a Hash diff --git a/app/services/perm.rb b/app/services/perm.rb index 99897028..57e0816f 100644 --- a/app/services/perm.rb +++ b/app/services/perm.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class Perm # e.g. Perm::ISSIONS ISSIONS = [:commons, :public, :private].freeze diff --git a/app/services/webhook_service.rb b/app/services/webhook_service.rb index 965f2c91..0efe9392 100644 --- a/app/services/webhook_service.rb +++ b/app/services/webhook_service.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class WebhookService def self.publish!(webhook:, event:) return false unless webhook.event_types.include? event.kind diff --git a/config.ru b/config.ru index 8a0f42cc..ab79c07d 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,4 @@ +# frozen_string_literal: true # This file is used by Rack-based servers to start the application. require ::File.expand_path('../config/environment', __FILE__) diff --git a/config/application.rb b/config/application.rb index b80306c5..b629682a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require_relative 'boot' require 'csv' diff --git a/config/boot.rb b/config/boot.rb index e49b6649..f17b883c 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rubygems' require 'rails/commands/server' diff --git a/config/environment.rb b/config/environment.rb index 426333bb..12ea62f8 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Load the Rails application. require_relative 'application' diff --git a/config/environments/development.rb b/config/environments/development.rb index 407a30f3..b1654921 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/config/environments/production.rb b/config/environments/production.rb index 24ceed21..f9c94af6 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/config/environments/test.rb b/config/environments/test.rb index dac060f1..5f0b1ee2 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Metamaps::Application.configure do # Settings specified here will take precedence over those in config/application.rb diff --git a/config/initializers/access_codes.rb b/config/initializers/access_codes.rb index 4a220c97..543ce6e9 100644 --- a/config/initializers/access_codes.rb +++ b/config/initializers/access_codes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true $codes = [] if ActiveRecord::Base.connection.data_source_exists? 'users' $codes = ActiveRecord::Base.connection.execute('SELECT code FROM users').map { |user| user['code'] } diff --git a/config/initializers/active_model_serializers.rb b/config/initializers/active_model_serializers.rb index aba3586b..929be340 100644 --- a/config/initializers/active_model_serializers.rb +++ b/config/initializers/active_model_serializers.rb @@ -1 +1,2 @@ +# frozen_string_literal: true ActiveModelSerializers.config.adapter = :json diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index 51639b67..315ac48a 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # ApplicationController.renderer.defaults.merge!( diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 31897cf4..4edab3b6 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. @@ -9,4 +10,4 @@ Rails.application.config.assets.quiet = true # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += %w( webpacked/metamaps.bundle.js ) +Rails.application.config.assets.precompile += %w(webpacked/metamaps.bundle.js) diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index 59385cdf..d0f0d3b5 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index f51a497e..74ea9274 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index a9a8dcff..cb46d3ef 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 8a87b3b9..a086584d 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 843fe831..33073f45 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Doorkeeper.configure do # Change the ORM that doorkeeper will use (needs plugins) orm :active_record diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb index 5423334e..db508b3c 100644 --- a/config/initializers/exception_notification.rb +++ b/config/initializers/exception_notification.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'exception_notification/rails' ExceptionNotification.configure do |config| diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e..b7fe1231 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9..aa7435fb 100644 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/kaminari_config.rb b/config/initializers/kaminari_config.rb index b1d87b01..5b9883ab 100644 --- a/config/initializers/kaminari_config.rb +++ b/config/initializers/kaminari_config.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Kaminari.configure do |config| # config.default_per_page = 25 # config.max_per_page = nil diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index c7b0c86d..5e8d015a 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb index 0706cafd..d3c12d7b 100644 --- a/config/initializers/new_framework_defaults.rb +++ b/config/initializers/new_framework_defaults.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains migration options to ease your Rails 5.0 upgrade. diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index 4a65d495..6f094b7c 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -1,2 +1,3 @@ +# frozen_string_literal: true Paperclip::Attachment.default_options[:url] = ':s3_domain_url' Paperclip::Attachment.default_options[:path] = '/:class/:attachment/:id_partition/:style/:filename' diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index e7f18911..4da6fb50 100644 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # Your secret key for verifying the integrity of signed cookies. diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index d2dc13b6..57d69156 100644 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_Metamaps_session' diff --git a/config/initializers/uservoice.rb b/config/initializers/uservoice.rb index df04eeaa..3aa65a46 100644 --- a/config/initializers/uservoice.rb +++ b/config/initializers/uservoice.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'uservoice-ruby' def current_sso_token diff --git a/config/initializers/version.rb b/config/initializers/version.rb index c378cb6f..ff08c330 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,2 +1,3 @@ -METAMAPS_VERSION = "2 build `git log -1 --pretty=%H`".freeze -METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1,2,4).join(' ').freeze +# frozen_string_literal: true +METAMAPS_VERSION = '2 build `git log -1 --pretty=%H`' +METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 36bb3e27..d65576db 100644 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Be sure to restart your server when you modify this file. # # This file contains settings for ActionController::ParamsWrapper which diff --git a/config/puma.rb b/config/puma.rb index c7f311f8..da41eff9 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true # Puma can serve each request in a thread from an internal thread pool. # The `threads` method setting takes two numbers a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum, this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }.to_i threads threads_count, threads_count # Specifies the `port` that Puma will listen on to receive requests, default is 3000. # -port ENV.fetch("PORT") { 3000 } +port ENV.fetch('PORT') { 3000 } # Specifies the `environment` that Puma will run in. # -environment ENV.fetch("RAILS_ENV") { "development" } +environment ENV.fetch('RAILS_ENV') { 'development' } # Specifies the number of `workers` to boot in clustered mode. # Workers are forked webserver processes. If using threads and workers together diff --git a/config/routes.rb b/config/routes.rb index 38fe274e..c64c188e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true Metamaps::Application.routes.draw do use_doorkeeper root to: 'main#home', via: :get diff --git a/config/spring.rb b/config/spring.rb index be72de67..b0ca9589 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true %w( .ruby-version .ruby-gemset diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake index bf07cec2..776c81e3 100644 --- a/lib/tasks/extensions.rake +++ b/lib/tasks/extensions.rake @@ -1,7 +1,8 @@ +# frozen_string_literal: true namespace :assets do task :js_compile do - system "npm install" - system "npm run build" + system 'npm install' + system 'npm run build' end end diff --git a/lib/tasks/heroku.rake b/lib/tasks/heroku.rake index a523a778..d7ce308f 100644 --- a/lib/tasks/heroku.rake +++ b/lib/tasks/heroku.rake @@ -1,9 +1,10 @@ +# frozen_string_literal: true require 'dotenv/tasks' namespace :heroku do desc 'Generate the Heroku gems manifest from gem dependencies' task gems: :dotenv do - RAILS_ENV = 'production'.freeze + RAILS_ENV = 'production' Rake::Task[:environment].invoke list = Rails.configuration.gems.collect do |g| _command, *options = g.send(:install_command) diff --git a/lib/tasks/perms.rake b/lib/tasks/perms.rake index 39cb9b27..bf087bd0 100644 --- a/lib/tasks/perms.rake +++ b/lib/tasks/perms.rake @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'dotenv/tasks' namespace :perms do diff --git a/script/rails b/script/rails index a861c543..1267847e 100644 --- a/script/rails +++ b/script/rails @@ -1,4 +1,5 @@ #!/usr/bin/env ruby.exe +# frozen_string_literal: true # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index 4a1e3298..f3ec6a75 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'mappings API', type: :request do diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb index 7356ca72..77cbc24b 100644 --- a/spec/api/v2/maps_api_spec.rb +++ b/spec/api/v2/maps_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'maps API', type: :request do diff --git a/spec/api/v2/synapses_api_spec.rb b/spec/api/v2/synapses_api_spec.rb index f232b879..c422f3bc 100644 --- a/spec/api/v2/synapses_api_spec.rb +++ b/spec/api/v2/synapses_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'synapses API', type: :request do diff --git a/spec/api/v2/tokens_api_spec.rb b/spec/api/v2/tokens_api_spec.rb index c2e480a5..cd424ba0 100644 --- a/spec/api/v2/tokens_api_spec.rb +++ b/spec/api/v2/tokens_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'tokens API', type: :request do diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb index 4781348a..9811071d 100644 --- a/spec/api/v2/topics_api_spec.rb +++ b/spec/api/v2/topics_api_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe 'topics API', type: :request do diff --git a/spec/controllers/mappings_controller_spec.rb b/spec/controllers/mappings_controller_spec.rb index bcd2b97f..8d1c424d 100644 --- a/spec/controllers/mappings_controller_spec.rb +++ b/spec/controllers/mappings_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MappingsController, type: :controller do diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 278ec559..0f053dd9 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MapsController, type: :controller do diff --git a/spec/controllers/metacodes_controller_spec.rb b/spec/controllers/metacodes_controller_spec.rb index cb4116d4..b25017b9 100644 --- a/spec/controllers/metacodes_controller_spec.rb +++ b/spec/controllers/metacodes_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MetacodesController, type: :controller do diff --git a/spec/controllers/synapses_controller_spec.rb b/spec/controllers/synapses_controller_spec.rb index 15d91250..3a5310e4 100644 --- a/spec/controllers/synapses_controller_spec.rb +++ b/spec/controllers/synapses_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe SynapsesController, type: :controller do diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 315b931f..0d7e3010 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe TopicsController, type: :controller do diff --git a/spec/factories/mappings.rb b/spec/factories/mappings.rb index bed0b754..1bcdf891 100644 --- a/spec/factories/mappings.rb +++ b/spec/factories/mappings.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :mapping do xloc 0 diff --git a/spec/factories/maps.rb b/spec/factories/maps.rb index 14450c00..a95590e4 100644 --- a/spec/factories/maps.rb +++ b/spec/factories/maps.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :map do sequence(:name) { |n| "Cool Map ##{n}" } diff --git a/spec/factories/metacodes.rb b/spec/factories/metacodes.rb index 543e4955..2ed71beb 100644 --- a/spec/factories/metacodes.rb +++ b/spec/factories/metacodes.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :metacode do sequence(:name) { |n| "Cool Metacode ##{n}" } diff --git a/spec/factories/synapses.rb b/spec/factories/synapses.rb index db82fc39..6af8ca9e 100644 --- a/spec/factories/synapses.rb +++ b/spec/factories/synapses.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :synapse do sequence(:desc) { |n| "Cool synapse ##{n}" } @@ -6,6 +7,6 @@ FactoryGirl.define do association :topic1, factory: :topic association :topic2, factory: :topic user - weight 1 # todo drop this column + weight 1 # TODO: drop this column end end diff --git a/spec/factories/tokens.rb b/spec/factories/tokens.rb index 3970d76f..6d5f110b 100644 --- a/spec/factories/tokens.rb +++ b/spec/factories/tokens.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :token do user diff --git a/spec/factories/topics.rb b/spec/factories/topics.rb index f4c73f4c..a6048d7c 100644 --- a/spec/factories/topics.rb +++ b/spec/factories/topics.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true FactoryGirl.define do factory :topic do user diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 746f52b1..b5b20b9a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # # This file supports three factories, because code and joinedwithcode # make things complicated! diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb index 60310bf4..96d07c07 100644 --- a/spec/mailers/previews/map_mailer_preview.rb +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # Preview all emails at http://localhost:3000/rails/mailers/map_mailer class MapMailerPreview < ActionMailer::Preview def invite_to_edit_email diff --git a/spec/models/map_spec.rb b/spec/models/map_spec.rb index e70429be..3f3089cf 100644 --- a/spec/models/map_spec.rb +++ b/spec/models/map_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Map, type: :model do diff --git a/spec/models/mapping_spec.rb b/spec/models/mapping_spec.rb index 54c72b88..343c19ee 100644 --- a/spec/models/mapping_spec.rb +++ b/spec/models/mapping_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Mapping, type: :model do diff --git a/spec/models/metacode_spec.rb b/spec/models/metacode_spec.rb index c9b34527..49354898 100644 --- a/spec/models/metacode_spec.rb +++ b/spec/models/metacode_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Metacode, type: :model do diff --git a/spec/models/synapse_spec.rb b/spec/models/synapse_spec.rb index 3bdb1ac4..c5b63a41 100644 --- a/spec/models/synapse_spec.rb +++ b/spec/models/synapse_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Synapse, type: :model do diff --git a/spec/models/token_spec.rb b/spec/models/token_spec.rb index 50e89c02..82582218 100644 --- a/spec/models/token_spec.rb +++ b/spec/models/token_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Token, type: :model do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index dbaac86d..50e6d74b 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe Topic, type: :model do diff --git a/spec/policies/map_policy_spec.rb b/spec/policies/map_policy_spec.rb index 7dd33707..c08432dd 100644 --- a/spec/policies/map_policy_spec.rb +++ b/spec/policies/map_policy_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MapPolicy, type: :policy do diff --git a/spec/policies/mapping_policy_spec.rb b/spec/policies/mapping_policy_spec.rb index 46b9c117..4010e589 100644 --- a/spec/policies/mapping_policy_spec.rb +++ b/spec/policies/mapping_policy_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe MappingPolicy, type: :policy do diff --git a/spec/policies/synapse_policy.rb b/spec/policies/synapse_policy.rb index 4c725e37..4d9d422c 100644 --- a/spec/policies/synapse_policy.rb +++ b/spec/policies/synapse_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe SynapsePolicy, type: :policy do diff --git a/spec/policies/topic_policy_spec.rb b/spec/policies/topic_policy_spec.rb index 7078496c..ef80e8dc 100644 --- a/spec/policies/topic_policy_spec.rb +++ b/spec/policies/topic_policy_spec.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require 'rails_helper' RSpec.describe TopicPolicy, type: :policy do diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index d14d6dbe..ddf0781c 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true ENV['RAILS_ENV'] ||= 'test' require 'spec_helper' require File.expand_path('../../config/environment', __FILE__) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a2b164b2..a3477eb7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/spec/support/controller_helpers.rb b/spec/support/controller_helpers.rb index 1672479f..1d24b7ca 100644 --- a/spec/support/controller_helpers.rb +++ b/spec/support/controller_helpers.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # https://github.com/plataformatec/devise/wiki/How-To:-Stub-authentication-in-controller-specs require 'devise' diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb index afae617a..0d10fa34 100644 --- a/spec/support/factory_girl.rb +++ b/spec/support/factory_girl.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # lets you type create(:user) instead of FactoryGirl.create(:user) RSpec.configure do |config| config.include FactoryGirl::Syntax::Methods diff --git a/spec/support/pundit.rb b/spec/support/pundit.rb index 1fd8e296..6e8dc0ce 100644 --- a/spec/support/pundit.rb +++ b/spec/support/pundit.rb @@ -1 +1,2 @@ +# frozen_string_literal: true require 'pundit/rspec' diff --git a/spec/support/schema_matcher.rb b/spec/support/schema_matcher.rb index 207c5fa6..998771d9 100644 --- a/spec/support/schema_matcher.rb +++ b/spec/support/schema_matcher.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true RSpec::Matchers.define :match_json_schema do |schema_name| match do |response| schema_directory = Rails.root.join('doc', 'api', 'schemas').to_s diff --git a/spec/support/simplecov.rb b/spec/support/simplecov.rb index 8017e897..1d48d7c8 100644 --- a/spec/support/simplecov.rb +++ b/spec/support/simplecov.rb @@ -1,2 +1,2 @@ +# frozen_string_literal: true require 'simplecov' - From 5fab6de48abd4e6ee084f0bbb509af026c6b6662 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 11:00:53 +0800 Subject: [PATCH 057/306] fiddle with metacodes controller --- .rubocop.yml | 6 ++++++ app/controllers/metacodes_controller.rb | 18 +++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9fb58aba..897484b7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,3 +12,9 @@ Rails: Metrics/LineLength: Max: 100 + +Metrics/AbcSize: + Max: 16 + +Style/Documentation: + Enabled: false diff --git a/app/controllers/metacodes_controller.rb b/app/controllers/metacodes_controller.rb index 313d9764..00f92878 100644 --- a/app/controllers/metacodes_controller.rb +++ b/app/controllers/metacodes_controller.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true class MetacodesController < ApplicationController before_action :require_admin, except: [:index, :show] + before_action :set_metacode, only: [:edit, :update] # GET /metacodes # GET /metacodes.json @@ -8,10 +10,7 @@ class MetacodesController < ApplicationController respond_to do |format| format.html do - unless authenticated? && user.admin - redirect_to root_url, notice: 'You need to be an admin for that.' - return false - end + return unless require_admin render :index end format.json { render json: @metacodes } @@ -23,7 +22,7 @@ class MetacodesController < ApplicationController # GET /metacodes/action.json def show @metacode = Metacode.where('DOWNCASE(name) = ?', downcase(params[:name])).first if params[:name] - @metacode = Metacode.find(params[:id]) unless @metacode + set_metacode unless @metacode respond_to do |format| format.json { render json: @metacode } @@ -36,14 +35,13 @@ class MetacodesController < ApplicationController @metacode = Metacode.new respond_to do |format| - format.html # new.html.erb + format.html format.json { render json: @metacode } end end # GET /metacodes/1/edit def edit - @metacode = Metacode.find(params[:id]) end # POST /metacodes @@ -65,8 +63,6 @@ class MetacodesController < ApplicationController # PUT /metacodes/1 # PUT /metacodes/1.json def update - @metacode = Metacode.find(params[:id]) - respond_to do |format| if @metacode.update(metacode_params) format.html { redirect_to metacodes_url, notice: 'Metacode was successfully updated.' } @@ -84,4 +80,8 @@ class MetacodesController < ApplicationController def metacode_params params.require(:metacode).permit(:id, :name, :aws_icon, :manual_icon, :color) end + + def set_metacode + @metacode = Metacode.find(params[:id]) + end end From f8c11f234dc5df16a4a392e179ba405ce8fcf5a5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:27:34 +0800 Subject: [PATCH 058/306] more rubocop updates --- .rubocop.yml | 1 + Vagrantfile | 1 - .../api/v1/deprecated_controller.rb | 2 + app/controllers/api/v2/restful_controller.rb | 46 ++++++------- app/controllers/application_controller.rb | 18 +++-- app/helpers/application_helper.rb | 12 ++-- app/helpers/maps_helper.rb | 65 ++++++++++--------- app/helpers/synapses_helper.rb | 35 ++++------ app/helpers/topics_helper.rb | 64 +++++++----------- app/policies/map_policy.rb | 23 ++++--- app/policies/synapse_policy.rb | 12 ++-- app/policies/topic_policy.rb | 18 +++-- .../api/v2/application_serializer.rb | 27 ++++++-- app/serializers/api/v2/user_serializer.rb | 2 + app/services/map_export_service.rb | 8 ++- app/views/layouts/_lightboxes.html.erb | 2 +- app/views/maps/_newtopic.html.erb | 29 +++++---- config/initializers/access_codes.rb | 4 +- config/initializers/backtrace_silencers.rb | 6 +- config/initializers/devise.rb | 6 +- config/initializers/doorkeeper.rb | 15 +++-- config/initializers/uservoice.rb | 9 ++- config/routes.rb | 6 +- script/rails | 1 - spec/api/v2/mappings_api_spec.rb | 8 ++- spec/api/v2/synapses_api_spec.rb | 8 ++- 26 files changed, 234 insertions(+), 194 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 897484b7..6bdcbfc3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ AllCops: - 'bin/**/*' - 'vendor/**/*' - 'app/assets/javascripts/node_modules/**/*' + - 'Vagrantfile' Rails: Enabled: true diff --git a/Vagrantfile b/Vagrantfile index 0fa3e8da..6ee6cb35 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,4 +1,3 @@ -# frozen_string_literal: true # -*- mode: ruby -*- # vi: set ft=ruby : diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index 6f6b5f15..b9e07214 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -2,9 +2,11 @@ module Api module V1 class DeprecatedController < ApplicationController + # rubocop:disable Style/MethodMissing def method_missing render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } end + # rubocop:enable Style/MethodMissing end end end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index d7a1856e..74a8f472 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -52,7 +52,8 @@ module Api "Api::V2::#{resource_name.camelize}Serializer".constantize end - def respond_with_resource(scope: default_scope, serializer: resource_serializer, root: serializer_root) + def respond_with_resource(scope: default_scope, serializer: resource_serializer, + root: serializer_root) if resource.errors.empty? render json: resource, scope: scope, serializer: serializer, root: root else @@ -60,8 +61,11 @@ module Api end end - def respond_with_collection(resources: collection, scope: default_scope, serializer: resource_serializer, root: serializer_root) - render json: resources, scope: scope, each_serializer: serializer, root: root, meta: pagination(resources), meta_key: :page + def respond_with_collection(resources: collection, scope: default_scope, + serializer: resource_serializer, root: serializer_root) + pagination_link_headers!(pagination(resources)) + render json: resources, scope: scope, each_serializer: serializer, root: root, + meta: pagination(resources), meta_key: :page end def default_scope @@ -95,33 +99,31 @@ module Api end def pagination(collection) - per = (params[:per] || 25).to_i - current_page = (params[:page] || 1).to_i - total_pages = (collection.total_count.to_f / per).ceil - prev_page = current_page > 1 ? current_page - 1 : 0 - next_page = current_page < total_pages ? current_page + 1 : 0 + @pagination_data ||= { + current_page: (params[:page] || 1).to_i, + next_page: current_page < total_pages ? current_page + 1 : 0, + prev_page: current_page > 1 ? current_page - 1 : 0, + total_pages: (collection.total_count.to_f / per).ceil, + total_count: collection.total_count, + per: (params[:per] || 25).to_i + } + end + def pagination_link_headers!(data) base_url = request.base_url + request.path - nxt = request.query_parameters.merge(page: next_page).map { |x| x.join('=') }.join('&') - prev = request.query_parameters.merge(page: prev_page).map { |x| x.join('=') }.join('&') - last = request.query_parameters.merge(page: total_pages).map { |x| x.join('=') }.join('&') + old_query = request_query_parameters + nxt = old_query.merge(page: data[:next_page]).map { |x| x.join('=') }.join('&') + prev = old_query.merge(page: data[:prev_page]).map { |x| x.join('=') }.join('&') + last = old_query.merge(page: data[:total_pages]).map { |x| x.join('=') }.join('&') + response.headers['Link'] = [ %(<#{base_url}?#{nxt}>; rel="next"), %(<#{base_url}?#{prev}>; rel="prev"), %(<#{base_url}?#{last}>; rel="last") ].join(',') - response.headers['X-Total-Pages'] = collection.total_pages.to_s - response.headers['X-Total-Count'] = collection.total_count.to_s + response.headers['X-Total-Pages'] = data[:total_pages].to_s + response.headers['X-Total-Count'] = data[:total_count].to_s response.headers['X-Per-Page'] = per.to_s - - { - current_page: current_page, - next_page: next_page, - prev_page: prev_page, - total_pages: total_pages, - total_count: collection.total_count, - per: per - } end def instantiate_collection diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 7735c681..83889619 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -6,7 +6,7 @@ class ApplicationController < ActionController::Base rescue_from Pundit::NotAuthorizedError, with: :handle_unauthorized protect_from_forgery(with: :exception) - before_action :get_invite_link + before_action :invite_link after_action :allow_embedding def default_serializer_options @@ -42,22 +42,20 @@ class ApplicationController < ActionController::Base private - def get_invite_link + def invite_link @invite_link = "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end def require_no_user - if authenticated? - redirect_to edit_user_path(user), notice: 'You must be logged out.' - return false - end + return true unless authenticated? + redirect_to edit_user_path(user), notice: 'You must be logged out.' + return false end def require_user - unless authenticated? - redirect_to new_user_session_path, notice: 'You must be logged in.' - return false - end + return true if authenticated? + redirect_to new_user_session_path, notice: 'You must be logged in.' + return false end def require_admin diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 57b24106..1c9b4da5 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,14 +1,14 @@ # frozen_string_literal: true module ApplicationHelper - def get_metacodeset - @m = current_user.settings.metacodes - set = @m[0].include?('metacodeset') ? MetacodeSet.find(@m[0].sub('metacodeset-', '').to_i) : false - set + def metacodeset + metacodes = current_user.settings.metacodes + return false unless metacodes[0].include?('metacodeset') + MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i) end def user_metacodes @m = current_user.settings.metacodes - set = get_metacodeset + set = metacodeset @metacodes = if set set.metacodes.to_a else @@ -17,7 +17,7 @@ module ApplicationHelper @metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) end - def determine_invite_link + def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end end diff --git a/app/helpers/maps_helper.rb b/app/helpers/maps_helper.rb index 8ca7b047..0ca99c15 100644 --- a/app/helpers/maps_helper.rb +++ b/app/helpers/maps_helper.rb @@ -1,35 +1,42 @@ # frozen_string_literal: true module MapsHelper - ## this one is for building our custom JSON autocomplete format for typeahead + # JSON autocomplete format for typeahead def autocomplete_map_array_json(maps) - temp = [] - maps.each do |m| - map = {} - map['id'] = m.id - map['label'] = m.name - map['value'] = m.name - map['description'] = m.desc.try(:truncate, 30) - map['permission'] = m.permission - map['topicCount'] = m.topics.count - map['synapseCount'] = m.synapses.count - map['contributorCount'] = m.contributors.count - map['rtype'] = 'map' - - contributorTip = '' - firstContributorImage = 'https://s3.amazonaws.com/metamaps-assets/site/user.png' - if m.contributors.count.positive? - firstContributorImage = m.contributors[0].image.url(:thirtytwo) - m.contributors.each_with_index do |c, _index| - userImage = c.image.url(:thirtytwo) - name = c.name - contributorTip += '<li> <img class="tipUserImage" width="25" height="25" src=' + userImage + ' />' + '<span>' + name + '</span> </li>' - end - end - map['contributorTip'] = contributorTip - map['mapContributorImage'] = firstContributorImage - - temp.push map + maps.map do |m| + { + id: m.id, + label: m.name, + value: m.name, + description: m.desc.try(:truncate, 30), + permission: m.permission, + topicCount: m.topics.count, + synapseCount: m.synapses.count, + contributorCount: m.contributors.count, + rtype: 'map', + contributorTip: contributor_tip(map), + mapContributorImage: first_contributor_image(map) + } end - temp + end + + def first_contributor_image(map) + if map.contributors.count.positive? + return map.contributors[0].image.url(:thirtytwo) + end + 'https://s3.amazonaws.com/metamaps-assets/site/user.png' + end + + def contributor_tip(map) + output = '' + if map.contributors.count.positive? + map.contributors.each_with_index do |contributor, _index| + user_image = contributor.image.url(:thirtytwo) + output += '<li>' + output += %(<img class="tipUserImage" width="25" height="25" src="#{user_image}" />) + output += "<span>#{contributor.name}</span>" + output += '</li>' + end + end + output end end diff --git a/app/helpers/synapses_helper.rb b/app/helpers/synapses_helper.rb index 471f0e05..def3b985 100644 --- a/app/helpers/synapses_helper.rb +++ b/app/helpers/synapses_helper.rb @@ -2,33 +2,24 @@ module SynapsesHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_synapse_generic_json(unique) - temp = [] - unique.each do |s| - synapse = {} - synapse['label'] = s.desc - synapse['value'] = s.desc - - temp.push synapse + unique.map do |s| + { label: s.desc, value: s.desc } end - temp end ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_synapse_array_json(synapses) - temp = [] - synapses.each do |s| - synapse = {} - synapse['id'] = s.id - synapse['label'] = s.desc.nil? || s.desc == '' ? '(no description)' : s.desc - synapse['value'] = s.desc - synapse['permission'] = s.permission - synapse['mapCount'] = s.maps.count - synapse['originator'] = s.user.name - synapse['originatorImage'] = s.user.image.url(:thirtytwo) - synapse['rtype'] = 'synapse' - - temp.push synapse + synapses.map do |s| + { + id: s.id, + label: s.desc.blank? ? '(no description)' : s.desc, + value: s.desc, + permission: s.permission, + mapCount: s.maps.count, + originator: s.user.name, + originatorImage: s.user.image.url(:thirtytwo), + rtype: 'synapse' + } end - temp end end diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 32697db5..e1a1d179 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -2,56 +2,38 @@ module TopicsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_array_json(topics) - temp = [] - topics.each do |t| - topic = {} - topic['id'] = t.id - topic['label'] = t.name - topic['value'] = t.name - topic['description'] = t.desc ? t.desc&.truncate(70) # make this return matched results - topic['type'] = t.metacode.name - topic['typeImageURL'] = t.metacode.icon - topic['permission'] = t.permission - topic['mapCount'] = t.maps.count - topic['synapseCount'] = t.synapses.count - topic['originator'] = t.user.name - topic['originatorImage'] = t.user.image.url(:thirtytwo) - topic['rtype'] = 'topic' - topic['inmaps'] = t.inmaps - topic['inmapsLinks'] = t.inmapsLinks - - temp.push topic + topics.map do |t| + { + id: t.id, + label: t.name, + value: t.name, + description: t.desc ? t.desc&.truncate(70) : '', # make this return matched results + type: t.metacode.name, + typeImageURL: t.metacode.icon, + permission: t.permission, + mapCount: t.maps.count, + synapseCount: t.synapses.count, + originator: t.user.name, + originatorImage: t.user.image.url(:thirtytwo), + rtype: :topic, + inmaps: t.inmaps, + inmapsLinks: t.inmapsLinks + } end - temp end - # find all nodes in any given nodes network + # recursively find all nodes in any given nodes network def network(node, array, count) - # recurse starting with a node to find all connected nodes and return an array of topics that constitutes the starting nodes network - - # if the array of nodes is empty initialize it array = [] if array.nil? - - # add the node to the array array.push(node) - return array if count.zero? - count -= 1 - # check if each relative is already in the array and if not, call the network function again - if !node.relatives.empty? - if (node.relatives - array).empty? - return array - else - (node.relatives - array).each do |relative| - array = (array | network(relative, array, count)) - end - return array - end - - elsif node.relatives.empty? - return array + remaining_relatives = node.relatives.to_a - array + remaining_relatives.each do |relative| + array = (array | network(relative, array, count - 1)) end + + array end end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 3894520c..b2a04cbe 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -3,13 +3,11 @@ class MapPolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - permission = 'maps.permission IN (?)' - if user - shared_maps = user.shared_maps.map(&:id) - scope.where(permission + ' OR maps.id IN (?) OR maps.user_id = ?', visible, shared_maps, user.id) - else - scope.where(permission, visible) - end + return scope.where(permission: visible) unless user + + scope.where(permission: visible) + .or(scope.where(id: user.shared_maps.map(&:id))) + .or(scope.where(user_id: user.id)) end end @@ -18,7 +16,9 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission == 'commons' || record.permission == 'public' || record.collaborators.include?(user) || record.user == user + record.permission.in?('commons', 'public') || + record.collaborators.include?(user) || + record.user == user end def create? @@ -26,7 +26,10 @@ class MapPolicy < ApplicationPolicy end def update? - user.present? && (record.permission == 'commons' || record.collaborators.include?(user) || record.user == user) + return false unless user.present? + record.permission == 'commons' || + record.collaborators.include?(user) || + record.user == user end def destroy? @@ -34,7 +37,7 @@ class MapPolicy < ApplicationPolicy end def access? - # note that this is to edit access + # note that this is to edit who can access the map user.present? && record.user == user end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index f9557e70..eae820b3 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -3,12 +3,12 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - permission = 'synapses.permission IN (?)' - if user - scope.where(permission + ' OR synapses.defer_to_map_id IN (?) OR synapses.user_id = ?', visible, user.shared_maps.map(&:id), user.id) - else - scope.where(permission, visible) - end + + return scope.where(permission: visible) unless user + + scope.where(permission: visible) + .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where(user_id: user.id)) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 2484ee54..cbde51d8 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -3,12 +3,11 @@ class TopicPolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - permission = 'topics.permission IN (?)' - if user - scope.where(permission + ' OR topics.defer_to_map_id IN (?) OR topics.user_id = ?', visible, user.shared_maps.map(&:id), user.id) - else - scope.where(permission, visible) - end + return scope.where(permission: visible) unless user + + scope.where(permission: visible) + .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where(user_id: user.id)) end end @@ -24,14 +23,13 @@ class TopicPolicy < ApplicationPolicy if record.defer_to_map.present? map_policy.show? else - record.permission == 'commons' || record.permission == 'public' || record.user == user + record.permission.in?('commons', 'public') || record.user == user end end def update? - if !user.present? - false - elsif record.defer_to_map.present? + return false unless user.present? + if record.defer_to_map.present? map_policy.update? else record.permission == 'commons' || record.user == user diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index 55a9b8a0..a5da830a 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -7,21 +7,40 @@ module Api end def embeds + # subclasses can override self.embeddable, and then it will whitelist + # scope[:embeds] based on the contents. That way scope[:embeds] can just pull + # from params and the whitelisting happens here @embeds ||= (scope[:embeds] || []).select { |e| self.class.embeddable.keys.include?(e) } end + # self.embeddable might look like this: + # topic1: { attr: :node1, serializer: TopicSerializer } + # topic2: { attr: :node2, serializer: TopicSerializer } + # contributors: { serializer: UserSerializer} + # This method will remove the :attr key if the underlying attribute name + # is different than the name provided in the final json output. All other keys + # in the hash will be passed to the ActiveModel::Serializer `attribute` method + # directly (e.g. serializer in the examples will be passed). + # + # This setup means if you passed this self.embeddable config and sent no + # ?embed= query param with your API request, you would get the regular attributes + # plus topic1_id, topic2_id, and contributor_ids. If you pass + # ?embed=topic1,topic2,contributors, then instead of two ids and an array of ids, + # you would get two serialized topics and an array of serialized users def self.embed_dat embeddable.each_pair do |key, opts| attr = opts.delete(:attr) || key if attr.to_s.pluralize == attr.to_s - attribute "#{attr.to_s.singularize}_ids".to_sym, opts.merge(unless: -> { embeds.include?(key) }) do + attribute("#{attr.to_s.singularize}_ids".to_sym, + opts.merge(unless: -> { embeds.include?(key) })) do object.send(attr).map(&:id) end - has_many attr, opts.merge(if: -> { embeds.include?(key) }) + has_many(attr, opts.merge(if: -> { embeds.include?(key) })) else id_opts = opts.merge(key: "#{key}_id") - attribute "#{attr}_id".to_sym, id_opts.merge(unless: -> { embeds.include?(key) }) - attribute key, opts.merge(if: -> { embeds.include?(key) }) + attribute("#{attr}_id".to_sym, + id_opts.merge(unless: -> { embeds.include?(key) })) + attribute(key, opts.merge(if: -> { embeds.include?(key) })) end end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index e97bc420..ec58775d 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -12,9 +12,11 @@ module Api object.image.url(:sixtyfour) end + # rubocop:disable Style/PredicateName def is_admin object.admin end + # rubocop:enable Style/PredicateName end end end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index bd256140..2ded756c 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -1,5 +1,11 @@ # frozen_string_literal: true -class MapExportService < Struct.new(:user, :map) +class MapExportService + attr_reader :user, :map + def initialize(user, map) + @user = user + @map = map + end + def json # marshal_dump turns OpenStruct into a Hash { diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 68fa9e27..89b5a6b4 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -94,7 +94,7 @@ <p>As a valued beta tester, you have the ability to invite your peers, colleagues and collaborators onto the platform.</p> <p>Below is a personal invite link containing your unique access code, which can be used multiple times.</p> <div id="joinCodesBox"> - <p class="joinCodes"><%= determine_invite_link %> + <p class="joinCodes"><%= invite_link() %> <button class="button" onclick="Metamaps.GlobalUI.shareInvite('<%= @invite_link %>');">COPY INVITE LINK!</button> </div> diff --git a/app/views/maps/_newtopic.html.erb b/app/views/maps/_newtopic.html.erb index ba3d1797..8e10c7a7 100644 --- a/app/views/maps/_newtopic.html.erb +++ b/app/views/maps/_newtopic.html.erb @@ -1,29 +1,34 @@ +<% @metacodes = user_metacodes() %> + <%= form_for Topic.new, url: topics_url, remote: true do |form| %> <div class="openMetacodeSwitcher openLightbox" data-open="switchMetacodes"> <div class="tooltipsAbove">Switch Metacodes</div> </div> + <div class="pinCarousel"> <div class="tooltipsAbove helpPin">Pin Open</div> <div class="tooltipsAbove helpUnpin">Unpin</div> </div> + <div id="metacodeImg"> - <% @metacodes = user_metacodes() %> - <% set = get_metacodeset() %> <% @metacodes.each do |metacode| %> <img class="cloudcarousel" width="40" height="40" src="<%= asset_path metacode.icon %>" alt="<%= metacode.name %>" title="<%= metacode.name %>" data-id="<%= metacode.id %>" /> <% end %> </div> + <%= form.text_field :name, :maxlength => 140, :placeholder => "title..." %> + <div id="metacodeImgTitle"></div> <div class="clearfloat"></div> -<script> -<% @metacodes.each do |metacode| %> - <% if !set %> - Metamaps.Create.selectedMetacodes.push("<%= metacode.id %>"); - Metamaps.Create.newSelectedMetacodes.push("<%= metacode.id %>"); - Metamaps.Create.selectedMetacodeNames.push("<%= metacode.name %>"); - Metamaps.Create.newSelectedMetacodeNames.push("<%= metacode.name %>"); - <% end %> -<% end %> -</script> + + <script> + <% @metacodes.each do |metacode| %> + <% if !metacodeset() %> + Metamaps.Create.selectedMetacodes.push("<%= metacode.id %>"); + Metamaps.Create.newSelectedMetacodes.push("<%= metacode.id %>"); + Metamaps.Create.selectedMetacodeNames.push("<%= metacode.name %>"); + Metamaps.Create.newSelectedMetacodeNames.push("<%= metacode.name %>"); + <% end %> + <% end %> + </script> <% end %> diff --git a/config/initializers/access_codes.rb b/config/initializers/access_codes.rb index 543ce6e9..fdf4e4b3 100644 --- a/config/initializers/access_codes.rb +++ b/config/initializers/access_codes.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true $codes = [] if ActiveRecord::Base.connection.data_source_exists? 'users' - $codes = ActiveRecord::Base.connection.execute('SELECT code FROM users').map { |user| user['code'] } + $codes = ActiveRecord::Base.connection + .execute('SELECT code FROM users') + .map { |user| user['code'] } end diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index d0f0d3b5..5b98aef4 100644 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,8 +1,10 @@ # frozen_string_literal: true # Be sure to restart your server when you modify this file. -# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# You can add backtrace silencers for libraries that you're using but don't +# wish to see in your backtraces. # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } -# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# You can also remove all the silencers if you're trying to debug a problem +# that might stem from framework code. # Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index a086584d..6740cdc9 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -7,7 +7,8 @@ Devise.setup do |config| # confirmation, reset password and unlock tokens in the database. # Devise will use the `secret_key_base` on Rails 4+ applications as its `secret_key` # by default. You can change it below and use your own secret key. - # config.secret_key = '4d38a819bcea6314ffccb156a8e84b1b52c51ed446d11877c973791b3cd88449e9dbd7990cbc6e7f37d84702168ec36391467000c842ed5bed4f0b05df2b9507' + # config.secret_key = '4d38a819bcea6314ffccb156a8e84b1b52c51ed446d11877c973791b3cd88' + + # '449e9dbd7990cbc6e7f37d84702168ec36391467000c842ed5bed4f0b05df2b9507' # ==> Mailer Configuration # Configure the e-mail address which will be shown in Devise::Mailer, @@ -92,7 +93,8 @@ Devise.setup do |config| config.stretches = Rails.env.test? ? 1 : 10 # Setup a pepper to generate the encrypted password. - # config.pepper = "640ad415cb5292ac9ddbfa6ad7d9653d1537f1184e4037c2453db3eccb98e1c82facc6d3de7bf9d4c41d9967d41194c6e120f36f430e195ba840cd00e02dea59" + # config.pepper = "640ad415cb5292ac9ddbfa6ad7d9653d1537f1184e4037c2453db3eccb98e1c82" + + # "facc6d3de7bf9d4c41d9967d41194c6e120f36f430e195ba840cd00e02dea59" # ==> Configuration for :confirmable # A period that the user is allowed to access the website even without diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb index 33073f45..40de1df8 100644 --- a/config/initializers/doorkeeper.rb +++ b/config/initializers/doorkeeper.rb @@ -8,7 +8,8 @@ Doorkeeper.configure do current_user || redirect_to(new_user_session_url) end - # If you want to restrict access to the web interface for adding oauth authorized applications, you need to declare the block below. + # If you want to restrict access to the web interface for adding oauth authorized applications, + # you need to declare the block below. admin_authenticator do current_user || redirect_to(new_user_session_url) end @@ -39,7 +40,9 @@ Doorkeeper.configure do # Provide support for an owner to be assigned to each registered application (disabled by default) # Optional parameter :confirmation => true (default false) if you want to enforce ownership of # a registered application - # Note: you must also run the rails g doorkeeper:application_owner generator to provide the necessary support + # Note: you must also run the rails g doorkeeper:application_owner generator to provide the + # necessary support + # # enable_application_owner :confirmation => false # Define access token scopes for your provider @@ -61,9 +64,11 @@ Doorkeeper.configure do # access_token_methods :from_bearer_authorization, :from_access_token_param, :from_bearer_param # Change the native redirect uri for client apps - # When clients register with the following redirect uri, they won't be redirected to any server and the authorization code will be displayed within the provider - # The value can be any string. Use nil to disable this feature. When disabled, clients must provide a valid URL - # (Similar behaviour: https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) + # When clients register with the following redirect uri, they won't be redirected to any server + # and the authorization code will be displayed within the provider + # The value can be any string. Use nil to disable this feature. When disabled, clients + # must provide a valid URL (Similar behaviour: + # https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi) # # native_redirect_uri 'urn:ietf:wg:oauth:2.0:oob' diff --git a/config/initializers/uservoice.rb b/config/initializers/uservoice.rb index 3aa65a46..9375e3a1 100644 --- a/config/initializers/uservoice.rb +++ b/config/initializers/uservoice.rb @@ -2,7 +2,10 @@ require 'uservoice-ruby' def current_sso_token - @current_sso_token ||= UserVoice.generate_sso_token('metamapscc', ENV['SSO_KEY'], { - email: current_user.email - }, 300) # Default expiry time is 5 minutes = 300 seconds + @current_sso_token ||= UserVoice.generate_sso_token( + 'metamapscc', + ENV['SSO_KEY'], + { email: current_user.email }, + 300 # Default expiry time is 5 minutes = 300 seconds + ) end diff --git a/config/routes.rb b/config/routes.rb index c64c188e..fe48b6ba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -64,7 +64,11 @@ Metamaps::Application.routes.draw do get 'explore/starred', to: 'maps#starredmaps' get 'explore/mapper/:id', to: 'maps#usermaps' - devise_for :users, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', sessions: 'devise/sessions' }, skip: :sessions + devise_for :users, skip: :sessions, controllers: { + registrations: 'users/registrations', + passwords: 'users/passwords', + sessions: 'devise/sessions' + } devise_scope :user do get 'login' => 'devise/sessions#new', :as => :new_user_session diff --git a/script/rails b/script/rails index 1267847e..6f9d9941 100644 --- a/script/rails +++ b/script/rails @@ -1,6 +1,5 @@ #!/usr/bin/env ruby.exe # frozen_string_literal: true -# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. APP_PATH = File.expand_path('../../config/application', __FILE__) require File.expand_path('../../config/boot', __FILE__) diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index f3ec6a75..4d802865 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -24,7 +24,9 @@ RSpec.describe 'mappings API', type: :request do end it 'POST /api/v2/mappings' do - post '/api/v2/mappings', params: { mapping: mapping.attributes, access_token: token } + post '/api/v2/mappings', params: { + mapping: mapping.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) @@ -32,7 +34,9 @@ RSpec.describe 'mappings API', type: :request do end it 'PATCH /api/v2/mappings/:id' do - patch "/api/v2/mappings/#{mapping.id}", params: { mapping: mapping.attributes, access_token: token } + patch "/api/v2/mappings/#{mapping.id}", params: { + mapping: mapping.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) diff --git a/spec/api/v2/synapses_api_spec.rb b/spec/api/v2/synapses_api_spec.rb index c422f3bc..093bc41e 100644 --- a/spec/api/v2/synapses_api_spec.rb +++ b/spec/api/v2/synapses_api_spec.rb @@ -24,7 +24,9 @@ RSpec.describe 'synapses API', type: :request do end it 'POST /api/v2/synapses' do - post '/api/v2/synapses', params: { synapse: synapse.attributes, access_token: token } + post '/api/v2/synapses', params: { + synapse: synapse.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:synapse) @@ -32,7 +34,9 @@ RSpec.describe 'synapses API', type: :request do end it 'PATCH /api/v2/synapses/:id' do - patch "/api/v2/synapses/#{synapse.id}", params: { synapse: synapse.attributes, access_token: token } + patch "/api/v2/synapses/#{synapse.id}", params: { + synapse: synapse.attributes, access_token: token + } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:synapse) From 20bd959c69ac4b4de976887f91534a1ebe6a06b2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:59:42 +0800 Subject: [PATCH 059/306] fix models that rubocop broke >:( --- app/models/synapse.rb | 13 +------------ app/models/topic.rb | 3 +-- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/models/synapse.rb b/app/models/synapse.rb index c7161469..798f6a54 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -22,17 +22,12 @@ class Synapse < ApplicationRecord where('node1_id = ? OR node2_id = ?', topic_id, topic_id) } - # :nocov: delegate :name, to: :user, prefix: true - # :nocov: - # :nocov: def user_image user.image.url end - # :nocov: - # :nocov: def collaborator_ids if defer_to_map defer_to_map.editors.select { |mapper| mapper != user }.map(&:id) @@ -40,18 +35,12 @@ class Synapse < ApplicationRecord [] end end - # :nocov: - # :nocov: def calculated_permission - if defer_to_map - defer_to_map&.permission + defer_to_map&.permission || permission end - # :nocov: - # :nocov: def as_json(_options = {}) super(methods: [:user_name, :user_image, :calculated_permission, :collaborator_ids]) end - # :nocov: end diff --git a/app/models/topic.rb b/app/models/topic.rb index 09d61897..fb635da3 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -74,8 +74,7 @@ class Topic < ApplicationRecord end def calculated_permission - if defer_to_map - defer_to_map&.permission + defer_to_map&.permission || permission end def as_json(_options = {}) From a164dccc946b8ab3ad1f30241dbee7891f81ebdc Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 13:55:52 +0800 Subject: [PATCH 060/306] fix errors!! --- app/controllers/api/v2/restful_controller.rb | 17 +++++++++++------ app/policies/map_policy.rb | 2 +- app/policies/topic_policy.rb | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index 74a8f472..f837957d 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -99,19 +99,24 @@ module Api end def pagination(collection) - @pagination_data ||= { - current_page: (params[:page] || 1).to_i, + return @pagination_data unless @pagination_data.nil? + + current_page = (params[:page] || 1).to_i + per = (params[:per] || 25).to_i + total_pages = (collection.total_count.to_f / per).ceil + @pagination_data = { + current_page: current_page, next_page: current_page < total_pages ? current_page + 1 : 0, prev_page: current_page > 1 ? current_page - 1 : 0, - total_pages: (collection.total_count.to_f / per).ceil, + total_pages: total_pages, total_count: collection.total_count, - per: (params[:per] || 25).to_i + per: per } end def pagination_link_headers!(data) base_url = request.base_url + request.path - old_query = request_query_parameters + old_query = request.query_parameters nxt = old_query.merge(page: data[:next_page]).map { |x| x.join('=') }.join('&') prev = old_query.merge(page: data[:prev_page]).map { |x| x.join('=') }.join('&') last = old_query.merge(page: data[:total_pages]).map { |x| x.join('=') }.join('&') @@ -123,7 +128,7 @@ module Api ].join(',') response.headers['X-Total-Pages'] = data[:total_pages].to_s response.headers['X-Total-Count'] = data[:total_count].to_s - response.headers['X-Per-Page'] = per.to_s + response.headers['X-Per-Page'] = data[:per].to_s end def instantiate_collection diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index b2a04cbe..4cb2db38 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -16,7 +16,7 @@ class MapPolicy < ApplicationPolicy end def show? - record.permission.in?('commons', 'public') || + record.permission.in?(['commons', 'public']) || record.collaborators.include?(user) || record.user == user end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index cbde51d8..7bcf585c 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -23,7 +23,7 @@ class TopicPolicy < ApplicationPolicy if record.defer_to_map.present? map_policy.show? else - record.permission.in?('commons', 'public') || record.user == user + record.permission.in?(['commons', 'public']) || record.user == user end end From 0bb7b1523da3e498b9003a5b52fcb23d64e56437 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 14:40:40 +0800 Subject: [PATCH 061/306] Metamaps.ReactComponents isn't needed anymore --- frontend/src/Metamaps/ReactComponents.js | 7 ------- frontend/src/Metamaps/Views/ExploreMaps.js | 4 ++-- frontend/src/Metamaps/index.js | 2 -- frontend/src/index.js | 8 +++----- 4 files changed, 5 insertions(+), 16 deletions(-) delete mode 100644 frontend/src/Metamaps/ReactComponents.js diff --git a/frontend/src/Metamaps/ReactComponents.js b/frontend/src/Metamaps/ReactComponents.js deleted file mode 100644 index a2495245..00000000 --- a/frontend/src/Metamaps/ReactComponents.js +++ /dev/null @@ -1,7 +0,0 @@ -import Maps from '../components/Maps' - -const ReactComponents = { - Maps -} - -export default ReactComponents diff --git a/frontend/src/Metamaps/Views/ExploreMaps.js b/frontend/src/Metamaps/Views/ExploreMaps.js index 155e8453..c9f9b78b 100644 --- a/frontend/src/Metamaps/Views/ExploreMaps.js +++ b/frontend/src/Metamaps/Views/ExploreMaps.js @@ -4,7 +4,7 @@ import React from 'react' import ReactDOM from 'react-dom' // TODO ensure this isn't a double import import Active from '../Active' -import ReactComponents from '../ReactComponents' +import Maps from '../../components/Maps' /* * - Metamaps.Loading @@ -42,7 +42,7 @@ const ExploreMaps = { loadMore: self.loadMore } ReactDOM.render( - React.createElement(ReactComponents.Maps, exploreObj), + React.createElement(Maps, exploreObj), document.getElementById('explore') ) diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 5d15559c..e1aa8b34 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -30,7 +30,6 @@ import TopicCard from './TopicCard' import Util from './Util' import Views from './Views' import Visualize from './Visualize' -import ReactComponents from './ReactComponents' Metamaps.Account = Account Metamaps.Active = Active @@ -55,7 +54,6 @@ Metamaps.Mouse = Mouse Metamaps.Organize = Organize Metamaps.PasteInput = PasteInput Metamaps.Realtime = Realtime -Metamaps.ReactComponents = ReactComponents Metamaps.Router = Router Metamaps.Selected = Selected Metamaps.Settings = Settings diff --git a/frontend/src/index.js b/frontend/src/index.js index 176ac329..67f69141 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -1,8 +1,6 @@ -// create global references to some utility libraries -import ReactDOM from 'react-dom' +// create global references import _ from 'underscore' -window.ReactDOM = ReactDOM -window._ = _ - import Metamaps from './Metamaps' + +window._ = _ window.Metamaps = Metamaps From 79aa7717ed16f5db1d734a07ebb64b879ec6a36c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 14:53:32 +0800 Subject: [PATCH 062/306] exact versions in package.json --- package.json | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index bb32377d..08c4f086 100644 --- a/package.json +++ b/package.json @@ -18,26 +18,24 @@ }, "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { - "autolinker": "^0.17.1", - "babel-cli": "^6.11.4", - "babel-loader": "^6.2.4", - "babel-plugin-transform-class-properties": "^6.11.5", - "babel-preset-es2015": "^6.9.0", - "babel-preset-react": "^6.11.1", - "backbone": "^1.0.0", - "chai": "^3.5.0", - "jquery": "1.12.1", - "mocha": "^3.0.2", - "mocha-jsdom": "^1.1.0", + "autolinker": "0.17.1", + "babel-cli": "6.11.4", + "babel-loader": "6.2.4", + "babel-plugin-transform-class-properties": "6.11.5", + "babel-preset-es2015": "6.9.0", + "babel-preset-react": "6.11.1", + "backbone": "1.0.0", "node-uuid": "1.2.0", - "react": "^15.3.0", - "react-dom": "^15.3.0", - "requirejs": "^2.1.1", + "react": "15.3.0", + "react-dom": "15.3.0", + "requirejs": "2.1.1", "socket.io": "0.9.12", - "underscore": "^1.4.4", - "webpack": "^1.13.1" + "underscore": "1.4.4", + "webpack": "1.13.1" }, "devDependencies": { + "chai": "^3.5.0", + "mocha": "^3.0.2", "babel-eslint": "^6.1.2", "eslint": "^3.5.0", "eslint-config-standard": "^6.0.1", From 045bd3fd73f39cb44c876464cc4a5eafa06b302a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 23:23:12 +0800 Subject: [PATCH 063/306] Metamaps.Filter bug and use _.omit instead of util function --- frontend/src/Metamaps/Filter.js | 2 +- frontend/src/components/Header.js | 4 ++-- frontend/src/utils/index.js | 9 --------- 3 files changed, 3 insertions(+), 12 deletions(-) delete mode 100644 frontend/src/utils/index.js diff --git a/frontend/src/Metamaps/Filter.js b/frontend/src/Metamaps/Filter.js index f67c6ec8..59aa1bae 100644 --- a/frontend/src/Metamaps/Filter.js +++ b/frontend/src/Metamaps/Filter.js @@ -186,7 +186,7 @@ const Filter = { $('#filter_by_' + listToModify + ' li[data-id="' + identifier + '"]').fadeOut('fast', function () { $(this).remove() }) - index = self.visible[filtersToUse].indexOf(identifier) + const index = self.visible[filtersToUse].indexOf(identifier) self.visible[filtersToUse].splice(index, 1) }) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Header.js index c8c67619..ee4184d5 100644 --- a/frontend/src/components/Header.js +++ b/frontend/src/components/Header.js @@ -1,9 +1,9 @@ import React, { Component, PropTypes } from 'react' -import { objectWithoutProperties } from '../utils' +import _ from 'lodash' const MapLink = props => { const { show, text, href, linkClass } = props - const otherProps = objectWithoutProperties(props, ['show', 'text', 'href', 'linkClass']) + const otherProps = _.omit(props, ['show', 'text', 'href', 'linkClass']) if (!show) { return null } diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js deleted file mode 100644 index 1743b9b5..00000000 --- a/frontend/src/utils/index.js +++ /dev/null @@ -1,9 +0,0 @@ -export const objectWithoutProperties = (obj, keys) => { - const target = {} - for (let i in obj) { - if (keys.indexOf(i) !== -1) continue - if (!Object.prototype.hasOwnProperty.call(obj, i)) continue - target[i] = obj[i] - } - return target -} From 0a0ff2fdab38c44a0367598eb1e449b42776449f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 23:28:11 +0800 Subject: [PATCH 064/306] remove fetch api - we don't want no polyfills, and already have jQuery --- frontend/src/Metamaps/Mapper.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frontend/src/Metamaps/Mapper.js b/frontend/src/Metamaps/Mapper.js index 3858101d..70cbd81a 100644 --- a/frontend/src/Metamaps/Mapper.js +++ b/frontend/src/Metamaps/Mapper.js @@ -1,17 +1,19 @@ +/* global $ */ + /* - * Metamaps.Backbone + * Dependencies: + * Metamaps.Backbone */ const Mapper = { // this function is to retrieve a mapper JSON object from the database // @param id = the id of the mapper to retrieve get: function (id, callback) { - return fetch(`/users/${id}.json`, { - }).then(response => { - if (!response.ok) throw response - return response.json() - }).then(payload => { - callback(new Metamaps.Backbone.Mapper(payload)) + $.ajax({ + url: `/users/${id}.json`, + success: data => { + callback(new Metamaps.Backbone.Mapper(data)) + } }) } } From 40f89b1c61bbd80ed31274360464bdc7136ce160 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 15:22:42 +0800 Subject: [PATCH 065/306] enable csv import using csv-parse module --- frontend/src/Metamaps/Import.js | 99 ++++++++++++++++++++--------- frontend/src/Metamaps/PasteInput.js | 16 ++--- package.json | 1 + 3 files changed, 75 insertions(+), 41 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index d5a4b4e1..52a8f21a 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -1,5 +1,8 @@ /* global Metamaps, $ */ +import parse from 'csv-parse' +import _ from 'lodash' + import Active from './Active' import GlobalUI from './GlobalUI' import Map from './Map' @@ -33,6 +36,40 @@ const Import = { self.handle(results) }, + handleCSV: function (text, parserOpts = {}) { + var self = Import + + const topicsRegex = /("?Topics"?)([\s\S]*)/mi + const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi + let topicsText = text.match(topicsRegex) + if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '') + let synapsesText = text.match(synapsesRegex) + if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') + + // merge default options and extra options passed in parserOpts argument + const csv_parser_options = Object.assign({ + columns: true, // get headers + relax_column_count: true, + skip_empty_lines: true + }, parserOpts) + + const topicsPromise = $.Deferred() + parse(topicsText, csv_parser_options, (err, data) => { + if (err) topicsPromise.reject(err) + topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) + }) + + const synapsesPromise = $.Deferred() + parse(synapsesText, csv_parser_options, (err, data) => { + if (err) synapsesPromise.reject(err) + synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) + }) + + $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { + self.handle({ topics, synapses}) + }) + }, + handleJSON: function (text) { var self = Import results = JSON.parse(text) @@ -53,16 +90,6 @@ const Import = { } // if }, - abort: function (message) { - console.error(message) - }, - - simplify: function (string) { - return string - .replace(/(^\s*|\s*$)/g, '') - .toLowerCase() - }, - parseTabbedString: function (text) { var self = Import @@ -235,30 +262,22 @@ const Import = { return true } - var synapse_created = false - topic1.once('sync', function () { - if (topic1.id && topic2.id && !synapse_created) { - synapse_created = true - self.createSynapseWithParameters( - synapse.desc, synapse.category, synapse.permission, - topic1, topic2 - ) - } // if - }) - topic2.once('sync', function () { - if (topic1.id && topic2.id && !synapse_created) { - synapse_created = true - self.createSynapseWithParameters( - synapse.desc, synapse.category, synapse.permission, - topic1, topic2 - ) - } // if + // ensure imported topics have a chance to get a real id attr before creating synapses + const topic1Promise = $.Deferred() + topic1.once('sync', () => topic1Promise.resolve()) + const topic2Promise = $.Deferred() + topic2.once('sync', () => topic2Promise.resolve()) + $.when(topic1Promise, topic2Promise).done(() => { + self.createSynapseWithParameters( + synapse.desc, synapse.category, synapse.permission, + topic1, topic2 + ) }) }) }, createTopicWithParameters: function (name, metacode_name, permission, desc, - link, xloc, yloc, import_id, opts) { + link, xloc, yloc, import_id, opts = {}) { var self = Import $(document).trigger(Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.where({name: metacode_name})[0] || null @@ -326,6 +345,28 @@ const Import = { Metamaps.Mappings.add(mapping) Synapse.renderSynapse(mapping, synapse, node1, node2, true) + }, + + /* + * helper functions + */ + + abort: function (message) { + console.error(message) + }, + + simplify: function (string) { + return string + .replace(/(^\s*|\s*$)/g, '') + .toLowerCase() + }, + + + // thanks to http://stackoverflow.com/a/25290114/5332286 + lowercaseKeys: function(obj) { + return _.transform(obj, (result, val, key) => { + result[key.toLowerCase()] = val + }) } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index e0620329..d14a0cf4 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -60,11 +60,11 @@ const PasteInput = { if (text.match(self.URL_REGEX)) { self.handleURL(text, coords) } else if (text[0] === '{') { - self.handleJSON(text) + Import.handleJSON(text) } else if (text.match(/\t/)) { - self.handleTSV(text) - } else { - // fail silently + Import.handleTSV(text) + } else if (text.match(/","/)) { + Import.handleCSV(text) } }, @@ -95,14 +95,6 @@ const PasteInput = { } } ) - }, - - handleJSON: function (text) { - Import.handleJSON(text) - }, - - handleTSV: function (text) { - Import.handleTSV(text) } } diff --git a/package.json b/package.json index 08c4f086..aa2caef9 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "babel-preset-es2015": "6.9.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", + "csv-parse": "1.1.7", "node-uuid": "1.2.0", "react": "15.3.0", "react-dom": "15.3.0", From 35d6dbd0b43a1882c45af0dc03b57d240b7ddea8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 15:04:14 +0800 Subject: [PATCH 066/306] hide double click to add topic message if can't edit map --- frontend/src/Metamaps/JIT.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 5eccbc6c..1863fa75 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -150,7 +150,9 @@ const JIT = { if (self.vizData.length == 0) { $('#instructions div').hide() - $('#instructions div.addTopic').show() + if (Metamaps.Active.Map.authorizeToEdit()) { + $('#instructions div.addTopic').show() + } GlobalUI.showDiv('#instructions') Visualize.loadLater = true } From 5819447828caaef4d07ce29e102e8d7f038be2fb Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 18:49:16 +0800 Subject: [PATCH 067/306] fix git versioning --- app/assets/javascripts/src/Metamaps.Erb.js.erb | 2 ++ app/views/layouts/_lightboxes.html.erb | 2 ++ config/initializers/version.rb | 3 ++- frontend/src/Metamaps/index.js | 1 + 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index 60b64e46..e8f3a25b 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -18,3 +18,5 @@ Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %> Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> Metamaps.VERSION = '<%= METAMAPS_VERSION %>' +Metamaps.BUILD = '<%= METAMAPS_BUILD %>' +Metamaps.LAST_UPDATED = '<%= METAMAPS_LAST_UPDATED %>' diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 89b5a6b4..42c959d6 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -13,12 +13,14 @@ <div id="leftAboutParms"> <p>STATUS: </p> <p>VERSION:</p> + <p>BUILD:</p> <p>LAST UPDATE:</p> </div> <div id="rightAboutParms"> <p>PRIVATE BETA</p> <p><%= METAMAPS_VERSION %></p> + <p><%= METAMAPS_BUILD %></p> <p><%= METAMAPS_LAST_UPDATED %></p> </div> <div class="clearfloat"> diff --git a/config/initializers/version.rb b/config/initializers/version.rb index ff08c330..be0330c3 100644 --- a/config/initializers/version.rb +++ b/config/initializers/version.rb @@ -1,3 +1,4 @@ # frozen_string_literal: true -METAMAPS_VERSION = '2 build `git log -1 --pretty=%H`' +METAMAPS_VERSION = '2.9' +METAMAPS_BUILD = `git log -1 --pretty=%H`.chomp.freeze METAMAPS_LAST_UPDATED = `git log -1 --pretty='%ad'`.split(' ').values_at(1, 2, 4).join(' ').freeze diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index e1aa8b34..21a3fb8d 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -31,6 +31,7 @@ import Util from './Util' import Views from './Views' import Visualize from './Visualize' +Metamaps = window.Metamaps || {} Metamaps.Account = Account Metamaps.Active = Active Metamaps.Admin = Admin From 77342727370d80a102382f42c5761a8365d6a42a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 20:10:18 +0800 Subject: [PATCH 068/306] hide circles when transitioning from topic view to map view fixes #389 --- frontend/src/Metamaps/Router.js | 9 ++------- frontend/src/Metamaps/Visualize.js | 8 +++++++- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 073c1d1b..8bcd3590 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -6,7 +6,6 @@ import Backbone from 'backbone' import Active from './Active' import GlobalUI from './GlobalUI' -import JIT from './JIT' import Map from './Map' import Topic from './Topic' import Views from './Views' @@ -170,9 +169,7 @@ const _Router = Backbone.Router.extend({ // clear the visualization, if there was one, before showing its div again if (Visualize.mGraph) { - Visualize.mGraph.graph.empty() - Visualize.mGraph.plot() - JIT.centerMap(Visualize.mGraph.canvas) + Visualize.clearVisualization() } GlobalUI.showDiv('#infovis') Topic.end() @@ -198,9 +195,7 @@ const _Router = Backbone.Router.extend({ // clear the visualization, if there was one, before showing its div again if (Visualize.mGraph) { - Visualize.mGraph.graph.empty() - Visualize.mGraph.plot() - JIT.centerMap(Visualize.mGraph.canvas) + Visualize.clearVisualization() } GlobalUI.showDiv('#infovis') Map.end() diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index df5bab99..3804b6a8 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -220,7 +220,13 @@ const Visualize = { Router.navigate('/topics/' + t.id) } }, 800) - } + }, + clearVisualization: function() { + Visualize.mGraph.graph.empty() + Visualize.mGraph.plot() + JIT.centerMap(Visualize.mGraph.canvas) + $('#infovis').empty() + }, } export default Visualize From 11d13445fbff30bc450a7e1313f3ab71fb1b7774 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 20:14:45 +0800 Subject: [PATCH 069/306] fix authorizeToEdit call --- frontend/src/Metamaps/JIT.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 1863fa75..2b14d18c 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -150,13 +150,14 @@ const JIT = { if (self.vizData.length == 0) { $('#instructions div').hide() - if (Metamaps.Active.Map.authorizeToEdit()) { + if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { $('#instructions div.addTopic').show() } GlobalUI.showDiv('#instructions') Visualize.loadLater = true + } else { + GlobalUI.hideDiv('#instructions') } - else GlobalUI.hideDiv('#instructions') Visualize.render() }, // prepareVizData From 2ade375c204301dfbc4121f1e47d1c191d8c89e1 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 21:53:26 +0800 Subject: [PATCH 070/306] babel-plugin-lodash to slim down bundle size by 300 KB --- .babelrc | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.babelrc b/.babelrc index 2ad52bf1..f9151299 100644 --- a/.babelrc +++ b/.babelrc @@ -4,6 +4,7 @@ "es2015" ], "plugins": [ + "lodash", "transform-class-properties" ] } diff --git a/package.json b/package.json index aa2caef9..02dc7237 100644 --- a/package.json +++ b/package.json @@ -21,17 +21,18 @@ "autolinker": "0.17.1", "babel-cli": "6.11.4", "babel-loader": "6.2.4", + "babel-plugin-lodash": "^3.2.9", "babel-plugin-transform-class-properties": "6.11.5", "babel-preset-es2015": "6.9.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", "csv-parse": "1.1.7", + "lodash": "4.16.1", "node-uuid": "1.2.0", "react": "15.3.0", "react-dom": "15.3.0", "requirejs": "2.1.1", "socket.io": "0.9.12", - "underscore": "1.4.4", "webpack": "1.13.1" }, "devDependencies": { From 0df17c4aa05dfbe44cc2201e2303449e2dc6580f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 21:53:40 +0800 Subject: [PATCH 071/306] update deps in package.json --- package.json | 15 +++++++-------- webpack.config.js | 1 + 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 02dc7237..b71e34bf 100644 --- a/package.json +++ b/package.json @@ -19,21 +19,20 @@ "homepage": "https://github.com/metamaps/metamaps#readme", "dependencies": { "autolinker": "0.17.1", - "babel-cli": "6.11.4", - "babel-loader": "6.2.4", + "babel-cli": "6.14.0", + "babel-loader": "6.2.5", "babel-plugin-lodash": "^3.2.9", "babel-plugin-transform-class-properties": "6.11.5", - "babel-preset-es2015": "6.9.0", + "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", "csv-parse": "1.1.7", "lodash": "4.16.1", - "node-uuid": "1.2.0", - "react": "15.3.0", - "react-dom": "15.3.0", - "requirejs": "2.1.1", + "node-uuid": "1.4.7", + "react": "15.3.2", + "react-dom": "15.3.2", "socket.io": "0.9.12", - "webpack": "1.13.1" + "webpack": "1.13.2" }, "devDependencies": { "chai": "^3.5.0", diff --git a/webpack.config.js b/webpack.config.js index 87881667..fcdcbc04 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const plugins = [ }) ] if (NODE_ENV === 'production') { + plugins.push(new webpack.optimize.DedupePlugin()) plugins.push(new webpack.optimize.UglifyJsPlugin({ compress: { warnings: false } })) From 8c16c60554d6cd50419c0ca8321b463dceb63052 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 22:44:07 +0800 Subject: [PATCH 072/306] show link remover for invalid links too --- frontend/src/Metamaps/TopicCard.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index dad58565..68061d96 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -72,9 +72,11 @@ const TopicCard = { } $('.CardOnGraph').addClass('hasAttachment') - if (self.authorizedToEdit) { + }, + showLinkRemover: function() { + if (TopicCard.authorizedToEdit && $('#linkremove').length === 0) { $('.embeds').append('<div id="linkremove"></div>') - $('#linkremove').click(self.removeLink) + $('#linkremove').click(TopicCard.removeLink) } }, removeLink: function () { @@ -151,6 +153,7 @@ const TopicCard = { loader.setRange(0.9); // default is 1.3 loader.show() // Hidden by default var e = embedly('card', document.getElementById('embedlyLink')) + self.showLinkRemover() if (!e) { self.handleInvalidLink() } From cc2e3b9358ef1f30e004bbf6e32759a8fb4cf918 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 14:29:28 +0800 Subject: [PATCH 073/306] hack to get the <title> tag when importing a url, without CORS issues --- app/controllers/hacks_controller.rb | 35 +++++++++++++++++++++++++++++ app/policies/hack_policy.rb | 5 +++++ config/routes.rb | 4 ++++ frontend/src/Metamaps/PasteInput.js | 12 ++++++++++ 4 files changed, 56 insertions(+) create mode 100644 app/controllers/hacks_controller.rb create mode 100644 app/policies/hack_policy.rb diff --git a/app/controllers/hacks_controller.rb b/app/controllers/hacks_controller.rb new file mode 100644 index 00000000..42bafd6f --- /dev/null +++ b/app/controllers/hacks_controller.rb @@ -0,0 +1,35 @@ +# bad code that should be seriously checked over before entering one of the +# other prim and proper files in the nice section of this repo +class HacksController < ApplicationController + include ActionView::Helpers::TextHelper # string truncate method + + def load_url_title + authorize :Hack + url = params[:url] # TODO verify!?!?!?! + response, url = get_with_redirects(url) + title = get_encoded_title(response) + render json: { success: true, title: title, url: url } + rescue StandardError => e + render json: { success: false, error_type: e.class.name, error_message: e.message } + end + + private + + def get_with_redirects(url) + uri = URI.parse(url) + response = Net::HTTP.get_response(uri) + while response.code == '301' + uri = URI.parse(response['location']) + response = Net::HTTP.get_response(uri) + end + [response, uri.to_s] + end + + def get_encoded_title(http_response) + title = http_response.body.sub(/.*<title>(.*)<\/title>.*/m, '\1') + charset = http_response['content-type'].sub(/.*charset=(.*);?.*/, '\1') + charset = nil if charset == 'text/html' + title = title.force_encoding(charset) if charset + truncate(title, length: 140) + end +end diff --git a/app/policies/hack_policy.rb b/app/policies/hack_policy.rb new file mode 100644 index 00000000..b6fbf6ce --- /dev/null +++ b/app/policies/hack_policy.rb @@ -0,0 +1,5 @@ +class HackPolicy < ApplicationPolicy + def load_url_title? + true + end +end diff --git a/config/routes.rb b/config/routes.rb index fe48b6ba..84112d23 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,4 +80,8 @@ Metamaps::Application.routes.draw do get 'users/:id/details', to: 'users#details', as: :details post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes resources :users, except: [:index, :destroy] + + namespace :hacks do + get 'load_url_title' + end end diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index d14a0cf4..13258857 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -88,6 +88,18 @@ const PasteInput = { import_id, { success: function(topic) { + $.get('/hacks/load_url_title', { + url: text + }, function success(data, textStatus) { + var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' + if ($(selector).find('form').length > 0) { + $(selector).find('textarea, input').val(data.title) + } else { + $(selector).html(data.title) + } + topic.set('name', data.title) + topic.save() + }) TopicCard.showCard(topic.get('node'), function() { $('#showcard #titleActivator').click() .find('textarea, input').focus() From ceb26997606d9ca9f464b2f2647aaf86efc78945 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 22:54:40 +0800 Subject: [PATCH 074/306] install rack-attack --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index b4a0967b..d5b42d83 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'pg' gem 'pundit' gem 'pundit_extra' gem 'rack-cors' +gem 'rack-attack' gem 'redis' gem 'slack-notifier' gem 'snorlax' diff --git a/Gemfile.lock b/Gemfile.lock index 7e2590c1..23d4c827 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -177,6 +177,8 @@ GEM activesupport (>= 3.0.0) pundit_extra (0.3.0) rack (2.0.1) + rack-attack (5.0.1) + rack rack-cors (0.4.0) rack-test (0.6.3) rack (>= 1.0) @@ -316,6 +318,7 @@ DEPENDENCIES pry-rails pundit pundit_extra + rack-attack rack-cors rails (~> 5.0.0) rails3-jquery-autocomplete From 7f8110b6be5c486b71084fe6683594f1db2bbb23 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:00:07 +0800 Subject: [PATCH 075/306] configure rack attack to allow 5r/s for the load_url_title route --- config/application.rb | 2 ++ config/initializers/rack-attack.rb | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 config/initializers/rack-attack.rb diff --git a/config/application.rb b/config/application.rb index b629682a..96505b32 100644 --- a/config/application.rb +++ b/config/application.rb @@ -26,6 +26,8 @@ module Metamaps Doorkeeper::ApplicationController.helper ApplicationHelper end + config.middleware.use Rack::Attack + # Configure sensitive parameters which will be filtered from the log file. config.filter_parameters += [:password] diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb new file mode 100644 index 00000000..6c23e151 --- /dev/null +++ b/config/initializers/rack-attack.rb @@ -0,0 +1,15 @@ +class Rack::Attack +end + +Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new + +# Throttle requests to 5 requests per second per ip +Rack::Attack.throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| + # If the return value is truthy, the cache key for the return value + # is incremented and compared with the limit. In this case: + # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" + # + # If falsy, the cache key is neither incremented nor checked. + + req.ip if req.path === 'hacks/load_url_title' +end From 959aa693f357888aa77ff17378ab03ac0a082e00 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:06:09 +0800 Subject: [PATCH 076/306] ok, i guess this is ready --- app/controllers/hacks_controller.rb | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/controllers/hacks_controller.rb b/app/controllers/hacks_controller.rb index 42bafd6f..1abe3e60 100644 --- a/app/controllers/hacks_controller.rb +++ b/app/controllers/hacks_controller.rb @@ -1,16 +1,18 @@ -# bad code that should be seriously checked over before entering one of the -# other prim and proper files in the nice section of this repo +# bad code that should be checked over before entering one of the +# nice files from the right side of this repo class HacksController < ApplicationController include ActionView::Helpers::TextHelper # string truncate method + # rate limited by rack-attack - currently 5r/s + # TODO: what else can we do to make get_with_redirects safer? def load_url_title authorize :Hack - url = params[:url] # TODO verify!?!?!?! + url = params[:url] response, url = get_with_redirects(url) title = get_encoded_title(response) render json: { success: true, title: title, url: url } rescue StandardError => e - render json: { success: false, error_type: e.class.name, error_message: e.message } + render json: { success: false } end private From eed5ff76efc021fad81b363874ca86f4c615fa56 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:21:51 +0800 Subject: [PATCH 077/306] add rate limiting headers --- config/initializers/rack-attack.rb | 64 +++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 6c23e151..9dfe3746 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -1,15 +1,59 @@ class Rack::Attack -end + Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new -Rack::Attack.cache.store = ActiveSupport::Cache::MemoryStore.new - -# Throttle requests to 5 requests per second per ip -Rack::Attack.throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| - # If the return value is truthy, the cache key for the return value - # is incremented and compared with the limit. In this case: - # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" + # Throttle all requests by IP (60rpm) # - # If falsy, the cache key is neither incremented nor checked. + # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" + throttle('req/ip', :limit => 300, :period => 5.minutes) do |req| + req.ip # unless req.path.start_with?('/assets') + end - req.ip if req.path === 'hacks/load_url_title' + # Throttle POST requests to /login by IP address + # + # Key: "rack::attack:#{Time.now.to_i/:period}:logins/ip:#{req.ip}" + throttle('logins/ip', :limit => 5, :period => 20.seconds) do |req| + if req.path == '/login' && req.post? + req.ip + end + end + + # Throttle POST requests to /login by email param + # + # Key: "rack::attack:#{Time.now.to_i/:period}:logins/email:#{req.email}" + # + # Note: This creates a problem where a malicious user could intentionally + # throttle logins for another user and force their login requests to be + # denied, but that's not very common and shouldn't happen to you. (Knock + # on wood!) + throttle("logins/email", :limit => 5, :period => 20.seconds) do |req| + if req.path == '/login' && req.post? + # return the email if present, nil otherwise + req.params['email'].presence + end + end + + throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| + # If the return value is truthy, the cache key for the return value + # is incremented and compared with the limit. In this case: + # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" + # + # If falsy, the cache key is neither incremented nor checked. + + req.ip if req.path == 'hacks/load_url_title' + end + + self.throttled_response = lambda do |env| + now = Time.now + match_data = env['rack.attack.match_data'] + period = match_data[:period] + limit = match_data[:limit] + + headers = { + 'X-RateLimit-Limit' => limit.to_s, + 'X-RateLimit-Remaining' => '0', + 'X-RateLimit-Reset' => (now + (period - now.to_i % period)).to_s + } + + [429, headers, ['']] + end end From 1ab87030081b54d0725ae2f65fbb5498d2717959 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 16:41:08 +0800 Subject: [PATCH 078/306] move explore maps methods into their own controller --- app/controllers/explore_controller.rb | 114 +++++++++++++++ app/controllers/maps_controller.rb | 201 ++++++-------------------- app/policies/explore_policy.rb | 25 ++++ 3 files changed, 183 insertions(+), 157 deletions(-) create mode 100644 app/controllers/explore_controller.rb create mode 100644 app/policies/explore_policy.rb diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb new file mode 100644 index 00000000..c21454d1 --- /dev/null +++ b/app/controllers/explore_controller.rb @@ -0,0 +1,114 @@ +class ExploreController < ApplicationController + before_action :authorize_explore + after_action :verify_authorized + after_action :verify_policy_scoped + + respond_to :html, :json, :csv + + # TODO remove? + #autocomplete :map, :name, full: true, extra_data: [:user_id] + + # GET /explore/active + def activemaps + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope(Map).order('updated_at DESC') + .page(page).per(20) + + respond_to do |format| + format.html do + # root url => main/home. main/home renders maps/activemaps view. + redirect_to(root_url) && return if authenticated? + respond_with(@maps, @user) + end + format.json { render json: @maps } + end + end + + # GET /explore/featured + def featuredmaps + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where('maps.featured = ? AND maps.permission != ?', + true, 'private') + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/mine + def mymaps + unless authenticated? + skip_policy_scope + return redirect_to explore_active_path + end + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where('maps.user_id = ?', current_user.id) + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/shared + def sharedmaps + unless authenticated? + skip_policy_scope + return redirect_to explore_active_path + end + + page = params[:page].present? ? params[:page] : 1 + @maps = policy_scope( + Map.where('maps.id IN (?)', current_user.shared_maps.map(&:id)) + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/starred + def starredmaps + unless authenticated? + skip_policy_scope + return redirect_to explore_active_path + end + + page = params[:page].present? ? params[:page] : 1 + stars = current_user.stars.map(&:map_id) + @maps = policy_scope( + Map.where('maps.id IN (?)', stars) + ).order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + # GET /explore/mapper/:id + def usermaps + page = params[:page].present? ? params[:page] : 1 + @user = User.find(params[:id]) + @maps = policy_scope(Map.where(user: @user)) + .order('updated_at DESC').page(page).per(20) + + respond_to do |format| + format.html { respond_with(@maps, @user) } + format.json { render json: @maps } + end + end + + private + + def authorize_explore + authorize :Explore + end +end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 4a091e17..0d308f1d 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,111 +1,12 @@ # frozen_string_literal: true class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] - after_action :verify_authorized, except: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] - after_action :verify_policy_scoped, only: [:activemaps, :featuredmaps, :mymaps, :sharedmaps, :starredmaps, :usermaps] + after_action :verify_authorized respond_to :html, :json, :csv autocomplete :map, :name, full: true, extra_data: [:user_id] - # GET /explore/active - def activemaps - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope(Map).order('updated_at DESC') - .page(page).per(20) - - respond_to do |format| - format.html do - # root url => main/home. main/home renders maps/activemaps view. - redirect_to(root_url) && return if authenticated? - respond_with(@maps, @user) - end - format.json { render json: @maps.to_json } - end - end - - # GET /explore/featured - def featuredmaps - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.featured = ? AND maps.permission != ?', - true, 'private') - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/mine - def mymaps - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.user_id = ?', current_user.id) - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/shared - def sharedmaps - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.id IN (?)', current_user.shared_maps.map(&:id)) - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/starred - def starredmaps - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - stars = current_user.stars.map(&:map_id) - @maps = policy_scope( - Map.where('maps.id IN (?)', stars) - ).order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - - # GET /explore/mapper/:id - def usermaps - page = params[:page].present? ? params[:page] : 1 - @user = User.find(params[:id]) - @maps = policy_scope(Map.where(user: @user)) - .order('updated_at DESC').page(page).per(20) - - respond_to do |format| - format.html { respond_with(@maps, @user) } - format.json { render json: @maps.to_json } - end - end - # GET maps/new def new @map = Map.new(name: 'Untitled Map', permission: 'public', arranged: true) @@ -182,78 +83,31 @@ class MapsController < ApplicationController @map = Map.find(params[:id]) authorize @map - @allmappers = @map.contributors - @allcollaborators = @map.editors - @alltopics = @map.topics.to_a.delete_if { |t| !policy(t).show? } - @allsynapses = @map.synapses.to_a.delete_if { |s| !policy(s).show? } - @allmappings = @map.mappings.to_a.delete_if { |m| !policy(m).show? } - - @json = {} - @json['map'] = @map - @json['topics'] = @alltopics - @json['synapses'] = @allsynapses - @json['mappings'] = @allmappings - @json['mappers'] = @allmappers - @json['collaborators'] = @allcollaborators - @json['messages'] = @map.messages.sort_by(&:created_at) - @json['stars'] = @map.stars - respond_to do |format| - format.json { render json: @json } + format.json { render json: @map.contains(current_user) } end end # POST maps def create @user = current_user - @map = Map.new - @map.name = params[:name] - @map.desc = params[:desc] - @map.permission = params[:permission] + @map = Map.new(create_map_params) @map.user = @user @map.arranged = false - if params[:topicsToMap] - @all = params[:topicsToMap] - @all = @all.split(',') - @all.each do |topic| - topic = topic.split('/') - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Topic.find(topic[0]) - mapping.xloc = topic[1] - mapping.yloc = topic[2] - authorize mapping, :create? - mapping.save - end - - if params[:synapsesToMap] - @synAll = params[:synapsesToMap] - @synAll = @synAll.split(',') - @synAll.each do |synapse_id| - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Synapse.find(synapse_id) - authorize mapping, :create? - mapping.save - end - end - + if params[:topicsToMap].present? + create_topics! + create_synapses! if params[:synapsesToMap].present? @map.arranged = true end authorize @map + respond_to do |format| if @map.save - respond_to do |format| - format.json { render json: @map } - end + format.json { render json: @map } else - respond_to do |format| - format.json { render json: 'invalid params' } - end + format.json { render json: 'invalid params' } end end @@ -263,7 +117,7 @@ class MapsController < ApplicationController authorize @map respond_to do |format| - if @map.update_attributes(map_params) + if @map.update_attributes(update_map_params) format.json { head :no_content } else format.json { render json: @map.errors, status: :unprocessable_entity } @@ -366,7 +220,40 @@ class MapsController < ApplicationController private # Never trust parameters from the scary internet, only allow the white list through. - def map_params + def create_map_params + params.require(:map).permit(:name, :desc, :permission) + end + + def update_map_params params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end + + def create_topics! + topics = params[:topicsToMap] + topics = topics.split(',') + topics.each do |topic| + topic = topic.split('/') + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Topic.find(topic[0]) + mapping.xloc = topic[1] + mapping.yloc = topic[2] + authorize mapping, :create? + mapping.save + end + end + + def create_synapses! + @synAll = params[:synapsesToMap] + @synAll = @synAll.split(',') + @synAll.each do |synapse_id| + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Synapse.find(synapse_id) + authorize mapping, :create? + mapping.save + end + end end diff --git a/app/policies/explore_policy.rb b/app/policies/explore_policy.rb new file mode 100644 index 00000000..6cbdab15 --- /dev/null +++ b/app/policies/explore_policy.rb @@ -0,0 +1,25 @@ +class ExplorePolicy < ApplicationPolicy + def activemaps? + true + end + + def featuredmaps? + true + end + + def mymaps? + true + end + + def sharedmaps? + true + end + + def starredmaps? + true + end + + def usermaps? + true + end +end From 40bd9ed95a30996c6785b33688eda02a6ef24d60 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 16:41:15 +0800 Subject: [PATCH 079/306] refactor maps controller a bit --- app/models/map.rb | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/models/map.rb b/app/models/map.rb index 9c30479f..7de744a2 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -108,4 +108,23 @@ class Map < ApplicationRecord self.screenshot = data save end + + # user param helps determine what records are visible + def contains(user) + allmappers = contributors + allcollaborators = editors + alltopics = Pundit.policy_scope(user, topics).to_a + allsynapses = Pundit.policy_scope(user, synapses).to_a + allmappings = Pundit.policy_scope(user, mappings).to_a + + json = {} + json['map'] = self + json['topics'] = alltopics + json['synapses'] = allsynapses + json['mappings'] = allmappings + json['mappers'] = allmappers + json['collaborators'] = allcollaborators + json['messages'] = messages.sort_by(&:created_at) + json['stars'] = stars + end end From 7275beb163d585ed733df25d2a41079fbfba40c4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 16:48:43 +0800 Subject: [PATCH 080/306] put CRUD at top of maps controller, and alphabetize other actions below --- app/controllers/maps_controller.rb | 243 +++++++++++++---------------- 1 file changed, 111 insertions(+), 132 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 0d308f1d..ff7a1686 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,12 +1,33 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :access, :star, :unstar, :screenshot, :events, :destroy] + before_action :require_user, only: [:create, :update, :destroy, :access, :events, :screenshot, :star, :unstar] + before_action :set_map, only: [:show, :update, :destroy, :access, :contains, :events, :export, :screenshot, :star, :unstar] after_action :verify_authorized respond_to :html, :json, :csv autocomplete :map, :name, full: true, extra_data: [:user_id] + # GET maps/:id + def show + respond_to do |format| + format.html do + @allmappers = @map.contributors + @allcollaborators = @map.editors + @alltopics = policy_scope(@map.topics) + @allsynapses = policy_scope(@map.synapses) + @allmappings = policy_scope(@map.mappings) + @allmessages = @map.messages.sort_by(&:created_at) + @allstars = @map.stars + + respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @allstars, @map) + end + format.json { render json: @map } + format.csv { redirect_to action: :export, format: :csv } + format.xls { redirect_to action: :export, format: :xls } + end + end + # GET maps/new def new @map = Map.new(name: 'Untitled Map', permission: 'public', arranged: true) @@ -21,73 +42,6 @@ class MapsController < ApplicationController end end - # GET maps/:id - def show - @map = Map.find(params[:id]) - authorize @map - - respond_to do |format| - format.html do - @allmappers = @map.contributors - @allcollaborators = @map.editors - @alltopics = @map.topics.to_a.delete_if { |t| !policy(t).show? } - @allsynapses = @map.synapses.to_a.delete_if { |s| !policy(s).show? } - @allmappings = @map.mappings.to_a.delete_if { |m| !policy(m).show? } - @allmessages = @map.messages.sort_by(&:created_at) - @allstars = @map.stars - - respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @allstars, @map) - end - format.json { render json: @map } - format.csv { redirect_to action: :export, format: :csv } - format.xls { redirect_to action: :export, format: :xls } - end - end - - # GET maps/:id/export - def export - map = Map.find(params[:id]) - authorize map - exporter = MapExportService.new(current_user, map) - respond_to do |format| - format.json { render json: exporter.json } - format.csv { send_data exporter.csv } - format.xls { @spreadsheet = exporter.xls } - end - end - - # POST maps/:id/events/:event - def events - map = Map.find(params[:id]) - authorize map - - valid_event = false - if params[:event] == 'conversation' - Events::ConversationStartedOnMap.publish!(map, current_user) - valid_event = true - elsif params[:event] == 'user_presence' - Events::UserPresentOnMap.publish!(map, current_user) - valid_event = true - end - - respond_to do |format| - format.json do - head :ok if valid_event - head :bad_request unless valid_event - end - end - end - - # GET maps/:id/contains - def contains - @map = Map.find(params[:id]) - authorize @map - - respond_to do |format| - format.json { render json: @map.contains(current_user) } - end - end - # POST maps def create @user = current_user @@ -104,18 +58,16 @@ class MapsController < ApplicationController authorize @map respond_to do |format| - if @map.save - format.json { render json: @map } - else - format.json { render json: 'invalid params' } + if @map.save + format.json { render json: @map } + else + format.json { render json: 'invalid params' } + end end end # PUT maps/:id def update - @map = Map.find(params[:id]) - authorize @map - respond_to do |format| if @map.update_attributes(update_map_params) format.json { head :no_content } @@ -125,10 +77,19 @@ class MapsController < ApplicationController end end + # DELETE maps/:id + def destroy + @map.delete + + respond_to do |format| + format.json do + head :no_content + end + end + end + # POST maps/:id/access def access - @map = Map.find(params[:id]) - authorize @map userIds = params[:access] || [] added = userIds.select do |uid| user = User.find(uid) @@ -155,39 +116,44 @@ class MapsController < ApplicationController end end - # POST maps/:id/star - def star - @map = Map.find(params[:id]) - authorize @map - star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - star = Star.create(map_id: @map.id, user_id: current_user.id) unless star - + # GET maps/:id/contains + def contains respond_to do |format| - format.json do - render json: { message: 'Successfully starred map' } - end + format.json { render json: @map.contains(current_user) } end end - # POST maps/:id/unstar - def unstar - @map = Map.find(params[:id]) - authorize @map - star = Star.find_by_map_id_and_user_id(@map.id, current_user.id) - star&.delete + # GET maps/:id/export + def export + exporter = MapExportService.new(current_user, @map) + respond_to do |format| + format.json { render json: exporter.json } + format.csv { send_data exporter.csv } + format.xls { @spreadsheet = exporter.xls } + end + end + + # POST maps/:id/events/:event + def events + valid_event = false + if params[:event] == 'conversation' + Events::ConversationStartedOnMap.publish!(@map, current_user) + valid_event = true + elsif params[:event] == 'user_presence' + Events::UserPresentOnMap.publish!(@map, current_user) + valid_event = true + end respond_to do |format| format.json do - render json: { message: 'Successfully unstarred map' } + head :bad_request unless valid_event + head :ok end end end # POST maps/:id/upload_screenshot def screenshot - @map = Map.find(params[:id]) - authorize @map - png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length..-1]) StringIO.open(png) do |data| data.class.class_eval { attr_accessor :original_filename, :content_type } @@ -203,23 +169,36 @@ class MapsController < ApplicationController end end - # DELETE maps/:id - def destroy - @map = Map.find(params[:id]) - authorize @map - - @map.delete + # POST maps/:id/star + def star + star = Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) respond_to do |format| format.json do - head :no_content + render json: { message: 'Successfully starred map' } + end + end + end + + # POST maps/:id/unstar + def unstar + star = Star.find_by(map_id: @map.id, user_id: current_user.id) + star&.delete + + respond_to do |format| + format.json do + render json: { message: 'Successfully unstarred map' } end end end private - # Never trust parameters from the scary internet, only allow the white list through. + def set_map + @map = Map.find(params[:id]) + authorize @map + end + def create_map_params params.require(:map).permit(:name, :desc, :permission) end @@ -228,32 +207,32 @@ class MapsController < ApplicationController params.require(:map).permit(:id, :name, :arranged, :desc, :permission) end - def create_topics! - topics = params[:topicsToMap] - topics = topics.split(',') - topics.each do |topic| - topic = topic.split('/') - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Topic.find(topic[0]) - mapping.xloc = topic[1] - mapping.yloc = topic[2] - authorize mapping, :create? - mapping.save - end - end + def create_topics! + topics = params[:topicsToMap] + topics = topics.split(',') + topics.each do |topic| + topic = topic.split('/') + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Topic.find(topic[0]) + mapping.xloc = topic[1] + mapping.yloc = topic[2] + authorize mapping, :create? + mapping.save + end + end - def create_synapses! - @synAll = params[:synapsesToMap] - @synAll = @synAll.split(',') - @synAll.each do |synapse_id| - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Synapse.find(synapse_id) - authorize mapping, :create? - mapping.save - end - end + def create_synapses! + @synAll = params[:synapsesToMap] + @synAll = @synAll.split(',') + @synAll.each do |synapse_id| + mapping = Mapping.new + mapping.map = @map + mapping.user = @user + mapping.mappable = Synapse.find(synapse_id) + authorize mapping, :create? + mapping.save + end + end end From 686d80e27412bbe9a088a300ed86b57f91810467 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:02:52 +0800 Subject: [PATCH 081/306] move more logic into map model --- app/controllers/maps_controller.rb | 31 ++++++--------------------- app/models/map.rb | 34 ++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index ff7a1686..1eba68e9 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -90,24 +90,13 @@ class MapsController < ApplicationController # POST maps/:id/access def access - userIds = params[:access] || [] - added = userIds.select do |uid| - user = User.find(uid) - if user.nil? || (current_user && user == current_user) - false - else - !@map.collaborators.include?(user) - end - end - removed = @map.collaborators.select { |user| !userIds.include?(user.id.to_s) }.map(&:id) - added.each do |uid| - UserMap.create(user_id: uid.to_i, map_id: @map.id) - user = User.find(uid.to_i) - MapMailer.invite_to_edit_email(@map, current_user, user).deliver_later - end - removed.each do |uid| - @map.user_maps.select { |um| um.user_id == uid }.each(&:destroy) + user_ids = params[:access] || [] + + added = @map.add_new_collaborators(user_ids) + added.each do |user_id| + MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later end + @map.remove_old_collaborators(user_ids) respond_to do |format| format.json do @@ -154,13 +143,7 @@ class MapsController < ApplicationController # POST maps/:id/upload_screenshot def screenshot - png = Base64.decode64(params[:encoded_image]['data:image/png;base64,'.length..-1]) - StringIO.open(png) do |data| - data.class.class_eval { attr_accessor :original_filename, :content_type } - data.original_filename = 'map-' + @map.id.to_s + '-screenshot.png' - data.content_type = 'image/png' - @map.screenshot = data - end + @map.base64_screenshot(params[:encoded_image]) if @map.save render json: { message: 'Successfully uploaded the map screenshot.' } diff --git a/app/models/map.rb b/app/models/map.rb index 7de744a2..ab3f3eb1 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -127,4 +127,38 @@ class Map < ApplicationRecord json['messages'] = messages.sort_by(&:created_at) json['stars'] = stars end + + def add_new_collaborators(user_ids) + added = [] + users = User.where(id: user_ids) + users.each do |user| + if user && user != current_user && !collaborators.include?(user) + UserMap.create(user_id: uid.to_i, map_id: id) + user = User.find(uid.to_i) + added << user.id + end + end + added + end + + def remove_old_collaborators(user_ids) + removed = [] + collaborators.map(&:id).each do |user_id| + if !user_ids.include?(user_id) + user_maps.select { |um| um.user_id == user_id }.each(&:destroy) + removed << user_id + end + end + removed + end + + def base64_screenshot(encoded_image) + png = Base64.decode64(encoded_image['data:image/png;base64,'.length..-1]) + StringIO.open(png) do |data| + data.class.class_eval { attr_accessor :original_filename, :content_type } + data.original_filename = 'map-' + @map.id.to_s + '-screenshot.png' + data.content_type = 'image/png' + @map.screenshot = data + end + end end From 5e180ac10e5d0348c9b5b4a2f30d865df2331ec2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 17:07:06 +0800 Subject: [PATCH 082/306] set up explore controller routes and rename methods --- app/controllers/explore_controller.rb | 12 ++++++------ config/routes.rb | 14 ++++++++------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index c21454d1..023a6866 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,7 +9,7 @@ class ExploreController < ApplicationController #autocomplete :map, :name, full: true, extra_data: [:user_id] # GET /explore/active - def activemaps + def active page = params[:page].present? ? params[:page] : 1 @maps = policy_scope(Map).order('updated_at DESC') .page(page).per(20) @@ -25,7 +25,7 @@ class ExploreController < ApplicationController end # GET /explore/featured - def featuredmaps + def featured page = params[:page].present? ? params[:page] : 1 @maps = policy_scope( Map.where('maps.featured = ? AND maps.permission != ?', @@ -39,7 +39,7 @@ class ExploreController < ApplicationController end # GET /explore/mine - def mymaps + def mine unless authenticated? skip_policy_scope return redirect_to explore_active_path @@ -57,7 +57,7 @@ class ExploreController < ApplicationController end # GET /explore/shared - def sharedmaps + def shared unless authenticated? skip_policy_scope return redirect_to explore_active_path @@ -75,7 +75,7 @@ class ExploreController < ApplicationController end # GET /explore/starred - def starredmaps + def starred unless authenticated? skip_policy_scope return redirect_to explore_active_path @@ -94,7 +94,7 @@ class ExploreController < ApplicationController end # GET /explore/mapper/:id - def usermaps + def mapper page = params[:page].present? ? params[:page] : 1 @user = User.find(params[:id]) @maps = policy_scope(Map.where(user: @user)) diff --git a/config/routes.rb b/config/routes.rb index fe48b6ba..f9a1be8f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -57,12 +57,14 @@ Metamaps::Application.routes.draw do post 'maps/:id/star', to: 'maps#star', defaults: { format: :json } post 'maps/:id/unstar', to: 'maps#unstar', defaults: { format: :json } - get 'explore/active', to: 'maps#activemaps' - get 'explore/featured', to: 'maps#featuredmaps' - get 'explore/mine', to: 'maps#mymaps' - get 'explore/shared', to: 'maps#sharedmaps' - get 'explore/starred', to: 'maps#starredmaps' - get 'explore/mapper/:id', to: 'maps#usermaps' + namespace :explore do + get 'active' + get 'featured' + get 'mine' + get 'shared' + get 'starred' + get 'mapper/:id', action: 'mapper' + end devise_for :users, skip: :sessions, controllers: { registrations: 'users/registrations', From b722d2d3b07e4a74901b0fd98e723ed55490a047 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 23 Sep 2016 18:20:06 +0800 Subject: [PATCH 083/306] fix map controller create spec --- app/controllers/maps_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 1eba68e9..a74a35c2 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -183,7 +183,7 @@ class MapsController < ApplicationController end def create_map_params - params.require(:map).permit(:name, :desc, :permission) + params.permit(:name, :desc, :permission) end def update_map_params From 3f9077b3805e77735c9673ea5c81ebb7b47e380f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:35:06 +0800 Subject: [PATCH 084/306] clean up --- app/policies/map_policy.rb | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 4cb2db38..84d24ca4 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -41,10 +41,6 @@ class MapPolicy < ApplicationPolicy user.present? && record.user == user end - def activemaps? - user.blank? # redirect to root url if authenticated for some reason - end - def contains? show? end @@ -57,14 +53,6 @@ class MapPolicy < ApplicationPolicy show? end - def featuredmaps? - true - end - - def mymaps? - user.present? - end - def star? unstar? end @@ -76,8 +64,4 @@ class MapPolicy < ApplicationPolicy def screenshot? update? end - - def usermaps? - true - end end From c76de5b1d5cb18add72499ff41d72ae74bbaf4e7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:54:14 +0800 Subject: [PATCH 085/306] refactor map model a bit and fix bugs --- app/models/map.rb | 52 ++++++++++++++++++++--------------------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index ab3f3eb1..0cab398d 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -111,45 +111,37 @@ class Map < ApplicationRecord # user param helps determine what records are visible def contains(user) - allmappers = contributors - allcollaborators = editors - alltopics = Pundit.policy_scope(user, topics).to_a - allsynapses = Pundit.policy_scope(user, synapses).to_a - allmappings = Pundit.policy_scope(user, mappings).to_a - - json = {} - json['map'] = self - json['topics'] = alltopics - json['synapses'] = allsynapses - json['mappings'] = allmappings - json['mappers'] = allmappers - json['collaborators'] = allcollaborators - json['messages'] = messages.sort_by(&:created_at) - json['stars'] = stars + { + map: self, + topics: Pundit.policy_scope(user, topics).to_a, + synapses: Pundit.policy_scope(user, synapses).to_a, + mappings: Pundit.policy_scope(user, mappings).to_a, + mappers: contributors, + collaborators: editors, + messages: messages.sort_by(&:created_at), + stars: stars + } end def add_new_collaborators(user_ids) - added = [] users = User.where(id: user_ids) - users.each do |user| - if user && user != current_user && !collaborators.include?(user) - UserMap.create(user_id: uid.to_i, map_id: id) - user = User.find(uid.to_i) - added << user.id - end + current_collaborators = collaborators + [user] + added = users.map do |new_user| + next nil if current_collaborators.include?(new_user) + UserMap.create(user_id: new_user.id, map_id: id) + new_user.id end - added + added.compact end def remove_old_collaborators(user_ids) - removed = [] - collaborators.map(&:id).each do |user_id| - if !user_ids.include?(user_id) - user_maps.select { |um| um.user_id == user_id }.each(&:destroy) - removed << user_id - end + current_collaborators = collaborators + [user] + removed = current_collaborators.map(&:id).map do |old_user_id| + next nil if user_ids.include?(old_user_id) + user_maps.where(user_id: old_user_id).find_each(&:destroy) + old_user_id end - removed + removed.compact end def base64_screenshot(encoded_image) From dad048eb20341a6acd27bf2e72dd65b5c17e0809 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 12:58:09 +0800 Subject: [PATCH 086/306] rubocop --- app/controllers/explore_controller.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 023a6866..3c099920 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class ExploreController < ApplicationController before_action :authorize_explore after_action :verify_authorized @@ -5,8 +6,8 @@ class ExploreController < ApplicationController respond_to :html, :json, :csv - # TODO remove? - #autocomplete :map, :name, full: true, extra_data: [:user_id] + # TODO: remove? + # autocomplete :map, :name, full: true, extra_data: [:user_id] # GET /explore/active def active From 50f98aebea73fab221a4d0534fb07aef7da6b48b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 24 Sep 2016 14:35:23 +0800 Subject: [PATCH 087/306] explore controller spec --- app/controllers/explore_controller.rb | 12 +++++------ app/policies/explore_policy.rb | 12 +++++------ spec/controllers/explore_controller_spec.rb | 23 +++++++++++++++++++++ spec/controllers/maps_controller_spec.rb | 15 -------------- 4 files changed, 35 insertions(+), 27 deletions(-) create mode 100644 spec/controllers/explore_controller_spec.rb diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 3c099920..6f24eba5 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -21,7 +21,7 @@ class ExploreController < ApplicationController redirect_to(root_url) && return if authenticated? respond_with(@maps, @user) end - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -35,7 +35,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -53,7 +53,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -71,7 +71,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -90,7 +90,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end @@ -103,7 +103,7 @@ class ExploreController < ApplicationController respond_to do |format| format.html { respond_with(@maps, @user) } - format.json { render json: @maps } + format.json { render json: @maps.to_json } end end diff --git a/app/policies/explore_policy.rb b/app/policies/explore_policy.rb index 6cbdab15..b4d52fe5 100644 --- a/app/policies/explore_policy.rb +++ b/app/policies/explore_policy.rb @@ -1,25 +1,25 @@ class ExplorePolicy < ApplicationPolicy - def activemaps? + def active? true end - def featuredmaps? + def featured? true end - def mymaps? + def mine? true end - def sharedmaps? + def shared? true end - def starredmaps? + def starred? true end - def usermaps? + def mapper? true end end diff --git a/spec/controllers/explore_controller_spec.rb b/spec/controllers/explore_controller_spec.rb new file mode 100644 index 00000000..4e298a92 --- /dev/null +++ b/spec/controllers/explore_controller_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe ExploreController, type: :controller do + before :each do + sign_in create(:user) + end + + describe 'GET explore/active' do + context 'always returns an array' do + it 'with 0 records' do + Map.delete_all + get :active, format: :json + expect(JSON.parse(response.body)).to eq [] + end + it 'with 1 record' do + map = create(:map) + get :active, format: :json + expect(JSON.parse(response.body).class).to be Array + end + end + end +end diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index b877dc88..0f053dd9 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -9,21 +9,6 @@ RSpec.describe MapsController, type: :controller do sign_in create(:user) end - describe 'GET #activemaps' do - context 'always returns an array' do - it 'with 0 records' do - Map.delete_all - get :activemaps, format: :json - expect(JSON.parse(response.body)).to eq [] - end - it 'with 1 record' do - map = create(:map) - get :activemaps, format: :json - expect(JSON.parse(response.body).class).to be Array - end - end - end - describe 'POST #create' do context 'with valid params' do it 'creates a new Map' do From 18d8929bf159821d2231a4833c443b3eb8c571d7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:35:26 +0800 Subject: [PATCH 088/306] use .or to fix all sorts of @map.mappings bugs --- app/models/map.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/models/map.rb b/app/models/map.rb index 0cab398d..f9fe6312 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -16,11 +16,12 @@ class Map < ApplicationRecord has_many :events, -> { includes :user }, as: :eventable, dependent: :destroy # This method associates the attribute ":image" with a file attachment - has_attached_file :screenshot, styles: { - thumb: ['188x126#', :png] - #:full => ['940x630#', :png] - }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' + has_attached_file :screenshot, + styles: { + thumb: ['188x126#', :png] + #:full => ['940x630#', :png] + }, + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } @@ -31,7 +32,7 @@ class Map < ApplicationRecord validates_attachment_content_type :screenshot, content_type: /\Aimage\/.*\Z/ def mappings - topicmappings + synapsemappings + topicmappings.or(synapsemappings) end def mk_permission From 05495b02246afa85845e0c70b5668283295f2c50 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:35:35 +0800 Subject: [PATCH 089/306] move explore views to their own folder --- app/views/{maps/activemaps.html.erb => explore/active.html.erb} | 0 .../{maps/featuredmaps.html.erb => explore/featured.html.erb} | 0 app/views/{maps/usermaps.html.erb => explore/mapper.html.erb} | 0 app/views/{maps/mymaps.html.erb => explore/mine.html.erb} | 0 app/views/{maps/sharedmaps.html.erb => explore/shared.html.erb} | 0 app/views/{maps/starredmaps.html.erb => explore/starred.html.erb} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename app/views/{maps/activemaps.html.erb => explore/active.html.erb} (100%) rename app/views/{maps/featuredmaps.html.erb => explore/featured.html.erb} (100%) rename app/views/{maps/usermaps.html.erb => explore/mapper.html.erb} (100%) rename app/views/{maps/mymaps.html.erb => explore/mine.html.erb} (100%) rename app/views/{maps/sharedmaps.html.erb => explore/shared.html.erb} (100%) rename app/views/{maps/starredmaps.html.erb => explore/starred.html.erb} (100%) diff --git a/app/views/maps/activemaps.html.erb b/app/views/explore/active.html.erb similarity index 100% rename from app/views/maps/activemaps.html.erb rename to app/views/explore/active.html.erb diff --git a/app/views/maps/featuredmaps.html.erb b/app/views/explore/featured.html.erb similarity index 100% rename from app/views/maps/featuredmaps.html.erb rename to app/views/explore/featured.html.erb diff --git a/app/views/maps/usermaps.html.erb b/app/views/explore/mapper.html.erb similarity index 100% rename from app/views/maps/usermaps.html.erb rename to app/views/explore/mapper.html.erb diff --git a/app/views/maps/mymaps.html.erb b/app/views/explore/mine.html.erb similarity index 100% rename from app/views/maps/mymaps.html.erb rename to app/views/explore/mine.html.erb diff --git a/app/views/maps/sharedmaps.html.erb b/app/views/explore/shared.html.erb similarity index 100% rename from app/views/maps/sharedmaps.html.erb rename to app/views/explore/shared.html.erb diff --git a/app/views/maps/starredmaps.html.erb b/app/views/explore/starred.html.erb similarity index 100% rename from app/views/maps/starredmaps.html.erb rename to app/views/explore/starred.html.erb From 03ba3a89f1cf4c871f691d32fed7721fde605063 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 23:37:08 +0800 Subject: [PATCH 090/306] main controller renders by name --- app/controllers/main_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 0d6af64b..4624c7a6 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -17,7 +17,7 @@ class MainController < ApplicationController if !authenticated? render 'main/home' else - render 'maps/activemaps' + render 'explore/active' end end end From c20e5037854ee05cb3296af8326fba83267c56e8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 22:27:11 +0800 Subject: [PATCH 091/306] show/hide add a topic instructions more consistently --- frontend/src/Metamaps/Backbone/index.js | 1 - frontend/src/Metamaps/Control.js | 27 ++++++++++++++++--------- frontend/src/Metamaps/Create.js | 6 ++++++ frontend/src/Metamaps/JIT.js | 11 ++++++---- package.json | 1 + 5 files changed, 31 insertions(+), 15 deletions(-) diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index 2c7ae530..b1ba9e78 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -98,7 +98,6 @@ _Backbone.Map = Backbone.Model.extend({ $.ajax({ url: '/maps/' + this.id + '/contains.json', success: start, - error: errorFunc, async: false }) }, diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index 2c14cfca..c6c963ac 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -1,6 +1,7 @@ /* global Metamaps, $ */ import _ from 'lodash' +import outdent from 'outdent' import Active from './Active' import Filter from './Filter' @@ -52,9 +53,8 @@ const Control = { var n = Selected.Nodes.length var e = Selected.Edges.length - var ntext = n == 1 ? '1 topic' : n + ' topics' - var etext = e == 1 ? '1 synapse' : e + ' synapses' - var text = 'You have ' + ntext + ' and ' + etext + ' selected. ' + var ntext = n === 1 ? '1 topic' : n + ' topics' + var etext = e === 1 ? '1 synapse' : e + ' synapses' var authorized = Active.Map.authorizeToEdit(Active.Mapper) @@ -63,11 +63,18 @@ const Control = { return } - var r = confirm(text + 'Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.') - if (r == true) { + var r = confirm(outdent` + You have ${ntext} and ${etext} selected. Are you sure you want + to permanently delete them all? This will remove them from all + maps they appear on.`) + if (r) { Control.deleteSelectedEdges() Control.deleteSelectedNodes() } + + if (Metamaps.Topics.length === 0) { + GlobalUI.showDiv('#instructions') + } }, deleteSelectedNodes: function () { // refers to deleting topics permanently if (!Active.Map) return @@ -191,7 +198,7 @@ const Control = { duration: 500 }) setTimeout(function () { - if (nodeid == Visualize.mGraph.root) { // && Visualize.type === "RGraph" + if (nodeid === Visualize.mGraph.root) { // && Visualize.type === "RGraph" var newroot = _.find(graph.graph.nodes, function (n) { return n.id !== nodeid; }) graph.root = newroot ? newroot.id : null } @@ -231,7 +238,7 @@ const Control = { color: Settings.colors.synapses.normal }) - if (Mouse.edgeHoveringOver == edge) { + if (Mouse.edgeHoveringOver === edge) { edge.setDataset('current', { showDesc: true, lineWidth: 4 @@ -414,8 +421,8 @@ const Control = { } } - var nString = nCount == 1 ? (nCount.toString() + ' topic and ') : (nCount.toString() + ' topics and ') - var sString = sCount == 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') + var nString = nCount === 1 ? (nCount.toString() + ' topic and ') : (nCount.toString() + ' topics and ') + var sString = sCount === 1 ? (sCount.toString() + ' synapse') : (sCount.toString() + ' synapses') var message = nString + sString + ' you created updated to ' + permission GlobalUI.notifyUser(message) @@ -444,7 +451,7 @@ const Control = { } } - var nString = nCount == 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') + var nString = nCount === 1 ? (nCount.toString() + ' topic') : (nCount.toString() + ' topics') var message = nString + ' you can edit updated to ' + metacode.get('name') GlobalUI.notifyUser(message) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index c9252aba..1fc18b87 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -13,6 +13,7 @@ import GlobalUI from './GlobalUI' * Dependencies: * - Metamaps.Backbone * - Metamaps.Metacodes + * - Metamaps.Topics */ const Create = { @@ -223,8 +224,10 @@ const Create = { }) Create.newTopic.beingCreated = true Create.newTopic.name = '' + GlobalUI.hideDiv('#instructions') }, hide: function (force) { + if (Create.newTopic.beingCreated === false) return if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') Create.newTopic.beingCreated = false @@ -234,6 +237,9 @@ const Create = { Create.newTopic.pinned = false } $('#topic_name').typeahead('val', '') + if (Metamaps.Topics.length === 0) { + GlobalUI.showDiv('#instructions') + } } }, newSynapse: { diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 2b14d18c..fa5c894f 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -148,11 +148,14 @@ const JIT = { if (Metamaps.Mappings) Metamaps.Mappings.remove(mapping) }) + // set up addTopic instructions in case they delete all the topics + // i.e. if there are 0 topics at any time, it should have instructions again + $('#instructions div').hide() + if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { + $('#instructions div.addTopic').show() + } + if (self.vizData.length == 0) { - $('#instructions div').hide() - if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { - $('#instructions div.addTopic').show() - } GlobalUI.showDiv('#instructions') Visualize.loadLater = true } else { diff --git a/package.json b/package.json index b71e34bf..37d953ee 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "csv-parse": "1.1.7", "lodash": "4.16.1", "node-uuid": "1.4.7", + "outdent": "0.2.1", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", From 0e17ec11ec17ca9d137201669dff33f6aae01180 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:04:09 +0800 Subject: [PATCH 092/306] fix eslint config for code climate this is MOSTLY the same as feross/standard --- .eslintrc.js | 166 +++++++++++++++++++++++++++++++++++++++++++++++++-- package.json | 8 +-- 2 files changed, 164 insertions(+), 10 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index bc65fe94..26319254 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,9 +1,165 @@ module.exports = { - "sourceType": "module", + "sourceType": "module", "parser": "babel-eslint", - "extends": "standard", "installedESLint": true, + "env": { + "es6": true, + "node": true + }, "plugins": [ - "standard" - ] -}; + "react" + ], + "globals": { + "document": false, + "navigator": false, + "window": false + }, + "rules": { + "accessor-pairs": 2, + "arrow-spacing": [2, { "before": true, "after": true }], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs", { "allowSingleLine": true }], + "camelcase": [2, { "properties": "never" }], + "comma-dangle": [2, "never"], + "comma-spacing": [2, { "before": false, "after": true }], + "comma-style": [2, "last"], + "constructor-super": 2, + "curly": [2, "multi-line"], + "dot-location": [2, "property"], + "eol-last": 2, + "eqeqeq": [2, "allow-null"], + // errors on code climate - disable for now + //"func-call-spacing": [2, "never"], + "handle-callback-err": [2, "^(err|error)$" ], + "indent": [2, 2, { "SwitchCase": 1 }], + "key-spacing": [2, { "beforeColon": false, "afterColon": true }], + // errors on code climate - disable for now + //"keyword-spacing": [2, { "before": true, "after": true }], + "new-cap": [2, { "newIsCap": true, "capIsNew": false }], + "new-parens": 2, + "no-array-constructor": 2, + "no-caller": 2, + "no-class-assign": 2, + "no-cond-assign": 2, + "no-const-assign": 2, + // errors on code climate - disable for now + //"no-constant-condition": [2, { "checkLoops": false }], + "no-control-regex": 2, + "no-debugger": 2, + "no-delete-var": 2, + "no-dupe-args": 2, + "no-dupe-class-members": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + // errors on code climate - disable for now + //"no-duplicate-imports": 2, + "no-empty-character-class": 2, + "no-empty-pattern": 2, + "no-eval": 2, + "no-ex-assign": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-extra-boolean-cast": 2, + "no-extra-parens": [2, "functions"], + "no-fallthrough": 2, + "no-floating-decimal": 2, + "no-func-assign": 2, + // errors on code climate - disable for now + //"no-global-assign": 2, + "no-implied-eval": 2, + "no-inner-declarations": [2, "functions"], + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-label-var": 2, + // errors on code climate - disable for now + //"no-labels": [2, { "allowLoop": false, "allowSwitch": false }], + "no-lone-blocks": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-multiple-empty-lines": [2, { "max": 1 }], + "no-native-reassign": 2, + "no-negated-in-lhs": 2, + "no-new": 2, + "no-new-func": 2, + "no-new-object": 2, + "no-new-require": 2, + // errors on code climate - disable for now + //"no-new-symbol": 2, + "no-new-wrappers": 2, + "no-obj-calls": 2, + "no-octal": 2, + "no-octal-escape": 2, + "no-path-concat": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-regex-spaces": 2, + "no-return-assign": [2, "except-parens"], + // errors on code climate - disable for now + //"no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-shadow-restricted-names": 2, + "no-sparse-arrays": 2, + // errors on code climate - disable for now + //"no-tabs": 2, + // errors on code climate - disable for now + //"no-template-curly-in-string": 2, + "no-this-before-super": 2, + "no-throw-literal": 2, + "no-trailing-spaces": 2, + "no-undef": 2, + "no-undef-init": 2, + "no-unexpected-multiline": 2, + // errors on code climate - disable for now + //"no-unmodified-loop-condition": 2, + "no-unneeded-ternary": [2, { "defaultAssignment": false }], + "no-unreachable": 2, + // errors on code climate - disable for now + //"no-unsafe-finally": 2, + // errors on code climate - disable for now + //"no-unsafe-negation": 2, + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-useless-call": 2, + // errors on code climate - disable for now + //"no-useless-computed-key": 2, + // errors on code climate - disable for now + //"no-useless-constructor": 2, + // errors on code climate - disable for now + //"no-useless-escape": 2, + // errors on code climate - disable for now + //"no-useless-rename": 2, + // errors on code climate - disable for now + //"no-whitespace-before-property": 2, + "no-with": 2, + // errors on code climate - disable for now + //"object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], + "one-var": [2, { "initialized": "never" }], + "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], + "padded-blocks": [2, "never"], + // errors on code climate - disable for now + //"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + // errors on code climate - disable for now + //"rest-spread-spacing": [2, "never"], + "semi": [2, "never"], + "semi-spacing": [2, { "before": false, "after": true }], + "space-before-blocks": [2, "always"], + "space-before-function-paren": [2, "always"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + // errors on code climate - disable for now + //"spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], + // errors on code climate - disable for now + //"template-curly-spacing": [2, "never"], + // errors on code climate - disable for now + //"unicode-bom": [2, "never"], + "use-isnan": 2, + "valid-typeof": 2, + "wrap-iife": [2, "any"], + // errors on code climate - disable for now + //"yield-star-spacing": [2, "both"], + "yoda": [2, "never"], + } +} diff --git a/package.json b/package.json index 37d953ee..c6c74f44 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,10 @@ "webpack": "1.13.2" }, "devDependencies": { - "chai": "^3.5.0", - "mocha": "^3.0.2", "babel-eslint": "^6.1.2", + "chai": "^3.5.0", "eslint": "^3.5.0", - "eslint-config-standard": "^6.0.1", - "eslint-plugin-promise": "^2.0.1", - "eslint-plugin-standard": "^2.0.0" + "eslint-plugin-react": "^6.3.0", + "mocha": "^3.0.2" } } From ebaae084ae27890274c581b6f4fbe4b305ea034c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 13:37:42 +0800 Subject: [PATCH 093/306] simple eslint fixes --- frontend/src/Metamaps/Create.js | 4 ++-- frontend/src/Metamaps/GlobalUI.js | 2 +- frontend/src/Metamaps/JIT.js | 20 ++++++++------------ frontend/src/Metamaps/Map/InfoBox.js | 6 +++--- frontend/src/Metamaps/Realtime.js | 4 ++-- frontend/src/Metamaps/Views/ChatView.js | 8 +++++++- frontend/src/Metamaps/index.js | 2 +- 7 files changed, 24 insertions(+), 22 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 1fc18b87..5d290fcd 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, Hogan, Bloodhound */ import Mouse from './Mouse' import Selected from './Selected' @@ -54,7 +54,7 @@ const Create = { }, updateMetacodeSet: function (set, index, custom) { if (custom && Create.newSelectedMetacodes.length == 0) { - alert('Please select at least one metacode to use!') + window.alert('Please select at least one metacode to use!') return false } diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index b24b31c7..7af133de 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,3 +1,4 @@ +/* global Metamaps, $, Hogan, Bloodhound */ import Active from './Active' import Create from './Create' import Filter from './Filter' @@ -585,7 +586,6 @@ GlobalUI.Search = { if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { self.close(0, true); - var win; if (datum.rtype == "topic") { Router.topics(datum.id); } else if (datum.rtype == "map") { diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index fa5c894f..ba8bdb8f 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,4 +1,4 @@ -/* global Metamaps, $jit */ +/* global Metamaps, $, Image, CanvasLoader */ import _ from 'lodash' @@ -79,7 +79,6 @@ const JIT = { var jitReady = [] var synapsesToRemove = [] - var topic var mapping var node var nodes = {} @@ -183,8 +182,6 @@ const JIT = { if (!synapse) return // this means there are no corresponding synapses for // this edge, don't render it - var directionCat = synapse.get('category') - // label placement on edges if (canvas.denySelected) { var color = Settings.colors.synapses.normal @@ -282,8 +279,8 @@ const JIT = { if (synapseNum > 1) { var ctx = canvas.getCtx() - var x = (pos.x + posChild.x) / 2 - var y = (pos.y + posChild.y) / 2 + const x = (pos.x + posChild.x) / 2 + const y = (pos.y + posChild.y) / 2 drawSynapseCount(ctx, x, y, synapseNum) } } @@ -847,8 +844,8 @@ const JIT = { // set the draw synapse start positions var l = Selected.Nodes.length if (l > 0) { - for (var i = l - 1; i >= 0; i -= 1) { - var n = Selected.Nodes[i] + for (let i = l - 1; i >= 0; i -= 1) { + const n = Selected.Nodes[i] Mouse.synapseStartCoordinates.push({ x: n.pos.getc().x, y: n.pos.getc().y @@ -1541,8 +1538,6 @@ const JIT = { if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered - var authorized - e.preventDefault() e.stopPropagation() @@ -1827,10 +1822,11 @@ const JIT = { width = canvas.getSize().width, maxX, minX, maxY, minY, counter = 0 + let nodes if (!denySelected && Selected.Nodes.length > 0) { - var nodes = Selected.Nodes + nodes = Selected.Nodes } else { - var nodes = _.values(Visualize.mGraph.graph.nodes) + nodes = _.values(Visualize.mGraph.graph.nodes) } if (nodes.length > 1) { diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index ec5c1405..c36f70c3 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, Hogan, Bloodhound, Countable */ import Active from '../Active' import GlobalUI from '../GlobalUI' @@ -236,7 +236,7 @@ const InfoBox = { Metamaps.Collaborators.add(mapper) var mapperIds = Metamaps.Collaborators.models.map(function (mapper) { return mapper.id }) $.post('/maps/' + Active.Map.id + '/access', { access: mapperIds }) - var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') + var name = Metamaps.Collaborators.get(newCollaboratorId).get('name') GlobalUI.notifyUser(name + ' will be notified by email') self.updateNumbers() } @@ -349,7 +349,7 @@ const InfoBox = { var confirmString = 'Are you sure you want to delete this map? ' confirmString += 'This action is irreversible. It will not delete the topics and synapses on the map.' - var doIt = confirm(confirmString) + var doIt = window.confirm(confirmString) var map = Active.Map var mapper = Active.Mapper var authorized = map.authorizePermissionChange(mapper) diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 355e73f8..608523c8 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, SocketIoConnection, SimpleWebRTC */ import _ from 'lodash' @@ -1106,7 +1106,7 @@ const Realtime = { } }, newSynapse: function (data) { - var topic1, topic2, node1, node2, synapse, mapping, cancel + var topic1, topic2, node1, node2, synapse, mapping, cancel, mapper var self = Realtime var socket = self.socket diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index a49aaa4d..473c6f1e 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -1,7 +1,13 @@ -/* global $ */ +/* global Metamaps, $, Howl */ + +/* + * Dependencies: + * Metamaps.Erb + */ import Backbone from 'backbone' import Autolinker from 'autolinker' +import _ from 'lodash' // TODO is this line good or bad // Backbone.$ = window.$ diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index 21a3fb8d..db67409b 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -1,4 +1,4 @@ -/* global $ */ +/* global Metamaps */ import Account from './Account' import Active from './Active' From c9a79468f477273013ab4456d8f0a9ac8cb35449 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 13:40:02 +0800 Subject: [PATCH 094/306] switch to eslint-3 --- .codeclimate.yml | 1 + .eslintrc.js | 75 ++++++++++++++++-------------------------------- 2 files changed, 26 insertions(+), 50 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index d3c19ad6..53f90d17 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -12,6 +12,7 @@ engines: - javascript eslint: enabled: true + channel: "eslint-3" fixme: enabled: true rubocop: diff --git a/.eslintrc.js b/.eslintrc.js index 26319254..55c4bec8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,13 +28,11 @@ module.exports = { "dot-location": [2, "property"], "eol-last": 2, "eqeqeq": [2, "allow-null"], - // errors on code climate - disable for now - //"func-call-spacing": [2, "never"], + "func-call-spacing": [2, "never"], "handle-callback-err": [2, "^(err|error)$" ], "indent": [2, 2, { "SwitchCase": 1 }], "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - // errors on code climate - disable for now - //"keyword-spacing": [2, { "before": true, "after": true }], + "keyword-spacing": [2, { "before": true, "after": true }], "new-cap": [2, { "newIsCap": true, "capIsNew": false }], "new-parens": 2, "no-array-constructor": 2, @@ -42,8 +40,7 @@ module.exports = { "no-class-assign": 2, "no-cond-assign": 2, "no-const-assign": 2, - // errors on code climate - disable for now - //"no-constant-condition": [2, { "checkLoops": false }], + "no-constant-condition": [2, { "checkLoops": false }], "no-control-regex": 2, "no-debugger": 2, "no-delete-var": 2, @@ -51,8 +48,7 @@ module.exports = { "no-dupe-class-members": 2, "no-dupe-keys": 2, "no-duplicate-case": 2, - // errors on code climate - disable for now - //"no-duplicate-imports": 2, + "no-duplicate-imports": 2, "no-empty-character-class": 2, "no-empty-pattern": 2, "no-eval": 2, @@ -64,16 +60,14 @@ module.exports = { "no-fallthrough": 2, "no-floating-decimal": 2, "no-func-assign": 2, - // errors on code climate - disable for now - //"no-global-assign": 2, + "no-global-assign": 2, "no-implied-eval": 2, "no-inner-declarations": [2, "functions"], "no-invalid-regexp": 2, "no-irregular-whitespace": 2, "no-iterator": 2, "no-label-var": 2, - // errors on code climate - disable for now - //"no-labels": [2, { "allowLoop": false, "allowSwitch": false }], + "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], "no-lone-blocks": 2, "no-mixed-spaces-and-tabs": 2, "no-multi-spaces": 2, @@ -85,8 +79,7 @@ module.exports = { "no-new-func": 2, "no-new-object": 2, "no-new-require": 2, - // errors on code climate - disable for now - //"no-new-symbol": 2, + "no-new-symbol": 2, "no-new-wrappers": 2, "no-obj-calls": 2, "no-octal": 2, @@ -96,52 +89,38 @@ module.exports = { "no-redeclare": 2, "no-regex-spaces": 2, "no-return-assign": [2, "except-parens"], - // errors on code climate - disable for now - //"no-self-assign": 2, + "no-self-assign": 2, "no-self-compare": 2, "no-sequences": 2, "no-shadow-restricted-names": 2, "no-sparse-arrays": 2, - // errors on code climate - disable for now - //"no-tabs": 2, - // errors on code climate - disable for now - //"no-template-curly-in-string": 2, + "no-tabs": 2, + "no-template-curly-in-string": 2, "no-this-before-super": 2, "no-throw-literal": 2, "no-trailing-spaces": 2, "no-undef": 2, "no-undef-init": 2, "no-unexpected-multiline": 2, - // errors on code climate - disable for now - //"no-unmodified-loop-condition": 2, + "no-unmodified-loop-condition": 2, "no-unneeded-ternary": [2, { "defaultAssignment": false }], "no-unreachable": 2, - // errors on code climate - disable for now - //"no-unsafe-finally": 2, - // errors on code climate - disable for now - //"no-unsafe-negation": 2, + "no-unsafe-finally": 2, + "no-unsafe-negation": 2, "no-unused-vars": [2, { "vars": "all", "args": "none" }], "no-useless-call": 2, - // errors on code climate - disable for now - //"no-useless-computed-key": 2, - // errors on code climate - disable for now - //"no-useless-constructor": 2, - // errors on code climate - disable for now - //"no-useless-escape": 2, - // errors on code climate - disable for now - //"no-useless-rename": 2, - // errors on code climate - disable for now - //"no-whitespace-before-property": 2, + "no-useless-computed-key": 2, + "no-useless-constructor": 2, + "no-useless-escape": 2, + "no-useless-rename": 2, + "no-whitespace-before-property": 2, "no-with": 2, - // errors on code climate - disable for now - //"object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], + "object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], "one-var": [2, { "initialized": "never" }], "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], "padded-blocks": [2, "never"], - // errors on code climate - disable for now - //"quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], - // errors on code climate - disable for now - //"rest-spread-spacing": [2, "never"], + "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], + "rest-spread-spacing": [2, "never"], "semi": [2, "never"], "semi-spacing": [2, { "before": false, "after": true }], "space-before-blocks": [2, "always"], @@ -149,17 +128,13 @@ module.exports = { "space-in-parens": [2, "never"], "space-infix-ops": 2, "space-unary-ops": [2, { "words": true, "nonwords": false }], - // errors on code climate - disable for now - //"spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], - // errors on code climate - disable for now - //"template-curly-spacing": [2, "never"], - // errors on code climate - disable for now - //"unicode-bom": [2, "never"], + "spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], + "template-curly-spacing": [2, "never"], + "unicode-bom": [2, "never"], "use-isnan": 2, "valid-typeof": 2, "wrap-iife": [2, "any"], - // errors on code climate - disable for now - //"yield-star-spacing": [2, "both"], + "yield-star-spacing": [2, "both"], "yoda": [2, "never"], } } From 12cb675bb5be164f74f63fdf63e12c93596e14fd Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 13:44:23 +0800 Subject: [PATCH 095/306] switch to using the eslint-standard plugin again --- .eslintrc.js | 133 ++------------------------------------------------- 1 file changed, 4 insertions(+), 129 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 55c4bec8..11a46fd1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,140 +1,15 @@ module.exports = { "sourceType": "module", "parser": "babel-eslint", + "extends": "standard", "installedESLint": true, "env": { "es6": true, "node": true }, "plugins": [ + "promise", + "standard", "react" - ], - "globals": { - "document": false, - "navigator": false, - "window": false - }, - "rules": { - "accessor-pairs": 2, - "arrow-spacing": [2, { "before": true, "after": true }], - "block-spacing": [2, "always"], - "brace-style": [2, "1tbs", { "allowSingleLine": true }], - "camelcase": [2, { "properties": "never" }], - "comma-dangle": [2, "never"], - "comma-spacing": [2, { "before": false, "after": true }], - "comma-style": [2, "last"], - "constructor-super": 2, - "curly": [2, "multi-line"], - "dot-location": [2, "property"], - "eol-last": 2, - "eqeqeq": [2, "allow-null"], - "func-call-spacing": [2, "never"], - "handle-callback-err": [2, "^(err|error)$" ], - "indent": [2, 2, { "SwitchCase": 1 }], - "key-spacing": [2, { "beforeColon": false, "afterColon": true }], - "keyword-spacing": [2, { "before": true, "after": true }], - "new-cap": [2, { "newIsCap": true, "capIsNew": false }], - "new-parens": 2, - "no-array-constructor": 2, - "no-caller": 2, - "no-class-assign": 2, - "no-cond-assign": 2, - "no-const-assign": 2, - "no-constant-condition": [2, { "checkLoops": false }], - "no-control-regex": 2, - "no-debugger": 2, - "no-delete-var": 2, - "no-dupe-args": 2, - "no-dupe-class-members": 2, - "no-dupe-keys": 2, - "no-duplicate-case": 2, - "no-duplicate-imports": 2, - "no-empty-character-class": 2, - "no-empty-pattern": 2, - "no-eval": 2, - "no-ex-assign": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-extra-boolean-cast": 2, - "no-extra-parens": [2, "functions"], - "no-fallthrough": 2, - "no-floating-decimal": 2, - "no-func-assign": 2, - "no-global-assign": 2, - "no-implied-eval": 2, - "no-inner-declarations": [2, "functions"], - "no-invalid-regexp": 2, - "no-irregular-whitespace": 2, - "no-iterator": 2, - "no-label-var": 2, - "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], - "no-lone-blocks": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-multiple-empty-lines": [2, { "max": 1 }], - "no-native-reassign": 2, - "no-negated-in-lhs": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-object": 2, - "no-new-require": 2, - "no-new-symbol": 2, - "no-new-wrappers": 2, - "no-obj-calls": 2, - "no-octal": 2, - "no-octal-escape": 2, - "no-path-concat": 2, - "no-proto": 2, - "no-redeclare": 2, - "no-regex-spaces": 2, - "no-return-assign": [2, "except-parens"], - "no-self-assign": 2, - "no-self-compare": 2, - "no-sequences": 2, - "no-shadow-restricted-names": 2, - "no-sparse-arrays": 2, - "no-tabs": 2, - "no-template-curly-in-string": 2, - "no-this-before-super": 2, - "no-throw-literal": 2, - "no-trailing-spaces": 2, - "no-undef": 2, - "no-undef-init": 2, - "no-unexpected-multiline": 2, - "no-unmodified-loop-condition": 2, - "no-unneeded-ternary": [2, { "defaultAssignment": false }], - "no-unreachable": 2, - "no-unsafe-finally": 2, - "no-unsafe-negation": 2, - "no-unused-vars": [2, { "vars": "all", "args": "none" }], - "no-useless-call": 2, - "no-useless-computed-key": 2, - "no-useless-constructor": 2, - "no-useless-escape": 2, - "no-useless-rename": 2, - "no-whitespace-before-property": 2, - "no-with": 2, - "object-property-newline": [2, { "allowMultiplePropertiesPerLine": true }], - "one-var": [2, { "initialized": "never" }], - "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], - "padded-blocks": [2, "never"], - "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], - "rest-spread-spacing": [2, "never"], - "semi": [2, "never"], - "semi-spacing": [2, { "before": false, "after": true }], - "space-before-blocks": [2, "always"], - "space-before-function-paren": [2, "always"], - "space-in-parens": [2, "never"], - "space-infix-ops": 2, - "space-unary-ops": [2, { "words": true, "nonwords": false }], - "spaced-comment": [2, "always", { "line": { "markers": ["*package", "!", ","] }, "block": { "balanced": true, "markers": ["*package", "!", ","], "exceptions": ["*"] } }], - "template-curly-spacing": [2, "never"], - "unicode-bom": [2, "never"], - "use-isnan": 2, - "valid-typeof": 2, - "wrap-iife": [2, "any"], - "yield-star-spacing": [2, "both"], - "yoda": [2, "never"], - } + ] } From bc8ce0fee4e49abbce34a7eee515aebf855742f9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 14:04:31 +0800 Subject: [PATCH 096/306] topic view bug fix --- frontend/src/Metamaps/JIT.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index fa5c894f..0a75a036 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -151,7 +151,7 @@ const JIT = { // set up addTopic instructions in case they delete all the topics // i.e. if there are 0 topics at any time, it should have instructions again $('#instructions div').hide() - if (Metamaps.Active.Map.authorizeToEdit(Active.Mapper)) { + if (Active.Map && Active.Map.authorizeToEdit(Active.Mapper)) { $('#instructions div.addTopic').show() } From c60e103d972310a4a1d8dcb076afdbd3fdc6a3b4 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Mon, 26 Sep 2016 20:28:06 -0400 Subject: [PATCH 097/306] Update _switchmetacodes.html.erb --- app/views/shared/_switchmetacodes.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index 067d4d6b..bd6b8129 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -5,7 +5,7 @@ <% metacodes = current_user.settings.metacodes %> <% selectedSet = metacodes[0].include?("metacodeset") ? metacodes[0].sub("metacodeset-","") : "custom" %> -<% allMetacodeSets = MetacodeSet.order("name").all %> +<% allMetacodeSets = MetacodeSet.order("name").all.to_a %> <% if selectedSet == "custom" index = allMetacodeSets.length else From 8f0b350a2dbddbb306fcf08c6562557be9153cce Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Mon, 26 Sep 2016 20:39:33 -0400 Subject: [PATCH 098/306] Fix underscore bug (#674) * Update package.json * Update ChatView.js --- frontend/src/Metamaps/Views/ChatView.js | 7 ++++--- package.json | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 473c6f1e..9bd8b563 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -8,6 +8,7 @@ import Backbone from 'backbone' import Autolinker from 'autolinker' import _ from 'lodash' +import underscore from 'underscore' // TODO is this line good or bad // Backbone.$ = window.$ @@ -29,12 +30,12 @@ var Private = { "<div class='clearfloat'></div>" + "</div>", templates: function() { - _.templateSettings = { + underscore.templateSettings = { interpolate: /\{\{(.+?)\}\}/g }; - this.messageTemplate = _.template(Private.messageHTML); + this.messageTemplate = underscore.template(Private.messageHTML); - this.participantTemplate = _.template(Private.participantHTML); + this.participantTemplate = underscore.template(Private.participantHTML); }, createElements: function() { this.$unread = $('<div class="chat-unread"></div>'); diff --git a/package.json b/package.json index c6c74f44..106294fd 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", + "underscore": "1.4.4", "csv-parse": "1.1.7", "lodash": "4.16.1", "node-uuid": "1.4.7", From a86101dda0dd61f16fa7a0e41d682a76dd148d59 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 27 Sep 2016 21:10:14 +0800 Subject: [PATCH 099/306] remove excel export --- app/controllers/maps_controller.rb | 4 ---- app/services/map_export_service.rb | 4 ---- app/views/maps/export.xls.erb | 9 --------- config/initializers/mime_types.rb | 2 -- 4 files changed, 19 deletions(-) delete mode 100644 app/views/maps/export.xls.erb diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index a74a35c2..0a0b11d5 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -4,8 +4,6 @@ class MapsController < ApplicationController before_action :set_map, only: [:show, :update, :destroy, :access, :contains, :events, :export, :screenshot, :star, :unstar] after_action :verify_authorized - respond_to :html, :json, :csv - autocomplete :map, :name, full: true, extra_data: [:user_id] # GET maps/:id @@ -24,7 +22,6 @@ class MapsController < ApplicationController end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } - format.xls { redirect_to action: :export, format: :xls } end end @@ -118,7 +115,6 @@ class MapsController < ApplicationController respond_to do |format| format.json { render json: exporter.json } format.csv { send_data exporter.csv } - format.xls { @spreadsheet = exporter.xls } end end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 2ded756c..4e1d216e 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -22,10 +22,6 @@ class MapExportService end end - def xls - to_spreadsheet - end - private def topic_headings diff --git a/app/views/maps/export.xls.erb b/app/views/maps/export.xls.erb deleted file mode 100644 index 7030d501..00000000 --- a/app/views/maps/export.xls.erb +++ /dev/null @@ -1,9 +0,0 @@ -<table> - <% @spreadsheet.each do |line| %> - <tr> - <% line.each do |field| %> - <td><%= field %></td> - <% end %> - </tr> - <% end %> -</table> diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 5e8d015a..6e1d16f0 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -3,5 +3,3 @@ # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf - -Mime::Type.register 'application/xls', :xls From 743c9b3af91db83353c8fd3011de6ea9ab8e3a41 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 28 Sep 2016 10:32:28 +0800 Subject: [PATCH 100/306] node{1,2}_id => topic{1,2}_id migration and code changes --- app/controllers/main_controller.rb | 4 ++-- app/controllers/synapses_controller.rb | 2 +- app/controllers/topics_controller.rb | 2 +- app/models/permitted_params.rb | 2 +- app/models/synapse.rb | 10 ++++---- app/models/topic.rb | 24 +++++++++---------- .../api/v2/application_serializer.rb | 9 ++++--- app/serializers/api/v2/synapse_serializer.rb | 4 ++-- app/services/map_export_service.rb | 4 ++-- ...ename_node1_id_to_topic1_id_in_synapses.rb | 6 +++++ db/schema.rb | 14 +++++------ frontend/src/Metamaps/Backbone/index.js | 8 +++---- frontend/src/Metamaps/Import.js | 4 ++-- frontend/src/Metamaps/JIT.js | 2 +- frontend/src/Metamaps/Map/index.js | 4 ++-- frontend/src/Metamaps/Synapse.js | 4 ++-- frontend/src/Metamaps/SynapseCard.js | 6 ++--- 17 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 4624c7a6..0a5ccb68 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -156,8 +156,8 @@ class MainController < ApplicationController @synapses = @synapses.uniq(&:desc) elsif topic1id && !topic1id.empty? - @one = policy_scope(Synapse).where('node1_id = ? AND node2_id = ?', topic1id, topic2id) - @two = policy_scope(Synapse).where('node2_id = ? AND node1_id = ?', topic1id, topic2id) + @one = policy_scope(Synapse).where('topic1_id = ? AND topic2_id = ?', topic1id, topic2id) + @two = policy_scope(Synapse).where('topic2_id = ? AND topic1_id = ?', topic1id, topic2id) @synapses = @one + @two @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a else diff --git a/app/controllers/synapses_controller.rb b/app/controllers/synapses_controller.rb index 8fc31688..e0b8f727 100644 --- a/app/controllers/synapses_controller.rb +++ b/app/controllers/synapses_controller.rb @@ -63,6 +63,6 @@ class SynapsesController < ApplicationController private def synapse_params - params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :node1_id, :node2_id, :user_id) + params.require(:synapse).permit(:id, :desc, :category, :weight, :permission, :topic1_id, :topic2_id, :user_id) end end diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 1b966ca2..f909626a 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -96,7 +96,7 @@ class TopicsController < ApplicationController # find synapses between topics in alltopics array allsynapses = policy_scope(Synapse.for_topic(@topic.id)).to_a - synapse_ids = (allsynapses.map(&:node1_id) + allsynapses.map(&:node2_id)).uniq + synapse_ids = (allsynapses.map(&:topic1_id) + allsynapses.map(&:topic2_id)).uniq allsynapses.delete_if do |synapse| !synapse_ids.index(synapse.id).nil? end diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index d0696985..207854ac 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -19,7 +19,7 @@ class PermittedParams < Struct.new(:params) end def synapse_attributes - [:desc, :category, :weight, :permission, :node1_id, :node2_id] + [:desc, :category, :weight, :permission, :topic1_id, :topic2_id] end def topic_attributes diff --git a/app/models/synapse.rb b/app/models/synapse.rb index 798f6a54..37c9c72d 100644 --- a/app/models/synapse.rb +++ b/app/models/synapse.rb @@ -3,8 +3,8 @@ class Synapse < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' - belongs_to :topic1, class_name: 'Topic', foreign_key: 'node1_id' - belongs_to :topic2, class_name: 'Topic', foreign_key: 'node2_id' + belongs_to :topic1, class_name: 'Topic', foreign_key: 'topic1_id' + belongs_to :topic2, class_name: 'Topic', foreign_key: 'topic2_id' has_many :mappings, as: :mappable, dependent: :destroy has_many :maps, through: :mappings @@ -12,14 +12,14 @@ class Synapse < ApplicationRecord validates :desc, length: { minimum: 0, allow_nil: false } validates :permission, presence: true - validates :node1_id, presence: true - validates :node2_id, presence: true + validates :topic1_id, presence: true + validates :topic2_id, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } validates :category, inclusion: { in: ['from-to', 'both'], allow_nil: true } scope :for_topic, ->(topic_id = nil) { - where('node1_id = ? OR node2_id = ?', topic_id, topic_id) + where(topic1_id: topic_id).or(where(topic2_id: topic_id)) } delegate :name, to: :user, prefix: true diff --git a/app/models/topic.rb b/app/models/topic.rb index fb635da3..62f81cec 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -5,8 +5,8 @@ class Topic < ApplicationRecord belongs_to :user belongs_to :defer_to_map, class_name: 'Map', foreign_key: 'defer_to_map_id' - has_many :synapses1, class_name: 'Synapse', foreign_key: 'node1_id', dependent: :destroy - has_many :synapses2, class_name: 'Synapse', foreign_key: 'node2_id', dependent: :destroy + has_many :synapses1, class_name: 'Synapse', foreign_key: 'topic1_id', dependent: :destroy + has_many :synapses2, class_name: 'Synapse', foreign_key: 'topic2_id', dependent: :destroy has_many :topics1, through: :synapses2, source: :topic1 has_many :topics2, through: :synapses1, source: :topic2 @@ -46,8 +46,8 @@ class Topic < ApplicationRecord scope :relatives, ->(topic_id = nil, user = nil) { # should only see topics through *visible* synapses # e.g. Topic A (commons) -> synapse (private) -> Topic B (commons) must be filtered out - synapses = Pundit.policy_scope(user, Synapse.where(node1_id: topic_id)).pluck(:node2_id) - synapses += Pundit.policy_scope(user, Synapse.where(node2_id: topic_id)).pluck(:node1_id) + synapses = Pundit.policy_scope(user, Synapse.where(topic1_id: topic_id)).pluck(:topic2_id) + synapses += Pundit.policy_scope(user, Synapse.where(topic2_id: topic_id)).pluck(:topic1_id) where(id: synapses.uniq) } @@ -94,18 +94,18 @@ class Topic < ApplicationRecord output = [] synapses.each do |synapse| if synapse.category == 'from-to' - if synapse.node1_id == id - output << synapse.node1_id.to_s + '->' + synapse.node2_id.to_s - elsif synapse.node2_id == id - output << synapse.node2_id.to_s + '<-' + synapse.node1_id.to_s + if synapse.topic1_id == id + output << synapse.topic1_id.to_s + '->' + synapse.topic2_id.to_s + elsif synapse.topic2_id == id + output << synapse.topic2_id.to_s + '<-' + synapse.topic1_id.to_s else raise 'invalid synapse on topic in synapse_csv' end elsif synapse.category == 'both' - if synapse.node1_id == id - output << synapse.node1_id.to_s + '<->' + synapse.node2_id.to_s - elsif synapse.node2_id == id - output << synapse.node2_id.to_s + '<->' + synapse.node1_id.to_s + if synapse.topic1_id == id + output << synapse.topic1_id.to_s + '<->' + synapse.topic2_id.to_s + elsif synapse.topic2_id == id + output << synapse.topic2_id.to_s + '<->' + synapse.topic1_id.to_s else raise 'invalid synapse on topic in synapse_csv' end diff --git a/app/serializers/api/v2/application_serializer.rb b/app/serializers/api/v2/application_serializer.rb index a5da830a..2d7c1b9a 100644 --- a/app/serializers/api/v2/application_serializer.rb +++ b/app/serializers/api/v2/application_serializer.rb @@ -14,8 +14,7 @@ module Api end # self.embeddable might look like this: - # topic1: { attr: :node1, serializer: TopicSerializer } - # topic2: { attr: :node2, serializer: TopicSerializer } + # creator: { attr: :first_creator, serializer: UserSerializer } # contributors: { serializer: UserSerializer} # This method will remove the :attr key if the underlying attribute name # is different than the name provided in the final json output. All other keys @@ -24,9 +23,9 @@ module Api # # This setup means if you passed this self.embeddable config and sent no # ?embed= query param with your API request, you would get the regular attributes - # plus topic1_id, topic2_id, and contributor_ids. If you pass - # ?embed=topic1,topic2,contributors, then instead of two ids and an array of ids, - # you would get two serialized topics and an array of serialized users + # plus creator_id and contributor_ids. If you passed ?embed=creator,contributors + # then instead of an id and an array of ids, you would get a serialized user + # (the first_creator) and an array of serialized users (the contributors). def self.embed_dat embeddable.each_pair do |key, opts| attr = opts.delete(:attr) || key diff --git a/app/serializers/api/v2/synapse_serializer.rb b/app/serializers/api/v2/synapse_serializer.rb index f647022c..3f95af35 100644 --- a/app/serializers/api/v2/synapse_serializer.rb +++ b/app/serializers/api/v2/synapse_serializer.rb @@ -11,8 +11,8 @@ module Api def self.embeddable { - topic1: { attr: :node1, serializer: TopicSerializer }, - topic2: { attr: :node2, serializer: TopicSerializer }, + topic1: { serializer: TopicSerializer }, + topic2: { serializer: TopicSerializer }, user: {} } end diff --git a/app/services/map_export_service.rb b/app/services/map_export_service.rb index 2ded756c..6a89a15d 100644 --- a/app/services/map_export_service.rb +++ b/app/services/map_export_service.rb @@ -62,8 +62,8 @@ class MapExportService visible_synapses.map do |synapse| next nil if synapse.nil? OpenStruct.new( - topic1: synapse.node1_id, - topic2: synapse.node2_id, + topic1: synapse.topic1_id, + topic2: synapse.topic2_id, category: synapse.category, description: synapse.desc, user: synapse.user.name, diff --git a/db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb b/db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb new file mode 100644 index 00000000..fc5b4a1b --- /dev/null +++ b/db/migrate/20160928022635_rename_node1_id_to_topic1_id_in_synapses.rb @@ -0,0 +1,6 @@ +class RenameNode1IdToTopic1IdInSynapses < ActiveRecord::Migration[5.0] + def change + rename_column :synapses, :node1_id, :topic1_id + rename_column :synapses, :node2_id, :topic2_id + end +end diff --git a/db/schema.rb b/db/schema.rb index ea06b679..0bfa7f1a 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160820231717) do +ActiveRecord::Schema.define(version: 20160928022635) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -175,16 +175,16 @@ ActiveRecord::Schema.define(version: 20160820231717) do t.text "category" t.text "weight" t.text "permission" - t.integer "node1_id" - t.integer "node2_id" + t.integer "topic1_id" + t.integer "topic2_id" t.integer "user_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "defer_to_map_id" - t.index ["node1_id", "node1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree - t.index ["node1_id"], name: "index_synapses_on_node1_id", using: :btree - t.index ["node2_id", "node2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree - t.index ["node2_id"], name: "index_synapses_on_node2_id", using: :btree + t.index ["topic1_id", "topic1_id"], name: "index_synapses_on_node1_id_and_node1_id", using: :btree + t.index ["topic1_id"], name: "index_synapses_on_topic1_id", using: :btree + t.index ["topic2_id", "topic2_id"], name: "index_synapses_on_node2_id_and_node2_id", using: :btree + t.index ["topic2_id"], name: "index_synapses_on_topic2_id", using: :btree t.index ["user_id"], name: "index_synapses_on_user_id", using: :btree end diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index b1ba9e78..1994c483 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -531,10 +531,10 @@ _Backbone.init = function () { else return false }, getTopic1: function () { - return Metamaps.Topics.get(this.get('node1_id')) + return Metamaps.Topics.get(this.get('topic1_id')) }, getTopic2: function () { - return Metamaps.Topics.get(this.get('node2_id')) + return Metamaps.Topics.get(this.get('topic2_id')) }, getDirection: function () { var t1 = this.getTopic1(), @@ -559,8 +559,8 @@ _Backbone.init = function () { var synapseID = this.isNew() ? this.cid : this.id var edge = { - nodeFrom: this.get('node1_id'), - nodeTo: this.get('node2_id'), + nodeFrom: this.get('topic1_id'), + nodeTo: this.get('topic2_id'), data: { $synapses: [], $synapseIDs: [synapseID], diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 52a8f21a..193cdf5e 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -333,8 +333,8 @@ const Import = { desc: desc || "", category: category, permission: permission, - node1_id: topic1.id, - node2_id: topic2.id + topic1_id: topic1.id, + topic2_id: topic2.id }) Metamaps.Synapses.add(synapse) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 63723dbd..4a665bdc 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -93,7 +93,7 @@ const JIT = { synapses.each(function (s) { edge = s.createEdge() - if (topics.get(s.get('node1_id')) === undefined || topics.get(s.get('node2_id')) === undefined) { + if (topics.get(s.get('topic1_id')) === undefined || topics.get(s.get('topic2_id')) === undefined) { // this means it's an invalid synapse synapsesToRemove.push(s) } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 944a387b..625a549e 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -200,8 +200,8 @@ const Map = { var descNotFiltered = Filter.visible.synapses.indexOf(desc) > -1 // make sure that both topics are being added, otherwise, it // doesn't make sense to add the synapse - var topicsNotFiltered = nodes_array.indexOf(synapse.get('node1_id')) > -1 - topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('node2_id')) > -1 + var topicsNotFiltered = nodes_array.indexOf(synapse.get('topic1_id')) > -1 + topicsNotFiltered = topicsNotFiltered && nodes_array.indexOf(synapse.get('topic2_id')) > -1 if (descNotFiltered && topicsNotFiltered) { synapses_array.push(synapse.id) } diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index b50e50e6..400cb0b0 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -128,8 +128,8 @@ const Synapse = { topic1 = node1.getData('topic') synapse = new Metamaps.Backbone.Synapse({ desc: Create.newSynapse.description, - node1_id: topic1.isNew() ? topic1.cid : topic1.id, - node2_id: topic2.isNew() ? topic2.cid : topic2.id, + topic1_id: topic1.isNew() ? topic1.cid : topic1.id, + topic2_id: topic2.isNew() ? topic2.cid : topic2.id, }) Metamaps.Synapses.add(synapse) diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 28ff1e32..8203657d 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -238,7 +238,7 @@ const SynapseCard = { var directionCat = synapse.get('category'); // both, none, from-to if (directionCat == 'from-to') { - var from_to = [synapse.get('node1_id'), synapse.get('node2_id')] + var from_to = [synapse.get('topic1_id'), synapse.get('topic2_id')] if (from_to[0] == left.id) { // check left checkbox $('#edit_synapse_left').addClass('checked') @@ -273,8 +273,8 @@ const SynapseCard = { synapse.save({ category: dirCat, - node1_id: dir[0], - node2_id: dir[1] + topic1_id: dir[0], + topic2_id: dir[1] }) Visualize.mGraph.plot() }) From 40b7e95b686c985bae009d14b1a6891971c39ed9 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 04:35:41 -0400 Subject: [PATCH 101/306] Update index.js Prevents the default chrome context menu from appearing overtop the Metamaps context menu --- frontend/src/Metamaps/Map/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 944a387b..29cb940b 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -39,7 +39,7 @@ const Map = { var self = Map // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#center-container').bind('contextmenu', function (e) { + $('#wrapper').on('contextmenu', function (e) { return false }) From bb87c9c2db48a7baff5e24dec4ffd21bd1d1a07a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:16:18 +0800 Subject: [PATCH 102/306] simplify explore controller a bit --- app/controllers/explore_controller.rb | 57 ++++++++------------------- 1 file changed, 17 insertions(+), 40 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 6f24eba5..187e4ba0 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -1,19 +1,16 @@ # frozen_string_literal: true class ExploreController < ApplicationController + before_action :require_authentication, only: [:mine, :shared, :starred] before_action :authorize_explore after_action :verify_authorized after_action :verify_policy_scoped respond_to :html, :json, :csv - # TODO: remove? - # autocomplete :map, :name, full: true, extra_data: [:user_id] - # GET /explore/active def active - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope(Map).order('updated_at DESC') - .page(page).per(20) + @maps = policy_scope(Map).order(updated_at: :desc) + .page(params[:page]).per(20) respond_to do |format| format.html do @@ -27,11 +24,8 @@ class ExploreController < ApplicationController # GET /explore/featured def featured - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.featured = ? AND maps.permission != ?', - true, 'private') - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(featured: true).order(updated_at: :desc) + .page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -41,15 +35,8 @@ class ExploreController < ApplicationController # GET /explore/mine def mine - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.user_id = ?', current_user.id) - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(user_id: current_user.id) + .order(updated_at: :desc).page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -59,15 +46,8 @@ class ExploreController < ApplicationController # GET /explore/shared def shared - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 - @maps = policy_scope( - Map.where('maps.id IN (?)', current_user.shared_maps.map(&:id)) - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(id: current_user.shared_maps.map(&:id)) + .order(updated_at: :desc).page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -77,16 +57,9 @@ class ExploreController < ApplicationController # GET /explore/starred def starred - unless authenticated? - skip_policy_scope - return redirect_to explore_active_path - end - - page = params[:page].present? ? params[:page] : 1 stars = current_user.stars.map(&:map_id) - @maps = policy_scope( - Map.where('maps.id IN (?)', stars) - ).order('updated_at DESC').page(page).per(20) + @maps = policy_scope(Map).where(id: stars).order(updated_at: :desc) + .page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -96,10 +69,9 @@ class ExploreController < ApplicationController # GET /explore/mapper/:id def mapper - page = params[:page].present? ? params[:page] : 1 @user = User.find(params[:id]) @maps = policy_scope(Map.where(user: @user)) - .order('updated_at DESC').page(page).per(20) + .order(updated_at: :desc).page(params[:page]).per(20) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -112,4 +84,9 @@ class ExploreController < ApplicationController def authorize_explore authorize :Explore end + + def require_authentication + # skip_policy_scope + redirect_to explore_active_path unless authenticated? + end end From f75ad41a82c7d1679b2a68c4d5260fbf3d0f61da Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:28:23 +0800 Subject: [PATCH 103/306] factor out map_scope function --- app/controllers/explore_controller.rb | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 187e4ba0..59045d5d 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,8 +9,7 @@ class ExploreController < ApplicationController # GET /explore/active def active - @maps = policy_scope(Map).order(updated_at: :desc) - .page(params[:page]).per(20) + @maps = map_scope(Map) respond_to do |format| format.html do @@ -24,8 +23,7 @@ class ExploreController < ApplicationController # GET /explore/featured def featured - @maps = policy_scope(Map).where(featured: true).order(updated_at: :desc) - .page(params[:page]).per(20) + @maps = map_scope(Map.where(featured: true)) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -35,8 +33,7 @@ class ExploreController < ApplicationController # GET /explore/mine def mine - @maps = policy_scope(Map).where(user_id: current_user.id) - .order(updated_at: :desc).page(params[:page]).per(20) + @maps = map_scope(Map.where(user_id: current_user.id)) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -46,8 +43,7 @@ class ExploreController < ApplicationController # GET /explore/shared def shared - @maps = policy_scope(Map).where(id: current_user.shared_maps.map(&:id)) - .order(updated_at: :desc).page(params[:page]).per(20) + @maps = map_scope(Map.where(id: current_user.shared_maps.map(&:id))) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -57,9 +53,7 @@ class ExploreController < ApplicationController # GET /explore/starred def starred - stars = current_user.stars.map(&:map_id) - @maps = policy_scope(Map).where(id: stars).order(updated_at: :desc) - .page(params[:page]).per(20) + @maps = map_scope(Map.where(id: current_user.stars.map(&:map_id))) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -70,8 +64,7 @@ class ExploreController < ApplicationController # GET /explore/mapper/:id def mapper @user = User.find(params[:id]) - @maps = policy_scope(Map.where(user: @user)) - .order(updated_at: :desc).page(params[:page]).per(20) + @maps = map_scope(Map.where(user: @user)) respond_to do |format| format.html { respond_with(@maps, @user) } @@ -81,6 +74,10 @@ class ExploreController < ApplicationController private + def map_scope(scope) + policy_scope(scope).order(updated_at: :desc).page(params[:page]).per(20) + end + def authorize_explore authorize :Explore end From 3ee8d41298cafc0ffbc3b8370ca5321a39aac6e7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:36:08 +0800 Subject: [PATCH 104/306] maps controller code climate --- app/controllers/maps_controller.rb | 45 +++++++++++++----------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 0a0b11d5..351b1c0d 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,7 +1,9 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy, :access, :events, :screenshot, :star, :unstar] - before_action :set_map, only: [:show, :update, :destroy, :access, :contains, :events, :export, :screenshot, :star, :unstar] + before_action :require_user, only: [:create, :update, :destroy, :access, :events, + :screenshot, :star, :unstar] + before_action :set_map, only: [:show, :update, :destroy, :access, :contains, + :events, :export, :screenshot, :star, :unstar] after_action :verify_authorized autocomplete :map, :name, full: true, extra_data: [:user_id] @@ -18,7 +20,8 @@ class MapsController < ApplicationController @allmessages = @map.messages.sort_by(&:created_at) @allstars = @map.stars - respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, @alltopics, @allmessages, @allstars, @map) + respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, + @alltopics, @allmessages, @allstars, @map) end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } @@ -41,10 +44,10 @@ class MapsController < ApplicationController # POST maps def create - @user = current_user @map = Map.new(create_map_params) - @map.user = @user + @map.user = current_user @map.arranged = false + authorize @map if params[:topicsToMap].present? create_topics! @@ -52,8 +55,6 @@ class MapsController < ApplicationController @map.arranged = true end - authorize @map - respond_to do |format| if @map.save format.json { render json: @map } @@ -89,8 +90,9 @@ class MapsController < ApplicationController def access user_ids = params[:access] || [] - added = @map.add_new_collaborators(user_ids) - added.each do |user_id| + @map.add_new_collaborators(user_ids).each do |user_id| + # add_new_collaborators returns array of added users, + # who we then send an email to MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later end @map.remove_old_collaborators(user_ids) @@ -150,7 +152,7 @@ class MapsController < ApplicationController # POST maps/:id/star def star - star = Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) + Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) respond_to do |format| format.json do @@ -187,29 +189,20 @@ class MapsController < ApplicationController end def create_topics! - topics = params[:topicsToMap] - topics = topics.split(',') - topics.each do |topic| + params[:topicsToMap].split(',').each do |topic| topic = topic.split('/') - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Topic.find(topic[0]) - mapping.xloc = topic[1] - mapping.yloc = topic[2] + mapping = Mapping.new(map: @map, user: current_user, + mappable: Topic.find(topic[0]), + xloc: topic[1], yloc: topic[2]) authorize mapping, :create? mapping.save end end def create_synapses! - @synAll = params[:synapsesToMap] - @synAll = @synAll.split(',') - @synAll.each do |synapse_id| - mapping = Mapping.new - mapping.map = @map - mapping.user = @user - mapping.mappable = Synapse.find(synapse_id) + params[:synapsesToMap].split(',').each do |synapse_id| + mapping = Mapping.new(map: @map, user: current_user, + mappable: Synapse.find(synapse_id)) authorize mapping, :create? mapping.save end From 50656554369a38f55002fe8c96d8462fb47f2089 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 00:50:52 +0800 Subject: [PATCH 105/306] factor stars into their own controller --- app/controllers/maps_controller.rb | 27 ++------------------- app/controllers/stars_controller.rb | 37 +++++++++++++++++++++++++++++ config/routes.rb | 19 ++++++++------- 3 files changed, 50 insertions(+), 33 deletions(-) create mode 100644 app/controllers/stars_controller.rb diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 351b1c0d..b24f85fc 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true class MapsController < ApplicationController before_action :require_user, only: [:create, :update, :destroy, :access, :events, - :screenshot, :star, :unstar] + :screenshot] before_action :set_map, only: [:show, :update, :destroy, :access, :contains, - :events, :export, :screenshot, :star, :unstar] + :events, :export, :screenshot] after_action :verify_authorized autocomplete :map, :name, full: true, extra_data: [:user_id] @@ -150,29 +150,6 @@ class MapsController < ApplicationController end end - # POST maps/:id/star - def star - Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) - - respond_to do |format| - format.json do - render json: { message: 'Successfully starred map' } - end - end - end - - # POST maps/:id/unstar - def unstar - star = Star.find_by(map_id: @map.id, user_id: current_user.id) - star&.delete - - respond_to do |format| - format.json do - render json: { message: 'Successfully unstarred map' } - end - end - end - private def set_map diff --git a/app/controllers/stars_controller.rb b/app/controllers/stars_controller.rb new file mode 100644 index 00000000..ec4d1347 --- /dev/null +++ b/app/controllers/stars_controller.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +class StarsController < ApplicationController + before_action :require_user + before_action :set_map + after_action :verify_authorized + + # POST maps/:id/star + def create + authorize @map, :star? + Star.find_or_create_by(map_id: @map.id, user_id: current_user.id) + + respond_to do |format| + format.json do + render json: { message: 'Successfully starred map' } + end + end + end + + # POST maps/:id/unstar + def destroy + authorize @map, :unstar? + star = Star.find_by(map_id: @map.id, user_id: current_user.id) + star&.delete + + respond_to do |format| + format.json do + render json: { message: 'Successfully unstarred map' } + end + end + end + + private + + def set_map + @map = Map.find(params[:id]) + end +end diff --git a/config/routes.rb b/config/routes.rb index 3ad59cb2..b114d3d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,14 +48,17 @@ Metamaps::Application.routes.draw do get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - resources :maps, except: [:index, :edit] - get 'maps/:id/export', to: 'maps#export' - post 'maps/:id/events/:event', to: 'maps#events' - get 'maps/:id/contains', to: 'maps#contains', as: :contains - post 'maps/:id/upload_screenshot', to: 'maps#screenshot', as: :screenshot - post 'maps/:id/access', to: 'maps#access', as: :access, defaults: { format: :json } - post 'maps/:id/star', to: 'maps#star', defaults: { format: :json } - post 'maps/:id/unstar', to: 'maps#unstar', defaults: { format: :json } + resources :maps, except: [:index, :edit] do + member do + get :export + post 'events/:event', action: :events + get :contains + post :upload_screenshot, action: :screenshot + post :access, default: { format: :json } + post :star, to: 'stars#create', defaults: { format: :json } + post :unstar, to: 'stars#destroy', defaults: { format: :json } + end + end namespace :explore do get 'active' From 5b9eedc8300740c6dbad18e58f0043e949b25d79 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:00:30 +0800 Subject: [PATCH 106/306] pull search routes into their own controller --- app/controllers/main_controller.rb | 162 +-------------------------- app/controllers/search_controller.rb | 162 +++++++++++++++++++++++++++ app/policies/main_policy.rb | 21 ---- app/policies/search_policy.rb | 18 +++ config/routes.rb | 10 +- 5 files changed, 191 insertions(+), 182 deletions(-) create mode 100644 app/controllers/search_controller.rb create mode 100644 app/policies/search_policy.rb diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 0a5ccb68..f89b6b78 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,172 +1,20 @@ # frozen_string_literal: true class MainController < ApplicationController - include TopicsHelper - include MapsHelper - include UsersHelper - include SynapsesHelper + after_action :verify_authorized + after_action :verify_policy_scoped, only: [:home] - after_action :verify_policy_scoped, except: [:requestinvite, :searchmappers] - - respond_to :html, :json - - # home page + # GET / def home - @maps = policy_scope(Map).order('updated_at DESC').page(1).per(20) + authorize :Main respond_to do |format| format.html do if !authenticated? render 'main/home' else + @maps = policy_scope(Map).order(updated_at: :desc).page(1).per(20) render 'explore/active' end end end end - - ### SEARCHING ### - - # get /search/topics?term=SOMETERM - def searchtopics - term = params[:term] - user = params[:user] ? params[:user] : false - - if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('topic:').zero? - - # remove "topic:" if appended at beginning - term = term[6..-1] if term.downcase[0..5] == 'topic:' - - # if desc: search desc instead - desc = false - if term.downcase[0..4] == 'desc:' - term = term[5..-1] - desc = true - end - - # if link: search link instead - link = false - if term.downcase[0..4] == 'link:' - term = term[5..-1] - link = true - end - - # check whether there's a filter by metacode as part of the query - filterByMetacode = false - Metacode.all.each do |m| - lOne = m.name.length + 1 - lTwo = m.name.length - - if term.downcase[0..lTwo] == m.name.downcase + ':' - term = term[lOne..-1] - filterByMetacode = m - end - end - - search = '%' + term.downcase + '%' - builder = policy_scope(Topic) - - if filterByMetacode - if term == '' - builder = builder.none - else - builder = builder.where('LOWER("name") like ? OR - LOWER("desc") like ? OR - LOWER("link") like ?', search, search, search) - builder = builder.where(metacode_id: filterByMetacode.id) - end - elsif desc - builder = builder.where('LOWER("desc") like ?', search) - elsif link - builder = builder.where('LOWER("link") like ?', search) - else # regular case, just search the name - builder = builder.where('LOWER("name") like ? OR - LOWER("desc") like ? OR - LOWER("link") like ?', search, search, search) - end - - builder = builder.where(user: user) if user - @topics = builder.order(:name) - else - @topics = [] - end - - render json: autocomplete_array_json(@topics) - end - - # get /search/maps?term=SOMETERM - def searchmaps - term = params[:term] - user = params[:user] ? params[:user] : nil - - if term && !term.empty? && term.downcase[0..5] != 'topic:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('map:').zero? - - # remove "map:" if appended at beginning - term = term[4..-1] if term.downcase[0..3] == 'map:' - - # if desc: search desc instead - desc = false - if term.downcase[0..4] == 'desc:' - term = term[5..-1] - desc = true - end - - search = '%' + term.downcase + '%' - builder = policy_scope(Map) - - builder = if desc - builder.where('LOWER("desc") like ?', search) - else - builder.where('LOWER("name") like ?', search) - end - builder = builder.where(user: user) if user - @maps = builder.order(:name) - else - @maps = [] - end - - render json: autocomplete_map_array_json(@maps) - end - - # get /search/mappers?term=SOMETERM - def searchmappers - term = params[:term] - if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..5] != 'topic:' && !term.casecmp('mapper:').zero? - - # remove "mapper:" if appended at beginning - term = term[7..-1] if term.downcase[0..6] == 'mapper:' - search = term.downcase + '%' - - skip_policy_scope # TODO: builder = policy_scope(User) - builder = User.where('LOWER("name") like ?', search) - @mappers = builder.order(:name) - else - @mappers = [] - end - render json: autocomplete_user_array_json(@mappers) - end - - # get /search/synapses?term=SOMETERM OR - # get /search/synapses?topic1id=SOMEID&topic2id=SOMEID - def searchsynapses - term = params[:term] - topic1id = params[:topic1id] - topic2id = params[:topic2id] - - if term && !term.empty? - @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') - - @synapses = @synapses.uniq(&:desc) - elsif topic1id && !topic1id.empty? - @one = policy_scope(Synapse).where('topic1_id = ? AND topic2_id = ?', topic1id, topic2id) - @two = policy_scope(Synapse).where('topic2_id = ? AND topic1_id = ?', topic1id, topic2id) - @synapses = @one + @two - @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a - else - @synapses = [] - end - - # limit to 5 results - @synapses = @synapses.to_a.slice(0, 5) - - render json: autocomplete_synapse_array_json(@synapses) - end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100644 index 00000000..91b0b44d --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true +class MainController < ApplicationController + include TopicsHelper + include MapsHelper + include UsersHelper + include SynapsesHelper + + before_action :authorize_search + after_action :verify_authorized + after_action :verify_policy_scoped, only: [:maps, :mappers, :synapses, :topics] + + # get /search/topics?term=SOMETERM + def topics + term = params[:term] + user = params[:user] ? params[:user] : false + + if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('topic:').zero? + + # remove "topic:" if appended at beginning + term = term[6..-1] if term.downcase[0..5] == 'topic:' + + # if desc: search desc instead + desc = false + if term.downcase[0..4] == 'desc:' + term = term[5..-1] + desc = true + end + + # if link: search link instead + link = false + if term.downcase[0..4] == 'link:' + term = term[5..-1] + link = true + end + + # check whether there's a filter by metacode as part of the query + filterByMetacode = false + Metacode.all.each do |m| + lOne = m.name.length + 1 + lTwo = m.name.length + + if term.downcase[0..lTwo] == m.name.downcase + ':' + term = term[lOne..-1] + filterByMetacode = m + end + end + + search = '%' + term.downcase + '%' + builder = policy_scope(Topic) + + if filterByMetacode + if term == '' + builder = builder.none + else + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) + builder = builder.where(metacode_id: filterByMetacode.id) + end + elsif desc + builder = builder.where('LOWER("desc") like ?', search) + elsif link + builder = builder.where('LOWER("link") like ?', search) + else # regular case, just search the name + builder = builder.where('LOWER("name") like ? OR + LOWER("desc") like ? OR + LOWER("link") like ?', search, search, search) + end + + builder = builder.where(user: user) if user + @topics = builder.order(:name) + else + @topics = [] + end + + render json: autocomplete_array_json(@topics) + end + + # get /search/maps?term=SOMETERM + def maps + term = params[:term] + user = params[:user] ? params[:user] : nil + + if term && !term.empty? && term.downcase[0..5] != 'topic:' && term.downcase[0..6] != 'mapper:' && !term.casecmp('map:').zero? + + # remove "map:" if appended at beginning + term = term[4..-1] if term.downcase[0..3] == 'map:' + + # if desc: search desc instead + desc = false + if term.downcase[0..4] == 'desc:' + term = term[5..-1] + desc = true + end + + search = '%' + term.downcase + '%' + builder = policy_scope(Map) + + builder = if desc + builder.where('LOWER("desc") like ?', search) + else + builder.where('LOWER("name") like ?', search) + end + builder = builder.where(user: user) if user + @maps = builder.order(:name) + else + @maps = [] + end + + render json: autocomplete_map_array_json(@maps) + end + + # get /search/mappers?term=SOMETERM + def mappers + term = params[:term] + if term && !term.empty? && term.downcase[0..3] != 'map:' && term.downcase[0..5] != 'topic:' && !term.casecmp('mapper:').zero? + + # remove "mapper:" if appended at beginning + term = term[7..-1] if term.downcase[0..6] == 'mapper:' + search = term.downcase + '%' + + skip_policy_scope # TODO: builder = policy_scope(User) + builder = User.where('LOWER("name") like ?', search) + @mappers = builder.order(:name) + else + @mappers = [] + end + render json: autocomplete_user_array_json(@mappers) + end + + # get /search/synapses?term=SOMETERM OR + # get /search/synapses?topic1id=SOMEID&topic2id=SOMEID + def synapses + term = params[:term] + topic1id = params[:topic1id] + topic2id = params[:topic2id] + + if term && !term.empty? + @synapses = policy_scope(Synapse).where('LOWER("desc") like ?', '%' + term.downcase + '%').order('"desc"') + + @synapses = @synapses.uniq(&:desc) + elsif topic1id && !topic1id.empty? + @one = policy_scope(Synapse).where(topic1_id: topic1id, topic2_id: topic2id) + @two = policy_scope(Synapse).where(topic2_id: topic1id, topic1_id: topic2id) + @synapses = @one + @two + @synapses.sort! { |s1, s2| s1.desc <=> s2.desc }.to_a + else + @synapses = [] + end + + # limit to 5 results + @synapses = @synapses.to_a.slice(0, 5) + + render json: autocomplete_synapse_array_json(@synapses) + end + + private + + def authorize_search + authorize :Search + end +end diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb index e0ffc30b..1c7a00e5 100644 --- a/app/policies/main_policy.rb +++ b/app/policies/main_policy.rb @@ -1,27 +1,6 @@ # frozen_string_literal: true class MainPolicy < ApplicationPolicy - def initialize(user, _record) - @user = user - @record = nil - end - def home? true end - - def searchtopics? - true - end - - def searchmaps? - true - end - - def searchmappers? - true - end - - def searchsynapses? - true - end end diff --git a/app/policies/search_policy.rb b/app/policies/search_policy.rb new file mode 100644 index 00000000..5b783457 --- /dev/null +++ b/app/policies/search_policy.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +class SearchPolicy < ApplicationPolicy + def topics? + true + end + + def maps? + true + end + + def mappers? + true + end + + def synapses? + true + end +end diff --git a/config/routes.rb b/config/routes.rb index b114d3d6..4800780d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -5,10 +5,12 @@ Metamaps::Application.routes.draw do get 'request', to: 'main#requestinvite', as: :request - get 'search/topics', to: 'main#searchtopics', as: :searchtopics - get 'search/maps', to: 'main#searchmaps', as: :searchmaps - get 'search/mappers', to: 'main#searchmappers', as: :searchmappers - get 'search/synapses', to: 'main#searchsynapses', as: :searchsynapses + namespace :search do + get :topics + get :maps + get :mappers + get :synapses + end namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do From c1acaba941b15950165a120cb615653951c7ce3b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:02:45 +0800 Subject: [PATCH 107/306] re-order config/routes.rb --- config/routes.rb | 83 +++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 40 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index 4800780d..bdee276b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,49 @@ Metamaps::Application.routes.draw do get :synapses end + namespace :explore do + get 'active' + get 'featured' + get 'mine' + get 'shared' + get 'starred' + get 'mapper/:id', action: 'mapper' + end + + resources :maps, except: [:index, :edit] do + member do + get :export + post 'events/:event', action: :events + get :contains + post :upload_screenshot, action: :screenshot + post :access, default: { format: :json } + post :star, to: 'stars#create', defaults: { format: :json } + post :unstar, to: 'stars#destroy', defaults: { format: :json } + end + end + + resources :mappings, except: [:index, :new, :edit] + + resources :messages, only: [:show, :create, :update, :destroy] + + resources :metacode_sets, except: [:show] + + resources :metacodes, except: [:destroy] + get 'metacodes/:name', to: 'metacodes#show' + + resources :synapses, except: [:index, :new, :edit] + + resources :topics, except: [:index, :new, :edit] do + get :autocomplete_topic, on: :collection + end + get 'topics/:id/network', to: 'topics#network', as: :network + get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers + get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives + + resources :users, except: [:index, :destroy] + get 'users/:id/details', to: 'users#details', as: :details + post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes + namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do resources :maps, only: [:index, :create, :show, :update, :destroy] @@ -35,42 +78,6 @@ Metamaps::Application.routes.draw do end end - resources :messages, only: [:show, :create, :update, :destroy] - resources :mappings, except: [:index, :new, :edit] - resources :metacode_sets, except: [:show] - - resources :metacodes, except: [:destroy] - get 'metacodes/:name', to: 'metacodes#show' - - resources :synapses, except: [:index, :new, :edit] - resources :topics, except: [:index, :new, :edit] do - get :autocomplete_topic, on: :collection - end - get 'topics/:id/network', to: 'topics#network', as: :network - get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers - get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - - resources :maps, except: [:index, :edit] do - member do - get :export - post 'events/:event', action: :events - get :contains - post :upload_screenshot, action: :screenshot - post :access, default: { format: :json } - post :star, to: 'stars#create', defaults: { format: :json } - post :unstar, to: 'stars#destroy', defaults: { format: :json } - end - end - - namespace :explore do - get 'active' - get 'featured' - get 'mine' - get 'shared' - get 'starred' - get 'mapper/:id', action: 'mapper' - end - devise_for :users, skip: :sessions, controllers: { registrations: 'users/registrations', passwords: 'users/passwords', @@ -84,10 +91,6 @@ Metamaps::Application.routes.draw do get 'join' => 'devise/registrations#new', :as => :new_user_registration_path end - get 'users/:id/details', to: 'users#details', as: :details - post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes - resources :users, except: [:index, :destroy] - namespace :hacks do get 'load_url_title' end From 9699b4115943ba16ce648a7f8e24541144597ad4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:04:58 +0800 Subject: [PATCH 108/306] make requestinvite controller method explicit --- app/controllers/main_controller.rb | 12 +++++++++++- app/policies/main_policy.rb | 4 ++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index f89b6b78..d655ea91 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -1,11 +1,11 @@ # frozen_string_literal: true class MainController < ApplicationController + before_action :authorize_main after_action :verify_authorized after_action :verify_policy_scoped, only: [:home] # GET / def home - authorize :Main respond_to do |format| format.html do if !authenticated? @@ -17,4 +17,14 @@ class MainController < ApplicationController end end end + + # GET /request + def requestinvite + end + + private + + def authorize_main + authorize :Main + end end diff --git a/app/policies/main_policy.rb b/app/policies/main_policy.rb index 1c7a00e5..2eb5b3e8 100644 --- a/app/policies/main_policy.rb +++ b/app/policies/main_policy.rb @@ -3,4 +3,8 @@ class MainPolicy < ApplicationPolicy def home? true end + + def requestinvite? + true + end end From 466b1716a51587eae9c65876aece07f9149391e2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 26 Sep 2016 01:10:41 +0800 Subject: [PATCH 109/306] more changes to routes.rb --- config/routes.rb | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index bdee276b..d96aa982 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,17 +1,10 @@ # frozen_string_literal: true Metamaps::Application.routes.draw do use_doorkeeper + root to: 'main#home', via: :get - get 'request', to: 'main#requestinvite', as: :request - namespace :search do - get :topics - get :maps - get :mappers - get :synapses - end - namespace :explore do get 'active' get 'featured' @@ -42,17 +35,29 @@ Metamaps::Application.routes.draw do resources :metacodes, except: [:destroy] get 'metacodes/:name', to: 'metacodes#show' + namespace :search do + get :topics + get :maps + get :mappers + get :synapses + end + resources :synapses, except: [:index, :new, :edit] resources :topics, except: [:index, :new, :edit] do - get :autocomplete_topic, on: :collection + get :autocomplete_topic + member do + get :network + get :relative_numbers + get :relatives + end end - get 'topics/:id/network', to: 'topics#network', as: :network - get 'topics/:id/relative_numbers', to: 'topics#relative_numbers', as: :relative_numbers - get 'topics/:id/relatives', to: 'topics#relatives', as: :relatives - resources :users, except: [:index, :destroy] - get 'users/:id/details', to: 'users#details', as: :details + resources :users, except: [:index, :destroy] do + member do + get :details + end + end post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes namespace :api, path: '/api', default: { format: :json } do From 2c3b387e4231452fb54eb584d7d13d9295933556 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 13:00:32 -0400 Subject: [PATCH 110/306] Update index.js --- frontend/src/Metamaps/Map/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 24ea08ea..ce277331 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -38,11 +38,6 @@ const Map = { init: function () { var self = Map - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#wrapper').on('contextmenu', function (e) { - return false - }) - $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() From 10a2782f8553e1fad14f9686b1e7e0d3e3b5bba7 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 13:03:44 -0400 Subject: [PATCH 111/306] Update JIT.js --- frontend/src/Metamaps/JIT.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 4a665bdc..54ec74b1 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1305,6 +1305,9 @@ const JIT = { // create new menu for clicked on node var rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' + //prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu','return false') + // add the proper options to the menu var menustring = '<ul>' @@ -1550,6 +1553,8 @@ const JIT = { // create new menu for clicked on node var rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' + //prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu','return false') // add the proper options to the menu var menustring = '<ul>' From 67c4912c62bb08b3d317a7b87efc34fa47e49988 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 13:37:08 -0400 Subject: [PATCH 112/306] Update index.js --- frontend/src/Metamaps/Map/index.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index ce277331..24ea08ea 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -38,6 +38,11 @@ const Map = { init: function () { var self = Map + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#wrapper').on('contextmenu', function (e) { + return false + }) + $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() From 4e506ad290a8e13823f81c29a7fbcd19f4996f05 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 15:18:44 -0400 Subject: [PATCH 113/306] Update JIT.js --- frontend/src/Metamaps/JIT.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 54ec74b1..519bffd4 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -888,6 +888,7 @@ const JIT = { var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') + $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element Create.newTopic.x = eventInfo.getPos().x Create.newTopic.y = eventInfo.getPos().y Visualize.mGraph.plot() From a37f60060c2bdac219a025234ffbdefcf1b6a2b5 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 15:31:08 -0400 Subject: [PATCH 114/306] Update JIT.js --- frontend/src/Metamaps/JIT.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 519bffd4..54ec74b1 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -888,7 +888,6 @@ const JIT = { var myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') - $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element Create.newTopic.x = eventInfo.getPos().x Create.newTopic.y = eventInfo.getPos().y Visualize.mGraph.plot() From e8746ee7d9523f17cbcb4064a932cc81b5de14b2 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Wed, 28 Sep 2016 15:32:49 -0400 Subject: [PATCH 115/306] Update Create.js --- frontend/src/Metamaps/Create.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 5d290fcd..db71fb8c 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -209,6 +209,7 @@ const Create = { bringToFront: true }) $('.new_topic').hide() + $('#new_topic').attr('oncontextmenu','return false') //prevents the mouse up event from opening the default context menu on this element }, name: null, newId: 1, From db3cf0490fb391d1a94d62232b1f9534ad53863d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 08:02:25 +0800 Subject: [PATCH 116/306] fix develop branch bugs (#679) * bugfix - rename SearchController so it works * remove unneeded respond_with * fix to_json calls --- app/controllers/maps_controller.rb | 3 --- app/controllers/search_controller.rb | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index b24f85fc..8d4c6e27 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -19,9 +19,6 @@ class MapsController < ApplicationController @allmappings = policy_scope(@map.mappings) @allmessages = @map.messages.sort_by(&:created_at) @allstars = @map.stars - - respond_with(@allmappers, @allcollaborators, @allmappings, @allsynapses, - @alltopics, @allmessages, @allstars, @map) end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 91b0b44d..0fb9c808 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -class MainController < ApplicationController +class SearchController < ApplicationController include TopicsHelper include MapsHelper include UsersHelper @@ -73,7 +73,7 @@ class MainController < ApplicationController @topics = [] end - render json: autocomplete_array_json(@topics) + render json: autocomplete_array_json(@topics).to_json end # get /search/maps?term=SOMETERM @@ -107,7 +107,7 @@ class MainController < ApplicationController @maps = [] end - render json: autocomplete_map_array_json(@maps) + render json: autocomplete_map_array_json(@maps).to_json end # get /search/mappers?term=SOMETERM @@ -125,7 +125,7 @@ class MainController < ApplicationController else @mappers = [] end - render json: autocomplete_user_array_json(@mappers) + render json: autocomplete_user_array_json(@mappers).to_json end # get /search/synapses?term=SOMETERM OR @@ -151,7 +151,7 @@ class MainController < ApplicationController # limit to 5 results @synapses = @synapses.to_a.slice(0, 5) - render json: autocomplete_synapse_array_json(@synapses) + render json: autocomplete_synapse_array_json(@synapses).to_json end private From 93341719a9373bc5f696d27cb2b7457868138732 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 28 Sep 2016 20:22:55 -0400 Subject: [PATCH 117/306] Update main_controller.rb (#682) --- app/controllers/main_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index d655ea91..0ea9ba97 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -9,6 +9,7 @@ class MainController < ApplicationController respond_to do |format| format.html do if !authenticated? + skip_policy_scope render 'main/home' else @maps = policy_scope(Map).order(updated_at: :desc).page(1).per(20) From 88297b4eaad21b27aeda8de75afe33d1cb0efd68 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 08:38:02 +0800 Subject: [PATCH 118/306] fix routes.rb --- config/routes.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/routes.rb b/config/routes.rb index d96aa982..13b2a5ba 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -45,12 +45,14 @@ Metamaps::Application.routes.draw do resources :synapses, except: [:index, :new, :edit] resources :topics, except: [:index, :new, :edit] do - get :autocomplete_topic member do get :network get :relative_numbers get :relatives end + collection do + get :autocomplete_topic + end end resources :users, except: [:index, :destroy] do From e858a2a7732e27616484b420edfcf6227a764332 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 09:24:17 +0800 Subject: [PATCH 119/306] update ChatView.js eslint style --- frontend/src/Metamaps/Views/ChatView.js | 538 ++++++++++++------------ 1 file changed, 267 insertions(+), 271 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 9bd8b563..54236bb4 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -12,323 +12,319 @@ import underscore from 'underscore' // TODO is this line good or bad // Backbone.$ = window.$ -const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }); +const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }) var Private = { - messageHTML: "<div class='chat-message'>" + - "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + - "<div class='chat-message-text'>{{ message }}</div>" + - "<div class='chat-message-time'>{{ timestamp }}</div>" + - "<div class='clearfloat'></div>" + - "</div>", - participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + - "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + - "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + - "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + - "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + - "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + - "<div class='clearfloat'></div>" + - "</div>", - templates: function() { - underscore.templateSettings = { - interpolate: /\{\{(.+?)\}\}/g - }; - this.messageTemplate = underscore.template(Private.messageHTML); - - this.participantTemplate = underscore.template(Private.participantHTML); - }, - createElements: function() { - this.$unread = $('<div class="chat-unread"></div>'); - this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>'); - this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>'); - this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>'); - this.$videoToggle = $('<div class="video-toggle"></div>'); - this.$cursorToggle = $('<div class="cursor-toggle"></div>'); - this.$participants = $('<div class="participants"></div>'); - this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>'); - this.$chatHeader = $('<div class="chat-header">CHAT</div>'); - this.$soundToggle = $('<div class="sound-toggle"></div>'); - this.$messages = $('<div class="chat-messages"></div>'); - this.$container = $('<div class="chat-box"></div>'); - }, - attachElements: function() { - this.$button.append(this.$unread); - - this.$juntoHeader.append(this.$videoToggle); - this.$juntoHeader.append(this.$cursorToggle); - - this.$chatHeader.append(this.$soundToggle); - - this.$participants.append(this.$conversationInProgress); - - this.$container.append(this.$juntoHeader); - this.$container.append(this.$participants); - this.$container.append(this.$chatHeader); - this.$container.append(this.$button); - this.$container.append(this.$messages); - this.$container.append(this.$messageInput); - }, - addEventListeners: function() { - var self = this; - - this.participants.on('add', function (participant) { - Private.addParticipant.call(self, participant); - }); - - this.participants.on('remove', function (participant) { - Private.removeParticipant.call(self, participant); - }); - - this.$button.on('click', function () { - Handlers.buttonClick.call(self); - }); - this.$videoToggle.on('click', function () { - Handlers.videoToggleClick.call(self); - }); - this.$cursorToggle.on('click', function () { - Handlers.cursorToggleClick.call(self); - }); - this.$soundToggle.on('click', function () { - Handlers.soundToggleClick.call(self); - }); - this.$messageInput.on('keyup', function (event) { - Handlers.keyUp.call(self, event); - }); - this.$messageInput.on('focus', function () { - Handlers.inputFocus.call(self); - }); - this.$messageInput.on('blur', function () { - Handlers.inputBlur.call(self); - }); - }, - initializeSounds: function() { - this.sound = new Howl({ - urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], - sprite: { - joinmap: [0, 561], - leavemap: [1000, 592], - receivechat: [2000, 318], - sendchat: [3000, 296], - sessioninvite: [4000, 5393, true] - } - }); - }, - incrementUnread: function() { - this.unreadMessages++; - this.$unread.html(this.unreadMessages); - this.$unread.show(); - }, - addMessage: function(message, isInitial, wasMe) { - - if (!this.isOpen && !isInitial) Private.incrementUnread.call(this); - - function addZero(i) { - if (i < 10) { - i = "0" + i; - } - return i; - } - var m = _.clone(message.attributes); - - var today = new Date(); - m.timestamp = new Date(m.created_at); - - var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate(); - date += " " + addZero(m.timestamp.getHours()) + ":" + addZero(m.timestamp.getMinutes()); - m.timestamp = date; - m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png'; // TODO: remove - m.message = linker.link(m.message); - var $html = $(this.messageTemplate(m)); - this.$messages.append($html); - if (!isInitial) this.scrollMessages(200); - - if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat'); - }, - initialMessages: function() { - var messages = this.messages.models; - for (var i = 0; i < messages.length; i++) { - Private.addMessage.call(this, messages[i], true); - } - }, - handleInputMessage: function() { - var message = { - message: this.$messageInput.val(), - }; - this.$messageInput.val(''); - $(document).trigger(ChatView.events.message + '-' + this.room, [message]); - }, - addParticipant: function(participant) { - var p = _.clone(participant.attributes); - if (p.self) { - p.selfClass = 'is-self'; - p.selfName = '(me)'; - } else { - p.selfClass = ''; - p.selfName = ''; - } - var html = this.participantTemplate(p); - this.$participants.append(html); - }, - removeParticipant: function(participant) { - this.$container.find('.participant-' + participant.get('id')).remove(); + messageHTML: "<div class='chat-message'>" + + "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + + "<div class='chat-message-text'>{{ message }}</div>" + + "<div class='chat-message-time'>{{ timestamp }}</div>" + + "<div class='clearfloat'></div>" + + '</div>', + participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + + "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + + "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + + "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + + "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + + "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + + "<div class='clearfloat'></div>" + + '</div>', + templates: function () { + underscore.templateSettings = { + interpolate: /\{\{(.+?)\}\}/g } -}; + this.messageTemplate = underscore.template(Private.messageHTML) + + this.participantTemplate = underscore.template(Private.participantHTML) + }, + createElements: function () { + this.$unread = $('<div class="chat-unread"></div>') + this.$button = $('<div class="chat-button"><div class="tooltips">Chat</div></div>') + this.$messageInput = $('<textarea placeholder="Send a message..." class="chat-input"></textarea>') + this.$juntoHeader = $('<div class="junto-header">PARTICIPANTS</div>') + this.$videoToggle = $('<div class="video-toggle"></div>') + this.$cursorToggle = $('<div class="cursor-toggle"></div>') + this.$participants = $('<div class="participants"></div>') + this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>') + this.$chatHeader = $('<div class="chat-header">CHAT</div>') + this.$soundToggle = $('<div class="sound-toggle"></div>') + this.$messages = $('<div class="chat-messages"></div>') + this.$container = $('<div class="chat-box"></div>') + }, + attachElements: function () { + this.$button.append(this.$unread) + + this.$juntoHeader.append(this.$videoToggle) + this.$juntoHeader.append(this.$cursorToggle) + + this.$chatHeader.append(this.$soundToggle) + + this.$participants.append(this.$conversationInProgress) + + this.$container.append(this.$juntoHeader) + this.$container.append(this.$participants) + this.$container.append(this.$chatHeader) + this.$container.append(this.$button) + this.$container.append(this.$messages) + this.$container.append(this.$messageInput) + }, + addEventListeners: function () { + var self = this + + this.participants.on('add', function (participant) { + Private.addParticipant.call(self, participant) + }) + + this.participants.on('remove', function (participant) { + Private.removeParticipant.call(self, participant) + }) + + this.$button.on('click', function () { + Handlers.buttonClick.call(self) + }) + this.$videoToggle.on('click', function () { + Handlers.videoToggleClick.call(self) + }) + this.$cursorToggle.on('click', function () { + Handlers.cursorToggleClick.call(self) + }) + this.$soundToggle.on('click', function () { + Handlers.soundToggleClick.call(self) + }) + this.$messageInput.on('keyup', function (event) { + Handlers.keyUp.call(self, event) + }) + this.$messageInput.on('focus', function () { + Handlers.inputFocus.call(self) + }) + this.$messageInput.on('blur', function () { + Handlers.inputBlur.call(self) + }) + }, + initializeSounds: function () { + this.sound = new Howl({ + urls: [Metamaps.Erb['sounds/MM_sounds.mp3'], Metamaps.Erb['sounds/MM_sounds.ogg']], + sprite: { + joinmap: [0, 561], + leavemap: [1000, 592], + receivechat: [2000, 318], + sendchat: [3000, 296], + sessioninvite: [4000, 5393, true] + } + }) + }, + incrementUnread: function () { + this.unreadMessages++ + this.$unread.html(this.unreadMessages) + this.$unread.show() + }, + addMessage: function (message, isInitial, wasMe) { + if (!this.isOpen && !isInitial) Private.incrementUnread.call(this) + + function addZero (i) { + if (i < 10) { + i = '0' + i + } + return i + } + var m = _.clone(message.attributes) + + m.timestamp = new Date(m.created_at) + + var date = (m.timestamp.getMonth() + 1) + '/' + m.timestamp.getDate() + date += ' ' + addZero(m.timestamp.getHours()) + ':' + addZero(m.timestamp.getMinutes()) + m.timestamp = date + m.image = m.user_image || 'http://www.hotpepper.ca/wp-content/uploads/2014/11/default_profile_1_200x200.png' // TODO: remove + m.message = linker.link(m.message) + var $html = $(this.messageTemplate(m)) + this.$messages.append($html) + if (!isInitial) this.scrollMessages(200) + + if (!wasMe && !isInitial && this.alertSound) this.sound.play('receivechat') + }, + initialMessages: function () { + var messages = this.messages.models + for (var i = 0; i < messages.length; i++) { + Private.addMessage.call(this, messages[i], true) + } + }, + handleInputMessage: function () { + var message = { + message: this.$messageInput.val() + } + this.$messageInput.val('') + $(document).trigger(ChatView.events.message + '-' + this.room, [message]) + }, + addParticipant: function (participant) { + var p = _.clone(participant.attributes) + if (p.self) { + p.selfClass = 'is-self' + p.selfName = '(me)' + } else { + p.selfClass = '' + p.selfName = '' + } + var html = this.participantTemplate(p) + this.$participants.append(html) + }, + removeParticipant: function (participant) { + this.$container.find('.participant-' + participant.get('id')).remove() + } +} var Handlers = { - buttonClick: function() { - if (this.isOpen) this.close(); - else if (!this.isOpen) this.open(); - }, - videoToggleClick: function() { - this.$videoToggle.toggleClass('active'); - this.videosShowing = !this.videosShowing; - $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff); - }, - cursorToggleClick: function() { - this.$cursorToggle.toggleClass('active'); - this.cursorsShowing = !this.cursorsShowing; - $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff); - }, - soundToggleClick: function() { - this.alertSound = !this.alertSound; - this.$soundToggle.toggleClass('active'); - }, - keyUp: function(event) { - switch(event.which) { - case 13: // enter - Private.handleInputMessage.call(this); - break; - } - }, - inputFocus: function() { - $(document).trigger(ChatView.events.inputFocus); - }, - inputBlur: function() { - $(document).trigger(ChatView.events.inputBlur); + buttonClick: function () { + if (this.isOpen) this.close() + else if (!this.isOpen) this.open() + }, + videoToggleClick: function () { + this.$videoToggle.toggleClass('active') + this.videosShowing = !this.videosShowing + $(document).trigger(this.videosShowing ? ChatView.events.videosOn : ChatView.events.videosOff) + }, + cursorToggleClick: function () { + this.$cursorToggle.toggleClass('active') + this.cursorsShowing = !this.cursorsShowing + $(document).trigger(this.cursorsShowing ? ChatView.events.cursorsOn : ChatView.events.cursorsOff) + }, + soundToggleClick: function () { + this.alertSound = !this.alertSound + this.$soundToggle.toggleClass('active') + }, + keyUp: function (event) { + switch (event.which) { + case 13: // enter + Private.handleInputMessage.call(this) + break } -}; + }, + inputFocus: function () { + $(document).trigger(ChatView.events.inputFocus) + }, + inputBlur: function () { + $(document).trigger(ChatView.events.inputBlur) + } +} -const ChatView = function(messages, mapper, room) { - var self = this; +const ChatView = function (messages, mapper, room) { + this.room = room + this.mapper = mapper + this.messages = messages // backbone collection - this.room = room; - this.mapper = mapper; - this.messages = messages; // backbone collection + this.isOpen = false + this.alertSound = true // whether to play sounds on arrival of new messages or not + this.cursorsShowing = true + this.videosShowing = true + this.unreadMessages = 0 + this.participants = new Backbone.Collection() - this.isOpen = false; - this.alertSound = true; // whether to play sounds on arrival of new messages or not - this.cursorsShowing = true; - this.videosShowing = true; - this.unreadMessages = 0; - this.participants = new Backbone.Collection(); - - Private.templates.call(this); - Private.createElements.call(this); - Private.attachElements.call(this); - Private.addEventListeners.call(this); - Private.initialMessages.call(this); - Private.initializeSounds.call(this); - this.$container.css({ - right: '-300px' - }); -}; + Private.templates.call(this) + Private.createElements.call(this) + Private.attachElements.call(this) + Private.addEventListeners.call(this) + Private.initialMessages.call(this) + Private.initializeSounds.call(this) + this.$container.css({ + right: '-300px' + }) +} ChatView.prototype.conversationInProgress = function (participating) { - this.$conversationInProgress.show(); - this.$participants.addClass('is-live'); - if (participating) this.$participants.addClass('is-participating'); - this.$button.addClass('active'); + this.$conversationInProgress.show() + this.$participants.addClass('is-live') + if (participating) this.$participants.addClass('is-participating') + this.$button.addClass('active') - // hide invite to call buttons +// hide invite to call buttons } ChatView.prototype.conversationEnded = function () { - this.$conversationInProgress.hide(); - this.$participants.removeClass('is-live'); - this.$participants.removeClass('is-participating'); - this.$button.removeClass('active'); - this.$participants.find('.participant').removeClass('active'); - this.$participants.find('.participant').removeClass('pending'); + this.$conversationInProgress.hide() + this.$participants.removeClass('is-live') + this.$participants.removeClass('is-participating') + this.$button.removeClass('active') + this.$participants.find('.participant').removeClass('active') + this.$participants.find('.participant').removeClass('pending') } ChatView.prototype.leaveConversation = function () { - this.$participants.removeClass('is-participating'); + this.$participants.removeClass('is-participating') } ChatView.prototype.mapperJoinedCall = function (id) { - this.$participants.find('.participant-' + id).addClass('active'); + this.$participants.find('.participant-' + id).addClass('active') } ChatView.prototype.mapperLeftCall = function (id) { - this.$participants.find('.participant-' + id).removeClass('active'); + this.$participants.find('.participant-' + id).removeClass('active') } ChatView.prototype.invitationPending = function (id) { - this.$participants.find('.participant-' + id).addClass('pending'); + this.$participants.find('.participant-' + id).addClass('pending') } ChatView.prototype.invitationAnswered = function (id) { - this.$participants.find('.participant-' + id).removeClass('pending'); + this.$participants.find('.participant-' + id).removeClass('pending') } ChatView.prototype.addParticipant = function (participant) { - this.participants.add(participant); + this.participants.add(participant) } ChatView.prototype.removeParticipant = function (username) { - var p = this.participants.find(function (p) { return p.get('username') === username; }); - if (p) { - this.participants.remove(p); - } + var p = this.participants.find(p => p.get('username') === username) + if (p) { + this.participants.remove(p) + } } ChatView.prototype.removeParticipants = function () { - this.participants.remove(this.participants.models); + this.participants.remove(this.participants.models) } ChatView.prototype.open = function () { - this.$container.css({ - right: '0' - }); - this.$messageInput.focus(); - this.isOpen = true; - this.unreadMessages = 0; - this.$unread.hide(); - this.scrollMessages(0); - $(document).trigger(ChatView.events.openTray); + this.$container.css({ + right: '0' + }) + this.$messageInput.focus() + this.isOpen = true + this.unreadMessages = 0 + this.$unread.hide() + this.scrollMessages(0) + $(document).trigger(ChatView.events.openTray) } -ChatView.prototype.addMessage = function(message, isInitial, wasMe) { - this.messages.add(message); - Private.addMessage.call(this, message, isInitial, wasMe); +ChatView.prototype.addMessage = function (message, isInitial, wasMe) { + this.messages.add(message) + Private.addMessage.call(this, message, isInitial, wasMe) } -ChatView.prototype.scrollMessages = function(duration) { - duration = duration || 0; +ChatView.prototype.scrollMessages = function (duration) { + duration = duration || 0 - this.$messages.animate({ - scrollTop: this.$messages[0].scrollHeight - }, duration); + this.$messages.animate({ + scrollTop: this.$messages[0].scrollHeight + }, duration) } ChatView.prototype.clearMessages = function () { - this.unreadMessages = 0; - this.$unread.hide(); - this.$messages.empty(); + this.unreadMessages = 0 + this.$unread.hide() + this.$messages.empty() } ChatView.prototype.close = function () { - this.$container.css({ - right: '-300px' - }); - this.$messageInput.blur(); - this.isOpen = false; - $(document).trigger(ChatView.events.closeTray); + this.$container.css({ + right: '-300px' + }) + this.$messageInput.blur() + this.isOpen = false + $(document).trigger(ChatView.events.closeTray) } ChatView.prototype.remove = function () { - this.$button.off(); - this.$container.remove(); + this.$button.off() + this.$container.remove() } /** @@ -336,15 +332,15 @@ ChatView.prototype.remove = function () { * @static */ ChatView.events = { - message: 'ChatView:message', - openTray: 'ChatView:openTray', - closeTray: 'ChatView:closeTray', - inputFocus: 'ChatView:inputFocus', - inputBlur: 'ChatView:inputBlur', - cursorsOff: 'ChatView:cursorsOff', - cursorsOn: 'ChatView:cursorsOn', - videosOff: 'ChatView:videosOff', - videosOn: 'ChatView:videosOn' -}; + message: 'ChatView:message', + openTray: 'ChatView:openTray', + closeTray: 'ChatView:closeTray', + inputFocus: 'ChatView:inputFocus', + inputBlur: 'ChatView:inputBlur', + cursorsOff: 'ChatView:cursorsOff', + cursorsOn: 'ChatView:cursorsOn', + videosOff: 'ChatView:videosOff', + videosOn: 'ChatView:videosOn' +} export default ChatView From bca85337cc05922665a9d252d53e4da2423a4060 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 09:33:13 +0800 Subject: [PATCH 120/306] add template strings + outdent to chatview --- frontend/src/Metamaps/Views/ChatView.js | 54 ++++++++++++++++++------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/frontend/src/Metamaps/Views/ChatView.js b/frontend/src/Metamaps/Views/ChatView.js index 54236bb4..6b63cd70 100644 --- a/frontend/src/Metamaps/Views/ChatView.js +++ b/frontend/src/Metamaps/Views/ChatView.js @@ -9,26 +9,41 @@ import Backbone from 'backbone' import Autolinker from 'autolinker' import _ from 'lodash' import underscore from 'underscore' +import outdent from 'outdent' // TODO is this line good or bad // Backbone.$ = window.$ const linker = new Autolinker({ newWindow: true, truncate: 50, email: false, phone: false, twitter: false }) var Private = { - messageHTML: "<div class='chat-message'>" + - "<div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div>" + - "<div class='chat-message-text'>{{ message }}</div>" + - "<div class='chat-message-time'>{{ timestamp }}</div>" + - "<div class='clearfloat'></div>" + - '</div>', - participantHTML: "<div class='participant participant-{{ id }} {{ selfClass }}'>" + - "<div class='chat-participant-image'><img src='{{ image }}' style='border: 2px solid {{ color }};' /></div>" + - "<div class='chat-participant-name'>{{ username }} {{ selfName }}</div>" + - "<button type='button' class='button chat-participant-invite-call' onclick='Metamaps.Realtime.inviteACall({{ id}});'></button>" + - "<button type='button' class='button chat-participant-invite-join' onclick='Metamaps.Realtime.inviteToJoin({{ id}});'></button>" + - "<span class='chat-participant-participating'><div class='green-dot'></div></span>" + - "<div class='clearfloat'></div>" + - '</div>', + messageHTML: outdent` + <div class='chat-message'> + <div class='chat-message-user'><img src='{{ user_image }}' title='{{user_name }}'/></div> + <div class='chat-message-text'>{{ message }}</div> + <div class='chat-message-time'>{{ timestamp }}</div> + <div class='clearfloat'></div> + </div>`, + participantHTML: outdent` + <div class='participant participant-{{ id }} {{ selfClass }}'> + <div class='chat-participant-image'> + <img src='{{ image }}' style='border: 2px solid {{ color }};' /> + </div> + <div class='chat-participant-name'> + {{ username }} {{ selfName }} + </div> + <button type='button' + class='button chat-participant-invite-call' + onclick='Metamaps.Realtime.inviteACall({{ id}});' + ></button> + <button type='button' + class='button chat-participant-invite-join' + onclick='Metamaps.Realtime.inviteToJoin({{ id}});' + ></button> + <span class='chat-participant-participating'> + <div class='green-dot'></div> + </span> + <div class='clearfloat'></div> + </div>`, templates: function () { underscore.templateSettings = { interpolate: /\{\{(.+?)\}\}/g @@ -45,7 +60,16 @@ var Private = { this.$videoToggle = $('<div class="video-toggle"></div>') this.$cursorToggle = $('<div class="cursor-toggle"></div>') this.$participants = $('<div class="participants"></div>') - this.$conversationInProgress = $('<div class="conversation-live">LIVE <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();">LEAVE</span><span class="call-action join" onclick="Metamaps.Realtime.joinCall();">JOIN</span></div>') + this.$conversationInProgress = $(outdent` + <div class="conversation-live"> + LIVE + <span class="call-action leave" onclick="Metamaps.Realtime.leaveCall();"> + LEAVE + </span> + <span class="call-action join" onclick="Metamaps.Realtime.joinCall();"> + JOIN + </span> + </div>`) this.$chatHeader = $('<div class="chat-header">CHAT</div>') this.$soundToggle = $('<div class="sound-toggle"></div>') this.$messages = $('<div class="chat-messages"></div>') From 1bbc72fff09e63017d9d5066fdaa0105313acdc6 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 28 Sep 2016 22:36:53 -0400 Subject: [PATCH 121/306] was destroying and not reinitializing --- frontend/src/Metamaps/Visualize.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/Visualize.js b/frontend/src/Metamaps/Visualize.js index 3804b6a8..a70ae21c 100644 --- a/frontend/src/Metamaps/Visualize.js +++ b/frontend/src/Metamaps/Visualize.js @@ -118,7 +118,7 @@ const Visualize = { render: function () { var self = Visualize, RGraphSettings, FDSettings - if (self.type == 'RGraph' && (!self.mGraph || self.mGraph instanceof $jit.ForceDirected)) { + if (self.type == 'RGraph') { // clear the previous canvas from #infovis $('#infovis').empty() @@ -133,7 +133,7 @@ const Visualize = { RGraphSettings.levelDistance = JIT.RGraph.levelDistance self.mGraph = new $jit.RGraph(RGraphSettings) - } else if (self.type == 'ForceDirected' && (!self.mGraph || self.mGraph instanceof $jit.RGraph)) { + } else if (self.type == 'ForceDirected') { // clear the previous canvas from #infovis $('#infovis').empty() From 26977d06a881cffa802c3f820aff7c116593e052 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 13:15:14 +0800 Subject: [PATCH 122/306] disable 5 minute request limit on rack attack --- config/initializers/rack-attack.rb | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/config/initializers/rack-attack.rb b/config/initializers/rack-attack.rb index 9dfe3746..1cb90f0f 100644 --- a/config/initializers/rack-attack.rb +++ b/config/initializers/rack-attack.rb @@ -4,9 +4,9 @@ class Rack::Attack # Throttle all requests by IP (60rpm) # # Key: "rack::attack:#{Time.now.to_i/:period}:req/ip:#{req.ip}" - throttle('req/ip', :limit => 300, :period => 5.minutes) do |req| - req.ip # unless req.path.start_with?('/assets') - end + # throttle('req/ip', :limit => 300, :period => 5.minutes) do |req| + # req.ip # unless req.path.start_with?('/assets') + # end # Throttle POST requests to /login by IP address # @@ -32,7 +32,10 @@ class Rack::Attack end end - throttle('load_url_title/req/ip', :limit => 5, :period => 1.second) do |req| + throttle('load_url_title/req/5mins/ip', :limit => 300, :period => 5.minutes) do |req| + req.ip if req.path == 'hacks/load_url_title' + end + throttle('load_url_title/req/1s/ip', :limit => 5, :period => 1.second) do |req| # If the return value is truthy, the cache key for the return value # is incremented and compared with the limit. In this case: # "rack::attack:#{Time.now.to_i/1.second}:load_url_title/req/ip:#{req.ip}" From 1d4d7f07e29fef30a08035998128b82870947886 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 18:38:54 +0800 Subject: [PATCH 123/306] fix error when searching for synapse with undefined topic1id --- frontend/src/Metamaps/Create.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index db71fb8c..e18ed1b3 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -262,7 +262,7 @@ const Create = { url: '/search/synapses?topic1id=%TOPIC1&topic2id=%TOPIC2', prepare: function (query, settings) { var self = Create.newSynapse - if (Selected.Nodes.length < 2) { + if (Selected.Nodes.length < 2 && self.topic1id && self.topic2id) { settings.url = settings.url.replace('%TOPIC1', self.topic1id).replace('%TOPIC2', self.topic2id) return settings } else { From 3b8199aac663dd8c98444f4fc791d512e9b2c648 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 00:05:49 +0800 Subject: [PATCH 124/306] eslint updates for GlobalUI.js --- frontend/src/Metamaps/GlobalUI.js | 612 +++++++++++++++--------------- 1 file changed, 302 insertions(+), 310 deletions(-) diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI.js index 7af133de..6b6ca003 100644 --- a/frontend/src/Metamaps/GlobalUI.js +++ b/frontend/src/Metamaps/GlobalUI.js @@ -1,4 +1,4 @@ -/* global Metamaps, $, Hogan, Bloodhound */ +/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ import Active from './Active' import Create from './Create' import Filter from './Filter' @@ -14,44 +14,44 @@ const GlobalUI = { notifyTimeout: null, lightbox: null, init: function () { - var self = GlobalUI; + var self = GlobalUI - self.Search.init(); - self.CreateMap.init(); - self.Account.init(); + self.Search.init() + self.CreateMap.init() + self.Account.init() if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) - //bind lightbox clicks + // bind lightbox clicks $('.openLightbox').click(function (event) { - self.openLightbox($(this).attr('data-open')); - event.preventDefault(); - return false; - }); + self.openLightbox($(this).attr('data-open')) + event.preventDefault() + return false + }) - $('#lightbox_screen, #lightbox_close').click(self.closeLightbox); + $('#lightbox_screen, #lightbox_close').click(self.closeLightbox) // initialize global backbone models and collections - if (Active.Mapper) Active.Mapper = new Metamaps.Backbone.Mapper(Active.Mapper); + if (Active.Mapper) Active.Mapper = new Metamaps.Backbone.Mapper(Active.Mapper) - var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : []; - var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : []; - var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : []; - var mapperCollection = []; - var mapperOptionsObj = {id: 'mapper', sortBy: 'updated_at' }; + var myCollection = Metamaps.Maps.Mine ? Metamaps.Maps.Mine : [] + var sharedCollection = Metamaps.Maps.Shared ? Metamaps.Maps.Shared : [] + var starredCollection = Metamaps.Maps.Starred ? Metamaps.Maps.Starred : [] + var mapperCollection = [] + var mapperOptionsObj = { id: 'mapper', sortBy: 'updated_at' } if (Metamaps.Maps.Mapper) { - mapperCollection = Metamaps.Maps.Mapper.models; - mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id; + mapperCollection = Metamaps.Maps.Mapper.models + mapperOptionsObj.mapperId = Metamaps.Maps.Mapper.id } - var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : []; - var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : []; - Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, {id: 'mine', sortBy: 'updated_at' }); - Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, {id: 'shared', sortBy: 'updated_at' }); - Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, {id: 'starred', sortBy: 'updated_at' }); + var featuredCollection = Metamaps.Maps.Featured ? Metamaps.Maps.Featured : [] + var activeCollection = Metamaps.Maps.Active ? Metamaps.Maps.Active : [] + Metamaps.Maps.Mine = new Metamaps.Backbone.MapsCollection(myCollection, { id: 'mine', sortBy: 'updated_at' }) + Metamaps.Maps.Shared = new Metamaps.Backbone.MapsCollection(sharedCollection, { id: 'shared', sortBy: 'updated_at' }) + Metamaps.Maps.Starred = new Metamaps.Backbone.MapsCollection(starredCollection, { id: 'starred', sortBy: 'updated_at' }) // 'Mapper' refers to another mapper - Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj); - Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, {id: 'featured', sortBy: 'updated_at' }); - Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, {id: 'active', sortBy: 'updated_at' }); + Metamaps.Maps.Mapper = new Metamaps.Backbone.MapsCollection(mapperCollection, mapperOptionsObj) + Metamaps.Maps.Featured = new Metamaps.Backbone.MapsCollection(featuredCollection, { id: 'featured', sortBy: 'updated_at' }) + Metamaps.Maps.Active = new Metamaps.Backbone.MapsCollection(activeCollection, { id: 'active', sortBy: 'updated_at' }) }, showDiv: function (selector) { $(selector).show() @@ -65,261 +65,252 @@ const GlobalUI = { }, 200, 'easeInCubic', function () { $(this).hide() }) }, openLightbox: function (which) { - var self = GlobalUI; + var self = GlobalUI - $('.lightboxContent').hide(); - $('#' + which).show(); + $('.lightboxContent').hide() + $('#' + which).show() - self.lightbox = which; + self.lightbox = which - $('#lightbox_overlay').show(); + $('#lightbox_overlay').show() - var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px'; + var heightOfContent = '-' + ($('#lightbox_main').height() / 2) + 'px' // animate the content in from the bottom $('#lightbox_main').animate({ 'top': '50%', 'margin-top': heightOfContent - }, 200, 'easeOutCubic'); + }, 200, 'easeOutCubic') // fade the black overlay in $('#lightbox_screen').animate({ 'opacity': '0.42' - }, 200); + }, 200) - if (which == "switchMetacodes") { - Create.isSwitchingSet = true; + if (which === 'switchMetacodes') { + Create.isSwitchingSet = true } }, closeLightbox: function (event) { - var self = GlobalUI; + var self = GlobalUI - if (event) event.preventDefault(); + if (event) event.preventDefault() // animate the lightbox content offscreen $('#lightbox_main').animate({ 'top': '100%', 'margin-top': '0' - }, 200, 'easeInCubic'); + }, 200, 'easeInCubic') // fade the black overlay out $('#lightbox_screen').animate({ 'opacity': '0.0' }, 200, function () { - $('#lightbox_overlay').hide(); - }); + $('#lightbox_overlay').hide() + }) - if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map'); - if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map'); + if (self.lightbox === 'forkmap') GlobalUI.CreateMap.reset('fork_map') + if (self.lightbox === 'newmap') GlobalUI.CreateMap.reset('new_map') if (Create && Create.isSwitchingSet) { - Create.cancelMetacodeSetSwitch(); + Create.cancelMetacodeSetSwitch() } - self.lightbox = null; + self.lightbox = null }, notifyUser: function (message, leaveOpen) { - var self = GlobalUI; + var self = GlobalUI $('#toast').html(message) self.showDiv('#toast') - clearTimeout(self.notifyTimeOut); + clearTimeout(self.notifyTimeOut) if (!leaveOpen) { self.notifyTimeOut = setTimeout(function () { self.hideDiv('#toast') - }, 8000); + }, 8000) } }, - clearNotify: function() { - var self = GlobalUI; + clearNotify: function () { + var self = GlobalUI - clearTimeout(self.notifyTimeOut); + clearTimeout(self.notifyTimeOut) self.hideDiv('#toast') }, - shareInvite: function(inviteLink) { - window.prompt("To copy the invite link, press: Ctrl+C, Enter", inviteLink); + shareInvite: function (inviteLink) { + window.prompt('To copy the invite link, press: Ctrl+C, Enter', inviteLink) } } GlobalUI.CreateMap = { newMap: null, - emptyMapForm: "", - emptyForkMapForm: "", + emptyMapForm: '', + emptyForkMapForm: '', topicsToMap: [], synapsesToMap: [], init: function () { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - self.bindFormEvents(); - - self.emptyMapForm = $('#new_map').html(); + self.bindFormEvents() + self.emptyMapForm = $('#new_map').html() }, bindFormEvents: function () { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function(event) { + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { if (event.keyCode === 13) self.submit() }) $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault(); - GlobalUI.closeLightbox(); - }); - $('.new_map button.submitMap').unbind().bind('click', self.submit); + event.preventDefault() + GlobalUI.closeLightbox() + }) + $('.new_map button.submitMap').unbind().bind('click', self.submit) // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission); + $('.permIcon').unbind().bind('click', self.switchPermission) }, closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function(){ - $(this).remove(); - }); + $('#mapCreatedSuccess').fadeOut(300, function () { + $(this).remove() + }) }, generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/"; - stringStart += id; - stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>"; - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this "; - var page = Active.Map ? 'map' : 'page'; - var stringEnd = "</a></div>"; - return stringStart + page + stringEnd; + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/" + stringStart += id + stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>" + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this " + var page = Active.Map ? 'map' : 'page' + var stringEnd = '</a></div>' + return stringStart + page + stringEnd }, switchPermission: function () { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - self.newMap.set('permission', $(this).attr('data-permission')); - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected'); - $(this).find('.mapPermIcon').addClass('selected'); + self.newMap.set('permission', $(this).attr('data-permission')) + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected') + $(this).find('.mapPermIcon').addClass('selected') - var permText = $(this).find('.tip').html(); - $(this).parents('.new_map').find('.permText').html(permText); + var permText = $(this).find('.tip').html() + $(this).parents('.new_map').find('.permText').html(permText) }, submit: function (event) { - if (event) event.preventDefault(); + if (event) event.preventDefault() - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap if (GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap); - self.newMap.set('synapsesToMap', self.synapsesToMap); + self.newMap.set('topicsToMap', self.topicsToMap) + self.newMap.set('synapsesToMap', self.synapsesToMap) } - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) - self.newMap.set('name', $form.find('#map_name').val()); - self.newMap.set('desc', $form.find('#map_desc').val()); + self.newMap.set('name', $form.find('#map_name').val()) + self.newMap.set('desc', $form.find('#map_desc').val()) - if (self.newMap.get('name').length===0){ - self.throwMapNameError(); - return; + if (self.newMap.get('name').length === 0) { + self.throwMapNameError() + return } self.newMap.save(null, { success: self.success - // TODO add error message - }); + // TODO add error message + }) - GlobalUI.closeLightbox(); - GlobalUI.notifyUser('Working...'); + GlobalUI.closeLightbox() + GlobalUI.notifyUser('Working...') }, throwMapNameError: function () { - var self = GlobalUI.CreateMap; - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var $form = $(formId); + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) - var message = $("<div class='feedback_message'>Please enter a map name...</div>"); + var message = $("<div class='feedback_message'>Please enter a map name...</div>") - $form.find('#map_name').after(message); - setTimeout(function(){ - message.fadeOut('fast', function(){ - message.remove(); - }); - }, 5000); + $form.find('#map_name').after(message) + setTimeout(function () { + message.fadeOut('fast', function () { + message.remove() + }) + }, 5000) }, success: function (model) { - var self = GlobalUI.CreateMap; - - //push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model); - - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map'; - var form = $(formId); - - GlobalUI.clearNotify(); - $('#wrapper').append(self.generateSuccessMessage(model.id)); + var self = GlobalUI.CreateMap + // push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model) + GlobalUI.clearNotify() + $('#wrapper').append(self.generateSuccessMessage(model.id)) }, reset: function (id) { - var self = GlobalUI.CreateMap; + var self = GlobalUI.CreateMap - var form = $('#' + id); + var form = $('#' + id) - if (id === "fork_map") { - self.topicsToMap = []; - self.synapsesToMap = []; - form.html(self.emptyForkMapForm); - } - else { - form.html(self.emptyMapForm); + if (id === 'fork_map') { + self.topicsToMap = [] + self.synapsesToMap = [] + form.html(self.emptyForkMapForm) + } else { + form.html(self.emptyMapForm) } - self.bindFormEvents(); - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }); + self.bindFormEvents() + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - return false; - }, + return false + } } GlobalUI.Account = { isOpen: false, changing: false, init: function () { - var self = GlobalUI.Account; + var self = GlobalUI.Account - $('.sidebarAccountIcon').click(self.toggleBox); - $('.sidebarAccountBox').click(function(event){ - event.stopPropagation(); - }); - $('body').click(self.close); + $('.sidebarAccountIcon').click(self.toggleBox) + $('.sidebarAccountBox').click(function (event) { + event.stopPropagation() + }) + $('body').click(self.close) }, toggleBox: function (event) { - var self = GlobalUI.Account; + var self = GlobalUI.Account - if (self.isOpen) self.close(); - else self.open(); + if (self.isOpen) self.close() + else self.open() - event.stopPropagation(); + event.stopPropagation() }, open: function () { - var self = GlobalUI.Account; - - Filter.close(); - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide'); + var self = GlobalUI.Account + Filter.close() + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') if (!self.isOpen && !self.changing) { - self.changing = true; + self.changing = true $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false; - self.isOpen = true; - $('.sidebarAccountBox #user_email').focus(); - }); + self.changing = false + self.isOpen = true + $('.sidebarAccountBox #user_email').focus() + }) } }, close: function () { - var self = GlobalUI.Account; + var self = GlobalUI.Account - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide'); + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') if (!self.changing) { - self.changing = true; - $('.sidebarAccountBox #user_email').blur(); + self.changing = true + $('.sidebarAccountBox #user_email').blur() $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false; - self.isOpen = false; - }); + self.changing = false + self.isOpen = false + }) } } } @@ -333,31 +324,33 @@ GlobalUI.Search = { changing: false, optionsInitialized: false, init: function () { - var self = GlobalUI.Search; + var self = GlobalUI.Search - var loader = new CanvasLoader('searchLoading'); - loader.setColor('#4fb5c0'); // default is '#000000' - loader.setDiameter(24); // default is 40 - loader.setDensity(41); // default is 40 - loader.setRange(0.9); // default is 1.3 - loader.show(); // Hidden by default + // TODO does this overlap with Metamaps.Loading? + // devin sez: I'd like to remove Metamaps.Loading from the rails code + var loader = new CanvasLoader('searchLoading') + loader.setColor('#4fb5c0') // default is '#000000' + loader.setDiameter(24) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9) // default is 1.3 + loader.show() // Hidden by default // bind the hover events - $(".sidebarSearch").hover(function () { + $('.sidebarSearch').hover(function () { self.open() }, function () { self.close(800, false) - }); + }) $('.sidebarSearchIcon').click(function (e) { - $('.sidebarSearchField').focus(); - }); + $('.sidebarSearchField').focus() + }) $('.sidebarSearch').click(function (e) { - e.stopPropagation(); - }); + e.stopPropagation() + }) $('body').click(function (e) { - self.close(0, false); - }); + self.close(0, false) + }) // open if the search is closed and user hits ctrl+/ // close if they hit ESC @@ -365,281 +358,280 @@ GlobalUI.Search = { switch (e.which) { case 191: if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true); // true for focus + self.open(true) // true for focus } - break; + break case 27: if (self.isOpen) { - self.close(0, true); + self.close(0, true) } - break; + break default: - break; //console.log(e.which); + break // console.log(e.which) } - }); + }) - self.startTypeahead(); + self.startTypeahead() }, - lock: function() { - var self = GlobalUI.Search; - self.locked = true; + lock: function () { + var self = GlobalUI.Search + self.locked = true }, - unlock: function() { - var self = GlobalUI.Search; - self.locked = false; + unlock: function () { + var self = GlobalUI.Search + self.locked = false }, open: function (focus) { - var self = GlobalUI.Search; + var self = GlobalUI.Search - clearTimeout(self.timeOut); + clearTimeout(self.timeOut) if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true; + self.changing = true $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ width: '400px' }, 300, function () { - if (focus) $('.sidebarSearchField').focus(); + if (focus) $('.sidebarSearchField').focus() $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ padding: '7px 10px 3px 10px', width: '380px' - }); - self.changing = false; - self.isOpen = true; - }); + }) + self.changing = false + self.isOpen = true + }) } }, close: function (closeAfter, bypass) { // for now return - var self = GlobalUI.Search; + var self = GlobalUI.Search self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() == '')) { - self.changing = true; + if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { + self.changing = true $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ padding: '7px 0 3px 0', width: '400px' - }); + }) $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ width: '0' }, 300, function () { - $('.sidebarSearchField').typeahead('val', ''); - $('.sidebarSearchField').blur(); - self.changing = false; - self.isOpen = false; - }); + $('.sidebarSearchField').typeahead('val', '') + $('.sidebarSearchField').blur() + self.changing = false + self.isOpen = false + }) } - }, closeAfter); + }, closeAfter) }, startTypeahead: function () { - var self = GlobalUI.Search; + var self = GlobalUI.Search - var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>'; - var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>'; - var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>'; + var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' + var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' + var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>' var topics = { name: 'topics', limit: 9999, - display: function(s) { return s.label; }, + display: s => s.label, templates: { - notFound: function(s) { + notFound: function (s) { return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ - value: "No results", - label: "No results", + value: 'No results', + label: 'No results', typeImageURL: Metamaps.Erb['icons/wildcard.png'], - rtype: "noresult" - }); + rtype: 'noresult' + }) }, header: topicheader, - suggestion: function(s) { - return Hogan.compile($('#topicSearchTemplate').html()).render(s); - }, + suggestion: function (s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s) + } }, source: new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/topics', - prepare: function(query, settings) { - settings.url += '?term=' + query; + prepare: function (query, settings) { + settings.url += '?term=' + query if (Active.Mapper && self.limitTopicsToMe) { - settings.url += "&user=" + Active.Mapper.id.toString(); + settings.url += '&user=' + Active.Mapper.id.toString() } - return settings; - }, - }, - }), - }; + return settings + } + } + }) + } var maps = { name: 'maps', limit: 9999, - display: function(s) { return s.label; }, + display: s => s.label, templates: { - notFound: function(s) { + notFound: function (s) { return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult" - }); + value: 'No results', + label: 'No results', + rtype: 'noresult' + }) }, header: mapheader, - suggestion: function(s) { - return Hogan.compile($('#mapSearchTemplate').html()).render(s); - }, + suggestion: function (s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s) + } }, source: new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/maps', - prepare: function(query, settings) { - settings.url += '?term=' + query; + prepare: function (query, settings) { + settings.url += '?term=' + query if (Active.Mapper && self.limitMapsToMe) { - settings.url += "&user=" + Active.Mapper.id.toString(); + settings.url += '&user=' + Active.Mapper.id.toString() } - return settings; - }, - }, - }), - }; + return settings + } + } + }) + } var mappers = { name: 'mappers', limit: 9999, - display: function(s) { return s.label; }, + display: s => s.label, templates: { - notFound: function(s) { + notFound: function (s) { return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ - value: "No results", - label: "No results", - rtype: "noresult", + value: 'No results', + label: 'No results', + rtype: 'noresult', profile: Metamaps.Erb['user.png'] - }); + }) }, header: mapperheader, - suggestion: function(s) { - return Hogan.compile($('#mapperSearchTemplate').html()).render(s); - }, + suggestion: function (s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s) + } }, source: new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY', - }, - }), - }; + wildcard: '%QUERY' + } + }) + } // Take all that crazy setup data and put it together into one beautiful typeahead call! $('.sidebarSearchField').typeahead( { - highlight: true, + highlight: true }, [topics, maps, mappers] - ); + ) - //Set max height of the search results box to prevent it from covering bottom left footer + // Set max height of the search results box to prevent it from covering bottom left footer $('.sidebarSearchField').bind('typeahead:render', function (event) { - self.initSearchOptions(); - self.hideLoader(); - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); + self.initSearchOptions() + self.hideLoader() + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) if (self.limitTopicsToMe) { - $('#limitTopicsToMe').prop('checked', true); + $('#limitTopicsToMe').prop('checked', true) } if (self.limitMapsToMe) { - $('#limitMapsToMe').prop('checked', true); + $('#limitMapsToMe').prop('checked', true) } - }); + }) $(window).resize(function () { - var h = $(window).height(); - $(".tt-dropdown-menu").css('max-height', h - 100); - }); + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) + }) // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick); + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) // don't do it, if they clicked on a 'addToMap' button $('.sidebarSearch button.addToMap').click(function (event) { - event.stopPropagation(); - }); + event.stopPropagation() + }) // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function(){ + $('.sidebarSearchField.tt-input').keyup(function () { if ($('.sidebarSearchField.tt-input').val() === '') { - self.hideLoader(); + self.hideLoader() } else { - self.showLoader(); + self.showLoader() } - }); - + }) }, handleResultClick: function (event, datum, dataset) { - var self = GlobalUI.Search; + var self = GlobalUI.Search - self.hideLoader(); + self.hideLoader() - if (["topic", "map", "mapper"].indexOf(datum.rtype) !== -1) { - self.close(0, true); - if (datum.rtype == "topic") { - Router.topics(datum.id); - } else if (datum.rtype == "map") { - Router.maps(datum.id); - } else if (datum.rtype == "mapper") { - Router.explore("mapper", datum.id); + if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { + self.close(0, true) + if (datum.rtype === 'topic') { + Router.topics(datum.id) + } else if (datum.rtype === 'map') { + Router.maps(datum.id) + } else if (datum.rtype === 'mapper') { + Router.explore('mapper', datum.id) } } }, initSearchOptions: function () { - var self = GlobalUI.Search; + var self = GlobalUI.Search - function toggleResultSet(set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult'); + function toggleResultSet (set) { + var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') if (s.is(':visible')) { - s.hide(); - $(this).removeClass('minimizeResults').addClass('maximizeResults'); + s.hide() + $(this).removeClass('minimizeResults').addClass('maximizeResults') } else { - s.show(); - $(this).removeClass('maximizeResults').addClass('minimizeResults'); + s.show() + $(this).removeClass('maximizeResults').addClass('minimizeResults') } } - $('.limitToMe').unbind().bind("change", function (e) { - if ($(this).attr('id') == 'limitTopicsToMe') { - self.limitTopicsToMe = !self.limitTopicsToMe; + $('.limitToMe').unbind().bind('change', function (e) { + if ($(this).attr('id') === 'limitTopicsToMe') { + self.limitTopicsToMe = !self.limitTopicsToMe } - if ($(this).attr('id') == 'limitMapsToMe') { - self.limitMapsToMe = !self.limitMapsToMe; + if ($(this).attr('id') === 'limitMapsToMe') { + self.limitMapsToMe = !self.limitMapsToMe } // set the value of the search equal to itself to retrigger the // autocomplete event - var searchQuery = $('.sidebarSearchField.tt-input').val(); - $(".sidebarSearchField").typeahead('val', '') - .typeahead('val', searchQuery); - }); + var searchQuery = $('.sidebarSearchField.tt-input').val() + $('.sidebarSearchField').typeahead('val', '') + .typeahead('val', searchQuery) + }) // when the user clicks minimize section, hide the results for that section $('.minimizeMapperResults').unbind().click(function (e) { - toggleResultSet.call(this, 'mappers'); - }); + toggleResultSet.call(this, 'mappers') + }) $('.minimizeTopicResults').unbind().click(function (e) { - toggleResultSet.call(this, 'topics'); - }); + toggleResultSet.call(this, 'topics') + }) $('.minimizeMapResults').unbind().click(function (e) { - toggleResultSet.call(this, 'maps'); - }); + toggleResultSet.call(this, 'maps') + }) }, hideLoader: function () { - $('#searchLoading').hide(); + $('#searchLoading').hide() }, showLoader: function () { - $('#searchLoading').show(); + $('#searchLoading').show() } } From 24caafba746df0eac42a510e60fd1d883af37c1a Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 00:08:45 +0800 Subject: [PATCH 125/306] move GlobalUI into a folder --- frontend/src/Metamaps/{GlobalUI.js => GlobalUI/index.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/src/Metamaps/{GlobalUI.js => GlobalUI/index.js} (100%) diff --git a/frontend/src/Metamaps/GlobalUI.js b/frontend/src/Metamaps/GlobalUI/index.js similarity index 100% rename from frontend/src/Metamaps/GlobalUI.js rename to frontend/src/Metamaps/GlobalUI/index.js From e4e6043ded7439f5ae400a48d123820f2c62a2c3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 00:20:16 +0800 Subject: [PATCH 126/306] split GlobalUI into files --- frontend/src/Metamaps/GlobalUI/Account.js | 55 +++ frontend/src/Metamaps/GlobalUI/CreateMap.js | 137 ++++++ frontend/src/Metamaps/GlobalUI/Search.js | 331 +++++++++++++ frontend/src/Metamaps/GlobalUI/index.js | 511 +------------------- frontend/src/Metamaps/index.js | 56 +-- 5 files changed, 561 insertions(+), 529 deletions(-) create mode 100644 frontend/src/Metamaps/GlobalUI/Account.js create mode 100644 frontend/src/Metamaps/GlobalUI/CreateMap.js create mode 100644 frontend/src/Metamaps/GlobalUI/Search.js diff --git a/frontend/src/Metamaps/GlobalUI/Account.js b/frontend/src/Metamaps/GlobalUI/Account.js new file mode 100644 index 00000000..210627ff --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/Account.js @@ -0,0 +1,55 @@ +/* global $ */ + +import Filter from '../Filter' + +const Account = { + isOpen: false, + changing: false, + init: function () { + var self = Account + + $('.sidebarAccountIcon').click(self.toggleBox) + $('.sidebarAccountBox').click(function (event) { + event.stopPropagation() + }) + $('body').click(self.close) + }, + toggleBox: function (event) { + var self = Account + + if (self.isOpen) self.close() + else self.open() + + event.stopPropagation() + }, + open: function () { + var self = Account + + Filter.close() + $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') + + if (!self.isOpen && !self.changing) { + self.changing = true + $('.sidebarAccountBox').fadeIn(200, function () { + self.changing = false + self.isOpen = true + $('.sidebarAccountBox #user_email').focus() + }) + } + }, + close: function () { + var self = Account + + $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') + if (!self.changing) { + self.changing = true + $('.sidebarAccountBox #user_email').blur() + $('.sidebarAccountBox').fadeOut(200, function () { + self.changing = false + self.isOpen = false + }) + } + } +} + +export default Account diff --git a/frontend/src/Metamaps/GlobalUI/CreateMap.js b/frontend/src/Metamaps/GlobalUI/CreateMap.js new file mode 100644 index 00000000..a24c73c8 --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/CreateMap.js @@ -0,0 +1,137 @@ +/* global Metamaps, $ */ + +import Active from '../Active' +import GlobalUI from './index' + +/* + * Metamaps.Backbone + * Metamaps.Maps + */ + +const CreateMap = { + newMap: null, + emptyMapForm: '', + emptyForkMapForm: '', + topicsToMap: [], + synapsesToMap: [], + init: function () { + var self = CreateMap + + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) + + self.bindFormEvents() + + self.emptyMapForm = $('#new_map').html() + }, + bindFormEvents: function () { + var self = CreateMap + + $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { + if (event.keyCode === 13) self.submit() + }) + + $('.new_map button.cancel').unbind().bind('click', function (event) { + event.preventDefault() + GlobalUI.closeLightbox() + }) + $('.new_map button.submitMap').unbind().bind('click', self.submit) + + // bind permission changer events on the createMap form + $('.permIcon').unbind().bind('click', self.switchPermission) + }, + closeSuccess: function () { + $('#mapCreatedSuccess').fadeOut(300, function () { + $(this).remove() + }) + }, + generateSuccessMessage: function (id) { + var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/" + stringStart += id + stringStart += "' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>" + stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='Metamaps.GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this " + var page = Active.Map ? 'map' : 'page' + var stringEnd = '</a></div>' + return stringStart + page + stringEnd + }, + switchPermission: function () { + var self = CreateMap + + self.newMap.set('permission', $(this).attr('data-permission')) + $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected') + $(this).find('.mapPermIcon').addClass('selected') + + var permText = $(this).find('.tip').html() + $(this).parents('.new_map').find('.permText').html(permText) + }, + submit: function (event) { + if (event) event.preventDefault() + + var self = CreateMap + + if (GlobalUI.lightbox === 'forkmap') { + self.newMap.set('topicsToMap', self.topicsToMap) + self.newMap.set('synapsesToMap', self.synapsesToMap) + } + + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) + + self.newMap.set('name', $form.find('#map_name').val()) + self.newMap.set('desc', $form.find('#map_desc').val()) + + if (self.newMap.get('name').length === 0) { + self.throwMapNameError() + return + } + + self.newMap.save(null, { + success: self.success + // TODO add error message + }) + + GlobalUI.closeLightbox() + GlobalUI.notifyUser('Working...') + }, + throwMapNameError: function () { + + var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' + var $form = $(formId) + + var message = $("<div class='feedback_message'>Please enter a map name...</div>") + + $form.find('#map_name').after(message) + setTimeout(function () { + message.fadeOut('fast', function () { + message.remove() + }) + }, 5000) + }, + success: function (model) { + var self = CreateMap + // push the new map onto the collection of 'my maps' + Metamaps.Maps.Mine.add(model) + + GlobalUI.clearNotify() + $('#wrapper').append(self.generateSuccessMessage(model.id)) + }, + reset: function (id) { + var self = CreateMap + + var form = $('#' + id) + + if (id === 'fork_map') { + self.topicsToMap = [] + self.synapsesToMap = [] + form.html(self.emptyForkMapForm) + } else { + form.html(self.emptyMapForm) + } + + self.bindFormEvents() + self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) + + return false + } +} + +export default CreateMap diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js new file mode 100644 index 00000000..4999e279 --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -0,0 +1,331 @@ +/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ + +import Active from '../Active' +import Router from '../Router' + +/* + * Metamaps.Erb + * Metamaps.Maps + */ + +const Search = { + locked: false, + isOpen: false, + limitTopicsToMe: false, + limitMapsToMe: false, + timeOut: null, + changing: false, + optionsInitialized: false, + init: function () { + var self = Search + + // TODO does this overlap with Metamaps.Loading? + // devin sez: I'd like to remove Metamaps.Loading from the rails code + var loader = new CanvasLoader('searchLoading') + loader.setColor('#4fb5c0') // default is '#000000' + loader.setDiameter(24) // default is 40 + loader.setDensity(41) // default is 40 + loader.setRange(0.9) // default is 1.3 + loader.show() // Hidden by default + + // bind the hover events + $('.sidebarSearch').hover(function () { + self.open() + }, function () { + self.close(800, false) + }) + + $('.sidebarSearchIcon').click(function (e) { + $('.sidebarSearchField').focus() + }) + $('.sidebarSearch').click(function (e) { + e.stopPropagation() + }) + $('body').click(function (e) { + self.close(0, false) + }) + + // open if the search is closed and user hits ctrl+/ + // close if they hit ESC + $('body').bind('keyup', function (e) { + switch (e.which) { + case 191: + if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { + self.open(true) // true for focus + } + break + case 27: + if (self.isOpen) { + self.close(0, true) + } + break + + default: + break // console.log(e.which) + } + }) + + self.startTypeahead() + }, + lock: function () { + var self = Search + self.locked = true + }, + unlock: function () { + var self = Search + self.locked = false + }, + open: function (focus) { + var self = Search + + clearTimeout(self.timeOut) + if (!self.isOpen && !self.changing && !self.locked) { + self.changing = true + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '400px' + }, 300, function () { + if (focus) $('.sidebarSearchField').focus() + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 10px 3px 10px', + width: '380px' + }) + self.changing = false + self.isOpen = true + }) + } + }, + close: function (closeAfter, bypass) { + // for now + return + + var self = Search + + self.timeOut = setTimeout(function () { + if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { + self.changing = true + $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ + padding: '7px 0 3px 0', + width: '400px' + }) + $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ + width: '0' + }, 300, function () { + $('.sidebarSearchField').typeahead('val', '') + $('.sidebarSearchField').blur() + self.changing = false + self.isOpen = false + }) + } + }, closeAfter) + }, + startTypeahead: function () { + var self = Search + + var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' + var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' + var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>' + + var topics = { + name: 'topics', + limit: 9999, + + display: s => s.label, + templates: { + notFound: function (s) { + return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + typeImageURL: Metamaps.Erb['icons/wildcard.png'], + rtype: 'noresult' + }) + }, + header: topicheader, + suggestion: function (s) { + return Hogan.compile($('#topicSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/topics', + prepare: function (query, settings) { + settings.url += '?term=' + query + if (Active.Mapper && self.limitTopicsToMe) { + settings.url += '&user=' + Active.Mapper.id.toString() + } + return settings + } + } + }) + } + + var maps = { + name: 'maps', + limit: 9999, + display: s => s.label, + templates: { + notFound: function (s) { + return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + rtype: 'noresult' + }) + }, + header: mapheader, + suggestion: function (s) { + return Hogan.compile($('#mapSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/maps', + prepare: function (query, settings) { + settings.url += '?term=' + query + if (Active.Mapper && self.limitMapsToMe) { + settings.url += '&user=' + Active.Mapper.id.toString() + } + return settings + } + } + }) + } + + var mappers = { + name: 'mappers', + limit: 9999, + display: s => s.label, + templates: { + notFound: function (s) { + return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ + value: 'No results', + label: 'No results', + rtype: 'noresult', + profile: Metamaps.Erb['user.png'] + }) + }, + header: mapperheader, + suggestion: function (s) { + return Hogan.compile($('#mapperSearchTemplate').html()).render(s) + } + }, + source: new Bloodhound({ + datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), + queryTokenizer: Bloodhound.tokenizers.whitespace, + remote: { + url: '/search/mappers?term=%QUERY', + wildcard: '%QUERY' + } + }) + } + + // Take all that crazy setup data and put it together into one beautiful typeahead call! + $('.sidebarSearchField').typeahead( + { + highlight: true + }, + [topics, maps, mappers] + ) + + // Set max height of the search results box to prevent it from covering bottom left footer + $('.sidebarSearchField').bind('typeahead:render', function (event) { + self.initSearchOptions() + self.hideLoader() + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) + if (self.limitTopicsToMe) { + $('#limitTopicsToMe').prop('checked', true) + } + if (self.limitMapsToMe) { + $('#limitMapsToMe').prop('checked', true) + } + }) + $(window).resize(function () { + var h = $(window).height() + $('.tt-dropdown-menu').css('max-height', h - 100) + }) + + // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on + $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) + + // don't do it, if they clicked on a 'addToMap' button + $('.sidebarSearch button.addToMap').click(function (event) { + event.stopPropagation() + }) + + // make sure that when you click on 'limit to me' or 'toggle section' it works + $('.sidebarSearchField.tt-input').keyup(function () { + if ($('.sidebarSearchField.tt-input').val() === '') { + self.hideLoader() + } else { + self.showLoader() + } + }) + }, + handleResultClick: function (event, datum, dataset) { + var self = Search + + self.hideLoader() + + if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { + self.close(0, true) + if (datum.rtype === 'topic') { + Router.topics(datum.id) + } else if (datum.rtype === 'map') { + Router.maps(datum.id) + } else if (datum.rtype === 'mapper') { + Router.explore('mapper', datum.id) + } + } + }, + initSearchOptions: function () { + var self = Search + + function toggleResultSet (set) { + var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') + if (s.is(':visible')) { + s.hide() + $(this).removeClass('minimizeResults').addClass('maximizeResults') + } else { + s.show() + $(this).removeClass('maximizeResults').addClass('minimizeResults') + } + } + + $('.limitToMe').unbind().bind('change', function (e) { + if ($(this).attr('id') === 'limitTopicsToMe') { + self.limitTopicsToMe = !self.limitTopicsToMe + } + if ($(this).attr('id') === 'limitMapsToMe') { + self.limitMapsToMe = !self.limitMapsToMe + } + + // set the value of the search equal to itself to retrigger the + // autocomplete event + var searchQuery = $('.sidebarSearchField.tt-input').val() + $('.sidebarSearchField').typeahead('val', '') + .typeahead('val', searchQuery) + }) + + // when the user clicks minimize section, hide the results for that section + $('.minimizeMapperResults').unbind().click(function (e) { + toggleResultSet.call(this, 'mappers') + }) + $('.minimizeTopicResults').unbind().click(function (e) { + toggleResultSet.call(this, 'topics') + }) + $('.minimizeMapResults').unbind().click(function (e) { + toggleResultSet.call(this, 'maps') + }) + }, + hideLoader: function () { + $('#searchLoading').hide() + }, + showLoader: function () { + $('#searchLoading').show() + } +} + +export default Search diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 6b6ca003..3b16375e 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -1,12 +1,14 @@ -/* global Metamaps, $, Hogan, Bloodhound, CanvasLoader */ -import Active from './Active' -import Create from './Create' -import Filter from './Filter' -import Router from './Router' +/* global Metamaps, $ */ + +import Active from '../Active' +import Create from '../Create' + +import Search from './Search' +import CreateMap from './CreateMap' +import Account from './Account' /* * Metamaps.Backbone - * Metamaps.Erb * Metamaps.Maps */ @@ -139,500 +141,5 @@ const GlobalUI = { } } -GlobalUI.CreateMap = { - newMap: null, - emptyMapForm: '', - emptyForkMapForm: '', - topicsToMap: [], - synapsesToMap: [], - init: function () { - var self = GlobalUI.CreateMap - - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - - self.bindFormEvents() - - self.emptyMapForm = $('#new_map').html() - }, - bindFormEvents: function () { - var self = GlobalUI.CreateMap - - $('.new_map input, .new_map div').unbind('keypress').bind('keypress', function (event) { - if (event.keyCode === 13) self.submit() - }) - - $('.new_map button.cancel').unbind().bind('click', function (event) { - event.preventDefault() - GlobalUI.closeLightbox() - }) - $('.new_map button.submitMap').unbind().bind('click', self.submit) - - // bind permission changer events on the createMap form - $('.permIcon').unbind().bind('click', self.switchPermission) - }, - closeSuccess: function () { - $('#mapCreatedSuccess').fadeOut(300, function () { - $(this).remove() - }) - }, - generateSuccessMessage: function (id) { - var stringStart = "<div id='mapCreatedSuccess'><h6>SUCCESS!</h6>Your map has been created. Do you want to: <a id='mapGo' href='/maps/" - stringStart += id - stringStart += "' onclick='GlobalUI.CreateMap.closeSuccess();'>Go to your new map</a>" - stringStart += "<span>OR</span><a id='mapStay' href='#' onclick='GlobalUI.CreateMap.closeSuccess(); return false;'>Stay on this " - var page = Active.Map ? 'map' : 'page' - var stringEnd = '</a></div>' - return stringStart + page + stringEnd - }, - switchPermission: function () { - var self = GlobalUI.CreateMap - - self.newMap.set('permission', $(this).attr('data-permission')) - $(this).siblings('.permIcon').find('.mapPermIcon').removeClass('selected') - $(this).find('.mapPermIcon').addClass('selected') - - var permText = $(this).find('.tip').html() - $(this).parents('.new_map').find('.permText').html(permText) - }, - submit: function (event) { - if (event) event.preventDefault() - - var self = GlobalUI.CreateMap - - if (GlobalUI.lightbox === 'forkmap') { - self.newMap.set('topicsToMap', self.topicsToMap) - self.newMap.set('synapsesToMap', self.synapsesToMap) - } - - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' - var $form = $(formId) - - self.newMap.set('name', $form.find('#map_name').val()) - self.newMap.set('desc', $form.find('#map_desc').val()) - - if (self.newMap.get('name').length === 0) { - self.throwMapNameError() - return - } - - self.newMap.save(null, { - success: self.success - // TODO add error message - }) - - GlobalUI.closeLightbox() - GlobalUI.notifyUser('Working...') - }, - throwMapNameError: function () { - - var formId = GlobalUI.lightbox === 'forkmap' ? '#fork_map' : '#new_map' - var $form = $(formId) - - var message = $("<div class='feedback_message'>Please enter a map name...</div>") - - $form.find('#map_name').after(message) - setTimeout(function () { - message.fadeOut('fast', function () { - message.remove() - }) - }, 5000) - }, - success: function (model) { - var self = GlobalUI.CreateMap - // push the new map onto the collection of 'my maps' - Metamaps.Maps.Mine.add(model) - - GlobalUI.clearNotify() - $('#wrapper').append(self.generateSuccessMessage(model.id)) - }, - reset: function (id) { - var self = GlobalUI.CreateMap - - var form = $('#' + id) - - if (id === 'fork_map') { - self.topicsToMap = [] - self.synapsesToMap = [] - form.html(self.emptyForkMapForm) - } else { - form.html(self.emptyMapForm) - } - - self.bindFormEvents() - self.newMap = new Metamaps.Backbone.Map({ permission: 'commons' }) - - return false - } -} - -GlobalUI.Account = { - isOpen: false, - changing: false, - init: function () { - var self = GlobalUI.Account - - $('.sidebarAccountIcon').click(self.toggleBox) - $('.sidebarAccountBox').click(function (event) { - event.stopPropagation() - }) - $('body').click(self.close) - }, - toggleBox: function (event) { - var self = GlobalUI.Account - - if (self.isOpen) self.close() - else self.open() - - event.stopPropagation() - }, - open: function () { - var self = GlobalUI.Account - - Filter.close() - $('.sidebarAccountIcon .tooltipsUnder').addClass('hide') - - if (!self.isOpen && !self.changing) { - self.changing = true - $('.sidebarAccountBox').fadeIn(200, function () { - self.changing = false - self.isOpen = true - $('.sidebarAccountBox #user_email').focus() - }) - } - }, - close: function () { - var self = GlobalUI.Account - - $('.sidebarAccountIcon .tooltipsUnder').removeClass('hide') - if (!self.changing) { - self.changing = true - $('.sidebarAccountBox #user_email').blur() - $('.sidebarAccountBox').fadeOut(200, function () { - self.changing = false - self.isOpen = false - }) - } - } -} - -GlobalUI.Search = { - locked: false, - isOpen: false, - limitTopicsToMe: false, - limitMapsToMe: false, - timeOut: null, - changing: false, - optionsInitialized: false, - init: function () { - var self = GlobalUI.Search - - // TODO does this overlap with Metamaps.Loading? - // devin sez: I'd like to remove Metamaps.Loading from the rails code - var loader = new CanvasLoader('searchLoading') - loader.setColor('#4fb5c0') // default is '#000000' - loader.setDiameter(24) // default is 40 - loader.setDensity(41) // default is 40 - loader.setRange(0.9) // default is 1.3 - loader.show() // Hidden by default - - // bind the hover events - $('.sidebarSearch').hover(function () { - self.open() - }, function () { - self.close(800, false) - }) - - $('.sidebarSearchIcon').click(function (e) { - $('.sidebarSearchField').focus() - }) - $('.sidebarSearch').click(function (e) { - e.stopPropagation() - }) - $('body').click(function (e) { - self.close(0, false) - }) - - // open if the search is closed and user hits ctrl+/ - // close if they hit ESC - $('body').bind('keyup', function (e) { - switch (e.which) { - case 191: - if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true) // true for focus - } - break - case 27: - if (self.isOpen) { - self.close(0, true) - } - break - - default: - break // console.log(e.which) - } - }) - - self.startTypeahead() - }, - lock: function () { - var self = GlobalUI.Search - self.locked = true - }, - unlock: function () { - var self = GlobalUI.Search - self.locked = false - }, - open: function (focus) { - var self = GlobalUI.Search - - clearTimeout(self.timeOut) - if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '400px' - }, 300, function () { - if (focus) $('.sidebarSearchField').focus() - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 10px 3px 10px', - width: '380px' - }) - self.changing = false - self.isOpen = true - }) - } - }, - close: function (closeAfter, bypass) { - // for now - return - - var self = GlobalUI.Search - - self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { - self.changing = true - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 0 3px 0', - width: '400px' - }) - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '0' - }, 300, function () { - $('.sidebarSearchField').typeahead('val', '') - $('.sidebarSearchField').blur() - self.changing = false - self.isOpen = false - }) - } - }, closeAfter) - }, - startTypeahead: function () { - var self = GlobalUI.Search - - var mapheader = Active.Mapper ? '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><input type="checkbox" class="limitToMe" id="limitMapsToMe"></input><label for="limitMapsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' : '<div class="searchMapsHeader searchHeader"><h3 class="search-heading">Maps</h3><div class="minimizeResults minimizeMapResults"></div><div class="clearfloat"></div></div>' - var topicheader = Active.Mapper ? '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><input type="checkbox" class="limitToMe" id="limitTopicsToMe"></input><label for="limitTopicsToMe" class="limitToMeLabel">added by me</label><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' : '<div class="searchTopicsHeader searchHeader"><h3 class="search-heading">Topics</h3><div class="minimizeResults minimizeTopicResults"></div><div class="clearfloat"></div></div>' - var mapperheader = '<div class="searchMappersHeader searchHeader"><h3 class="search-heading">Mappers</h3><div class="minimizeResults minimizeMapperResults"></div><div class="clearfloat"></div></div>' - - var topics = { - name: 'topics', - limit: 9999, - - display: s => s.label, - templates: { - notFound: function (s) { - return Hogan.compile(topicheader + $('#topicSearchTemplate').html()).render({ - value: 'No results', - label: 'No results', - typeImageURL: Metamaps.Erb['icons/wildcard.png'], - rtype: 'noresult' - }) - }, - header: topicheader, - suggestion: function (s) { - return Hogan.compile($('#topicSearchTemplate').html()).render(s) - } - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/topics', - prepare: function (query, settings) { - settings.url += '?term=' + query - if (Active.Mapper && self.limitTopicsToMe) { - settings.url += '&user=' + Active.Mapper.id.toString() - } - return settings - } - } - }) - } - - var maps = { - name: 'maps', - limit: 9999, - display: s => s.label, - templates: { - notFound: function (s) { - return Hogan.compile(mapheader + $('#mapSearchTemplate').html()).render({ - value: 'No results', - label: 'No results', - rtype: 'noresult' - }) - }, - header: mapheader, - suggestion: function (s) { - return Hogan.compile($('#mapSearchTemplate').html()).render(s) - } - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/maps', - prepare: function (query, settings) { - settings.url += '?term=' + query - if (Active.Mapper && self.limitMapsToMe) { - settings.url += '&user=' + Active.Mapper.id.toString() - } - return settings - } - } - }) - } - - var mappers = { - name: 'mappers', - limit: 9999, - display: s => s.label, - templates: { - notFound: function (s) { - return Hogan.compile(mapperheader + $('#mapperSearchTemplate').html()).render({ - value: 'No results', - label: 'No results', - rtype: 'noresult', - profile: Metamaps.Erb['user.png'] - }) - }, - header: mapperheader, - suggestion: function (s) { - return Hogan.compile($('#mapperSearchTemplate').html()).render(s) - } - }, - source: new Bloodhound({ - datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: '/search/mappers?term=%QUERY', - wildcard: '%QUERY' - } - }) - } - - // Take all that crazy setup data and put it together into one beautiful typeahead call! - $('.sidebarSearchField').typeahead( - { - highlight: true - }, - [topics, maps, mappers] - ) - - // Set max height of the search results box to prevent it from covering bottom left footer - $('.sidebarSearchField').bind('typeahead:render', function (event) { - self.initSearchOptions() - self.hideLoader() - var h = $(window).height() - $('.tt-dropdown-menu').css('max-height', h - 100) - if (self.limitTopicsToMe) { - $('#limitTopicsToMe').prop('checked', true) - } - if (self.limitMapsToMe) { - $('#limitMapsToMe').prop('checked', true) - } - }) - $(window).resize(function () { - var h = $(window).height() - $('.tt-dropdown-menu').css('max-height', h - 100) - }) - - // tell the autocomplete to launch a new tab with the topic, map, or mapper you clicked on - $('.sidebarSearchField').bind('typeahead:select', self.handleResultClick) - - // don't do it, if they clicked on a 'addToMap' button - $('.sidebarSearch button.addToMap').click(function (event) { - event.stopPropagation() - }) - - // make sure that when you click on 'limit to me' or 'toggle section' it works - $('.sidebarSearchField.tt-input').keyup(function () { - if ($('.sidebarSearchField.tt-input').val() === '') { - self.hideLoader() - } else { - self.showLoader() - } - }) - }, - handleResultClick: function (event, datum, dataset) { - var self = GlobalUI.Search - - self.hideLoader() - - if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { - self.close(0, true) - if (datum.rtype === 'topic') { - Router.topics(datum.id) - } else if (datum.rtype === 'map') { - Router.maps(datum.id) - } else if (datum.rtype === 'mapper') { - Router.explore('mapper', datum.id) - } - } - }, - initSearchOptions: function () { - var self = GlobalUI.Search - - function toggleResultSet (set) { - var s = $('.tt-dataset-' + set + ' .tt-suggestion, .tt-dataset-' + set + ' .resultnoresult') - if (s.is(':visible')) { - s.hide() - $(this).removeClass('minimizeResults').addClass('maximizeResults') - } else { - s.show() - $(this).removeClass('maximizeResults').addClass('minimizeResults') - } - } - - $('.limitToMe').unbind().bind('change', function (e) { - if ($(this).attr('id') === 'limitTopicsToMe') { - self.limitTopicsToMe = !self.limitTopicsToMe - } - if ($(this).attr('id') === 'limitMapsToMe') { - self.limitMapsToMe = !self.limitMapsToMe - } - - // set the value of the search equal to itself to retrigger the - // autocomplete event - var searchQuery = $('.sidebarSearchField.tt-input').val() - $('.sidebarSearchField').typeahead('val', '') - .typeahead('val', searchQuery) - }) - - // when the user clicks minimize section, hide the results for that section - $('.minimizeMapperResults').unbind().click(function (e) { - toggleResultSet.call(this, 'mappers') - }) - $('.minimizeTopicResults').unbind().click(function (e) { - toggleResultSet.call(this, 'topics') - }) - $('.minimizeMapResults').unbind().click(function (e) { - toggleResultSet.call(this, 'maps') - }) - }, - hideLoader: function () { - $('#searchLoading').hide() - }, - showLoader: function () { - $('#searchLoading').show() - } -} - +export { Search, CreateMap, Account } export default GlobalUI diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index db67409b..d179713e 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -9,7 +9,9 @@ import Control from './Control' import Create from './Create' import Debug from './Debug' import Filter from './Filter' -import GlobalUI from './GlobalUI' +import GlobalUI, { + Search, CreateMap, Account as GlobalUI_Account +} from './GlobalUI' import Import from './Import' import JIT from './JIT' import Listeners from './Listeners' @@ -42,6 +44,9 @@ Metamaps.Create = Create Metamaps.Debug = Debug Metamaps.Filter = Filter Metamaps.GlobalUI = GlobalUI +Metamaps.GlobalUI.Search = Search +Metamaps.GlobalUI.CreateMap = CreateMap +Metamaps.GlobalUI.Account = GlobalUI_Account Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners @@ -66,41 +71,38 @@ Metamaps.Util = Util Metamaps.Views = Views Metamaps.Visualize = Visualize -document.addEventListener("DOMContentLoaded", function() { +document.addEventListener('DOMContentLoaded', function () { // initialize all the modules for (const prop in Metamaps) { - // this runs the init function within each sub-object on the Metamaps one - if (Metamaps.hasOwnProperty(prop) && - Metamaps[prop] != null && - Metamaps[prop].hasOwnProperty('init') && - typeof (Metamaps[prop].init) == 'function' - ) { - Metamaps[prop].init() - } + // this runs the init function within each sub-object on the Metamaps one + if (Metamaps.hasOwnProperty(prop) && + Metamaps[prop] != null && + Metamaps[prop].hasOwnProperty('init') && + typeof (Metamaps[prop].init) === 'function' + ) { + Metamaps[prop].init() + } } // load whichever page you are on - if (Metamaps.currentSection === "explore") { - const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) + if (Metamaps.currentSection === 'explore') { + const capitalize = Metamaps.currentPage.charAt(0).toUpperCase() + Metamaps.currentPage.slice(1) - Metamaps.Views.ExploreMaps.setCollection( Metamaps.Maps[capitalize] ) - if (Metamaps.currentPage === "mapper") { - Views.ExploreMaps.fetchUserThenRender() - } - else { - Views.ExploreMaps.render() - } - GlobalUI.showDiv('#explore') - } - else if (Metamaps.currentSection === "" && Active.Mapper) { - Views.ExploreMaps.setCollection(Metamaps.Maps.Active) + Metamaps.Views.ExploreMaps.setCollection(Metamaps.Maps[capitalize]) + if (Metamaps.currentPage === 'mapper') { + Views.ExploreMaps.fetchUserThenRender() + } else { Views.ExploreMaps.render() - GlobalUI.showDiv('#explore') - } - else if (Active.Map || Active.Topic) { + } + GlobalUI.showDiv('#explore') + } else if (Metamaps.currentSection === '' && Active.Mapper) { + Views.ExploreMaps.setCollection(Metamaps.Maps.Active) + Views.ExploreMaps.render() + GlobalUI.showDiv('#explore') + } else if (Active.Map || Active.Topic) { Metamaps.Loading.show() JIT.prepareVizData() GlobalUI.showDiv('#infovis') } -}); +}) export default Metamaps From 44a183ed7bd7fdd5fc05b9971f6318e929cd8e63 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Thu, 29 Sep 2016 21:32:55 +0000 Subject: [PATCH 127/306] I changed how zoom by mouse-wheel works so that it zooms based on where your mouse pointer is --- frontend/src/patched/JIT.js | 38 ++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/frontend/src/patched/JIT.js b/frontend/src/patched/JIT.js index af7311be..f143fc08 100644 --- a/frontend/src/patched/JIT.js +++ b/frontend/src/patched/JIT.js @@ -2468,16 +2468,36 @@ Extras.Classes.Navigation = new Class({ ans = 1 + scroll * val; // START METAMAPS CODE - if (ans > 1) { - if (5 >= this.canvas.scaleOffsetX) { - this.canvas.scale(ans, ans); - } - } - else if (ans < 1) { - if (this.canvas.scaleOffsetX >= 0.2) { - this.canvas.scale(ans, ans); - } + if (((ans > 1) && (5 >= this.canvas.scaleOffsetX)) || ((ans < 1) && (this.canvas.scaleOffsetX >= 0.2))) { + var s = this.canvas.getSize(), + p = this.canvas.getPos(), + ox = this.canvas.translateOffsetX, + oy = this.canvas.translateOffsetY, + sx = this.canvas.scaleOffsetX, + sy = this.canvas.scaleOffsetY; + + //Basically this is just a duplication of the Util function pixelsToCoords, it finds the canvas coordinate of the mouse pointer + var pointerCoordX = (e.x - p.x - s.width / 2 - ox) * (1 / sx), + pointerCoordY = (e.y - p.y - s.height / 2 - oy) * (1 / sy); + + //This translates the canvas to be centred over the mouse pointer, then the canvas is zoomed as intended. + this.canvas.translate(-pointerCoordX,-pointerCoordY); + this.canvas.scale(ans, ans); + + //Get the canvas attributes again now that is has changed + s = this.canvas.getSize(), + p = this.canvas.getPos(), + ox = this.canvas.translateOffsetX, + oy = this.canvas.translateOffsetY, + sx = this.canvas.scaleOffsetX, + sy = this.canvas.scaleOffsetY; + var newX = (e.x - p.x - s.width / 2 - ox) * (1 / sx), + newY = (e.y - p.y - s.height / 2 - oy) * (1 / sy); + + //Translate the canvas to put the pointer back over top the same coordinate it was over before + this.canvas.translate(newX-pointerCoordX,newY-pointerCoordY); } + // END METAMAPS CODE // ORIGINAL CODE this.canvas.scale(ans, ans); From 816d5adf9490610460b48e10441cced1465187dc Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 11:32:58 +0800 Subject: [PATCH 128/306] remove old code from GlobalUI.Search --- frontend/src/Metamaps/GlobalUI/Search.js | 84 +----------------------- 1 file changed, 2 insertions(+), 82 deletions(-) diff --git a/frontend/src/Metamaps/GlobalUI/Search.js b/frontend/src/Metamaps/GlobalUI/Search.js index 4999e279..7ad1fbc1 100644 --- a/frontend/src/Metamaps/GlobalUI/Search.js +++ b/frontend/src/Metamaps/GlobalUI/Search.js @@ -13,7 +13,6 @@ const Search = { isOpen: false, limitTopicsToMe: false, limitMapsToMe: false, - timeOut: null, changing: false, optionsInitialized: false, init: function () { @@ -28,95 +27,17 @@ const Search = { loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - // bind the hover events - $('.sidebarSearch').hover(function () { - self.open() - }, function () { - self.close(800, false) - }) - $('.sidebarSearchIcon').click(function (e) { $('.sidebarSearchField').focus() }) $('.sidebarSearch').click(function (e) { e.stopPropagation() }) - $('body').click(function (e) { - self.close(0, false) - }) - - // open if the search is closed and user hits ctrl+/ - // close if they hit ESC - $('body').bind('keyup', function (e) { - switch (e.which) { - case 191: - if ((e.ctrlKey && !self.isOpen) || (e.ctrlKey && self.locked)) { - self.open(true) // true for focus - } - break - case 27: - if (self.isOpen) { - self.close(0, true) - } - break - - default: - break // console.log(e.which) - } - }) self.startTypeahead() }, - lock: function () { - var self = Search - self.locked = true - }, - unlock: function () { - var self = Search - self.locked = false - }, - open: function (focus) { - var self = Search - - clearTimeout(self.timeOut) - if (!self.isOpen && !self.changing && !self.locked) { - self.changing = true - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '400px' - }, 300, function () { - if (focus) $('.sidebarSearchField').focus() - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 10px 3px 10px', - width: '380px' - }) - self.changing = false - self.isOpen = true - }) - } - }, - close: function (closeAfter, bypass) { - // for now - return - - var self = Search - - self.timeOut = setTimeout(function () { - if (!self.locked && !self.changing && self.isOpen && (bypass || $('.sidebarSearchField.tt-input').val() === '')) { - self.changing = true - $('.sidebarSearchField, .sidebarSearch .tt-hint').css({ - padding: '7px 0 3px 0', - width: '400px' - }) - $('.sidebarSearch .twitter-typeahead, .sidebarSearch .tt-hint, .sidebarSearchField').animate({ - width: '0' - }, 300, function () { - $('.sidebarSearchField').typeahead('val', '') - $('.sidebarSearchField').blur() - self.changing = false - self.isOpen = false - }) - } - }, closeAfter) + focus: function() { + $('.sidebarSearchField').focus() }, startTypeahead: function () { var self = Search @@ -270,7 +191,6 @@ const Search = { self.hideLoader() if (['topic', 'map', 'mapper'].indexOf(datum.rtype) !== -1) { - self.close(0, true) if (datum.rtype === 'topic') { Router.topics(datum.id) } else if (datum.rtype === 'map') { From b396b94477307f6b47475ae0adf4851cd4edcb96 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 11:33:39 +0800 Subject: [PATCH 129/306] re-enable Ctrl+/ search box focus shortcut --- frontend/src/Metamaps/Listeners.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 78e881d4..a7f95fdf 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -8,6 +8,7 @@ import Realtime from './Realtime' import Selected from './Selected' import Topic from './Topic' import Visualize from './Visualize' +import { Search } from './GlobalUI' const Listeners = { init: function () { @@ -93,6 +94,11 @@ const Listeners = { }) } break + case 191: // if / is pressed + if (e.ctrlKey) { + Search.focus() + } + break default: // console.log(e.which) break From 7156fab3e29037dc9675be2b1bbc2d2a816203a7 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 14:42:07 +0800 Subject: [PATCH 130/306] fix topic controller bugs --- app/controllers/topics_controller.rb | 2 +- app/models/topic.rb | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index f909626a..ce430f43 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -88,7 +88,7 @@ class TopicsController < ApplicationController topicsAlreadyHas = params[:network] ? params[:network].split(',').map(&:to_i) : [] - alltopics = policy_scope(Topic.relatives(@topic.id)).to_a + alltopics = policy_scope(Topic.relatives(@topic.id, current_user)).to_a alltopics.delete_if { |topic| topic.metacode_id != params[:metacode].to_i } if params[:metacode].present? alltopics.delete_if do |topic| !topicsAlreadyHas.index(topic.id.to_s).nil? diff --git a/app/models/topic.rb b/app/models/topic.rb index 62f81cec..7d83ecac 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -36,19 +36,19 @@ class Topic < ApplicationRecord validates_attachment_content_type :audio, content_type: /\Aaudio\/.*\Z/ def synapses - synapses1 + synapses2 + synapses1.or(synapses2) end def relatives - topics1 + topics2 + topics1.or(topics2) end scope :relatives, ->(topic_id = nil, user = nil) { # should only see topics through *visible* synapses # e.g. Topic A (commons) -> synapse (private) -> Topic B (commons) must be filtered out - synapses = Pundit.policy_scope(user, Synapse.where(topic1_id: topic_id)).pluck(:topic2_id) - synapses += Pundit.policy_scope(user, Synapse.where(topic2_id: topic_id)).pluck(:topic1_id) - where(id: synapses.uniq) + topic_ids = Pundit.policy_scope(user, Synapse.where(topic1_id: topic_id)).pluck(:topic2_id) + topic_ids += Pundit.policy_scope(user, Synapse.where(topic2_id: topic_id)).pluck(:topic1_id) + where(id: topic_ids.uniq) } delegate :name, to: :user, prefix: true From 0e79f2ae4bf602d3728e9effa51fe17271544b7e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 30 Sep 2016 22:31:24 +0800 Subject: [PATCH 131/306] fix tsv --- frontend/src/Metamaps/Import.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 193cdf5e..aa75b382 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -31,13 +31,12 @@ const Import = { cidMappings: {}, // to be filled by import_id => cid mappings handleTSV: function (text) { - var self = Import - results = self.parseTabbedString(text) - self.handle(results) + const results = Import.parseTabbedString(text) + Import.handle(results) }, handleCSV: function (text, parserOpts = {}) { - var self = Import + const self = Import const topicsRegex = /("?Topics"?)([\s\S]*)/mi const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi @@ -71,9 +70,8 @@ const Import = { }, handleJSON: function (text) { - var self = Import - results = JSON.parse(text) - self.handle(results) + const results = JSON.parse(text) + Import.handle(results) }, handle: function(results) { From 6ae391265e3911a9e5e6b270a628748b471f18f3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 10:49:03 +0800 Subject: [PATCH 132/306] enable source maps --- webpack.config.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webpack.config.js b/webpack.config.js index fcdcbc04..f94f904a 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,4 +1,3 @@ -const path = require('path') const webpack = require('webpack') const NODE_ENV = process.env.NODE_ENV || 'development' @@ -15,9 +14,12 @@ if (NODE_ENV === 'production') { })) } +const devtool = NODE_ENV === 'production' ? undefined : 'cheap-module-eval-source-map' + const config = module.exports = { context: __dirname, plugins, + devtool, module: { loaders: [ { From 01872e740ea19c4a8b249e9cd73c888d2f66d4f5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 11:19:38 +0800 Subject: [PATCH 133/306] fix import if there are errors --- frontend/src/Metamaps/Import.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index aa75b382..1307aa45 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -54,13 +54,13 @@ const Import = { const topicsPromise = $.Deferred() parse(topicsText, csv_parser_options, (err, data) => { - if (err) topicsPromise.reject(err) + if (err) return topicsPromise.reject(err) topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) const synapsesPromise = $.Deferred() parse(synapsesText, csv_parser_options, (err, data) => { - if (err) synapsesPromise.reject(err) + if (err) return synapsesPromise.reject(err) synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) From 4328a6205feecf05b37a77281a059ea79a422b2d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 11:20:31 +0800 Subject: [PATCH 134/306] enable code duplication checks on code climate --- .codeclimate.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.codeclimate.yml b/.codeclimate.yml index 53f90d17..fbd96af2 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -5,11 +5,13 @@ engines: bundler-audit: enabled: true duplication: - enabled: false + enabled: true config: languages: - - ruby - - javascript + ruby: + mass_threshold: 36 # default: 18 + javascript: + mass_threshold: 80 # default: 40 eslint: enabled: true channel: "eslint-3" From e093ca5a30e0587b817911faf575ca8137a29152 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 11:21:42 +0800 Subject: [PATCH 135/306] more liberally import csv --- frontend/src/Metamaps/PasteInput.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 13258857..e7029d66 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -63,7 +63,8 @@ const PasteInput = { Import.handleJSON(text) } else if (text.match(/\t/)) { Import.handleTSV(text) - } else if (text.match(/","/)) { + } else { + // just try to see if CSV works Import.handleCSV(text) } }, From 1562d8fcfe63a8076ca0755bfda8c06018ed0678 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:14:38 +0800 Subject: [PATCH 136/306] topics imported with a link get Reference metacode --- frontend/src/Metamaps/Import.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 1307aa45..26a72952 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -40,9 +40,9 @@ const Import = { const topicsRegex = /("?Topics"?)([\s\S]*)/mi const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi - let topicsText = text.match(topicsRegex) + let topicsText = text.match(topicsRegex) || "" if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '') - let synapsesText = text.match(synapsesRegex) + let synapsesText = text.match(synapsesRegex) || "" if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') // merge default options and extra options passed in parserOpts argument @@ -54,13 +54,19 @@ const Import = { const topicsPromise = $.Deferred() parse(topicsText, csv_parser_options, (err, data) => { - if (err) return topicsPromise.reject(err) + if (err) { + console.warn(err) + return topicsPromise.resolve([]) + } topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) const synapsesPromise = $.Deferred() parse(synapsesText, csv_parser_options, (err, data) => { - if (err) return synapsesPromise.reject(err) + if (err) { + console.warn(err) + return synapsesPromise.resolve([]) + } synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) }) @@ -240,6 +246,10 @@ const Import = { } } + if (topic.name && topic.link && !topic.metacode) { + topic.metacode = "Reference" + } + self.createTopicWithParameters( topic.name, topic.metacode, topic.permission, topic.desc, topic.link, x, y, topic.id From fdf03ac83af1d4128f59ef5ae40fc51f70969780 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:32:40 +0800 Subject: [PATCH 137/306] source maps! (I think) --- webpack.config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index f94f904a..31adaaa1 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,7 @@ const config = module.exports = { test: /\.(js|jsx)?$/, exclude: /node_modules/, loaders: [ - "babel-loader?cacheDirectory" + "babel-loader?cacheDirectory&retainLines=true" ] } ] @@ -36,6 +36,7 @@ const config = module.exports = { }, output: { path: './app/assets/javascripts/webpacked', - filename: '[name].js' + filename: '[name].js', + devtoolModuleFilenameTemplate: '[absolute-resource-path]' } } From 4949f0dbd63c977b5cb88ac92dbeadce88287f6e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:43:02 +0800 Subject: [PATCH 138/306] eslint and use AutoLayout --- frontend/src/Metamaps/Import.js | 25 +++++++------------------ webpack.config.js | 2 +- 2 files changed, 8 insertions(+), 19 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 26a72952..e5e3e774 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -4,6 +4,7 @@ import parse from 'csv-parse' import _ from 'lodash' import Active from './Active' +import AutoLayout from './AutoLayout' import GlobalUI from './GlobalUI' import Map from './Map' import Synapse from './Synapse' @@ -40,9 +41,9 @@ const Import = { const topicsRegex = /("?Topics"?)([\s\S]*)/mi const synapsesRegex = /("?Synapses"?)([\s\S]*)/mi - let topicsText = text.match(topicsRegex) || "" + let topicsText = text.match(topicsRegex) || '' if (topicsText) topicsText = topicsText[2].replace(synapsesRegex, '') - let synapsesText = text.match(synapsesRegex) || "" + let synapsesText = text.match(synapsesRegex) || '' if (synapsesText) synapsesText = synapsesText[2].replace(topicsRegex, '') // merge default options and extra options passed in parserOpts argument @@ -223,31 +224,19 @@ const Import = { importTopics: function (parsedTopics) { var self = Import - // up to 25 topics: scale 100 - // up to 81 topics: scale 200 - // up to 169 topics: scale 300 - var scale = Math.floor((Math.sqrt(parsedTopics.length) - 1) / 4) * 100 - if (scale < 100) scale = 100 - var autoX = -scale - var autoY = -scale - parsedTopics.forEach(function (topic) { var x, y if (topic.x && topic.y) { x = topic.x y = topic.y } else { - x = autoX - y = autoY - autoX += 50 - if (autoX > scale) { - autoY += 50 - autoX = -scale - } + const coords = AutoLayout.getNextCoord() + x = coords.x + y = coords.y } if (topic.name && topic.link && !topic.metacode) { - topic.metacode = "Reference" + topic.metacode = 'Reference' } self.createTopicWithParameters( diff --git a/webpack.config.js b/webpack.config.js index 31adaaa1..91498abd 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -26,7 +26,7 @@ const config = module.exports = { test: /\.(js|jsx)?$/, exclude: /node_modules/, loaders: [ - "babel-loader?cacheDirectory&retainLines=true" + 'babel-loader?cacheDirectory&retainLines=true' ] } ] From c5564e02fcceebdfad60c80995d50057aaaedaf6 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:47:30 +0800 Subject: [PATCH 139/306] don't needt o open topic card --- frontend/src/Metamaps/PasteInput.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index e7029d66..8166f7c0 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -2,7 +2,6 @@ import AutoLayout from './AutoLayout' import Import from './Import' -import TopicCard from './TopicCard' import Util from './Util' const PasteInput = { @@ -101,10 +100,6 @@ const PasteInput = { topic.set('name', data.title) topic.save() }) - TopicCard.showCard(topic.get('node'), function() { - $('#showcard #titleActivator').click() - .find('textarea, input').focus() - }) } } ) From 20a32afe3b260fa9246c4cf555b2498763443793 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 12:57:19 +0800 Subject: [PATCH 140/306] integrate handleURL into Import --- frontend/src/Metamaps/Import.js | 78 +++++++++++++++++++++++------ frontend/src/Metamaps/PasteInput.js | 39 +-------------- 2 files changed, 64 insertions(+), 53 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index e5e3e774..2de7ca02 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -59,7 +59,7 @@ const Import = { console.warn(err) return topicsPromise.resolve([]) } - topicsPromise.resolve(data.map(row => self.lowercaseKeys(row))) + topicsPromise.resolve(data.map(row => self.normalizeKeys(row))) }) const synapsesPromise = $.Deferred() @@ -68,7 +68,7 @@ const Import = { console.warn(err) return synapsesPromise.resolve([]) } - synapsesPromise.resolve(data.map(row => self.lowercaseKeys(row))) + synapsesPromise.resolve(data.map(row => self.normalizeKeys(row))) }) $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { @@ -225,23 +225,25 @@ const Import = { var self = Import parsedTopics.forEach(function (topic) { - var x, y - if (topic.x && topic.y) { - x = topic.x - y = topic.y - } else { - const coords = AutoLayout.getNextCoord() - x = coords.x - y = coords.y + let coords = { x: topic.x, y: topic.y } + if (!coords.x || !coords.y) { + coords = AutoLayout.getNextCoord() } - if (topic.name && topic.link && !topic.metacode) { - topic.metacode = 'Reference' + if (!topic.name && topic.link || + topic.name && topic.link && !topic.metacode) { + self.handleURL(topic.link, { + coords, + name: topic.name, + permission: topic.permission, + import_id: topic.id + }) + return // "continue" } self.createTopicWithParameters( topic.name, topic.metacode, topic.permission, - topic.desc, topic.link, x, y, topic.id + topic.desc, topic.link, coords.x, coords.y, topic.id ) }) }, @@ -344,6 +346,47 @@ const Import = { Synapse.renderSynapse(mapping, synapse, node1, node2, true) }, + handleURL: function (url, opts = {}) { + let coords = opts.coords + if (!coords || coords.x === undefined || coords.y === undefined) { + coords = AutoLayout.getNextCoord() + } + + const name = opts.name || 'Link' + const metacode = opts.metacode || 'Reference' + const import_id = opts.import_id || null // don't store a cidMapping + const permission = opts.permission || null // use default + const desc = opts.desc || url + + Import.createTopicWithParameters( + name, + metacode, + permission, + desc, + url, + coords.x, + coords.y, + import_id, + { + success: function(topic) { + if (topic.get('name') !== 'Link') return + $.get('/hacks/load_url_title', { + url + }, function success(data, textStatus) { + var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' + if ($(selector).find('form').length > 0) { + $(selector).find('textarea, input').val(data.title) + } else { + $(selector).html(data.title) + } + topic.set('name', data.title) + topic.save() + }) + } + } + ) + }, + /* * helper functions */ @@ -352,6 +395,7 @@ const Import = { console.error(message) }, + // TODO investigate replacing with es6 (?) trim() simplify: function (string) { return string .replace(/(^\s*|\s*$)/g, '') @@ -360,9 +404,13 @@ const Import = { // thanks to http://stackoverflow.com/a/25290114/5332286 - lowercaseKeys: function(obj) { + normalizeKeys: function(obj) { return _.transform(obj, (result, val, key) => { - result[key.toLowerCase()] = val + let newKey = key.toLowerCase() + if (newKey === 'url') key = 'link' + if (newKey === 'title') key = 'name' + if (newKey === 'description') key = 'desc' + result[newKey] = val }) } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 8166f7c0..272ac030 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -1,6 +1,5 @@ /* global $ */ -import AutoLayout from './AutoLayout' import Import from './Import' import Util from './Util' @@ -57,7 +56,7 @@ const PasteInput = { var self = PasteInput if (text.match(self.URL_REGEX)) { - self.handleURL(text, coords) + Import.handleURL(text, coords) } else if (text[0] === '{') { Import.handleJSON(text) } else if (text.match(/\t/)) { @@ -68,42 +67,6 @@ const PasteInput = { } }, - handleURL: function (text, coords) { - var title = 'Link' - if (!coords || !coords.x || !coords.y) { - coords = AutoLayout.getNextCoord() - } - - var import_id = null // don't store a cidMapping - var permission = null // use default - - Import.createTopicWithParameters( - title, - 'Reference', // metacode - todo fix - permission, - text, // desc - todo load from url? - text, // link - todo fix because this isn't being POSTed - coords.x, - coords.y, - import_id, - { - success: function(topic) { - $.get('/hacks/load_url_title', { - url: text - }, function success(data, textStatus) { - var selector = '#showcard #topic_' + topic.get('id') + ' .best_in_place' - if ($(selector).find('form').length > 0) { - $(selector).find('textarea, input').val(data.title) - } else { - $(selector).html(data.title) - } - topic.set('name', data.title) - topic.save() - }) - } - } - ) - } } export default PasteInput From bb013787b6eb65298bc466674f0c9dd6df07442e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 13:34:52 +0800 Subject: [PATCH 141/306] make AutoLayout skip over coordinates if there is a mapping at that exact position --- frontend/src/Metamaps/AutoLayout.js | 23 +++++++++++++++++++---- frontend/src/Metamaps/Import.js | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index ee9dc33c..e0835473 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -1,3 +1,5 @@ +import Active from './Active' + const AutoLayout = { nextX: 0, nextY: 0, @@ -7,7 +9,7 @@ const AutoLayout = { nextYshift: 0, timeToTurn: 0, - getNextCoord: function () { + getNextCoord: function (opts = {}) { var self = AutoLayout var nextX = self.nextX var nextY = self.nextY @@ -49,9 +51,22 @@ const AutoLayout = { } } - return { - x: nextX, - y: nextY + if (opts.map && self.coordsTaken(nextX, nextY, opts.map)) { + // check if the coordinate is already taken on the current map + return self.getNextCoord(opts) + } else { + return { + x: nextX, + y: nextY + } + } + }, + coordsTaken: function(x, y, map) { + const mappings = map.getMappings() + if (mappings.findWhere({ xloc: x, yloc: y })) { + return true + } else { + return false } }, resetSpiral: function () { diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 2de7ca02..5d5f91a7 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -227,7 +227,7 @@ const Import = { parsedTopics.forEach(function (topic) { let coords = { x: topic.x, y: topic.y } if (!coords.x || !coords.y) { - coords = AutoLayout.getNextCoord() + coords = AutoLayout.getNextCoord({ map: Active.Map }) } if (!topic.name && topic.link || @@ -349,7 +349,7 @@ const Import = { handleURL: function (url, opts = {}) { let coords = opts.coords if (!coords || coords.x === undefined || coords.y === undefined) { - coords = AutoLayout.getNextCoord() + coords = AutoLayout.getNextCoord({ map: Active.Map }) } const name = opts.name || 'Link' From 8f230736dc11c4ea4158af993fc87216762112d5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 1 Oct 2016 13:47:16 +0800 Subject: [PATCH 142/306] code climate --- frontend/src/Metamaps/AutoLayout.js | 4 +--- frontend/src/Metamaps/PasteInput.js | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index e0835473..f3e91440 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -1,5 +1,3 @@ -import Active from './Active' - const AutoLayout = { nextX: 0, nextY: 0, @@ -61,7 +59,7 @@ const AutoLayout = { } } }, - coordsTaken: function(x, y, map) { + coordsTaken: function (x, y, map) { const mappings = map.getMappings() if (mappings.findWhere({ xloc: x, yloc: y })) { return true diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 272ac030..bc20ec43 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -65,8 +65,7 @@ const PasteInput = { // just try to see if CSV works Import.handleCSV(text) } - }, - + } } export default PasteInput From ca981898d43213a689e92922b34a6052a138a027 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 2 Oct 2016 00:09:55 +0800 Subject: [PATCH 143/306] arrow key panning - fixes #239 --- frontend/src/Metamaps/Listeners.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index a7f95fdf..2eb092dd 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -24,6 +24,18 @@ const Listeners = { case 27: // if esc key is pressed JIT.escKeyHandler() break + case 37: // if Left arrow key is pressed + Visualize.mGraph.canvas.translate(-20, 0) + break + case 38: // if Up arrow key is pressed + Visualize.mGraph.canvas.translate(0, -20) + break + case 39: // if Right arrow key is pressed + Visualize.mGraph.canvas.translate(20, 0) + break + case 40: // if Down arrow key is pressed + Visualize.mGraph.canvas.translate(0, 20) + break case 65: // if a or A is pressed if (e.ctrlKey) { Control.deselectAllNodes() From bc139608c2f8fe79b6d66dd649e899f810cbb11d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 2 Oct 2016 10:09:13 +0800 Subject: [PATCH 144/306] Search.focus() is the new Search.open() --- app/views/explore/active.html.erb | 3 +-- app/views/explore/featured.html.erb | 3 +-- app/views/explore/mapper.html.erb | 3 +-- app/views/explore/mine.html.erb | 3 +-- app/views/explore/shared.html.erb | 3 +-- app/views/explore/starred.html.erb | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/app/views/explore/active.html.erb b/app/views/explore/active.html.erb index a70cdcae..0ca442e5 100644 --- a/app/views/explore/active.html.erb +++ b/app/views/explore/active.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Recently Active" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/featured.html.erb b/app/views/explore/featured.html.erb index cfe4a627..806b2fac 100644 --- a/app/views/explore/featured.html.erb +++ b/app/views/explore/featured.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Featured Maps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/mapper.html.erb b/app/views/explore/mapper.html.erb index 8c5ecf89..68792f47 100644 --- a/app/views/explore/mapper.html.erb +++ b/app/views/explore/mapper.html.erb @@ -14,6 +14,5 @@ <% content_for :mobile_title, @user.name %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/mine.html.erb b/app/views/explore/mine.html.erb index 17f715f9..8e39c296 100644 --- a/app/views/explore/mine.html.erb +++ b/app/views/explore/mine.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "My Maps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/shared.html.erb b/app/views/explore/shared.html.erb index fd02c810..246498a0 100644 --- a/app/views/explore/shared.html.erb +++ b/app/views/explore/shared.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Shared With Me" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> diff --git a/app/views/explore/starred.html.erb b/app/views/explore/starred.html.erb index 4d0e0eb1..825dbf5d 100644 --- a/app/views/explore/starred.html.erb +++ b/app/views/explore/starred.html.erb @@ -11,6 +11,5 @@ <% content_for :mobile_title, "Starred Maps" %> Metamaps.currentSection = "explore"; - Metamaps.GlobalUI.Search.open(); - Metamaps.GlobalUI.Search.lock(); + Metamaps.GlobalUI.Search.focus(); </script> From b3c7e12d9a8ef5dc29147cc0c27247c53578ecd8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 2 Oct 2016 10:53:35 +0800 Subject: [PATCH 145/306] assets.debug was why assets were loud --- config/environments/development.rb | 5 +++-- config/initializers/assets.rb | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/config/environments/development.rb b/config/environments/development.rb index b1654921..dd6095b2 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -Metamaps::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb config.log_level = :info @@ -46,5 +46,6 @@ Metamaps::Application.configure do config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' # Expands the lines which load the assets - config.assets.debug = true + config.assets.debug = false + config.assets.quiet = true end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 4edab3b6..11f6601e 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -# Be sure to restart your server when you modify this file. +Rails.application.configure do + # Version of your assets, change this if you want to expire all your assets. + config.assets.version = '2.0' + config.assets.quiet = true -# Version of your assets, change this if you want to expire all your assets. -Rails.application.config.assets.version = '2.0' -Rails.application.config.assets.quiet = true + # Add additional assets to the asset load path + # Rails.application.config.assets.paths << Emoji.images_path -# Add additional assets to the asset load path -# Rails.application.config.assets.paths << Emoji.images_path - -# Precompile additional assets. -# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -Rails.application.config.assets.precompile += %w(webpacked/metamaps.bundle.js) + # Precompile additional assets. + # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. + config.assets.precompile += %w(webpacked/metamaps.bundle.js) +end From afa4422608f7e523cad0554a65dc60a30851530c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 29 Sep 2016 18:46:00 +0800 Subject: [PATCH 146/306] Custom formatter for slack exception notifications --- app/controllers/application_controller.rb | 7 +++ config/initializers/exception_notification.rb | 47 ++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 83889619..5dea17b5 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -7,6 +7,7 @@ class ApplicationController < ActionController::Base protect_from_forgery(with: :exception) before_action :invite_link + before_action :prepare_exception_notifier after_action :allow_embedding def default_serializer_options @@ -82,4 +83,10 @@ class ApplicationController < ActionController::Base # or allow a whitelist # response.headers['X-Frame-Options'] = 'ALLOW-FROM http://blog.metamaps.cc' end + + def prepare_exception_notifier + request.env['exception_notifier.exception_data'] = { + current_user: current_user + } + end end diff --git a/config/initializers/exception_notification.rb b/config/initializers/exception_notification.rb index db508b3c..a6c0d2b1 100644 --- a/config/initializers/exception_notification.rb +++ b/config/initializers/exception_notification.rb @@ -1,6 +1,51 @@ # frozen_string_literal: true require 'exception_notification/rails' +module ExceptionNotifier + class MetamapsNotifier < SlackNotifier + def call(exception, options = {}) + # trick original notifier to "ping" self, storing the result + # in @message_opts and then modifying the result + @old_notifier = @notifier + @notifier = self + super + @notifier = @old_notifier + + @message_opts[:attachments][0][:fields] = new_fields(exception, options[:env]) + @message_opts[:attachments][0][:text] = new_text(exception, options[:env]) + + @notifier.ping '', @message_opts + end + + def ping(message, message_opts) + @message = message + @message_opts = message_opts + end + + private + + def new_fields(exception, env) + new_fields = [] + + backtrace = exception.backtrace.reject { |line| line !~ %r{metamaps/(app|config|lib)} } + backtrace = backtrace[0..3] if backtrace.length > 4 + backtrace = "```\n#{backtrace.join("\n")}\n```" + new_fields << { title: 'Backtrace', value: backtrace } + + user = env.dig('exception_notifier.exception_data', :current_user) + new_fields << { title: 'Current User', value: "`#{user.name} <#{user.email}>`" } + + new_fields + end + + def new_text(exception, _env) + text = @message_opts[:attachments][0][:text].chomp + text += ': ' + exception.message + "\n" + text + end + end +end + ExceptionNotification.configure do |config| # Ignore additional exception types. # ActiveRecord::RecordNotFound, AbstractController::ActionNotFound and @@ -20,7 +65,7 @@ ExceptionNotification.configure do |config| # Notifiers ###### if ENV['SLACK_EN_WEBHOOK_URL'] - config.add_notifier :slack, webhook_url: ENV['SLACK_EN_WEBHOOK_URL'] + config.add_notifier :metamaps, webhook_url: ENV['SLACK_EN_WEBHOOK_URL'] end # Email notifier sends notifications by email. From 0f740e751a419c7666972dbeee70e8d9e249b906 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Sun, 2 Oct 2016 17:37:14 -0400 Subject: [PATCH 147/306] topics wasn't in backbone routes --- frontend/src/Metamaps/Create.js | 2 +- frontend/src/Metamaps/Router.js | 5 ++--- frontend/src/Metamaps/Topic.js | 2 ++ 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index e18ed1b3..bfb9b94c 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -346,7 +346,7 @@ const Create = { Create.newSynapse.topic1id = 0 Create.newSynapse.topic2id = 0 Mouse.synapseStartCoordinates = [] - Visualize.mGraph.plot() + if (Visualize.mGraph) Visualize.mGraph.plot() }, } } diff --git a/frontend/src/Metamaps/Router.js b/frontend/src/Metamaps/Router.js index 8bcd3590..9ad80187 100644 --- a/frontend/src/Metamaps/Router.js +++ b/frontend/src/Metamaps/Router.js @@ -27,7 +27,8 @@ const _Router = Backbone.Router.extend({ '': 'home', // #home 'explore/:section': 'explore', // #explore/active 'explore/:section/:id': 'explore', // #explore/mapper/1234 - 'maps/:id': 'maps' // #maps/7 + 'maps/:id': 'maps', // #maps/7 + 'topics/:id': 'topics' // #topics/7 }, home: function () { let self = this @@ -182,8 +183,6 @@ const _Router = Backbone.Router.extend({ topics: function (id) { clearTimeout(this.timeoutId) - document.title = 'Topic ' + id + ' | Metamaps' - this.currentSection = 'topic' this.currentPage = id diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index c2f3ff29..34e2bb64 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -69,6 +69,8 @@ const Topic = { Metamaps.Synapses = new bb.SynapseCollection(data.synapses) Metamaps.Backbone.attachCollectionEvents() + document.title = Active.Topic.get('name') + ' | Metamaps' + // set filter mapper H3 text $('#filter_by_mapper h3').html('CREATORS') From 87228a9631774be12693e36c6e18bb7a12aba676 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 3 Oct 2016 06:29:35 +0800 Subject: [PATCH 148/306] delete old gems and upgrade aws/paperclip (#676) * remove old gems from gemfile, upgrade aws/paperclip * update paperclip config * upload screenshots as a blob instead of base64 to maps controller --- .example-env | 1 + Gemfile | 26 +++++--------- Gemfile.lock | 54 ++++++++++-------------------- app/controllers/maps_controller.rb | 20 ++--------- app/models/map.rb | 25 -------------- app/policies/map_policy.rb | 4 --- config/application.rb | 12 +++++++ config/environments/development.rb | 11 ------ config/environments/production.rb | 18 ++++------ config/routes.rb | 1 - frontend/src/Metamaps/Map/index.js | 45 ++++++++++++++----------- 11 files changed, 74 insertions(+), 143 deletions(-) diff --git a/.example-env b/.example-env index c2c9a2e9..51d89c5d 100644 --- a/.example-env +++ b/.example-env @@ -14,6 +14,7 @@ export SECRET_KEY_BASE='267c8a84f63963282f45bc3010eaddf027abfab58fc759d6e239c800 # # you can safely leave these blank, unless you're deploying an instance, in # # which case you'll need to set them up # +# export S3_REGION # export S3_BUCKET_NAME # export AWS_ACCESS_KEY_ID # export AWS_SECRET_ACCESS_KEY diff --git a/Gemfile b/Gemfile index d5b42d83..7f34c12e 100644 --- a/Gemfile +++ b/Gemfile @@ -5,44 +5,34 @@ ruby '2.3.0' gem 'rails', '~> 5.0.0' gem 'active_model_serializers' -gem 'aws-sdk', '< 2.0' +gem 'aws-sdk' gem 'best_in_place' gem 'delayed_job' gem 'delayed_job_active_record' gem 'devise' -gem 'doorkeeper', '~> 4.0.0.rc4' +gem 'doorkeeper' gem 'dotenv-rails' gem 'exception_notification' -gem 'formtastic' -gem 'formula' gem 'httparty' gem 'json' gem 'kaminari' -gem 'paperclip', '~> 4.3.6' +gem 'paperclip' gem 'pg' gem 'pundit' gem 'pundit_extra' -gem 'rack-cors' gem 'rack-attack' +gem 'rack-cors' gem 'redis' gem 'slack-notifier' gem 'snorlax' gem 'uservoice-ruby' +# asset stuff +gem 'coffee-rails' gem 'jquery-rails' gem 'jquery-ui-rails' -gem 'jbuilder' -gem 'rails3-jquery-autocomplete' - -group :assets do - gem 'coffee-rails' - gem 'sass-rails' - gem 'uglifier' -end - -group :production do - gem 'rails_12factor' -end +gem 'sass-rails' +gem 'uglifier' group :test do gem 'factory_girl_rails' diff --git a/Gemfile.lock b/Gemfile.lock index 23d4c827..350585ae 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,11 +46,12 @@ GEM addressable (2.3.8) arel (7.1.2) ast (2.3.0) - aws-sdk (1.66.0) - aws-sdk-v1 (= 1.66.0) - aws-sdk-v1 (1.66.0) - json (~> 1.4) - nokogiri (>= 1.4.4) + aws-sdk (2.6.3) + aws-sdk-resources (= 2.6.3) + aws-sdk-core (2.6.3) + jmespath (~> 1.0) + aws-sdk-resources (2.6.3) + aws-sdk-core (= 2.6.3) bcrypt (3.1.11) best_in_place (3.1.0) actionpack (>= 3.2) @@ -91,7 +92,7 @@ GEM warden (~> 1.2.3) diff-lcs (1.2.5) docile (1.1.5) - doorkeeper (4.0.0) + doorkeeper (4.2.0) railties (>= 4.2) dotenv (2.1.1) dotenv-rails (2.1.1) @@ -108,18 +109,12 @@ GEM factory_girl_rails (4.7.0) factory_girl (~> 4.7.0) railties (>= 3.0.0) - formtastic (3.1.4) - actionpack (>= 3.2.13) - formula (1.1.1) - rails (> 3.0.0) globalid (0.3.7) activesupport (>= 4.1.0) httparty (0.14.0) multi_xml (>= 0.5.2) i18n (0.7.0) - jbuilder (2.6.0) - activesupport (>= 3.0.0, < 5.1) - multi_json (~> 1.2) + jmespath (1.3.1) jquery-rails (4.2.1) rails-dom-testing (>= 1, < 3) railties (>= 4.2.0) @@ -142,10 +137,9 @@ GEM mime-types (3.1) mime-types-data (~> 3.2015) mime-types-data (3.2016.0521) - mimemagic (0.3.0) + mimemagic (0.3.2) mini_portile2 (2.1.0) - minitest (5.9.0) - multi_json (1.12.1) + minitest (5.9.1) multi_xml (0.5.5) nio4r (1.2.1) nokogiri (1.6.8) @@ -153,12 +147,12 @@ GEM pkg-config (~> 1.1.7) oauth (0.5.1) orm_adapter (0.5.0) - paperclip (4.3.7) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) + paperclip (5.1.0) + activemodel (>= 4.2.0) + activesupport (>= 4.2.0) cocaine (~> 0.5.5) mime-types - mimemagic (= 0.3.0) + mimemagic (~> 0.3.0) parser (2.3.1.4) ast (~> 2.2) pg (0.19.0) @@ -199,13 +193,6 @@ GEM nokogiri (~> 1.6.0) rails-html-sanitizer (1.0.3) loofah (~> 2.0) - rails3-jquery-autocomplete (1.0.15) - rails (>= 3.2) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_serve_static_assets (0.0.5) - rails_stdout_logging (0.0.5) railties (5.0.0.1) actionpack (= 5.0.0.1) activesupport (= 5.0.0.1) @@ -290,7 +277,7 @@ PLATFORMS DEPENDENCIES active_model_serializers - aws-sdk (< 2.0) + aws-sdk best_in_place better_errors binding_of_caller @@ -299,20 +286,17 @@ DEPENDENCIES delayed_job delayed_job_active_record devise - doorkeeper (~> 4.0.0.rc4) + doorkeeper dotenv-rails exception_notification factory_girl_rails - formtastic - formula httparty - jbuilder jquery-rails jquery-ui-rails json json-schema kaminari - paperclip (~> 4.3.6) + paperclip pg pry-byebug pry-rails @@ -321,8 +305,6 @@ DEPENDENCIES rack-attack rack-cors rails (~> 5.0.0) - rails3-jquery-autocomplete - rails_12factor redis rspec-rails rubocop @@ -339,4 +321,4 @@ RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.12.5 + 1.13.2 diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 8d4c6e27..cdbbd900 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy, :access, :events, - :screenshot] + before_action :require_user, only: [:create, :update, :destroy, :access, :events] before_action :set_map, only: [:show, :update, :destroy, :access, :contains, - :events, :export, :screenshot] + :events, :export] after_action :verify_authorized - autocomplete :map, :name, full: true, extra_data: [:user_id] - # GET maps/:id def show respond_to do |format| @@ -136,17 +133,6 @@ class MapsController < ApplicationController end end - # POST maps/:id/upload_screenshot - def screenshot - @map.base64_screenshot(params[:encoded_image]) - - if @map.save - render json: { message: 'Successfully uploaded the map screenshot.' } - else - render json: { message: 'Failed to upload image.' } - end - end - private def set_map @@ -159,7 +145,7 @@ class MapsController < ApplicationController end def update_map_params - params.require(:map).permit(:id, :name, :arranged, :desc, :permission) + params.require(:map).permit(:id, :name, :arranged, :desc, :permission, :screenshot) end def create_topics! diff --git a/app/models/map.rb b/app/models/map.rb index f9fe6312..609b1be4 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -95,21 +95,6 @@ class Map < ApplicationRecord json end - def decode_base64(imgBase64) - decoded_data = Base64.decode64(imgBase64) - - data = StringIO.new(decoded_data) - data.class_eval do - attr_accessor :content_type, :original_filename - end - - data.content_type = 'image/png' - data.original_filename = File.basename('map-' + id.to_s + '-screenshot.png') - - self.screenshot = data - save - end - # user param helps determine what records are visible def contains(user) { @@ -144,14 +129,4 @@ class Map < ApplicationRecord end removed.compact end - - def base64_screenshot(encoded_image) - png = Base64.decode64(encoded_image['data:image/png;base64,'.length..-1]) - StringIO.open(png) do |data| - data.class.class_eval { attr_accessor :original_filename, :content_type } - data.original_filename = 'map-' + @map.id.to_s + '-screenshot.png' - data.content_type = 'image/png' - @map.screenshot = data - end - end end diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 84d24ca4..9999a055 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -60,8 +60,4 @@ class MapPolicy < ApplicationPolicy def unstar? user.present? end - - def screenshot? - update? - end end diff --git a/config/application.rb b/config/application.rb index 96505b32..9d8870a9 100644 --- a/config/application.rb +++ b/config/application.rb @@ -43,5 +43,17 @@ module Metamaps # pundit errors return 403 FORBIDDEN config.action_dispatch.rescue_responses['Pundit::NotAuthorizedError'] = :forbidden + + # S3 file storage + config.paperclip_defaults = { + storage: :s3, + s3_protocol: 'https', + s3_region: ENV['S3_REGION'], + s3_credentials: { + bucket: ENV['S3_BUCKET_NAME'], + access_key_id: ENV['AWS_ACCESS_KEY_ID'], + secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] + } + } end end diff --git a/config/environments/development.rb b/config/environments/development.rb index dd6095b2..5449e5e8 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -14,17 +14,6 @@ Rails.application.configure do config.consider_all_requests_local = true config.action_controller.perform_caching = false - # S3 file storage - config.paperclip_defaults = { - storage: :s3, - s3_credentials: { - bucket: ENV['S3_BUCKET_NAME'], - access_key_id: ENV['AWS_ACCESS_KEY_ID'], - secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] - }, - s3_protocol: 'https' - } - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: ENV['SMTP_SERVER'], diff --git a/config/environments/production.rb b/config/environments/production.rb index f9c94af6..d3f8794e 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -5,6 +5,11 @@ Rails.application.configure do config.log_level = :warn config.eager_load = true + # 12 factor: log to stdout + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + # Code is not reloaded between requests config.cache_classes = true @@ -13,24 +18,13 @@ Rails.application.configure do config.action_controller.perform_caching = true # Disable Rails's static asset server (Apache or nginx will already do this) - config.public_file_server.enabled = false + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Don't fallback to assets pipeline if a precompiled asset is missed config.assets.compile = false config.assets.js_compressor = :uglifier - # S3 file storage - config.paperclip_defaults = { - storage: :s3, - s3_credentials: { - bucket: ENV['S3_BUCKET_NAME'], - access_key_id: ENV['AWS_ACCESS_KEY_ID'], - secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] - }, - s3_protocol: 'https' - } - config.action_mailer.delivery_method = :smtp config.action_mailer.smtp_settings = { address: ENV['SMTP_SERVER'], diff --git a/config/routes.rb b/config/routes.rb index 13b2a5ba..41dd40c4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,7 +19,6 @@ Metamaps::Application.routes.draw do get :export post 'events/:event', action: :events get :contains - post :upload_screenshot, action: :screenshot post :access, default: { format: :json } post :star, to: 'stars#create', defaults: { format: :json } post :unstar, to: 'stars#destroy', defaults: { format: :json } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 24ea08ea..387311c2 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,5 +1,7 @@ /* global Metamaps, $ */ +import outdent from 'outdent' + import Active from '../Active' import AutoLayout from '../AutoLayout' import Create from '../Create' @@ -323,9 +325,7 @@ const Map = { node.visited = !T }) - var imageData = { - encoded_image: canvas.canvas.toDataURL() - } + var imageData = canvas.canvas.toDataURL() var map = Active.Map @@ -341,24 +341,31 @@ const Map = { } today = mm + '/' + dd + '/' + yyyy - var mapName = map.get('name').split(' ').join([separator = '-']) - var downloadMessage = '' - downloadMessage += 'Captured map screenshot! ' - downloadMessage += "<a href='" + imageData.encoded_image + "' " - downloadMessage += "download='metamap-" + map.id + '-' + mapName + '-' + today + ".png'>DOWNLOAD</a>" + var mapName = map.get('name').split(' ').join(['-']) + const filename = `metamap-${map.id}-${mapName}-${today}.png` + + var downloadMessage = outdent` + Captured map screenshot! + <a href="${imageData.encodedImage}" download="${filename}">DOWNLOAD</a>` GlobalUI.notifyUser(downloadMessage) - $.ajax({ - type: 'POST', - dataType: 'json', - url: '/maps/' + Active.Map.id + '/upload_screenshot', - data: imageData, - success: function (data) { - console.log('successfully uploaded map screenshot') - }, - error: function () { - console.log('failed to save map screenshot') - } + canvas.canvas.toBlob(imageBlob => { + const formData = new window.FormData(); + formData.append('map[screenshot]', imageBlob, filename) + $.ajax({ + type: 'PATCH', + dataType: 'json', + url: `/maps/${map.id}`, + data: formData, + processData: false, + contentType: false, + success: function (data) { + console.log('successfully uploaded map screenshot') + }, + error: function () { + console.log('failed to save map screenshot') + } + }) }) } } From da3795a2c2bee89f68a3edb57e6308ab3561d558 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Sun, 2 Oct 2016 22:49:45 -0400 Subject: [PATCH 149/306] new map improvements (#710) * prehighlight the text for editing when taken to a new map * style --- app/controllers/explore_controller.rb | 2 +- app/controllers/main_controller.rb | 11 +++++------ frontend/src/Metamaps/Map/InfoBox.js | 6 +++++- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index 59045d5d..dc4c2de9 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,7 +9,7 @@ class ExploreController < ApplicationController # GET /explore/active def active - @maps = map_scope(Map) + @maps = map_scope(Map.where.not(name: 'Untitled Map')) respond_to do |format| format.html do diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 0ea9ba97..7df4e366 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -2,18 +2,17 @@ class MainController < ApplicationController before_action :authorize_main after_action :verify_authorized - after_action :verify_policy_scoped, only: [:home] # GET / def home respond_to do |format| format.html do - if !authenticated? - skip_policy_scope - render 'main/home' - else - @maps = policy_scope(Map).order(updated_at: :desc).page(1).per(20) + if authenticated? + @maps = policy_scope(Map).where.not(name: 'Untitled Map') + .order(updated_at: :desc).page(1).per(20) render 'explore/active' + else + render 'main/home' end end end diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index c36f70c3..ba95df4b 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -38,6 +38,9 @@ const InfoBox = { if (querystring == 'new') { self.open() $('.mapInfoBox').addClass('mapRequestTitle') + $('#mapInfoName').trigger('click') + $('#mapInfoName textarea').focus() + $('#mapInfoName textarea').select() } }, toggleBox: function (event) { @@ -139,7 +142,8 @@ const InfoBox = { // mobile menu $('#header_content').html(name) $('.mapInfoBox').removeClass('mapRequestTitle') - document.title = name + ' | Metamaps' + document.title = `${name} | Metamaps` + window.history.replaceState('', `${name} | Metamaps`, window.location.pathname) }) $('.mapInfoDesc .best_in_place_desc').unbind('ajax:success').bind('ajax:success', function () { From a2cde20f8f4ffef04bdf20c39ef8c8156e709b16 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 12:11:58 +0800 Subject: [PATCH 150/306] raml2html with 1.0 syntax working --- doc/api/api.raml | 36 +++++++++++--------------- doc/api/apis/mappings.raml | 4 +-- doc/api/apis/maps.raml | 4 +-- doc/api/apis/synapses.raml | 4 +-- doc/api/apis/tokens.raml | 4 +-- doc/api/apis/topics.raml | 4 +-- doc/api/resourceTypes/collection.raml | 2 +- doc/api/resourceTypes/item.raml | 2 +- doc/api/schemas/_page.json | 1 - doc/api/schemas/error.json | 1 + doc/api/securitySchemes/oauth_2_0.raml | 7 +++++ doc/api/traits/orderable.raml | 2 +- doc/api/traits/pageable.raml | 2 ++ package.json | 3 ++- 14 files changed, 40 insertions(+), 36 deletions(-) create mode 100644 doc/api/schemas/error.json create mode 100644 doc/api/securitySchemes/oauth_2_0.raml diff --git a/doc/api/api.raml b/doc/api/api.raml index d61e66ac..d8a3afc3 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -2,35 +2,29 @@ --- title: Metamaps version: v2 -baseUri: http://metamaps.cc/api/v2 +baseUri: https://metamaps.cc/api/v2 mediaType: application/json securitySchemes: - - oauth_2_0: - description: | - OAuth 2.0 implementation - type: OAuth 2.0 - settings: - authorizationUri: https://metamaps.cc/api/v2/oauth/authorize - accessTokenUri: https://metamaps.cc/api/v2/oauth/token - authorizationGrants: [ authorization_code, password, client_credentials, implicit, refresh_token ] + oauth_2_0: !include securitySchemes/oauth_2_0.raml +securedBy: [ oauth_2_0 ] traits: - - pageable: !include traits/pageable.raml - - orderable: !include traits/orderable.raml - - searchable: !include traits/searchable.raml + pageable: !include traits/pageable.raml + orderable: !include traits/orderable.raml + searchable: !include traits/searchable.raml schemas: - - topic: !include schemas/_topic.json - - synapse: !include schemas/_synapse.json - - map: !include schemas/_map.json - - mapping: !include schemas/_mapping.json - - token: !include schemas/_token.json + topic: !include schemas/_topic.json + synapse: !include schemas/_synapse.json + map: !include schemas/_map.json + mapping: !include schemas/_mapping.json + token: !include schemas/_token.json -resourceTypes: - - base: !include resourceTypes/base.raml - - item: !include resourceTypes/item.raml - - collection: !include resourceTypes/collection.raml +#resourceTypes: +# base: !include resourceTypes/base.raml +# item: !include resourceTypes/item.raml +# collection: !include resourceTypes/collection.raml /topics: !include apis/topics.raml /synapses: !include apis/synapses.raml diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index 8b72b4df..fad67fd2 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -25,7 +25,7 @@ post: application/json: example: !include ../examples/mapping.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index c5499a33..8c2c2825 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -27,7 +27,7 @@ post: application/json: example: !include ../examples/map.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml index 3169c712..3fb1eee1 100644 --- a/doc/api/apis/synapses.raml +++ b/doc/api/apis/synapses.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -27,7 +27,7 @@ post: application/json: example: !include ../examples/synapse.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index 9f471615..b9c3aaff 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection post: body: application/json: @@ -18,7 +18,7 @@ post: application/json: example: !include ../examples/tokens.json /{id}: - type: item + #type: item delete: responses: 204: diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml index 7c214dd2..07eb8886 100644 --- a/doc/api/apis/topics.raml +++ b/doc/api/apis/topics.raml @@ -1,4 +1,4 @@ -type: collection +#type: collection get: responses: 200: @@ -25,7 +25,7 @@ post: application/json: example: !include ../examples/topic.json /{id}: - type: item + #type: item get: responses: 200: diff --git a/doc/api/resourceTypes/collection.raml b/doc/api/resourceTypes/collection.raml index d54e6c0c..e2710dae 100644 --- a/doc/api/resourceTypes/collection.raml +++ b/doc/api/resourceTypes/collection.raml @@ -16,7 +16,7 @@ get?: post?: description: Create a new <<resourcePathName | !singularize>> responses: - 200: + 201: body: application/json: schema: <<resourcePathName | !singularize>> diff --git a/doc/api/resourceTypes/item.raml b/doc/api/resourceTypes/item.raml index 1abf040e..5c227d61 100644 --- a/doc/api/resourceTypes/item.raml +++ b/doc/api/resourceTypes/item.raml @@ -1,3 +1,4 @@ +type: base get?: description: Get a <<resourcePathName | !singularize>> responses: @@ -26,4 +27,3 @@ delete?: responses: 204: description: Removed -type: base diff --git a/doc/api/schemas/_page.json b/doc/api/schemas/_page.json index 635f0286..47f69d95 100644 --- a/doc/api/schemas/_page.json +++ b/doc/api/schemas/_page.json @@ -35,4 +35,3 @@ "per" ] } - diff --git a/doc/api/schemas/error.json b/doc/api/schemas/error.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/doc/api/schemas/error.json @@ -0,0 +1 @@ +{} diff --git a/doc/api/securitySchemes/oauth_2_0.raml b/doc/api/securitySchemes/oauth_2_0.raml new file mode 100644 index 00000000..b271e03a --- /dev/null +++ b/doc/api/securitySchemes/oauth_2_0.raml @@ -0,0 +1,7 @@ +description: | + OAuth 2.0 implementation +type: OAuth 2.0 +settings: + authorizationUri: https://metamaps.cc/api/v2/oauth/authorize + accessTokenUri: https://metamaps.cc/api/v2/oauth/token + authorizationGrants: [ authorization_code, client_credentials ] diff --git a/doc/api/traits/orderable.raml b/doc/api/traits/orderable.raml index 708736ab..a2b45ce9 100644 --- a/doc/api/traits/orderable.raml +++ b/doc/api/traits/orderable.raml @@ -1,3 +1,3 @@ queryParameters: sort: - description: The name of the field to sort by, prefixed by "-" to sort descending + description: The name of the comma-separated fields to sort by, prefixed by "-" to sort descending diff --git a/doc/api/traits/pageable.raml b/doc/api/traits/pageable.raml index 88165861..31fcb9a8 100644 --- a/doc/api/traits/pageable.raml +++ b/doc/api/traits/pageable.raml @@ -2,6 +2,8 @@ queryParameters: page: description: The page number type: integer + default: 1 per: description: Number of records per page type: integer + default: 20 diff --git a/package.json b/package.json index 106294fd..12f42e6c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "chai": "^3.5.0", "eslint": "^3.5.0", "eslint-plugin-react": "^6.3.0", - "mocha": "^3.0.2" + "mocha": "^3.0.2", + "raml2html": "^4.0.0-beta5" } } From 3d7a2ef5b1ab678b7dcbcd11060ca9d93297e4a5 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 13:51:07 +0800 Subject: [PATCH 151/306] make raml traits work and be accurate/useful --- app/controllers/api/v2/mappings_controller.rb | 3 +++ app/controllers/api/v2/tokens_controller.rb | 4 ++++ app/controllers/api/v2/topics_controller.rb | 3 +++ doc/api/api.raml | 1 + doc/api/apis/mappings.raml | 2 ++ doc/api/apis/maps.raml | 2 ++ doc/api/apis/synapses.raml | 2 ++ doc/api/apis/tokens.raml | 1 + doc/api/apis/topics.raml | 5 ++++- doc/api/examples/topic.json | 6 +++--- doc/api/examples/topics.json | 6 +++--- doc/api/traits/embeddable.raml | 8 ++++++++ doc/api/traits/orderable.raml | 2 ++ doc/api/traits/pageable.raml | 4 +++- doc/api/traits/searchable.raml | 4 +++- 15 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 doc/api/traits/embeddable.raml diff --git a/app/controllers/api/v2/mappings_controller.rb b/app/controllers/api/v2/mappings_controller.rb index 86aba865..4490e4af 100644 --- a/app/controllers/api/v2/mappings_controller.rb +++ b/app/controllers/api/v2/mappings_controller.rb @@ -2,6 +2,9 @@ module Api module V2 class MappingsController < RestfulController + def searchable_columns + [] + end end end end diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index d1a6b255..291c33d4 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -2,6 +2,10 @@ module Api module V2 class TokensController < RestfulController + def searchable_columns + [:description] + end + def my_tokens authorize resource_class instantiate_collection diff --git a/app/controllers/api/v2/topics_controller.rb b/app/controllers/api/v2/topics_controller.rb index 22e534ce..b47dc8a0 100644 --- a/app/controllers/api/v2/topics_controller.rb +++ b/app/controllers/api/v2/topics_controller.rb @@ -2,6 +2,9 @@ module Api module V2 class TopicsController < RestfulController + def searchable_columns + [:name, :desc, :link] + end end end end diff --git a/doc/api/api.raml b/doc/api/api.raml index d8a3afc3..e59ae8d3 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -11,6 +11,7 @@ securedBy: [ oauth_2_0 ] traits: pageable: !include traits/pageable.raml + embeddable: !include traits/embeddable.raml orderable: !include traits/orderable.raml searchable: !include traits/searchable.raml diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index fad67fd2..9d0be18b 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ embeddable: { embedFields: "user,map" }, orderable, pageable ] responses: 200: body: @@ -27,6 +28,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "user,map" } ] responses: 200: body: diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index 8c2c2825..3cc7d13c 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ searchable: { searchFields: "name, desc" }, embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" }, orderable, pageable ] responses: 200: body: @@ -29,6 +30,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "user,topics,synapses,mappings,contributors,collaborators" } ] responses: 200: body: diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml index 3fb1eee1..cfd2f762 100644 --- a/doc/api/apis/synapses.raml +++ b/doc/api/apis/synapses.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ searchable: { searchFields: "desc" }, embeddable: { embedFields: "topic1,topic2,user" }, orderable, pageable ] responses: 200: body: @@ -29,6 +30,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "topic1,topic2,user" } ] responses: 200: body: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index b9c3aaff..70b69765 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -12,6 +12,7 @@ post: example: !include ../examples/token.json /my_tokens: get: + is: [ searchable: { searchFields: description }, pageable, orderable ] responses: 200: body: diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml index 07eb8886..09706754 100644 --- a/doc/api/apis/topics.raml +++ b/doc/api/apis/topics.raml @@ -1,5 +1,6 @@ #type: collection get: + is: [ searchable: { searchFields: "name, desc, link" }, embeddable: { embedFields: "user,metacode" }, orderable, pageable ] responses: 200: body: @@ -14,7 +15,8 @@ post: desc: description: description link: - description: (optional) link to content on the web + description: link to content on the web + required: false permission: description: commons, public, or private metacode_id: @@ -27,6 +29,7 @@ post: /{id}: #type: item get: + is: [ embeddable: { embedFields: "user,metacode" } ] responses: 200: body: diff --git a/doc/api/examples/topic.json b/doc/api/examples/topic.json index 90e702a2..d65eced1 100644 --- a/doc/api/examples/topic.json +++ b/doc/api/examples/topic.json @@ -1,9 +1,9 @@ { "data": { "id": 670, - "name": "Junto feedback and enhancements map", - "desc": "", - "link": "", + "name": "Metamaps.cc Website", + "desc": "Metamaps is a great website; check it out below!", + "link": "https://metamaps.cc", "permission": "commons", "created_at": "2016-07-02T09:23:30.397Z", "updated_at": "2016-07-02T09:23:30.397Z", diff --git a/doc/api/examples/topics.json b/doc/api/examples/topics.json index d4eba53e..5553c9e5 100644 --- a/doc/api/examples/topics.json +++ b/doc/api/examples/topics.json @@ -2,9 +2,9 @@ "data": [ { "id": 670, - "name": "Junto feedback and enhancements map", - "desc": "", - "link": "", + "name": "Metamaps.cc Website", + "desc": "Metamaps is a great website; check it out below!", + "link": "https://metamaps.cc", "permission": "commons", "created_at": "2016-07-02T09:23:30.397Z", "updated_at": "2016-07-02T09:23:30.397Z", diff --git a/doc/api/traits/embeddable.raml b/doc/api/traits/embeddable.raml new file mode 100644 index 00000000..e9eb61db --- /dev/null +++ b/doc/api/traits/embeddable.raml @@ -0,0 +1,8 @@ +queryParameters: + embed: + description: | + Comma-separated list of columns to embed. Each embedded column will be returned instead of the corresponding <code>field_id</code> or <code>field_ids</code> column. For instance, <code>?embed=user</code> would remove the <code>user_id</code> integer field from a response and replace it with a <code>user</code> object field. + + Possible embeddable fields are: <pre><< embedFields >></pre> + required: false + type: string diff --git a/doc/api/traits/orderable.raml b/doc/api/traits/orderable.raml index a2b45ce9..25baa756 100644 --- a/doc/api/traits/orderable.raml +++ b/doc/api/traits/orderable.raml @@ -1,3 +1,5 @@ queryParameters: sort: description: The name of the comma-separated fields to sort by, prefixed by "-" to sort descending + required: false + type: string diff --git a/doc/api/traits/pageable.raml b/doc/api/traits/pageable.raml index 31fcb9a8..cfb6810d 100644 --- a/doc/api/traits/pageable.raml +++ b/doc/api/traits/pageable.raml @@ -2,8 +2,10 @@ queryParameters: page: description: The page number type: integer + required: false default: 1 per: description: Number of records per page type: integer - default: 20 + required: false + default: 25 diff --git a/doc/api/traits/searchable.raml b/doc/api/traits/searchable.raml index 53ae8525..fb7700a9 100644 --- a/doc/api/traits/searchable.raml +++ b/doc/api/traits/searchable.raml @@ -1,4 +1,6 @@ queryParameters: q: - description: The search string to query by + description: | + Search text columns for this string. A query of <code>"example"</code> will be passed to SQL as <code>LIKE %example%</code>. The searchable columns are: <pre><< searchFields >></pre> + required: false type: string From 2466a0912fb54a534da864c850a0f1bb85dcb0b4 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 14:02:07 +0800 Subject: [PATCH 152/306] raml2html build script --- .gitignore | 1 + bin/build-apidocs.sh | 5 +++++ doc/production/first-deploy.md | 9 ++++++++- doc/production/pull-changes.md | 1 + package.json | 4 ++-- 5 files changed, 17 insertions(+), 3 deletions(-) create mode 100755 bin/build-apidocs.sh diff --git a/.gitignore b/.gitignore index 7f17330b..df92a1b7 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ #assety stuff public/assets public/metamaps_mobile +public/api/index.html vendor/ node_modules npm-debug.log diff --git a/bin/build-apidocs.sh b/bin/build-apidocs.sh new file mode 100755 index 00000000..677bbffd --- /dev/null +++ b/bin/build-apidocs.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +# Note: you need to run `npm install` before using this script or raml2html won't be installed + +./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html diff --git a/doc/production/first-deploy.md b/doc/production/first-deploy.md index cc3a1f4a..b118a2b5 100644 --- a/doc/production/first-deploy.md +++ b/doc/production/first-deploy.md @@ -65,7 +65,14 @@ Run this in the metamaps directory, still as metamaps: sudo aptitude install nodejs npm sudo ln -s /usr/bin/nodejs /usr/bin/node npm install - npm run build + +#### Precompile assets + +This step depends on running npm install first; assets:precompile will run `NODE_ENV=production npm run build`, and the build-apidocs.sh script requires the raml2html npm package. + + rake assets:precompile + rake perms:fix + bin/build-apidocs.sh #### Nginx and SSL diff --git a/doc/production/pull-changes.md b/doc/production/pull-changes.md index 30f41cf5..1bd1ebbe 100644 --- a/doc/production/pull-changes.md +++ b/doc/production/pull-changes.md @@ -26,6 +26,7 @@ Now that you have the code, run these commands: npm install rake db:migrate rake assets:precompile # includes `npm run build` + bin/build-apidocs.sh rake perms:fix passenger-config restart-app . diff --git a/package.json b/package.json index 12f42e6c..751ef284 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "lodash": "4.16.1", "node-uuid": "1.4.7", "outdent": "0.2.1", + "raml2html": "^4.0.0-beta5", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", @@ -41,7 +42,6 @@ "chai": "^3.5.0", "eslint": "^3.5.0", "eslint-plugin-react": "^6.3.0", - "mocha": "^3.0.2", - "raml2html": "^4.0.0-beta5" + "mocha": "^3.0.2" } } From 8ac8aad105a91a66f96a9a895150e47175f936e2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 15:30:06 +0800 Subject: [PATCH 153/306] PUT and PATCH parameters are optional --- doc/api/apis/mappings.raml | 25 +++++++++++++++++++++++-- doc/api/apis/maps.raml | 12 ++++++++++++ doc/api/apis/synapses.raml | 28 ++++++++++++++++++++++------ doc/api/apis/tokens.raml | 1 + doc/api/apis/topics.raml | 32 +++++++++++++++++++++++--------- 5 files changed, 81 insertions(+), 17 deletions(-) diff --git a/doc/api/apis/mappings.raml b/doc/api/apis/mappings.raml index 9d0be18b..a1643c86 100644 --- a/doc/api/apis/mappings.raml +++ b/doc/api/apis/mappings.raml @@ -17,9 +17,11 @@ post: map_id: description: id of the map xloc: - description: (for Topic mappings only) x location on the canvas + description: (only for Topic mappings) x location on the canvas + required: false yloc: - description: (for Topic mappings only) y location on the canvas + description: (only for Topic mappings) y location on the canvas + required: false responses: 201: body: @@ -40,10 +42,20 @@ post: properties: mappable_id: description: id of the topic/synapse to be mapped + required: false mappable_type: description: Topic or Synapse + required: false map_id: description: id of the map + required: false + xloc: + description: (only for Topic mappings) x location on the canvas + required: false + yloc: + description: (only for Topic mappings) y location on the canvas + required: false + responses: 200: body: @@ -55,10 +67,19 @@ post: properties: mappable_id: description: id of the topic/synapse to be mapped + required: false mappable_type: description: Topic or Synapse + required: false map_id: description: id of the map + required: false + xloc: + description: (only for Topic mappings) x location on the canvas + required: false + yloc: + description: (only for Topic mappings) y location on the canvas + required: false responses: 200: body: diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index 3cc7d13c..b742adce 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -42,16 +42,22 @@ post: properties: name: description: name + required: false desc: description: description + required: false permission: description: commons, public, or private + required: false screenshot: description: url to a screenshot of the map + required: false contributor_ids: description: the topic being linked from + required: false collaborator_ids: description: the topic being linked to + required: false responses: 200: body: @@ -63,16 +69,22 @@ post: properties: name: description: name + required: false desc: description: description + required: false permission: description: commons, public, or private + required: false screenshot: description: url to a screenshot of the map + required: false contributor_ids: description: the topic being linked from + required: false collaborator_ids: description: the topic being linked to + required: false responses: 200: body: diff --git a/doc/api/apis/synapses.raml b/doc/api/apis/synapses.raml index cfd2f762..dabcdad7 100644 --- a/doc/api/apis/synapses.raml +++ b/doc/api/apis/synapses.raml @@ -11,9 +11,11 @@ post: application/json: properties: desc: - description: name + description: text description of this synapse + required: false category: - description: from to or both + description: | + <code>from-to</code> or <code>both</code> permission: description: commons, public, or private topic1_id: @@ -41,17 +43,24 @@ post: application/json: properties: desc: - description: name + description: text description of this synapse + required: false category: - description: from-to or both + description: | + <code>from-to</code> or <code>both</code> + required: false permission: description: commons, public, or private + required: false topic1_id: description: the topic being linked from + required: false topic2_id: description: the topic being linked to + required: false user_id: description: the creator of the topic + required: false responses: 200: body: @@ -62,17 +71,24 @@ post: application/json: properties: desc: - description: name + description: text description of this synapse + required: false category: - description: from-to or both + description: | + <code>from-to</code> or <code>both</code> + required: false permission: description: commons, public, or private + required: false topic1_id: description: the topic being linked from + required: false topic2_id: description: the topic being linked to + required: false user_id: description: the creator of the topic + required: false responses: 200: body: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index 70b69765..ef7a8379 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -5,6 +5,7 @@ post: properties: description: description: short string describing this token + required: false responses: 201: body: diff --git a/doc/api/apis/topics.raml b/doc/api/apis/topics.raml index 09706754..15b94da4 100644 --- a/doc/api/apis/topics.raml +++ b/doc/api/apis/topics.raml @@ -11,11 +11,11 @@ post: application/json: properties: name: - description: name + description: Topic name; this will be visible on the map desc: - description: description + description: Longer topic description visible when opening a map card link: - description: link to content on the web + description: embed a link to content on the web in the topic card required: false permission: description: commons, public, or private @@ -40,13 +40,20 @@ post: application/json: properties: name: - description: name + description: Topic name; this will be visible on the map + required: false desc: - description: description + description: Longer topic description visible when opening a map card + required: false link: - description: (optional) link to content on the web + description: embed a link to content on the web in the topic card + required: false permission: description: commons, public, or private + required: false + metacode_id: + description: Topic's metacode + required: false responses: 200: body: @@ -57,13 +64,20 @@ post: application/json: properties: name: - description: name + description: Topic name; this will be visible on the map + required: false desc: - description: description + description: Longer topic description visible when opening a map card + required: false link: - description: (optional) link to content on the web + description: embed a link to content on the web in the topic card + required: false permission: description: commons, public, or private + required: false + metacode_id: + description: Topic's metacode + required: false responses: 200: body: From 8afef1bc4a66357ec11daa6af5dab94125c40089 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:02:58 +0800 Subject: [PATCH 154/306] make tokens description field optional --- app/controllers/api/v2/tokens_controller.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index 291c33d4..1170945f 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -6,6 +6,19 @@ module Api [:description] end + def create + if params[:token].blank? + self.resource = resource_class.new + else + instantiate_resource + end + + resource.user = current_user if current_user.present? + authorize resource + create_action + respond_with_resource + end + def my_tokens authorize resource_class instantiate_collection From 15b8440fbcaf4d51f8b98011b01fe8dcb030d9e3 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:21:09 +0800 Subject: [PATCH 155/306] move raml2html to optional dependencies so it can be installed globally --- .travis.yml | 2 +- bin/build-apidocs.sh | 4 ++++ package.json | 4 +++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37186702..99b9a655 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,7 +16,7 @@ before_script: - . $HOME/.nvm/nvm.sh - nvm install stable - nvm use stable - - npm install + - npm install --no-optional script: - bundle exec rspec && bundle exec brakeman -q -z && npm test addons: diff --git a/bin/build-apidocs.sh b/bin/build-apidocs.sh index 677bbffd..be85012c 100755 --- a/bin/build-apidocs.sh +++ b/bin/build-apidocs.sh @@ -2,4 +2,8 @@ # Note: you need to run `npm install` before using this script or raml2html won't be installed +if [[ ! -x ./node_modules/.bin/raml2html ]]; then + npm install +fi + ./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html diff --git a/package.json b/package.json index 751ef284..a976e5f1 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "lodash": "4.16.1", "node-uuid": "1.4.7", "outdent": "0.2.1", - "raml2html": "^4.0.0-beta5", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", @@ -43,5 +42,8 @@ "eslint": "^3.5.0", "eslint-plugin-react": "^6.3.0", "mocha": "^3.0.2" + }, + "optionalDependencies": { + "raml2html": "4.0.0-beta5" } } From a9831946d08e97b801852ce9222feda08b65729f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:31:07 +0800 Subject: [PATCH 156/306] ensure public/api directory exists --- public/api/.keep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 public/api/.keep diff --git a/public/api/.keep b/public/api/.keep new file mode 100644 index 00000000..e69de29b From c90460802e773ef78ea6534c869fb604c7091898 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 16:55:31 +0800 Subject: [PATCH 157/306] enable heroku to serve apidocs --- lib/tasks/extensions.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake index 776c81e3..6efd9026 100644 --- a/lib/tasks/extensions.rake +++ b/lib/tasks/extensions.rake @@ -3,6 +3,7 @@ namespace :assets do task :js_compile do system 'npm install' system 'npm run build' + system 'bin/build-apidocs.sh' if ENV['MAILER_DEFAULT_URL'] == 'metamaps.herokuapp.com' end end From 2eae89a6b7c7d46069e839013ac4bb7faa0f4fc9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 22:24:47 +0800 Subject: [PATCH 158/306] users and metacodes api endpoints --- .../api/v2/metacodes_controller.rb | 10 +++++ app/controllers/api/v2/restful_controller.rb | 3 +- app/controllers/api/v2/users_controller.rb | 24 +++++++++++ app/policies/metacode_policy.rb | 27 ++++++++++++ app/policies/user_policy.rb | 41 +++++++++++++++++++ app/serializers/api/v2/metacode_serializer.rb | 3 +- app/serializers/api/v2/user_serializer.rb | 4 +- config/routes.rb | 8 +++- doc/api/api.raml | 12 ++++-- doc/api/apis/metacodes.raml | 16 ++++++++ doc/api/apis/users.raml | 24 +++++++++++ doc/api/examples/metacode.json | 8 ++++ doc/api/examples/metacodes.json | 30 ++++++++++++++ doc/api/examples/user.json | 9 ++++ doc/api/examples/users.json | 24 +++++++++++ doc/api/schemas/_metacode.json | 24 +++++++++++ doc/api/schemas/_user.json | 28 +++++++++++++ doc/api/schemas/metacode.json | 12 ++++++ doc/api/schemas/metacodes.json | 19 +++++++++ doc/api/schemas/user.json | 12 ++++++ doc/api/schemas/users.json | 19 +++++++++ spec/api/v2/metacodes_api_spec.rb | 25 +++++++++++ spec/api/v2/users_api_spec.rb | 33 +++++++++++++++ 23 files changed, 405 insertions(+), 10 deletions(-) create mode 100644 app/controllers/api/v2/metacodes_controller.rb create mode 100644 app/controllers/api/v2/users_controller.rb create mode 100644 app/policies/metacode_policy.rb create mode 100644 app/policies/user_policy.rb create mode 100644 doc/api/apis/metacodes.raml create mode 100644 doc/api/apis/users.raml create mode 100644 doc/api/examples/metacode.json create mode 100644 doc/api/examples/metacodes.json create mode 100644 doc/api/examples/user.json create mode 100644 doc/api/examples/users.json create mode 100644 doc/api/schemas/_metacode.json create mode 100644 doc/api/schemas/_user.json create mode 100644 doc/api/schemas/metacode.json create mode 100644 doc/api/schemas/metacodes.json create mode 100644 doc/api/schemas/user.json create mode 100644 doc/api/schemas/users.json create mode 100644 spec/api/v2/metacodes_api_spec.rb create mode 100644 spec/api/v2/users_api_spec.rb diff --git a/app/controllers/api/v2/metacodes_controller.rb b/app/controllers/api/v2/metacodes_controller.rb new file mode 100644 index 00000000..71cd4a50 --- /dev/null +++ b/app/controllers/api/v2/metacodes_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Api + module V2 + class MetacodesController < RestfulController + def searchable_columns + [:name] + end + end + end +end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index f837957d..5d8f81b3 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -70,7 +70,8 @@ module Api def default_scope { - embeds: embeds + embeds: embeds, + current_user: current_user } end diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb new file mode 100644 index 00000000..9eba232f --- /dev/null +++ b/app/controllers/api/v2/users_controller.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true +module Api + module V2 + class UsersController < RestfulController + def current + @user = current_user + authorize @user + return show + end + + private + + def searchable_columns + [:name] + end + + # only ask serializer to return is_admin field if we're on the + # current_user action + def default_scope + super.merge(show_is_admin: action_name == 'current') + end + end + end +end diff --git a/app/policies/metacode_policy.rb b/app/policies/metacode_policy.rb new file mode 100644 index 00000000..e8787f8d --- /dev/null +++ b/app/policies/metacode_policy.rb @@ -0,0 +1,27 @@ +class MetacodePolicy < ApplicationPolicy + def index? + true + end + + def show? + true + end + + def create? + user.is_admin + end + + def update? + user.is_admin + end + + def destroy? + false + end + + class Scope < Scope + def resolve + scope.all + end + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb new file mode 100644 index 00000000..fa6158b8 --- /dev/null +++ b/app/policies/user_policy.rb @@ -0,0 +1,41 @@ +class UserPolicy < ApplicationPolicy + def index? + user.present? + end + + def show? + user.present? + end + + def create? + fail 'Create should be handled by Devise' + end + + def update? + user == record + end + + def destroy? + false + end + + def details? + show? + end + + def updatemetacodes? + update? + end + + # API action + def current? + user == record + end + + class Scope < Scope + def resolve + return scope.all if user.present? + scope.none + end + end +end diff --git a/app/serializers/api/v2/metacode_serializer.rb b/app/serializers/api/v2/metacode_serializer.rb index 16013e33..0b1ac553 100644 --- a/app/serializers/api/v2/metacode_serializer.rb +++ b/app/serializers/api/v2/metacode_serializer.rb @@ -4,9 +4,8 @@ module Api class MetacodeSerializer < ApplicationSerializer attributes :id, :name, - :manual_icon, :color, - :aws_icon + :icon end end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index ec58775d..3234205e 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -5,9 +5,11 @@ module Api attributes :id, :name, :avatar, - :is_admin, :generation + attribute :is_admin, + if: -> { scope[:show_is_admin] && scope[:current_user] == object } + def avatar object.image.url(:sixtyfour) end diff --git a/config/routes.rb b/config/routes.rb index 41dd40c4..76158105 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -63,13 +63,17 @@ Metamaps::Application.routes.draw do namespace :api, path: '/api', default: { format: :json } do namespace :v2, path: '/v2' do + resources :metacodes, only: [:index, :show] + resources :mappings, only: [:index, :create, :show, :update, :destroy] resources :maps, only: [:index, :create, :show, :update, :destroy] resources :synapses, only: [:index, :create, :show, :update, :destroy] - resources :topics, only: [:index, :create, :show, :update, :destroy] - resources :mappings, only: [:index, :create, :show, :update, :destroy] resources :tokens, only: [:create, :destroy] do get :my_tokens, on: :collection end + resources :topics, only: [:index, :create, :show, :update, :destroy] + resources :users, only: [:index, :show] do + get :current, on: :collection + end end namespace :v1, path: '/v1' do # api v1 routes all lead to a deprecation error method diff --git a/doc/api/api.raml b/doc/api/api.raml index e59ae8d3..624e5a46 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -16,19 +16,23 @@ traits: searchable: !include traits/searchable.raml schemas: - topic: !include schemas/_topic.json - synapse: !include schemas/_synapse.json map: !include schemas/_map.json mapping: !include schemas/_mapping.json + metacode: !include schemas/_metacode.json + synapse: !include schemas/_synapse.json token: !include schemas/_token.json + topic: !include schemas/_topic.json + user: !include schemas/_user.json #resourceTypes: # base: !include resourceTypes/base.raml # item: !include resourceTypes/item.raml # collection: !include resourceTypes/collection.raml -/topics: !include apis/topics.raml -/synapses: !include apis/synapses.raml /maps: !include apis/maps.raml /mappings: !include apis/mappings.raml +/metacodes: !include api/metacodes.raml +/synapses: !include apis/synapses.raml /tokens: !include apis/tokens.raml +/topics: !include apis/topics.raml +/users: !include apis/users.raml diff --git a/doc/api/apis/metacodes.raml b/doc/api/apis/metacodes.raml new file mode 100644 index 00000000..37cbd17a --- /dev/null +++ b/doc/api/apis/metacodes.raml @@ -0,0 +1,16 @@ +#type: collection +get: + is: [ searchable: { searchFields: "name" }, orderable, pageable ] + responses: + 200: + body: + application/json: + example: !include ../examples/metacodes.json +/{id}: + #type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/metacode.json diff --git a/doc/api/apis/users.raml b/doc/api/apis/users.raml new file mode 100644 index 00000000..7f421059 --- /dev/null +++ b/doc/api/apis/users.raml @@ -0,0 +1,24 @@ +#type: collection +get: + is: [ searchable: { searchFields: "name" }, orderable, pageable ] + responses: + 200: + body: + application/json: + example: !include ../examples/users.json +/current: + #type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/current_user.json +/{id}: + #type: item + get: + responses: + 200: + body: + application/json: + example: !include ../examples/user.json diff --git a/doc/api/examples/metacode.json b/doc/api/examples/metacode.json new file mode 100644 index 00000000..506a10c0 --- /dev/null +++ b/doc/api/examples/metacode.json @@ -0,0 +1,8 @@ +{ + "data": { + "id": 1, + "name": "Action", + "color": "#BD6C85", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_action.png" + } +} diff --git a/doc/api/examples/metacodes.json b/doc/api/examples/metacodes.json new file mode 100644 index 00000000..8e06f56c --- /dev/null +++ b/doc/api/examples/metacodes.json @@ -0,0 +1,30 @@ +{ + "data": [ + { + "id": 1, + "name": "Action", + "color": "#BD6C85", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_action.png" + }, + { + "id": 2, + "name": "Activity", + "color": "#6EBF65", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_activity.png" + }, + { + "id": 3, + "name": "Catalyst", + "color": "#EF8964", + "icon": "https://s3.amazonaws.com/metamaps-assets/metacodes/blueprint/96px/bp_catalyst.png" + } + ], + "page": { + "current_page": 1, + "next_page": 2, + "prev_page": 0, + "total_pages": 16, + "total_count": 47, + "per": 3 + } +} diff --git a/doc/api/examples/user.json b/doc/api/examples/user.json new file mode 100644 index 00000000..83476006 --- /dev/null +++ b/doc/api/examples/user.json @@ -0,0 +1,9 @@ +{ + "data": { + "id": 1, + "name": "user", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0, + "is_admin": false + } +} diff --git a/doc/api/examples/users.json b/doc/api/examples/users.json new file mode 100644 index 00000000..944f631e --- /dev/null +++ b/doc/api/examples/users.json @@ -0,0 +1,24 @@ +{ + "data": [ + { + "id": 1, + "name": "user", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0 + }, + { + "id": 2, + "name": "admin", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0 + } + ], + "page": { + "current_page": 1, + "next_page": 0, + "prev_page": 0, + "total_pages": 1, + "total_count": 2, + "per": 25 + } +} diff --git a/doc/api/schemas/_metacode.json b/doc/api/schemas/_metacode.json new file mode 100644 index 00000000..cc6b4f76 --- /dev/null +++ b/doc/api/schemas/_metacode.json @@ -0,0 +1,24 @@ +{ + "name": "Metacode", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string" + }, + "icon": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "color", + "icon" + ] +} diff --git a/doc/api/schemas/_user.json b/doc/api/schemas/_user.json new file mode 100644 index 00000000..e5805251 --- /dev/null +++ b/doc/api/schemas/_user.json @@ -0,0 +1,28 @@ +{ + "name": "User", + "type": "object", + "properties": { + "id": { + "$ref": "_id.json" + }, + "name": { + "type": "string" + }, + "avatar": { + "type": "string" + }, + "generation": { + "type": "integer", + "minimum": 0 + }, + "is_admin": { + "type": "boolean" + } + }, + "required": [ + "id", + "name", + "avatar", + "generation" + ] +} diff --git a/doc/api/schemas/metacode.json b/doc/api/schemas/metacode.json new file mode 100644 index 00000000..c5fa7106 --- /dev/null +++ b/doc/api/schemas/metacode.json @@ -0,0 +1,12 @@ +{ + "name": "Metacode Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_metacode.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/metacodes.json b/doc/api/schemas/metacodes.json new file mode 100644 index 00000000..c3869366 --- /dev/null +++ b/doc/api/schemas/metacodes.json @@ -0,0 +1,19 @@ +{ + "name": "Metacodes", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_metacode.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/doc/api/schemas/user.json b/doc/api/schemas/user.json new file mode 100644 index 00000000..a5c9d490 --- /dev/null +++ b/doc/api/schemas/user.json @@ -0,0 +1,12 @@ +{ + "name": "User Envelope", + "type": "object", + "properties": { + "data": { + "$ref": "_user.json" + } + }, + "required": [ + "data" + ] +} diff --git a/doc/api/schemas/users.json b/doc/api/schemas/users.json new file mode 100644 index 00000000..6cae2b80 --- /dev/null +++ b/doc/api/schemas/users.json @@ -0,0 +1,19 @@ +{ + "name": "Users", + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "_user.json" + } + }, + "page": { + "$ref": "_page.json" + } + }, + "required": [ + "data", + "page" + ] +} diff --git a/spec/api/v2/metacodes_api_spec.rb b/spec/api/v2/metacodes_api_spec.rb new file mode 100644 index 00000000..67d3d543 --- /dev/null +++ b/spec/api/v2/metacodes_api_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe 'metacodes API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:metacode) { create(:metacode) } + + it 'GET /api/v2/metacodes' do + create_list(:metacode, 5) + get '/api/v2/metacodes', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:metacodes) + expect(JSON.parse(response.body)['data'].count).to eq 5 + end + + it 'GET /api/v2/metacodes/:id' do + get "/api/v2/metacodes/#{metacode.id}", params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:metacode) + expect(JSON.parse(response.body)['data']['id']).to eq metacode.id + end +end diff --git a/spec/api/v2/users_api_spec.rb b/spec/api/v2/users_api_spec.rb new file mode 100644 index 00000000..70e22c2f --- /dev/null +++ b/spec/api/v2/users_api_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true +require 'rails_helper' + +RSpec.describe 'users API', type: :request do + let(:user) { create(:user, admin: true) } + let(:token) { create(:token, user: user).token } + let(:record) { create(:user) } + + it 'GET /api/v2/users' do + create_list(:user, 5) + get '/api/v2/users', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:users) + expect(JSON.parse(response.body)['data'].count).to eq 6 + end + + it 'GET /api/v2/users/:id' do + get "/api/v2/users/#{record.id}", params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:user) + expect(JSON.parse(response.body)['data']['id']).to eq record.id + end + + it 'GET /api/v2/users/current' do + get '/api/v2/users/current', params: { access_token: token } + + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:user) + expect(JSON.parse(response.body)['data']['id']).to eq user.id + end +end From df29e48d8c7e86a3193b942ff81987458f1d3486 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 22:51:21 +0800 Subject: [PATCH 159/306] rubocop + allow unauthed users to see all users --- app/controllers/api/v2/users_controller.rb | 4 ++-- app/policies/metacode_policy.rb | 1 + app/policies/user_policy.rb | 10 +++++----- app/serializers/api/v2/user_serializer.rb | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/controllers/api/v2/users_controller.rb b/app/controllers/api/v2/users_controller.rb index 9eba232f..b4b83e3f 100644 --- a/app/controllers/api/v2/users_controller.rb +++ b/app/controllers/api/v2/users_controller.rb @@ -5,9 +5,9 @@ module Api def current @user = current_user authorize @user - return show + show # delegate to the normal show function end - + private def searchable_columns diff --git a/app/policies/metacode_policy.rb b/app/policies/metacode_policy.rb index e8787f8d..626d23e3 100644 --- a/app/policies/metacode_policy.rb +++ b/app/policies/metacode_policy.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true class MetacodePolicy < ApplicationPolicy def index? true diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index fa6158b8..943200e8 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true class UserPolicy < ApplicationPolicy def index? - user.present? + true end def show? - user.present? + true end def create? - fail 'Create should be handled by Devise' + raise 'Create should be handled by Devise' end def update? @@ -34,8 +35,7 @@ class UserPolicy < ApplicationPolicy class Scope < Scope def resolve - return scope.all if user.present? - scope.none + scope.all end end end diff --git a/app/serializers/api/v2/user_serializer.rb b/app/serializers/api/v2/user_serializer.rb index 3234205e..c3b0c3fe 100644 --- a/app/serializers/api/v2/user_serializer.rb +++ b/app/serializers/api/v2/user_serializer.rb @@ -8,7 +8,7 @@ module Api :generation attribute :is_admin, - if: -> { scope[:show_is_admin] && scope[:current_user] == object } + if: -> { scope[:show_is_admin] && scope[:current_user] == object } def avatar object.image.url(:sixtyfour) From dbc2ff75df9ca1acdb5bb9b5f074d1df28e82991 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 23:06:49 +0800 Subject: [PATCH 160/306] make eslint work and update yoda config --- .eslintrc.js | 5 ++++- package.json | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 11a46fd1..aa594fa7 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -11,5 +11,8 @@ module.exports = { "promise", "standard", "react" - ] + ], + "rules": { + "yoda": [2, "never", { "exceptRange": true }] + } } diff --git a/package.json b/package.json index a976e5f1..c882fdd0 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,10 @@ "babel-eslint": "^6.1.2", "chai": "^3.5.0", "eslint": "^3.5.0", + "eslint-config-standard": "^6.2.0", + "eslint-plugin-promise": "^2.0.1", "eslint-plugin-react": "^6.3.0", + "eslint-plugin-standard": "^2.0.1", "mocha": "^3.0.2" }, "optionalDependencies": { From 113a5a253070817f806344bfae98b1d29c8fe574 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 23:38:32 +0800 Subject: [PATCH 161/306] fix a bunch of bug risk eslint warnings --- frontend/src/Metamaps/Account.js | 6 ++-- frontend/src/Metamaps/Admin.js | 2 +- frontend/src/Metamaps/Control.js | 4 +-- frontend/src/Metamaps/Create.js | 8 ++--- frontend/src/Metamaps/Import.js | 6 ++-- frontend/src/Metamaps/JIT.js | 40 +++++++++++++++++-------- frontend/src/Metamaps/Map/CheatSheet.js | 2 ++ frontend/src/Metamaps/Map/InfoBox.js | 4 +-- frontend/src/Metamaps/Map/index.js | 5 ++-- frontend/src/Metamaps/Organize.js | 7 ++--- frontend/src/Metamaps/PasteInput.js | 8 ++--- frontend/src/Metamaps/Realtime.js | 1 + frontend/src/Metamaps/Selected.js | 2 +- frontend/src/Metamaps/Settings.js | 2 +- frontend/src/Metamaps/TopicCard.js | 2 +- 15 files changed, 59 insertions(+), 40 deletions(-) diff --git a/frontend/src/Metamaps/Account.js b/frontend/src/Metamaps/Account.js index 10311cbd..1ac87811 100644 --- a/frontend/src/Metamaps/Account.js +++ b/frontend/src/Metamaps/Account.js @@ -1,3 +1,5 @@ +/* global $, CanvasLoader */ + /* * Metamaps.Erb */ @@ -43,7 +45,7 @@ const Account = { var file = $('#user_image')[0].files[0] - var reader = new FileReader() + var reader = new window.FileReader() reader.onload = function (e) { var $canvas = $('<canvas>').attr({ @@ -51,7 +53,7 @@ const Account = { height: 84 }) var context = $canvas[0].getContext('2d') - var imageObj = new Image() + var imageObj = new window.Image() imageObj.onload = function () { $('.userImageDiv canvas').remove() diff --git a/frontend/src/Metamaps/Admin.js b/frontend/src/Metamaps/Admin.js index 5d080c2e..d78fcecb 100644 --- a/frontend/src/Metamaps/Admin.js +++ b/frontend/src/Metamaps/Admin.js @@ -41,7 +41,7 @@ const Admin = { var self = Admin if (self.selectMetacodes.length == 0) { - alert('Would you pretty please select at least one metacode for the set?') + window.alert('Would you pretty please select at least one metacode for the set?') return false } } diff --git a/frontend/src/Metamaps/Control.js b/frontend/src/Metamaps/Control.js index c6c963ac..7662f47d 100644 --- a/frontend/src/Metamaps/Control.js +++ b/frontend/src/Metamaps/Control.js @@ -63,7 +63,7 @@ const Control = { return } - var r = confirm(outdent` + var r = window.confirm(outdent` You have ${ntext} and ${etext} selected. Are you sure you want to permanently delete them all? This will remove them from all maps they appear on.`) @@ -456,7 +456,7 @@ const Control = { var message = nString + ' you can edit updated to ' + metacode.get('name') GlobalUI.notifyUser(message) Visualize.mGraph.plot() - }, + } } export default Control diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index bfb9b94c..92271223 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -169,15 +169,15 @@ const Create = { queryTokenizer: Bloodhound.tokenizers.whitespace, remote: { url: '/topics/autocomplete_topic?term=%QUERY', - wildcard: '%QUERY', - }, + wildcard: '%QUERY' + } }) // initialize the autocomplete results for the metacode spinner $('#topic_name').typeahead( { highlight: true, - minLength: 2, + minLength: 2 }, [{ name: 'topic_autocomplete', @@ -186,7 +186,7 @@ const Create = { templates: { suggestion: function (s) { return Hogan.compile($('#topicAutocompleteTemplate').html()).render(s) - }, + } }, source: topicBloodhound, }] diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 5d5f91a7..6788335f 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -143,7 +143,7 @@ const Import = { // FALL THROUGH - if we're not sure what to do, pretend // we're on the TOPICS_NEED_HEADERS state and parse some headers - case STATES.TOPICS_NEED_HEADERS: // eslint-disable-line + case STATES.TOPICS_NEED_HEADERS: // eslint-disable-line no-fallthrough if (noblanks.length < 2) { self.abort('Not enough topic headers on line ' + index) state = STATES.ABORT @@ -207,8 +207,8 @@ const Import = { } break case STATES.ABORT: - - default: + // FALL THROUGH + default: // eslint-disable-line no-fallthrough self.abort('Invalid state while parsing import data. Check code.') state = STATES.ABORT } diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 54ec74b1..f8ffeb26 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -1,6 +1,7 @@ /* global Metamaps, $, Image, CanvasLoader */ import _ from 'lodash' +import outdent from 'outdent' import $jit from '../patched/JIT' @@ -1323,16 +1324,26 @@ const JIT = { if (Active.Topic) { menustring += '<li class="rc-center"><div class="rc-icon"></div>Center this topic<div class="rc-keyboard">Alt+E</div></li>' } + menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>' + if (Active.Mapper) { - var options = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ - <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ - <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ - </ul>' + var options = outdent` + <ul> + <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> + <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> + <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> + </ul>` menustring += '<li class="rc-spacer"></li>' - menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + options + '<div class="expandLi"></div></li>' + menustring += outdent` + <li class="rc-permission"> + <div class="rc-icon"></div> + Change permissions + ${options} + <div class="expandLi"></div> + </li>` var metacodeOptions = $('#metacodeOptions').html() @@ -1345,10 +1356,11 @@ const JIT = { // set up the get sibling menu as a "lazy load" // only fill in the submenu when they hover over the get siblings list item - var siblingMenu = '<ul id="fetchSiblingList"> \ - <li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li> \ - <li id="loadingSiblings"></li> \ - </ul>' + var siblingMenu = outdent` + <ul id="fetchSiblingList"> + <li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li> + <li id="loadingSiblings"></li> + </ul>` menustring += '<li class="rc-siblings"><div class="rc-icon"></div>Reveal siblings' + siblingMenu + '<div class="expandLi"></div></li>' } @@ -1571,10 +1583,12 @@ const JIT = { if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>' if (Active.Mapper) { - var permOptions = '<ul><li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> \ - <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ - <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ - </ul>' + var permOptions = outdent` + <ul> + <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> + <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ + <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ + </ul>` menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>' } diff --git a/frontend/src/Metamaps/Map/CheatSheet.js b/frontend/src/Metamaps/Map/CheatSheet.js index 969ee159..be9fbfab 100644 --- a/frontend/src/Metamaps/Map/CheatSheet.js +++ b/frontend/src/Metamaps/Map/CheatSheet.js @@ -1,3 +1,5 @@ +/* global $ */ + const CheatSheet = { init: function () { // tab the cheatsheet diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index ba95df4b..0d3a5c5f 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -343,7 +343,7 @@ const InfoBox = { permission: permission }) Active.Map.updateMapWrapper() - shareable = permission === 'private' ? '' : 'shareable' + const shareable = permission === 'private' ? '' : 'shareable' $('.mapPermission').removeClass('commons public private minimize').addClass(permission) $('.mapPermission .permissionSelect').remove() $('.mapInfoBox').removeClass('shareable').addClass(shareable) @@ -369,7 +369,7 @@ const InfoBox = { GlobalUI.notifyUser('Map eliminated!') } else if (!authorized) { - alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") + window.alert("Hey now. We can't just go around willy nilly deleting other people's maps now can we? Run off and find something constructive to do, eh?") } } } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 387311c2..dc9b4eb8 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -230,7 +230,7 @@ const Map = { canEditNow: function () { var confirmString = "You've been granted permission to edit this map. " confirmString += 'Do you want to reload and enable realtime collaboration?' - var c = confirm(confirmString) + var c = window.confirm(confirmString) if (c) { Router.maps(Active.Map.id) } @@ -256,10 +256,11 @@ const Map = { canvas.getSize = function () { if (this.size) return this.size var canvas = this.canvas - return this.size = { + this.size = { width: canvas.width, height: canvas.height } + return this.size } canvas.scale = function (x, y) { var px = this.scaleOffsetX * x, diff --git a/frontend/src/Metamaps/Organize.js b/frontend/src/Metamaps/Organize.js index ed005d39..c79bd8d7 100644 --- a/frontend/src/Metamaps/Organize.js +++ b/frontend/src/Metamaps/Organize.js @@ -1,5 +1,3 @@ -/* global $ */ - import _ from 'lodash' import $jit from '../patched/JIT' @@ -44,7 +42,7 @@ const Organize = { var column = floor(width / cellWidth) var totalCells = row * column - if (totalCells) + if (totalCells) { Visualize.mGraph.graph.eachNode(function (n) { if (column == numColumns) { column = 0 @@ -56,6 +54,7 @@ const Organize = { n.setPos(newPos, 'end') column += 1 }) + } Visualize.mGraph.animate(JIT.ForceDirected.animateSavedLayout) } else if (layout == 'radial') { var centerX = centerNode.getPos().x @@ -112,7 +111,7 @@ const Organize = { console.log(lowX, lowY, highX, highY) var newOriginX = (lowX + highX) / 2 var newOriginY = (lowY + highY) / 2 - } else alert('please call function with a valid layout dammit!') + } else window.alert('please call function with a valid layout dammit!') } } diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index bc20ec43..6f1cc03f 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -13,16 +13,16 @@ const PasteInput = { // intercept dragged files // see http://stackoverflow.com/questions/6756583 window.addEventListener("dragover", function(e) { - e = e || event; + e = e || window.event; e.preventDefault(); }, false); window.addEventListener("drop", function(e) { - e = e || event; + e = e || window.event; e.preventDefault(); var coords = Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { - var fileReader = new FileReader() - var text = fileReader.readAsText(e.dataTransfer.files[0]) + var fileReader = new window.FileReader() + fileReader.readAsText(e.dataTransfer.files[0]) fileReader.onload = function(e) { var text = e.currentTarget.result if (text.substring(0,5) === '<?xml') { diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 608523c8..6522d460 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -8,6 +8,7 @@ import GlobalUI from './GlobalUI' import JIT from './JIT' import Map from './Map' import Mapper from './Mapper' +import Synapse from './Synapse' import Topic from './Topic' import Util from './Util' import Views from './Views' diff --git a/frontend/src/Metamaps/Selected.js b/frontend/src/Metamaps/Selected.js index 396270ab..d23517b5 100644 --- a/frontend/src/Metamaps/Selected.js +++ b/frontend/src/Metamaps/Selected.js @@ -1,6 +1,6 @@ const Selected = { reset: function () { - var self = Metamaps.Selected + var self = Selected self.Nodes = [] self.Edges = [] }, diff --git a/frontend/src/Metamaps/Settings.js b/frontend/src/Metamaps/Settings.js index 687a6629..7010e543 100644 --- a/frontend/src/Metamaps/Settings.js +++ b/frontend/src/Metamaps/Settings.js @@ -15,7 +15,7 @@ const Settings = { background: '#18202E', text: '#DDD' } - }, + } } export default Settings diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 68061d96..0b2d1497 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, CanvasLoader, Countable, Hogan, embedly */ import Active from './Active' import GlobalUI from './GlobalUI' From e2c0ce7c22391c0c973734af4ecd814a8704b0d9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Tue, 4 Oct 2016 23:43:42 +0800 Subject: [PATCH 162/306] fix api documentation --- doc/api/api.raml | 2 +- doc/api/examples/current_user.json | 9 +++++++++ doc/api/examples/user.json | 3 +-- lib/tasks/extensions.rake | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 doc/api/examples/current_user.json diff --git a/doc/api/api.raml b/doc/api/api.raml index 624e5a46..50c2c992 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -31,7 +31,7 @@ schemas: /maps: !include apis/maps.raml /mappings: !include apis/mappings.raml -/metacodes: !include api/metacodes.raml +/metacodes: !include apis/metacodes.raml /synapses: !include apis/synapses.raml /tokens: !include apis/tokens.raml /topics: !include apis/topics.raml diff --git a/doc/api/examples/current_user.json b/doc/api/examples/current_user.json new file mode 100644 index 00000000..83476006 --- /dev/null +++ b/doc/api/examples/current_user.json @@ -0,0 +1,9 @@ +{ + "data": { + "id": 1, + "name": "user", + "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", + "generation": 0, + "is_admin": false + } +} diff --git a/doc/api/examples/user.json b/doc/api/examples/user.json index 83476006..ca54233d 100644 --- a/doc/api/examples/user.json +++ b/doc/api/examples/user.json @@ -3,7 +3,6 @@ "id": 1, "name": "user", "avatar": "https://s3.amazonaws.com/metamaps-assets/site/user.png", - "generation": 0, - "is_admin": false + "generation": 0 } } diff --git a/lib/tasks/extensions.rake b/lib/tasks/extensions.rake index 6efd9026..fc4a4855 100644 --- a/lib/tasks/extensions.rake +++ b/lib/tasks/extensions.rake @@ -3,7 +3,7 @@ namespace :assets do task :js_compile do system 'npm install' system 'npm run build' - system 'bin/build-apidocs.sh' if ENV['MAILER_DEFAULT_URL'] == 'metamaps.herokuapp.com' + system 'bin/build-apidocs.sh' if Rails.env.production? end end From 12417d8cd3b8548f376b8e48bc4153e40b2b871c Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 5 Oct 2016 01:45:21 +0800 Subject: [PATCH 163/306] update JIT eslint style --- frontend/src/Metamaps/JIT.js | 705 +++++++++++++++++------------------ 1 file changed, 343 insertions(+), 362 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index f8ffeb26..6c272a50 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -22,7 +22,6 @@ import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' - /* * Metamaps.Erb * Metamaps.Mappings @@ -48,19 +47,19 @@ const JIT = { removeSynapse: 'Metamaps:JIT:events:removeSynapse', pan: 'Metamaps:JIT:events:pan', zoom: 'Metamaps:JIT:events:zoom', - animationDone: 'Metamaps:JIT:events:animationDone', + animationDone: 'Metamaps:JIT:events:animationDone' }, vizData: [], // contains the visualization-compatible graph /** * This method will bind the event handlers it is interested and initialize the class. */ init: function () { - var self = JIT + const self = JIT $('.zoomIn').click(self.zoomIn) $('.zoomOut').click(self.zoomOut) - var zoomExtents = function (event) { + const zoomExtents = function (event) { self.zoomExtents(event, Visualize.mGraph.canvas) } $('.zoomExtents').click(zoomExtents) @@ -77,15 +76,15 @@ const JIT = { * convert our topic JSON into something JIT can use */ convertModelsToJIT: function (topics, synapses) { - var jitReady = [] + const jitReady = [] - var synapsesToRemove = [] - var mapping - var node - var nodes = {} - var existingEdge - var edge - var edges = [] + const synapsesToRemove = [] + let mapping + let node + const nodes = {} + let existingEdge + let edge + const edges = [] topics.each(function (t) { node = t.createNode() @@ -97,8 +96,7 @@ const JIT = { if (topics.get(s.get('topic1_id')) === undefined || topics.get(s.get('topic2_id')) === undefined) { // this means it's an invalid synapse synapsesToRemove.push(s) - } - else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { + } else if (nodes[edge.nodeFrom] && nodes[edge.nodeTo]) { existingEdge = _.find(edges, { nodeFrom: edge.nodeFrom, nodeTo: edge.nodeTo @@ -130,14 +128,14 @@ const JIT = { return [jitReady, synapsesToRemove] }, prepareVizData: function () { - var self = JIT - var mapping + const self = JIT + let mapping // reset/empty vizData self.vizData = [] Visualize.loadLater = false - var results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) + const results = self.convertModelsToJIT(Metamaps.Topics, Metamaps.Synapses) self.vizData = results[0] @@ -155,7 +153,7 @@ const JIT = { $('#instructions div.addTopic').show() } - if (self.vizData.length == 0) { + if (self.vizData.length === 0) { GlobalUI.showDiv('#instructions') Visualize.loadLater = true } else { @@ -165,11 +163,11 @@ const JIT = { Visualize.render() }, // prepareVizData edgeRender: function (adj, canvas) { - // get nodes cartesian coordinates - var pos = adj.nodeFrom.pos.getc(true) - var posChild = adj.nodeTo.pos.getc(true) + // get nodes cartesian coordinates + const pos = adj.nodeFrom.pos.getc(true) + const posChild = adj.nodeTo.pos.getc(true) - var synapse + let synapse if (adj.getData('displayIndex')) { synapse = adj.getData('synapses')[adj.getData('displayIndex')] if (!synapse) { @@ -185,17 +183,17 @@ const JIT = { // label placement on edges if (canvas.denySelected) { - var color = Settings.colors.synapses.normal + const color = Settings.colors.synapses.normal canvas.getCtx().fillStyle = canvas.getCtx().strokeStyle = color } JIT.renderEdgeArrows($jit.Graph.Plot.edgeHelper, adj, synapse, canvas) - // check for edge label in data - var desc = synapse.get('desc') + // check for edge label in data + let desc = synapse.get('desc') - var showDesc = adj.getData('showDesc') + const showDesc = adj.getData('showDesc') - var drawSynapseCount = function (context, x, y, count) { + const drawSynapseCount = function (context, x, y, count) { /* circle size: 16x16px positioning: overlay and center on top right corner of synapse label - 8px left and 8px down @@ -223,28 +221,28 @@ const JIT = { context.fillText(count, x, y + 5) } - if (!canvas.denySelected && desc != '' && showDesc) { + if (!canvas.denySelected && desc !== '' && showDesc) { // '&' to '&' desc = Util.decodeEntities(desc) - // now adjust the label placement - var ctx = canvas.getCtx() + // now adjust the label placement + const ctx = canvas.getCtx() ctx.font = 'bold 14px arial' ctx.fillStyle = '#FFF' ctx.textBaseline = 'alphabetic' - var arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') - var index, lineWidths = [] - for (index = 0; index < arrayOfLabelLines.length; ++index) { + const arrayOfLabelLines = Util.splitLine(desc, 30).split('\n') + let lineWidths = [] + for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } - var width = Math.max.apply(null, lineWidths) + 16 - var height = (16 * arrayOfLabelLines.length) + 8 + const width = Math.max.apply(null, lineWidths) + 16 + const height = (16 * arrayOfLabelLines.length) + 8 - var x = (pos.x + posChild.x - width) / 2 - var y = ((pos.y + posChild.y) / 2) - height / 2 + const x = (pos.x + posChild.x - width) / 2 + const y = ((pos.y + posChild.y) / 2) - height / 2 - var radius = 5 + const radius = 5 // render background ctx.beginPath() @@ -261,25 +259,24 @@ const JIT = { ctx.fill() // get number of synapses - var synapseNum = adj.getData('synapses').length + const synapseNum = adj.getData('synapses').length // render text ctx.fillStyle = '#424242' ctx.textAlign = 'center' - for (index = 0; index < arrayOfLabelLines.length; ++index) { + for (let index = 0; index < arrayOfLabelLines.length; ++index) { ctx.fillText(arrayOfLabelLines[index], x + (width / 2), y + 18 + (16 * index)) } if (synapseNum > 1) { drawSynapseCount(ctx, x + width, y, synapseNum) } - } - else if (!canvas.denySelected && showDesc) { + } else if (!canvas.denySelected && showDesc) { // get number of synapses - var synapseNum = adj.getData('synapses').length + const synapseNum = adj.getData('synapses').length if (synapseNum > 1) { - var ctx = canvas.getCtx() + const ctx = canvas.getCtx() const x = (pos.x + posChild.x) / 2 const y = (pos.y + posChild.y) / 2 drawSynapseCount(ctx, x, y, synapseNum) @@ -321,13 +318,13 @@ const JIT = { // background: { // type: 'Metamaps' // }, - // NodeStyles: { - // enable: true, - // type: 'Native', - // stylesHover: { - // dim: 30 - // }, - // duration: 300 + // NodeStyles: { + // enable: true, + // type: 'Native', + // stylesHover: { + // dim: 30 + // }, + // duration: 300 // }, // Change node and edge styles such as // color and width. @@ -400,8 +397,8 @@ const JIT = { Visualize.mGraph.busy = false Mouse.boxEndCoordinates = eventInfo.getPos() - var bS = Mouse.boxStartCoordinates - var bE = Mouse.boxEndCoordinates + const bS = Mouse.boxStartCoordinates + const bE = Mouse.boxEndCoordinates if (Math.abs(bS.x - bE.x) > 20 && Math.abs(bS.y - bE.y) > 20) { JIT.zoomToBox(e) return @@ -421,7 +418,7 @@ const JIT = { } } - if (e.target.id != 'infovis-canvas') return false + if (e.target.id !== 'infovis-canvas') return false // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { @@ -447,7 +444,7 @@ const JIT = { return } - if (e.target.id != 'infovis-canvas') return false + if (e.target.id !== 'infovis-canvas') return false // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { @@ -462,16 +459,16 @@ const JIT = { // Number of iterations for the FD algorithm iterations: 200, // Edge length - levelDistance: 200, + levelDistance: 200 }, nodeSettings: { 'customNode': { 'render': function (node, canvas) { - var pos = node.pos.getc(true), - dim = node.getData('dim'), - topic = node.getData('topic'), - metacode = topic ? topic.getMetacode() : false, - ctx = canvas.getCtx() + const pos = node.pos.getc(true) + const dim = node.getData('dim') + const topic = node.getData('topic') + const metacode = topic ? topic.getMetacode() : false + const ctx = canvas.getCtx() // if the topic is selected draw a circle around it if (!canvas.denySelected && node.selected) { @@ -496,9 +493,9 @@ const JIT = { } // if the topic has a link, draw a small image to indicate that - var hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null - var linkImage = JIT.topicLinkImage - var linkImageLoaded = linkImage.complete || + const hasLink = topic && topic.get('link') !== '' && topic.get('link') !== null + const linkImage = JIT.topicLinkImage + const linkImageLoaded = linkImage.complete || (typeof linkImage.naturalWidth !== 'undefined' && linkImage.naturalWidth !== 0) if (hasLink && linkImageLoaded) { @@ -506,9 +503,9 @@ const JIT = { } // if the topic has a desc, draw a small image to indicate that - var hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null - var descImage = JIT.topicDescImage - var descImageLoaded = descImage.complete || + const hasDesc = topic && topic.get('desc') !== '' && topic.get('desc') !== null + const descImage = JIT.topicDescImage + const descImageLoaded = descImage.complete || (typeof descImage.naturalWidth !== 'undefined' && descImage.naturalWidth !== 0) if (hasDesc && descImageLoaded) { @@ -516,21 +513,21 @@ const JIT = { } }, 'contains': function (node, pos) { - var npos = node.pos.getc(true), - dim = node.getData('dim'), - arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n'), - ctx = Visualize.mGraph.canvas.getCtx() + const npos = node.pos.getc(true) + const dim = node.getData('dim') + const arrayOfLabelLines = Util.splitLine(node.name, 30).split('\n') + const ctx = Visualize.mGraph.canvas.getCtx() - var height = 25 * arrayOfLabelLines.length + const height = 25 * arrayOfLabelLines.length - var index, lineWidths = [] - for (index = 0; index < arrayOfLabelLines.length; ++index) { + let lineWidths = [] + for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } - var width = Math.max.apply(null, lineWidths) + 8 - var labely = npos.y + node.getData('height') + 5 + height / 2 + const width = Math.max.apply(null, lineWidths) + 8 + const labely = npos.y + node.getData('height') + 5 + height / 2 - var overLabel = this.nodeHelper.rectangle.contains({ + const overLabel = this.nodeHelper.rectangle.contains({ x: npos.x, y: labely }, pos, width, height) @@ -545,8 +542,8 @@ const JIT = { JIT.edgeRender(adj, canvas) }, 'contains': function (adj, pos) { - var from = adj.nodeFrom.pos.getc(), - to = adj.nodeTo.pos.getc() + const from = adj.nodeFrom.pos.getc() + const to = adj.nodeTo.pos.getc() // this fixes an issue where when edges are perfectly horizontal or perfectly vertical // it becomes incredibly difficult to hover over them @@ -625,7 +622,7 @@ const JIT = { i: 0, onMouseMove: function (node, eventInfo, e) { // if(this.i++ % 3) return - var pos = eventInfo.getPos() + const pos = eventInfo.getPos() Visualize.cameraPosition.x += (pos.x - Visualize.cameraPosition.x) * 0.5 Visualize.cameraPosition.y += (-pos.y - Visualize.cameraPosition.y) * 0.5 Visualize.mGraph.plot() @@ -669,16 +666,16 @@ const JIT = { levelDistance: 200 }, onMouseEnter: function (edge) { - var filtered = edge.getData('alpha') === 0 + const filtered = edge.getData('alpha') === 0 // don't do anything if the edge is filtered - // or if the canvas is animating + // or if the canvas is animating if (filtered || Visualize.mGraph.busy) return $('canvas').css('cursor', 'pointer') - var edgeIsSelected = Selected.Edges.indexOf(edge) + const edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected - if (edgeIsSelected == -1) { + if (edgeIsSelected === -1) { edge.setData('showDesc', true, 'current') } @@ -692,11 +689,11 @@ const JIT = { Visualize.mGraph.plot() }, // onMouseEnter onMouseLeave: function (edge) { - if (edge.getData('alpha') === 0) return; // don't do anything if the edge is filtered + if (edge.getData('alpha') === 0) return // don't do anything if the edge is filtered $('canvas').css('cursor', 'default') - var edgeIsSelected = Selected.Edges.indexOf(edge) + const edgeIsSelected = Selected.Edges.indexOf(edge) // following if statement only executes if the edge being hovered over is not selected - if (edgeIsSelected == -1) { + if (edgeIsSelected === -1) { edge.setData('showDesc', false, 'current') } @@ -709,16 +706,16 @@ const JIT = { }) Visualize.mGraph.plot() }, // onMouseLeave - onMouseMoveHandler: function (node, eventInfo, e) { - var self = JIT + onMouseMoveHandler: function (_node, eventInfo, e) { + const self = JIT if (Visualize.mGraph.busy) return - var node = eventInfo.getNode() - var edge = eventInfo.getEdge() + const node = eventInfo.getNode() + const edge = eventInfo.getEdge() // if we're on top of a node object, act like there aren't edges under it - if (node != false) { + if (node !== false) { if (Mouse.edgeHoveringOver) { self.onMouseLeave(Mouse.edgeHoveringOver) } @@ -726,13 +723,13 @@ const JIT = { return } - if (edge == false && Mouse.edgeHoveringOver != false) { + if (edge === false && Mouse.edgeHoveringOver !== false) { // mouse not on an edge, but we were on an edge previously self.onMouseLeave(Mouse.edgeHoveringOver) - } else if (edge != false && Mouse.edgeHoveringOver == false) { + } else if (edge !== false && Mouse.edgeHoveringOver === false) { // mouse is on an edge, but there isn't a stored edge self.onMouseEnter(edge) - } else if (edge != false && Mouse.edgeHoveringOver != edge) { + } else if (edge !== false && Mouse.edgeHoveringOver !== edge) { // mouse is on an edge, but a different edge is stored self.onMouseLeave(Mouse.edgeHoveringOver) self.onMouseEnter(edge) @@ -746,16 +743,12 @@ const JIT = { } }, // onMouseMoveHandler enterKeyHandler: function () { - var creatingMap = GlobalUI.lightbox + const creatingMap = GlobalUI.lightbox if (creatingMap === 'newmap' || creatingMap === 'forkmap') { GlobalUI.CreateMap.submit() - } - // this is to submit new topic creation - else if (Create.newTopic.beingCreated) { + } else if (Create.newTopic.beingCreated) { Topic.createTopicLocally() - } - // to submit new synapse creation - else if (Create.newSynapse.beingCreated) { + } else if (Create.newSynapse.beingCreated) { Synapse.createSynapseLocally() } }, // enterKeyHandler @@ -764,27 +757,27 @@ const JIT = { Control.deselectAllNodes() }, // escKeyHandler onDragMoveTopicHandler: function (node, eventInfo, e) { - var self = JIT + const self = JIT - // this is used to send nodes that are moving to + // this is used to send nodes that are moving to // other realtime collaborators on the same map - var positionsToSend = {} - var topic + const positionsToSend = {} + let topic - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (node && !node.nodeFrom) { - var pos = eventInfo.getPos() + const pos = eventInfo.getPos() // if it's a left click, or a touch, move the node - if (e.touches || (e.button == 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons == 0 || e.buttons == 1 || e.buttons == undefined))) { + if (e.touches || (e.button === 0 && !e.altKey && !e.ctrlKey && !e.shiftKey && (e.buttons === 0 || e.buttons === 1 || e.buttons === undefined))) { // if the node dragged isn't already selected, select it - var whatToDo = self.handleSelectionBeforeDragging(node, e) + const whatToDo = self.handleSelectionBeforeDragging(node, e) if (node.pos.rho || node.pos.rho === 0) { // this means we're in topic view - var rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) - var theta = Math.atan2(pos.y, pos.x) + const rho = Math.sqrt(pos.x * pos.x + pos.y * pos.y) + const theta = Math.atan2(pos.y, pos.x) node.pos.setp(theta, rho) - } else if (whatToDo == 'only-drag-this-one') { + } else if (whatToDo === 'only-drag-this-one') { node.pos.setc(pos.x, pos.y) if (Active.Map) { @@ -797,21 +790,21 @@ const JIT = { $(document).trigger(JIT.events.topicDrag, [positionsToSend]) } } else { - var len = Selected.Nodes.length + const len = Selected.Nodes.length // first define offset for each node - var xOffset = [] - var yOffset = [] - for (var i = 0; i < len; i += 1) { - var n = Selected.Nodes[i] + const xOffset = [] + const yOffset = [] + for (let i = 0; i < len; i += 1) { + const n = Selected.Nodes[i] xOffset[i] = n.pos.x - node.pos.x yOffset[i] = n.pos.y - node.pos.y } // for - for (var i = 0; i < len; i += 1) { - var n = Selected.Nodes[i] - var x = pos.x + xOffset[i] - var y = pos.y + yOffset[i] + for (let i = 0; i < len; i += 1) { + const n = Selected.Nodes[i] + const x = pos.x + xOffset[i] + const y = pos.y + yOffset[i] n.pos.setc(x, y) if (Active.Map) { @@ -829,21 +822,20 @@ const JIT = { } } // if - if (whatToDo == 'deselect') { + if (whatToDo === 'deselect') { Control.deselectNode(node) } Visualize.mGraph.plot() - } - // if it's a right click or holding down alt, start synapse creation ->third option is for firefox - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && authorized) { - if (JIT.tempInit == false) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && authorized) { + // if it's a right click or holding down alt, start synapse creation ->third option is for firefox + if (JIT.tempInit === false) { JIT.tempNode = node JIT.tempInit = true Create.newTopic.hide() Create.newSynapse.hide() // set the draw synapse start positions - var l = Selected.Nodes.length + const l = Selected.Nodes.length if (l > 0) { for (let i = l - 1; i >= 0; i -= 1) { const n = Selected.Nodes[i] @@ -865,7 +857,7 @@ const JIT = { } // let temp = eventInfo.getNode() - if (temp != false && temp.id != node.id && Selected.Nodes.indexOf(temp) == -1) { // this means a Node has been returned + if (temp !== false && temp.id !== node.id && Selected.Nodes.indexOf(temp) === -1) { // this means a Node has been returned JIT.tempNode2 = temp Mouse.synapseEndCoordinates = { @@ -885,8 +877,8 @@ const JIT = { n.setData('dim', 25, 'current') }) // pop up node creation :) - var myX = e.clientX - 110 - var myY = e.clientY - 30 + const myX = e.clientX - 110 + const myY = e.clientY - 30 $('#new_topic').css('left', myX + 'px') $('#new_topic').css('top', myY + 'px') Create.newTopic.x = eventInfo.getPos().x @@ -898,11 +890,9 @@ const JIT = { y: pos.y } } - } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && Active.Topic) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && Active.Topic) { GlobalUI.notifyUser('Cannot create in Topic view.') - } - else if ((e.button == 2 || (e.button == 0 && e.altKey) || e.buttons == 2) && !authorized) { + } else if ((e.button === 2 || (e.button === 0 && e.altKey) || e.buttons === 2) && !authorized) { GlobalUI.notifyUser('Cannot edit Public map.') } } @@ -918,7 +908,9 @@ const JIT = { Visualize.mGraph.plot() }, // onDragCancelHandler onDragEndTopicHandler: function (node, eventInfo, e) { - var midpoint = {}, pixelPos, mapping + let midpoint = {} + let pixelPos + let mapping if (JIT.tempInit && JIT.tempNode2 == null) { // this means you want to add a new topic, and then a synapse @@ -944,20 +936,20 @@ const JIT = { // this means you dragged an existing node, autosave that to the database // check whether to save mappings - var checkWhetherToSave = function () { - var map = Active.Map + const checkWhetherToSave = function () { + const map = Active.Map if (!map) return false - var mapper = Active.Mapper + const mapper = Active.Mapper // this case // covers when it is a public map owned by you // and also when it's a private map - var activeMappersMap = map.authorizePermissionChange(mapper) - var commonsMap = map.get('permission') === 'commons' - var realtimeOn = Realtime.status + const activeMappersMap = map.authorizePermissionChange(mapper) + const commonsMap = map.get('permission') === 'commons' + const realtimeOn = Realtime.status - // don't save if commons map, and you have realtime off, + // don't save if commons map, and you have realtime off, // even if you're map creator return map && mapper && ((commonsMap && realtimeOn) || (activeMappersMap && !commonsMap)) } @@ -969,9 +961,9 @@ const JIT = { yloc: node.getPos().y }) // also save any other selected nodes that also got dragged along - var l = Selected.Nodes.length + const l = Selected.Nodes.length for (var i = l - 1; i >= 0; i -= 1) { - var n = Selected.Nodes[i] + const n = Selected.Nodes[i] if (n !== node) { mapping = n.getData('mapping') mapping.save({ @@ -984,24 +976,23 @@ const JIT = { } }, // onDragEndTopicHandler canvasClickHandler: function (canvasLoc, e) { - // grab the location and timestamp of the click - var storedTime = Mouse.lastCanvasClick - var now = Date.now() // not compatible with IE8 FYI + // grab the location and timestamp of the click + const storedTime = Mouse.lastCanvasClick + const now = Date.now() // not compatible with IE8 FYI Mouse.lastCanvasClick = now - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE && !Mouse.didPan) { if (Active.Map && !authorized) { GlobalUI.notifyUser('Cannot edit Public map.') return - } - else if (Active.Topic) { + } else if (Active.Topic) { GlobalUI.notifyUser('Cannot create in Topic view.') return } // DOUBLE CLICK - // pop up node creation :) + // pop up node creation :) Create.newTopic.addSynapse = false Create.newTopic.x = canvasLoc.x Create.newTopic.y = canvasLoc.y @@ -1026,7 +1017,7 @@ const JIT = { Control.deselectAllNodes() } } - }, // canvasClickHandler + }, // canvasClickHandler nodeDoubleClickHandler: function (node, e) { TopicCard.showCard(node) }, // nodeDoubleClickHandler @@ -1034,9 +1025,9 @@ const JIT = { SynapseCard.showCard(adj, e) }, // nodeDoubleClickHandler nodeWasDoubleClicked: function () { - // grab the timestamp of the click - var storedTime = Mouse.lastNodeClick - var now = Date.now() // not compatible with IE8 FYI + // grab the timestamp of the click + const storedTime = Mouse.lastNodeClick + const now = Date.now() // not compatible with IE8 FYI Mouse.lastNodeClick = now if (now - storedTime < Mouse.DOUBLE_CLICK_TOLERANCE) { @@ -1052,10 +1043,10 @@ const JIT = { // 3 others are selected only, no shift: drag only this one // 4 this node and others were selected, so drag them (just return false) // return value: deselect node again after? - if (Selected.Nodes.length == 0) { + if (Selected.Nodes.length === 0) { return 'only-drag-this-one' } - if (Selected.Nodes.indexOf(node) == -1) { + if (Selected.Nodes.indexOf(node) === -1) { if (e.shiftKey) { Control.selectNode(node, e) return 'nothing' @@ -1063,12 +1054,12 @@ const JIT = { return 'only-drag-this-one' } } - return 'nothing'; // case 4? + return 'nothing' // case 4? }, // handleSelectionBeforeDragging - getNodeXY: function(node) { - if (typeof node.pos.x === "number" && typeof node.pos.y === "number") { + getNodeXY: function (node) { + if (typeof node.pos.x === 'number' && typeof node.pos.y === 'number') { return node.pos - } else if (typeof node.pos.theta === "number" && typeof node.pos.rho === "number") { + } else if (typeof node.pos.theta === 'number' && typeof node.pos.rho === 'number') { return new $jit.Polar(node.pos.theta, node.pos.rho).getc(true) } else { console.error('getNodeXY: unrecognized node pos format') @@ -1076,11 +1067,11 @@ const JIT = { } }, selectWithBox: function (e) { - var self = this - var sX = Mouse.boxStartCoordinates.x, - sY = Mouse.boxStartCoordinates.y, - eX = Mouse.boxEndCoordinates.x, - eY = Mouse.boxEndCoordinates.y + const self = this + let sX = Mouse.boxStartCoordinates.x + let sY = Mouse.boxStartCoordinates.y + let eX = Mouse.boxEndCoordinates.x + let eY = Mouse.boxEndCoordinates.y if (!e.shiftKey) { Control.deselectAllNodes() @@ -1088,17 +1079,17 @@ const JIT = { } // select all nodes that are within the box - Visualize.mGraph.graph.eachNode(function(n) { - var pos = self.getNodeXY(n) - var x = pos.x, - y = pos.y + Visualize.mGraph.graph.eachNode(function (n) { + const pos = self.getNodeXY(n) + const x = pos.x + const y = pos.y // depending on which way the person dragged the box, check that // x and y are between the start and end values of the box if ((sX < x && x < eX && sY < y && y < eY) || - (sX > x && x > eX && sY > y && y > eY) || - (sX > x && x > eX && sY < y && y < eY) || - (sX < x && x < eX && sY > y && y > eY)) { + (sX > x && x > eX && sY > y && y > eY) || + (sX > x && x > eX && sY < y && y < eY) || + (sX < x && x < eX && sY > y && y > eY)) { if (e.shiftKey) { if (n.selected) { Control.deselectNode(n) @@ -1115,62 +1106,62 @@ const JIT = { sY = -1 * sY eY = -1 * eY - var edgesToToggle = [] + const edgesToToggle = [] Metamaps.Synapses.each(function (synapse) { - var e = synapse.get('edge') + const e = synapse.get('edge') if (edgesToToggle.indexOf(e) === -1) { edgesToToggle.push(e) } }) edgesToToggle.forEach(function (edge) { - var fromNodePos = self.getNodeXY(edge.nodeFrom) - var fromNodeX = fromNodePos.x - var fromNodeY = -1 * fromNodePos.y - var toNodePos = self.getNodeXY(edge.nodeTo) - var toNodeX = toNodePos.x - var toNodeY = -1 * toNodePos.y + const fromNodePos = self.getNodeXY(edge.nodeFrom) + const fromNodeX = fromNodePos.x + const fromNodeY = -1 * fromNodePos.y + const toNodePos = self.getNodeXY(edge.nodeTo) + const toNodeX = toNodePos.x + const toNodeY = -1 * toNodePos.y - var maxX = fromNodeX - var maxY = fromNodeY - var minX = fromNodeX - var minY = fromNodeY + let maxX = fromNodeX + let maxY = fromNodeY + let minX = fromNodeX + let minY = fromNodeY // Correct maxX, MaxY values ;(toNodeX > maxX) ? (maxX = toNodeX) : (minX = toNodeX) ;(toNodeY > maxY) ? (maxY = toNodeY) : (minY = toNodeY) - var maxBoxX = sX - var maxBoxY = sY - var minBoxX = sX - var minBoxY = sY + let maxBoxX = sX + let maxBoxY = sY + let minBoxX = sX + let minBoxY = sY // Correct maxBoxX, maxBoxY values ;(eX > maxBoxX) ? (maxBoxX = eX) : (minBoxX = eX) ;(eY > maxBoxY) ? (maxBoxY = eY) : (minBoxY = eY) // Find the slopes from the synapse fromNode to the 4 corners of the selection box - var slopes = [] + const slopes = [] slopes.push((sY - fromNodeY) / (sX - fromNodeX)) slopes.push((sY - fromNodeY) / (eX - fromNodeX)) slopes.push((eY - fromNodeY) / (eX - fromNodeX)) slopes.push((eY - fromNodeY) / (sX - fromNodeX)) - var minSlope = slopes[0] - var maxSlope = slopes[0] + let minSlope = slopes[0] + let maxSlope = slopes[0] slopes.forEach(function (entry) { if (entry > maxSlope) maxSlope = entry if (entry < minSlope) minSlope = entry }) // Find synapse-in-question's slope - var synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX) - var b = fromNodeY - synSlope * fromNodeX + const synSlope = (toNodeY - fromNodeY) / (toNodeX - fromNodeX) + const b = fromNodeY - synSlope * fromNodeX // Use the selection box edges as test cases for synapse intersection - var testX = sX - var testY = synSlope * testX + b + let testX = sX + let testY = synSlope * testX + b - var selectTest + let selectTest if (testX >= minX && testX <= maxX && testY >= minY && testY <= maxY && testY >= minBoxY && testY <= maxBoxY) { selectTest = true @@ -1205,9 +1196,9 @@ const JIT = { // The test synapse was selected! if (selectTest) { - // shiftKey = toggleSelect, otherwise + // shiftKey = toggleSelect, otherwise if (e.shiftKey) { - if (Selected.Edges.indexOf(edge) != -1) { + if (Selected.Edges.indexOf(edge) !== -1) { Control.deselectEdge(edge) } else { Control.selectEdge(edge) @@ -1222,12 +1213,12 @@ const JIT = { Visualize.mGraph.plot() }, // selectWithBox drawSelectBox: function (eventInfo, e) { - var ctx = Visualize.mGraph.canvas.getCtx() + const ctx = Visualize.mGraph.canvas.getCtx() - var startX = Mouse.boxStartCoordinates.x, - startY = Mouse.boxStartCoordinates.y, - currX = eventInfo.getPos().x, - currY = eventInfo.getPos().y + const startX = Mouse.boxStartCoordinates.x + const startY = Mouse.boxStartCoordinates.y + const currX = eventInfo.getPos().x + const currY = eventInfo.getPos().y Visualize.mGraph.canvas.clear() Visualize.mGraph.plot() @@ -1244,10 +1235,10 @@ const JIT = { selectNodeOnClickHandler: function (node, e) { if (Visualize.mGraph.busy) return - var self = JIT + const self = JIT // catch right click on mac, which is often like ctrl+click - if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { + if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { self.selectNodeOnRightClickHandler(node, e) return } @@ -1258,7 +1249,7 @@ const JIT = { return } - var check = self.nodeWasDoubleClicked() + const check = self.nodeWasDoubleClicked() if (check) { self.nodeDoubleClickHandler(node, e) return @@ -1266,7 +1257,7 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - var nodeAlreadySelected = node.selected + const nodeAlreadySelected = node.selected if (!e.shiftKey) { Control.deselectAllNodes() @@ -1304,17 +1295,17 @@ const JIT = { // delete old right click menu $('.rightclickmenu').remove() // create new menu for clicked on node - var rightclickmenu = document.createElement('div') + const rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' - //prevent the custom context menu from immediately opening the default context menu as well - rightclickmenu.setAttribute('oncontextmenu','return false') - + // prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu', 'return false') + // add the proper options to the menu - var menustring = '<ul>' + let menustring = '<ul>' - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) - var disabled = authorized ? '' : 'disabled' + const disabled = authorized ? '' : 'disabled' if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' @@ -1328,7 +1319,7 @@ const JIT = { menustring += '<li class="rc-popout"><div class="rc-icon"></div>Open in new tab</li>' if (Active.Mapper) { - var options = outdent` + const options = outdent` <ul> <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> @@ -1345,7 +1336,7 @@ const JIT = { <div class="expandLi"></div> </li>` - var metacodeOptions = $('#metacodeOptions').html() + const metacodeOptions = $('#metacodeOptions').html() menustring += '<li class="rc-metacode"><div class="rc-icon"></div>Change metacode' + metacodeOptions + '<div class="expandLi"></div></li>' } @@ -1356,7 +1347,7 @@ const JIT = { // set up the get sibling menu as a "lazy load" // only fill in the submenu when they hover over the get siblings list item - var siblingMenu = outdent` + const siblingMenu = outdent` <ul id="fetchSiblingList"> <li class="fetchAll">All<div class="rc-keyboard">Alt+R</div></li> <li id="loadingSiblings"></li> @@ -1368,36 +1359,35 @@ const JIT = { rightclickmenu.innerHTML = menustring // position the menu where the click happened - var position = {} - var RIGHTCLICK_WIDTH = 300 - var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static - var SUBMENUS_WIDTH = 256 - var MAX_SUBMENU_HEIGHT = 270 - var windowWidth = $(window).width() - var windowHeight = $(window).height() + const position = {} + const RIGHTCLICK_WIDTH = 300 + const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static + const SUBMENUS_WIDTH = 256 + const MAX_SUBMENU_HEIGHT = 270 + const windowWidth = $(window).width() + const windowHeight = $(window).height() if (windowWidth - e.clientX < SUBMENUS_WIDTH) { position.right = windowWidth - e.clientX $(rightclickmenu).addClass('moveMenusToLeft') - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { + } else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { position.right = windowWidth - e.clientX - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) { + } else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH + SUBMENUS_WIDTH) { position.left = e.clientX $(rightclickmenu).addClass('moveMenusToLeft') + } else { + position.left = e.clientX } - else position.left = e.clientX if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { position.bottom = windowHeight - e.clientY $(rightclickmenu).addClass('moveMenusUp') - } - else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { + } else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { position.top = e.clientY $(rightclickmenu).addClass('moveMenusUp') + } else { + position.top = e.clientY } - else position.top = e.clientY $(rightclickmenu).css(position) // add the menu to the page @@ -1438,7 +1428,7 @@ const JIT = { // open the entity in a new tab $('.rc-popout').click(function () { $('.rightclickmenu').remove() - var win = window.open('/topics/' + node.id, '_blank') + const win = window.open('/topics/' + node.id, '_blank') win.focus() }) @@ -1457,11 +1447,11 @@ const JIT = { }) // fetch relatives - var fetch_sent = false + let fetchSent = false $('.rc-siblings').hover(function () { - if (!fetch_sent) { + if (!fetchSent) { JIT.populateRightClickSiblings(node) - fetch_sent = true + fetchSent = true } }) $('.rc-siblings .fetchAll').click(function () { @@ -1471,28 +1461,25 @@ const JIT = { }) }, // selectNodeOnRightClickHandler, populateRightClickSiblings: function (node) { - var self = JIT - // depending on how many topics are selected, do different things - - var topic = node.getData('topic') + const topic = node.getData('topic') // add a loading icon for now - var loader = new CanvasLoader('loadingSiblings') - loader.setColor('#4FC059'); // default is '#000000' + const loader = new CanvasLoader('loadingSiblings') + loader.setColor('#4FC059') // default is '#000000' loader.setDiameter(15) // default is 40 loader.setDensity(41) // default is 40 - loader.setRange(0.9); // default is 1.3 + loader.setRange(0.9) // default is 1.3 loader.show() // Hidden by default - var topics = Metamaps.Topics.map(function (t) { return t.id }) - var topics_string = topics.join() + const topics = Metamaps.Topics.map(function (t) { return t.id }) + const topicsString = topics.join() - var successCallback = function (data) { + const successCallback = function (data) { $('#loadingSiblings').remove() for (var key in data) { - var string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')' + const string = Metamaps.Metacodes.get(key).get('name') + ' (' + data[key] + ')' $('#fetchSiblingList').append('<li class="getSiblings" data-id="' + key + '">' + string + '</li>') } @@ -1505,7 +1492,7 @@ const JIT = { $.ajax({ type: 'GET', - url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topics_string, + url: '/topics/' + topic.id + '/relative_numbers.json?network=' + topicsString, success: successCallback, error: function () {} }) @@ -1513,15 +1500,15 @@ const JIT = { selectEdgeOnClickHandler: function (adj, e) { if (Visualize.mGraph.busy) return - var self = JIT + const self = JIT // catch right click on mac, which is often like ctrl+click - if (navigator.platform.indexOf('Mac') != -1 && e.ctrlKey) { + if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { self.selectEdgeOnRightClickHandler(adj, e) return } - var check = self.nodeWasDoubleClicked() + const check = self.nodeWasDoubleClicked() if (check) { self.edgeDoubleClickHandler(adj, e) return @@ -1529,7 +1516,7 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - var edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 + const edgeAlreadySelected = Selected.Edges.indexOf(adj) !== -1 if (!e.shiftKey) { Control.deselectAllNodes() @@ -1551,7 +1538,7 @@ const JIT = { // the 'node' variable is a JIT node, the one that was clicked on // the 'e' variable is the click event - if (adj.getData('alpha') === 0) return; // don't do anything if the edge is filtered + if (adj.getData('alpha') === 0) return // don't do anything if the edge is filtered e.preventDefault() e.stopPropagation() @@ -1563,17 +1550,17 @@ const JIT = { // delete old right click menu $('.rightclickmenu').remove() // create new menu for clicked on node - var rightclickmenu = document.createElement('div') + const rightclickmenu = document.createElement('div') rightclickmenu.className = 'rightclickmenu' - //prevent the custom context menu from immediately opening the default context menu as well - rightclickmenu.setAttribute('oncontextmenu','return false') + // prevent the custom context menu from immediately opening the default context menu as well + rightclickmenu.setAttribute('oncontextmenu', 'return false') // add the proper options to the menu - var menustring = '<ul>' + let menustring = '<ul>' - var authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) + const authorized = Active.Map && Active.Map.authorizeToEdit(Active.Mapper) - var disabled = authorized ? '' : 'disabled' + const disabled = authorized ? '' : 'disabled' if (Active.Map) menustring += '<li class="rc-hide"><div class="rc-icon"></div>Hide until refresh<div class="rc-keyboard">Ctrl+H</div></li>' if (Active.Map && Active.Mapper) menustring += '<li class="rc-remove ' + disabled + '"><div class="rc-icon"></div>Remove from map<div class="rc-keyboard">Ctrl+M</div></li>' @@ -1583,12 +1570,10 @@ const JIT = { if (Active.Map && Active.Mapper) menustring += '<li class="rc-spacer"></li>' if (Active.Mapper) { - var permOptions = outdent` + const permOptions = outdent` <ul> <li class="changeP toCommons"><div class="rc-perm-icon"></div>commons</li> - <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> \ - <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> \ - </ul>` + <li class="changeP toPublic"><div class="rc-perm-icon"></div>public</li> <li class="changeP toPrivate"><div class="rc-perm-icon"></div>private</li> </ul>` menustring += '<li class="rc-permission"><div class="rc-icon"></div>Change permissions' + permOptions + '<div class="expandLi"></div></li>' } @@ -1597,32 +1582,28 @@ const JIT = { rightclickmenu.innerHTML = menustring // position the menu where the click happened - var position = {} - var RIGHTCLICK_WIDTH = 300 - var RIGHTCLICK_HEIGHT = 144; // this does vary somewhat, but we can use static - var SUBMENUS_WIDTH = 256 - var MAX_SUBMENU_HEIGHT = 270 - var windowWidth = $(window).width() - var windowHeight = $(window).height() + const position = {} + const RIGHTCLICK_WIDTH = 300 + const RIGHTCLICK_HEIGHT = 144 // this does vary somewhat, but we can use static + const SUBMENUS_WIDTH = 256 + const MAX_SUBMENU_HEIGHT = 270 + const windowWidth = $(window).width() + const windowHeight = $(window).height() if (windowWidth - e.clientX < SUBMENUS_WIDTH) { position.right = windowWidth - e.clientX $(rightclickmenu).addClass('moveMenusToLeft') - } - else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { + } else if (windowWidth - e.clientX < RIGHTCLICK_WIDTH) { position.right = windowWidth - e.clientX - } - else position.left = e.clientX + } else position.left = e.clientX if (windowHeight - e.clientY < MAX_SUBMENU_HEIGHT) { position.bottom = windowHeight - e.clientY $(rightclickmenu).addClass('moveMenusUp') - } - else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { + } else if (windowHeight - e.clientY < RIGHTCLICK_HEIGHT + MAX_SUBMENU_HEIGHT) { position.top = e.clientY $(rightclickmenu).addClass('moveMenusUp') - } - else position.top = e.clientY + } else position.top = e.clientY $(rightclickmenu).css(position) @@ -1663,20 +1644,19 @@ const JIT = { }) }, // selectEdgeOnRightClickHandler SmoothPanning: function () { - var sx = Visualize.mGraph.canvas.scaleOffsetX, - sy = Visualize.mGraph.canvas.scaleOffsetY, - y_velocity = Mouse.changeInY, // initial y velocity - x_velocity = Mouse.changeInX, // initial x velocity - easing = 1 // frictional value + const sx = Visualize.mGraph.canvas.scaleOffsetX + const sy = Visualize.mGraph.canvas.scaleOffsetY + const yVelocity = Mouse.changeInY // initial y velocity + const xVelocity = Mouse.changeInX // initial x velocity + let easing = 1 // frictional value - easing = 1 window.clearInterval(panningInt) panningInt = setInterval(function () { myTimer() }, 1) function myTimer () { - Visualize.mGraph.canvas.translate(x_velocity * easing * 1 / sx, y_velocity * easing * 1 / sy) + Visualize.mGraph.canvas.translate(xVelocity * easing * 1 / sx, yVelocity * easing * 1 / sy) $(document).trigger(JIT.events.pan) easing = easing * 0.75 @@ -1684,30 +1664,30 @@ const JIT = { } }, // SmoothPanning renderMidArrow: function (from, to, dim, swap, canvas, placement, newSynapse) { - var ctx = canvas.getCtx() - // invert edge direction + const ctx = canvas.getCtx() + // invert edge direction if (swap) { - var tmp = from + const tmp = from from = to to = tmp } - // vect represents a line from tip to tail of the arrow - var vect = new $jit.Complex(to.x - from.x, to.y - from.y) - // scale it + // vect represents a line from tip to tail of the arrow + const vect = new $jit.Complex(to.x - from.x, to.y - from.y) + // scale it vect.$scale(dim / vect.norm()) - // compute the midpoint of the edge line - var newX = (to.x - from.x) * placement + from.x - var newY = (to.y - from.y) * placement + from.y - var midPoint = new $jit.Complex(newX, newY) + // compute the midpoint of the edge line + const newX = (to.x - from.x) * placement + from.x + const newY = (to.y - from.y) * placement + from.y + const midPoint = new $jit.Complex(newX, newY) - // move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint - var arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y) - // compute the tail intersection point with the edge line - var intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y) - // vector perpendicular to vect - var normal = new $jit.Complex(-vect.y / 2, vect.x / 2) - var v1 = intermediatePoint.add(normal) - var v2 = intermediatePoint.$add(normal.$scale(-1)) + // move midpoint by half the "length" of the arrow so the arrow is centered on the midpoint + const arrowPoint = new $jit.Complex((vect.x / 0.7) + midPoint.x, (vect.y / 0.7) + midPoint.y) + // compute the tail intersection point with the edge line + const intermediatePoint = new $jit.Complex(arrowPoint.x - vect.x, arrowPoint.y - vect.y) + // vector perpendicular to vect + const normal = new $jit.Complex(-vect.y / 2, vect.x / 2) + const v1 = intermediatePoint.add(normal) + const v2 = intermediatePoint.$add(normal.$scale(-1)) if (newSynapse) { ctx.strokeStyle = '#4fc059' @@ -1725,18 +1705,18 @@ const JIT = { ctx.stroke() }, // renderMidArrow renderEdgeArrows: function (edgeHelper, adj, synapse, canvas) { - var self = JIT + const self = JIT - var directionCat = synapse.get('category') - var direction = synapse.getDirection() + const directionCat = synapse.get('category') + const direction = synapse.getDirection() - var pos = adj.nodeFrom.pos.getc(true) - var posChild = adj.nodeTo.pos.getc(true) + const pos = adj.nodeFrom.pos.getc(true) + const posChild = adj.nodeTo.pos.getc(true) - // plot arrow edge + // plot arrow edge if (!direction) { // render nothing for this arrow if the direction couldn't be retrieved - } else if (directionCat == 'none') { + } else if (directionCat === 'none') { edgeHelper.line.render({ x: pos.x, y: pos.y @@ -1744,7 +1724,7 @@ const JIT = { x: posChild.x, y: posChild.y }, canvas) - } else if (directionCat == 'both') { + } else if (directionCat === 'both') { self.renderMidArrow({ x: pos.x, y: pos.y @@ -1759,8 +1739,8 @@ const JIT = { x: posChild.x, y: posChild.y }, 13, false, canvas, 0.7) - } else if (directionCat == 'from-to') { - var inv = (direction[0] != adj.nodeFrom.id) + } else if (directionCat === 'from-to') { + const inv = (direction[0] !== adj.nodeFrom.id) self.renderMidArrow({ x: pos.x, y: pos.y @@ -1786,38 +1766,37 @@ const JIT = { $(document).trigger(JIT.events.zoom, [event]) }, centerMap: function (canvas) { - var offsetScale = canvas.scaleOffsetX + const offsetScale = canvas.scaleOffsetX canvas.scale(1 / offsetScale, 1 / offsetScale) - var offsetX = canvas.translateOffsetX - var offsetY = canvas.translateOffsetY + const offsetX = canvas.translateOffsetX + const offsetY = canvas.translateOffsetY canvas.translate(-1 * offsetX, -1 * offsetY) }, zoomToBox: function (event) { - var sX = Mouse.boxStartCoordinates.x, - sY = Mouse.boxStartCoordinates.y, - eX = Mouse.boxEndCoordinates.x, - eY = Mouse.boxEndCoordinates.y + const sX = Mouse.boxStartCoordinates.x + const sY = Mouse.boxStartCoordinates.y + const eX = Mouse.boxEndCoordinates.x + const eY = Mouse.boxEndCoordinates.y - var canvas = Visualize.mGraph.canvas + let canvas = Visualize.mGraph.canvas JIT.centerMap(canvas) - var height = $(document).height(), - width = $(document).width() + let height = $(document).height() + let width = $(document).width() - var spanX = Math.abs(sX - eX) - var spanY = Math.abs(sY - eY) - var ratioX = width / spanX - var ratioY = height / spanY + let spanX = Math.abs(sX - eX) + let spanY = Math.abs(sY - eY) + let ratioX = width / spanX + let ratioY = height / spanY - var newRatio = Math.min(ratioX, ratioY) + let newRatio = Math.min(ratioX, ratioY) if (canvas.scaleOffsetX * newRatio <= 5 && canvas.scaleOffsetX * newRatio >= 0.2) { canvas.scale(newRatio, newRatio) - } - else if (canvas.scaleOffsetX * newRatio > 5) { + } else if (canvas.scaleOffsetX * newRatio > 5) { newRatio = 5 / canvas.scaleOffsetX canvas.scale(newRatio, newRatio) } else { @@ -1825,8 +1804,8 @@ const JIT = { canvas.scale(newRatio, newRatio) } - var cogX = (sX + eX) / 2 - var cogY = (sY + eY) / 2 + const cogX = (sX + eX) / 2 + const cogY = (sY + eY) / 2 canvas.translate(-1 * cogX, -1 * cogY) $(document).trigger(JIT.events.zoom, [event]) @@ -1837,9 +1816,13 @@ const JIT = { }, zoomExtents: function (event, canvas, denySelected) { JIT.centerMap(canvas) - var height = canvas.getSize().height, - width = canvas.getSize().width, - maxX, minX, maxY, minY, counter = 0 + let height = canvas.getSize().height + let width = canvas.getSize().width + let maxX + let maxY + let minX + let minY + let counter = 0 let nodes if (!denySelected && Selected.Nodes.length > 0) { @@ -1850,30 +1833,30 @@ const JIT = { if (nodes.length > 1) { nodes.forEach(function (n) { - var x = n.pos.x, - y = n.pos.y + let x = n.pos.x + let y = n.pos.y - if (counter == 0 && n.getData('alpha') == 1) { + if (counter === 0 && n.getData('alpha') === 1) { maxX = x minX = x maxY = y minY = y } - var arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n'), - dim = n.getData('dim'), - ctx = canvas.getCtx() + let arrayOfLabelLines = Util.splitLine(n.name, 30).split('\n') + let dim = n.getData('dim') + let ctx = canvas.getCtx() - var height = 25 * arrayOfLabelLines.length + let height = 25 * arrayOfLabelLines.length - var index, lineWidths = [] - for (index = 0; index < arrayOfLabelLines.length; ++index) { + let lineWidths = [] + for (let index = 0; index < arrayOfLabelLines.length; ++index) { lineWidths.push(ctx.measureText(arrayOfLabelLines[index]).width) } - var width = Math.max.apply(null, lineWidths) + 8 + let width = Math.max.apply(null, lineWidths) + 8 // only adjust these values if the node is not filtered - if (n.getData('alpha') == 1) { + if (n.getData('alpha') === 1) { maxX = Math.max(x + width / 2, maxX) maxY = Math.max(y + n.getData('height') + 5 + height, maxY) minX = Math.min(x - width / 2, minX) @@ -1883,23 +1866,22 @@ const JIT = { } }) - var spanX = maxX - minX - var spanY = maxY - minY - var ratioX = spanX / width - var ratioY = spanY / height + let spanX = maxX - minX + let spanY = maxY - minY + let ratioX = spanX / width + let ratioY = spanY / height - var cogX = (maxX + minX) / 2 - var cogY = (maxY + minY) / 2 + let cogX = (maxX + minX) / 2 + let cogY = (maxY + minY) / 2 canvas.translate(-1 * cogX, -1 * cogY) - var newRatio = Math.max(ratioX, ratioY) - var scaleMultiplier = 1 / newRatio * 0.9 + let newRatio = Math.max(ratioX, ratioY) + let scaleMultiplier = 1 / newRatio * 0.9 if (canvas.scaleOffsetX * scaleMultiplier <= 3 && canvas.scaleOffsetX * scaleMultiplier >= 0.2) { canvas.scale(scaleMultiplier, scaleMultiplier) - } - else if (canvas.scaleOffsetX * scaleMultiplier > 3) { + } else if (canvas.scaleOffsetX * scaleMultiplier > 3) { scaleMultiplier = 3 / canvas.scaleOffsetX canvas.scale(scaleMultiplier, scaleMultiplier) } else { @@ -1908,11 +1890,10 @@ const JIT = { } $(document).trigger(JIT.events.zoom, [event]) - } - else if (nodes.length == 1) { + } else if (nodes.length === 1) { nodes.forEach(function (n) { - var x = n.pos.x, - y = n.pos.y + const x = n.pos.x + const y = n.pos.y canvas.translate(-1 * x, -1 * y) $(document).trigger(JIT.events.zoom, [event]) From d193c9a53cf905a611bb646c42994aa1510856d8 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 5 Oct 2016 22:36:03 +0800 Subject: [PATCH 164/306] add starred to maps API (#719) * add starred to maps API and endpoint to create/delete * add token to requests without token param * add minor version number to api version * metacode/user use uri in schema * make code climate happier --- app/controllers/api/v2/stars_controller.rb | 29 ++++++++++++++++++++++ app/models/map.rb | 13 ++++------ app/models/star.rb | 1 + app/serializers/api/v2/map_serializer.rb | 5 ++++ config/routes.rb | 5 +++- doc/api/api.raml | 2 +- doc/api/apis/maps.raml | 12 +++++++++ doc/api/examples/map.json | 1 + doc/api/examples/maps.json | 1 + doc/api/schemas/_map.json | 4 +++ doc/api/schemas/_metacode.json | 1 + doc/api/schemas/_user.json | 1 + spec/api/v2/mappings_api_spec.rb | 3 ++- spec/api/v2/maps_api_spec.rb | 19 +++++++++++++- spec/api/v2/topics_api_spec.rb | 3 ++- spec/factories/stars.rb | 5 ++++ 16 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 app/controllers/api/v2/stars_controller.rb create mode 100644 spec/factories/stars.rb diff --git a/app/controllers/api/v2/stars_controller.rb b/app/controllers/api/v2/stars_controller.rb new file mode 100644 index 00000000..8b62ee36 --- /dev/null +++ b/app/controllers/api/v2/stars_controller.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +module Api + module V2 + class StarsController < RestfulController + skip_before_action :load_resource + + def create + @map = Map.find(params[:id]) + @star = Star.new(user: current_user, map: @map) + authorize @map, :star? + create_action + + if @star.errors.empty? + render json: @map, scope: default_scope, serializer: MapSerializer, root: serializer_root + else + respond_with_errors + end + end + + def destroy + @map = Map.find(params[:id]) + authorize @map, :unstar? + @star = @map.stars.find_by(user: current_user) + @star.destroy if @star.present? + head :no_content + end + end + end +end diff --git a/app/models/map.rb b/app/models/map.rb index 609b1be4..a8e9c866 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -39,15 +39,8 @@ class Map < ApplicationRecord Perm.short(permission) end - # return an array of the contributors to the map def contributors - contributors = [] - - mappings.each do |m| - contributors.push(m.user) unless contributors.include?(m.user) - end - - contributors + mappings.map(&:user).uniq end def editors @@ -88,6 +81,10 @@ class Map < ApplicationRecord updated_at.strftime('%m/%d/%Y') end + def starred_by_user?(user) + user.stars.where(map: self).exists? + end + def as_json(_options = {}) json = super(methods: [:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json[:created_at_clean] = created_at_str diff --git a/app/models/star.rb b/app/models/star.rb index dcaaa559..a49ae2b1 100644 --- a/app/models/star.rb +++ b/app/models/star.rb @@ -2,4 +2,5 @@ class Star < ActiveRecord::Base belongs_to :user belongs_to :map + validates :map, uniqueness: { scope: :user, message: 'You have already starred this map' } end diff --git a/app/serializers/api/v2/map_serializer.rb b/app/serializers/api/v2/map_serializer.rb index 0a0be2c0..ff641c69 100644 --- a/app/serializers/api/v2/map_serializer.rb +++ b/app/serializers/api/v2/map_serializer.rb @@ -7,9 +7,14 @@ module Api :desc, :permission, :screenshot, + :starred, :created_at, :updated_at + def starred + object.starred_by_user?(scope[:current_user]) + end + def self.embeddable { user: {}, diff --git a/config/routes.rb b/config/routes.rb index 76158105..05fe5845 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -65,7 +65,10 @@ Metamaps::Application.routes.draw do namespace :v2, path: '/v2' do resources :metacodes, only: [:index, :show] resources :mappings, only: [:index, :create, :show, :update, :destroy] - resources :maps, only: [:index, :create, :show, :update, :destroy] + resources :maps, only: [:index, :create, :show, :update, :destroy] do + post :stars, to: 'stars#create', on: :member + delete :stars, to: 'stars#destroy', on: :member + end resources :synapses, only: [:index, :create, :show, :update, :destroy] resources :tokens, only: [:create, :destroy] do get :my_tokens, on: :collection diff --git a/doc/api/api.raml b/doc/api/api.raml index 50c2c992..6ffa29f1 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -1,7 +1,7 @@ #%RAML 1.0 --- title: Metamaps -version: v2 +version: v2.0 baseUri: https://metamaps.cc/api/v2 mediaType: application/json diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index b742adce..434dcc67 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -94,3 +94,15 @@ post: responses: 204: description: No content + /stars: + post: + responses: + 201: + description: Created + body: + application/json: + example: !include ../examples/map.json + delete: + responses: + 204: + description: No content diff --git a/doc/api/examples/map.json b/doc/api/examples/map.json index fe3796ca..20e63204 100644 --- a/doc/api/examples/map.json +++ b/doc/api/examples/map.json @@ -5,6 +5,7 @@ "desc": "Example map for the API", "permission": "commons", "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "starred": false, "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "topic_ids": [ diff --git a/doc/api/examples/maps.json b/doc/api/examples/maps.json index 8b963990..501c0325 100644 --- a/doc/api/examples/maps.json +++ b/doc/api/examples/maps.json @@ -6,6 +6,7 @@ "desc": "Example map for the API", "permission": "commons", "screenshot": "https://s3.amazonaws.com/metamaps-assets/site/missing-map.png", + "starred": false, "created_at": "2016-03-26T08:02:05.379Z", "updated_at": "2016-03-27T07:20:18.047Z", "topic_ids": [ diff --git a/doc/api/schemas/_map.json b/doc/api/schemas/_map.json index 469b4dbe..1234122d 100644 --- a/doc/api/schemas/_map.json +++ b/doc/api/schemas/_map.json @@ -18,6 +18,9 @@ "format": "uri", "type": "string" }, + "starred": { + "type": "boolean" + }, "created_at": { "$ref": "_datetimestamp.json" }, @@ -61,6 +64,7 @@ "desc", "permission", "screenshot", + "starred", "created_at", "updated_at" ] diff --git a/doc/api/schemas/_metacode.json b/doc/api/schemas/_metacode.json index cc6b4f76..2001be8e 100644 --- a/doc/api/schemas/_metacode.json +++ b/doc/api/schemas/_metacode.json @@ -12,6 +12,7 @@ "type": "string" }, "icon": { + "format": "uri", "type": "string" } }, diff --git a/doc/api/schemas/_user.json b/doc/api/schemas/_user.json index e5805251..ee2ef14f 100644 --- a/doc/api/schemas/_user.json +++ b/doc/api/schemas/_user.json @@ -9,6 +9,7 @@ "type": "string" }, "avatar": { + "format": "uri", "type": "string" }, "generation": { diff --git a/spec/api/v2/mappings_api_spec.rb b/spec/api/v2/mappings_api_spec.rb index 4d802865..6f225c6a 100644 --- a/spec/api/v2/mappings_api_spec.rb +++ b/spec/api/v2/mappings_api_spec.rb @@ -16,7 +16,8 @@ RSpec.describe 'mappings API', type: :request do end it 'GET /api/v2/mappings/:id' do - get "/api/v2/mappings/#{mapping.id}" + get "/api/v2/mappings/#{mapping.id}", params: { access_token: token } + expect(response).to have_http_status(:success) expect(response).to match_json_schema(:mapping) diff --git a/spec/api/v2/maps_api_spec.rb b/spec/api/v2/maps_api_spec.rb index 77cbc24b..abed255d 100644 --- a/spec/api/v2/maps_api_spec.rb +++ b/spec/api/v2/maps_api_spec.rb @@ -16,7 +16,7 @@ RSpec.describe 'maps API', type: :request do end it 'GET /api/v2/maps/:id' do - get "/api/v2/maps/#{map.id}" + get "/api/v2/maps/#{map.id}", params: { access_token: token } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:map) @@ -45,6 +45,23 @@ RSpec.describe 'maps API', type: :request do expect(Map.count).to eq 0 end + it 'POST /api/v2/maps/:id/stars' do + post "/api/v2/maps/#{map.id}/stars", params: { access_token: token } + expect(response).to have_http_status(:success) + expect(response).to match_json_schema(:map) + expect(user.stars.count).to eq 1 + expect(map.stars.count).to eq 1 + end + + it 'DELETE /api/v2/maps/:id/stars' do + create(:star, map: map, user: user) + delete "/api/v2/maps/#{map.id}/stars", params: { access_token: token } + + expect(response).to have_http_status(:no_content) + expect(user.stars.count).to eq 0 + expect(map.stars.count).to eq 0 + end + context 'RAML example' do let(:resource) { get_json_example(:map) } let(:collection) { get_json_example(:maps) } diff --git a/spec/api/v2/topics_api_spec.rb b/spec/api/v2/topics_api_spec.rb index 9811071d..3f781df9 100644 --- a/spec/api/v2/topics_api_spec.rb +++ b/spec/api/v2/topics_api_spec.rb @@ -16,7 +16,8 @@ RSpec.describe 'topics API', type: :request do end it 'GET /api/v2/topics/:id' do - get "/api/v2/topics/#{topic.id}" + get "/api/v2/topics/#{topic.id}", params: { access_token: token } + expect(response).to have_http_status(:success) expect(response).to match_json_schema(:topic) diff --git a/spec/factories/stars.rb b/spec/factories/stars.rb new file mode 100644 index 00000000..60b10cf1 --- /dev/null +++ b/spec/factories/stars.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true +FactoryGirl.define do + factory :star do + end +end From 8d613eab33cbdb1a0a037a8a77ee445cb1a724d6 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 10:38:16 -0400 Subject: [PATCH 165/306] improve descriptors --- doc/api/apis/maps.raml | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/doc/api/apis/maps.raml b/doc/api/apis/maps.raml index 434dcc67..3ae05d2e 100644 --- a/doc/api/apis/maps.raml +++ b/doc/api/apis/maps.raml @@ -19,9 +19,9 @@ post: screenshot: description: url to a screenshot of the map contributor_ids: - description: the topic being linked from + description: the ids of people who have contributed to the map collaborator_ids: - description: the topic being linked to + description: the ids of people who have edit access to the map responses: 201: body: @@ -52,12 +52,6 @@ post: screenshot: description: url to a screenshot of the map required: false - contributor_ids: - description: the topic being linked from - required: false - collaborator_ids: - description: the topic being linked to - required: false responses: 200: body: @@ -79,12 +73,6 @@ post: screenshot: description: url to a screenshot of the map required: false - contributor_ids: - description: the topic being linked from - required: false - collaborator_ids: - description: the topic being linked to - required: false responses: 200: body: From 6d6a5099e96e76b1f790a0726d9728abdf951ec1 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 10:45:39 -0400 Subject: [PATCH 166/306] Enable access to Most Used and Recently Used metacodes in lists and carousel (#708) * used and recent * enable most used and recent in all metacode select situations * selected changed to active at some point * switch recent and most used positions * remove index doc page --- app/helpers/application_helper.rb | 19 +++++++- app/models/user.rb | 22 +++++++++ app/views/shared/_metacodeoptions.html.erb | 26 +++++++++++ app/views/shared/_switchmetacodes.html.erb | 54 ++++++++++++++++++++-- frontend/src/Metamaps/Create.js | 2 +- 5 files changed, 117 insertions(+), 6 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1c9b4da5..03cbfbf4 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -3,13 +3,22 @@ module ApplicationHelper def metacodeset metacodes = current_user.settings.metacodes return false unless metacodes[0].include?('metacodeset') + if metacodes[0].sub('metacodeset-', '') == 'Most' + return 'Most' + elsif metacodes[0].sub('metacodeset-', '') == 'Recent' + return 'Recent' + end MetacodeSet.find(metacodes[0].sub('metacodeset-', '').to_i) end def user_metacodes @m = current_user.settings.metacodes set = metacodeset - @metacodes = if set + @metacodes = if set && set == 'Most' + Metacode.where(id: current_user.mostUsedMetacodes).to_a + elsif set && set == 'Recent' + Metacode.where(id: current_user.recentMetacodes).to_a + elsif set set.metacodes.to_a else Metacode.where(id: @m).to_a @@ -17,6 +26,14 @@ module ApplicationHelper @metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) end + def user_most_used_metacodes + @metacodes = current_user.mostUsedMetacodes.map { |id| Metacode.find(id) } + end + + def user_recent_metacodes + @metacodes = current_user.recentMetacodes.map { |id| Metacode.find(id) } + end + def invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') end diff --git a/app/models/user.rb b/app/models/user.rb index 4da66e57..4f679c1b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -64,6 +64,28 @@ class User < ApplicationRecord json['rtype'] = 'mapper' json end + + def recentMetacodes + array = [] + self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t| + if array.length < 5 and array.index(t.metacode_id) == nil + array.push(t.metacode_id) + end + end + array + end + + def mostUsedMetacodes + self.topics.to_a.reduce({}) { |memo, topic| + if memo[topic.metacode_id] == nil + memo[topic.metacode_id] = 1 + else + memo[topic.metacode_id] = memo[topic.metacode_id] + 1 + end + + memo + }.to_a.sort{ |a, b| b[1] <=> a[1] }.map{|i| i[0]}.slice(0, 5) + end # generate a random 8 letter/digit code that they can use to invite people def generate_code diff --git a/app/views/shared/_metacodeoptions.html.erb b/app/views/shared/_metacodeoptions.html.erb index a6092c3e..54fb9e48 100644 --- a/app/views/shared/_metacodeoptions.html.erb +++ b/app/views/shared/_metacodeoptions.html.erb @@ -5,6 +5,32 @@ <div id="metacodeOptions"> <ul> + <li> + <span>Recently Used</span> + <div class="expandMetacodeSet"></div> + <ul> + <% user_recent_metacodes().each do |m| %> + <li data-id="<%= m.id.to_s %>"> + <img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" /> + <div class="mSelectName"><%= m.name %></div> + <div class="clearfloat"></div> + </li> + <% end %> + </ul> + </li> + <li> + <span>Most Used</span> + <div class="expandMetacodeSet"></div> + <ul> + <% user_most_used_metacodes().each do |m| %> + <li data-id="<%= m.id.to_s %>"> + <img width="24" height="24" src="<%= asset_path m.icon %>" alt="<%= m.name %>" /> + <div class="mSelectName"><%= m.name %></div> + <div class="clearfloat"></div> + </li> + <% end %> + </ul> + </li> <% MetacodeSet.order("name").all.each do |set| %> <li> <span><%= set.name %></span> diff --git a/app/views/shared/_switchmetacodes.html.erb b/app/views/shared/_switchmetacodes.html.erb index bd6b8129..24739716 100644 --- a/app/views/shared/_switchmetacodes.html.erb +++ b/app/views/shared/_switchmetacodes.html.erb @@ -7,10 +7,14 @@ <% selectedSet = metacodes[0].include?("metacodeset") ? metacodes[0].sub("metacodeset-","") : "custom" %> <% allMetacodeSets = MetacodeSet.order("name").all.to_a %> <% if selectedSet == "custom" - index = allMetacodeSets.length + index = allMetacodeSets.length + 2 + elsif selectedSet == 'Recent' + index = 0 + elsif selectedSet == 'Most' + index = 1 else set = MetacodeSet.find(selectedSet.to_i) - index = allMetacodeSets.index(set) + index = allMetacodeSets.index(set) + 2 end %> <h3>Switch Metacode Set</h3> @@ -18,11 +22,53 @@ <div id="metacodeSwitchTabs"> <ul> + <li><a href="#metacodeSwitchTabsRecent" data-set-id="recent" id="metacodeSetRecent">RECENTLY USED</a></li> + <li><a href="#metacodeSwitchTabsMost" data-set-id="most" id="metacodeSetMost">MOST USED</a></li> <% allMetacodeSets.each do |m| %> <li><a href="#metacodeSwitchTabs<%= m.id %>" data-set-id="<%= m.id %>"><%= m.name %></a></li> <% end %> <li><a href="#metacodeSwitchTabsCustom" data-set-id="custom" id="metacodeSetCustom">CUSTOM SELECTION</a></li> </ul> + <% recent = user_recent_metacodes() %> + <div id="metacodeSwitchTabsRecent" + data-metacodes="<%= recent.map(&:id).join(',') %>"> + <% @list = '' %> + <% recent.each_with_index do |m, index| %> + <% @list += '<li><img src="' + asset_path(m.icon) + '" alt="' + m.name + '" /><p>' + m.name.downcase + '</p><div class="clearfloat"></div></li>' %> + <% end %> + <div class="metacodeSwitchTab"> + <p class="setDesc">The 5 Metacodes you've used most recently.</p> + <div class="metacodeSetList"> + <ul> + <%= @list.html_safe %> + </ul> + <div class="clearfloat"></div> + </div> + </div> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet('Recent', 0, false);"> + Switch Set + </button> + </div> + <% most_used = user_most_used_metacodes() %> + <div id="metacodeSwitchTabsMost" + data-metacodes="<%= most_used.map(&:id).join(',') %>"> + <% @list = '' %> + <% most_used.each_with_index do |m, index| %> + <% @list += '<li><img src="' + asset_path(m.icon) + '" alt="' + m.name + '" /><p>' + m.name.downcase + '</p><div class="clearfloat"></div></li>' %> + <% end %> + <div class="metacodeSwitchTab"> + <p class="setDesc">The 5 Metacodes you've used the most.</p> + <div class="metacodeSetList"> + <ul> + <%= @list.html_safe %> + </ul> + <div class="clearfloat"></div> + </div> + </div> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet('Most', 1, false);"> + Switch Set + </button> + </div> <% allMetacodeSets.each_with_index do |m, localindex| %> <div id="metacodeSwitchTabs<%= m.id %>" data-metacodes="<%= m.metacodes.map(&:id).join(',') %>"> @@ -39,7 +85,7 @@ <div class="clearfloat"></div> </div> </div> - <button class="button" onclick="Metamaps.Create.updateMetacodeSet(<%= m.id %>, <%= localindex %>, false);"> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet(<%= m.id %>, <%= localindex + 2 %>, false);"> Switch Set </button> </div> @@ -62,7 +108,7 @@ </ul> <div class="clearfloat"></div> </div> - <button class="button" onclick="Metamaps.Create.updateMetacodeSet('custom', <%= allMetacodeSets.length %>, true);"> + <button class="button" onclick="Metamaps.Create.updateMetacodeSet('custom', <%= allMetacodeSets.length + 2 %>, true);"> Switch to Custom Set </button> </div> diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 92271223..87b91540 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -32,7 +32,7 @@ const Create = { // // SWITCHING METACODE SETS $('#metacodeSwitchTabs').tabs({ - selected: self.selectedMetacodeSetIndex + active: self.selectedMetacodeSetIndex }).addClass('ui-tabs-vertical ui-helper-clearfix') $('#metacodeSwitchTabs .ui-tabs-nav li').removeClass('ui-corner-top').addClass('ui-corner-left') $('.customMetacodeList li').click(self.toggleMetacodeSelected) // within the custom metacode set tab From c256d0891b78f13dfc050a7c9a4a64c635bce44c Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 22:17:04 -0400 Subject: [PATCH 167/306] dont conflict message sending with topic creation --- frontend/src/Metamaps/Listeners.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 2eb092dd..f78a030b 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -18,7 +18,10 @@ const Listeners = { switch (e.which) { case 13: // if enter key is pressed - JIT.enterKeyHandler() + // prevent topic creation if sending a message + if (e.target.className !== 'chat-input') { + JIT.enterKeyHandler() + } e.preventDefault() break case 27: // if esc key is pressed From 0cfbe41d95acc52b7ec8604166d5712004fedd91 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 22:22:38 -0400 Subject: [PATCH 168/306] don't prevent all right clicking --- frontend/src/Metamaps/Map/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index dc9b4eb8..4c2f78bb 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -40,11 +40,6 @@ const Map = { init: function () { var self = Map - // prevent right clicks on the main canvas, so as to not get in the way of our right clicks - $('#wrapper').on('contextmenu', function (e) { - return false - }) - $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() From 98fae4b7213fa4e4a8beca9b295c53bc4e607604 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Wed, 5 Oct 2016 22:28:37 -0400 Subject: [PATCH 169/306] fixes #711 toast button styling --- app/assets/stylesheets/application.css.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index d6d80201..97276b9f 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -142,6 +142,7 @@ button.button.btn-no:hover { .toast .toast-button { margin-top: -10px; margin-left: 10px; + margin-bottom: -10px; } /* * Utility From eb4073c22818567f1a11e06a695f887f5141cbb9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 11:18:55 +0800 Subject: [PATCH 170/306] word wrap on chat message text. Fixes #726 --- app/assets/stylesheets/junto.css.erb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/junto.css.erb b/app/assets/stylesheets/junto.css.erb index 22471b19..91b610fc 100644 --- a/app/assets/stylesheets/junto.css.erb +++ b/app/assets/stylesheets/junto.css.erb @@ -339,6 +339,7 @@ margin-top: 12px; padding: 2px 8px 0; text-align: left; + word-wrap: break-word; } .chat-box .chat-messages .chat-message .chat-message-time { float: right; From c0a220abc9921a41b35f5746f0b03f712fc68154 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 11:52:05 +0800 Subject: [PATCH 171/306] allow synapses to be imported by topic name as well as id --- frontend/src/Metamaps/Import.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 6788335f..91da38cf 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -253,12 +253,16 @@ const Import = { parsedSynapses.forEach(function (synapse) { // only createSynapseWithParameters once both topics are persisted + // if there isn't a cidMapping, check by topic name instead var topic1 = Metamaps.Topics.get(self.cidMappings[synapse.topic1]) + if (!topic1) topic1 = Metamaps.Topics.findWhere({ name: synapse.topic1 }) var topic2 = Metamaps.Topics.get(self.cidMappings[synapse.topic2]) + if (!topic1) topic1 = Metamaps.Topics.findWhere({ name: synapse.topic1 }) + if (!topic1 || !topic2) { console.error("One of the two topics doesn't exist!") console.error(synapse) - return true + return // next } // ensure imported topics have a chance to get a real id attr before creating synapses @@ -407,6 +411,7 @@ const Import = { normalizeKeys: function(obj) { return _.transform(obj, (result, val, key) => { let newKey = key.toLowerCase() + newKey = newKey.replace(/\s/g, '') // remove whitespace if (newKey === 'url') key = 'link' if (newKey === 'title') key = 'name' if (newKey === 'description') key = 'desc' From b4d12509595e04aa3cb76cd557f74b2b15eb075b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 12:02:14 +0800 Subject: [PATCH 172/306] share normalizeKey between TSV, CSV, and JSON --- frontend/src/Metamaps/Import.js | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 91da38cf..f70a1290 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -27,7 +27,7 @@ const Import = { 'id', 'name', 'metacode', 'x', 'y', 'description', 'link', 'permission' ], synapseWhitelist: [ - 'topic1', 'topic2', 'category', 'desc', 'description', 'permission' + 'topic1', 'topic2', 'category', 'direction', 'desc', 'description', 'permission' ], cidMappings: {}, // to be filled by import_id => cid mappings @@ -59,7 +59,7 @@ const Import = { console.warn(err) return topicsPromise.resolve([]) } - topicsPromise.resolve(data.map(row => self.normalizeKeys(row))) + topicsPromise.resolve(data) }) const synapsesPromise = $.Deferred() @@ -68,7 +68,7 @@ const Import = { console.warn(err) return synapsesPromise.resolve([]) } - synapsesPromise.resolve(data.map(row => self.normalizeKeys(row))) + synapsesPromise.resolve(data) }) $.when(topicsPromise, synapsesPromise).done((topics, synapses) => { @@ -83,8 +83,8 @@ const Import = { handle: function(results) { var self = Import - var topics = results.topics - var synapses = results.synapses + var topics = results.topics.map(topic => self.normalizeKeys(topic)) + var synapses = results.synapses.map(synapse => self.normalizeKeys(synapse)) if (topics.length > 0 || synapses.length > 0) { if (window.confirm('Are you sure you want to create ' + topics.length + @@ -149,7 +149,7 @@ const Import = { state = STATES.ABORT } topicHeaders = line.map(function (header, index) { - return header.toLowerCase().replace('description', 'desc') + return self.normalizeKey(header) }) state = STATES.TOPICS break @@ -160,7 +160,7 @@ const Import = { state = STATES.ABORT } synapseHeaders = line.map(function (header, index) { - return header.toLowerCase().replace('description', 'desc') + return self.normalizeKey(header) }) state = STATES.SYNAPSES break @@ -406,15 +406,20 @@ const Import = { .toLowerCase() }, + normalizeKey: function(key) { + let newKey = key.toLowerCase() + newKey = newKey.replace(/\s/g, '') // remove whitespace + if (newKey === 'url') newKey = 'link' + if (newKey === 'title') newKey = 'name' + if (newKey === 'description') newKey = 'desc' + if (newKey === 'direction') newKey = 'category' + return newKey + }, // thanks to http://stackoverflow.com/a/25290114/5332286 normalizeKeys: function(obj) { return _.transform(obj, (result, val, key) => { - let newKey = key.toLowerCase() - newKey = newKey.replace(/\s/g, '') // remove whitespace - if (newKey === 'url') key = 'link' - if (newKey === 'title') key = 'name' - if (newKey === 'description') key = 'desc' + const newKey = Import.normalizeKey(key) result[newKey] = val }) } From 33bcfc150542b5226af215ead0a96bf49b142b97 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 00:02:42 +0800 Subject: [PATCH 173/306] move Maps into a folder --- frontend/src/components/{ => Maps}/Header.js | 0 frontend/src/components/{ => Maps}/MapCard.js | 0 frontend/src/components/{ => Maps}/MapListItem.js | 0 frontend/src/components/{ => Maps}/MapperCard.js | 0 frontend/src/components/{Maps.js => Maps/index.js} | 8 ++++---- 5 files changed, 4 insertions(+), 4 deletions(-) rename frontend/src/components/{ => Maps}/Header.js (100%) rename frontend/src/components/{ => Maps}/MapCard.js (100%) rename frontend/src/components/{ => Maps}/MapListItem.js (100%) rename frontend/src/components/{ => Maps}/MapperCard.js (100%) rename frontend/src/components/{Maps.js => Maps/index.js} (91%) diff --git a/frontend/src/components/Header.js b/frontend/src/components/Maps/Header.js similarity index 100% rename from frontend/src/components/Header.js rename to frontend/src/components/Maps/Header.js diff --git a/frontend/src/components/MapCard.js b/frontend/src/components/Maps/MapCard.js similarity index 100% rename from frontend/src/components/MapCard.js rename to frontend/src/components/Maps/MapCard.js diff --git a/frontend/src/components/MapListItem.js b/frontend/src/components/Maps/MapListItem.js similarity index 100% rename from frontend/src/components/MapListItem.js rename to frontend/src/components/Maps/MapListItem.js diff --git a/frontend/src/components/MapperCard.js b/frontend/src/components/Maps/MapperCard.js similarity index 100% rename from frontend/src/components/MapperCard.js rename to frontend/src/components/Maps/MapperCard.js diff --git a/frontend/src/components/Maps.js b/frontend/src/components/Maps/index.js similarity index 91% rename from frontend/src/components/Maps.js rename to frontend/src/components/Maps/index.js index 7931da5d..2c3e8ba1 100644 --- a/frontend/src/components/Maps.js +++ b/frontend/src/components/Maps/index.js @@ -1,8 +1,8 @@ import React, { Component, PropTypes } from 'react' -import Header from './Header.js' -import MapperCard from './MapperCard.js' -import MapCard from './MapCard.js' -import MapListItem from './MapListItem.js' +import Header from './Header' +import MapperCard from './MapperCard' +import MapCard from './MapCard' +import MapListItem from './MapListItem' class Maps extends Component { render = () => { From 518773d6e1c98008a37e3a204efebd798f10c9e6 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 25 Sep 2016 00:27:04 +0800 Subject: [PATCH 174/306] pop up a lightbox using React to help you export --- app/assets/images/import-example.png | Bin 0 -> 57176 bytes app/assets/images/import.png | Bin 0 -> 320 bytes .../javascripts/src/Metamaps.Erb.js.erb | 1 + ...plication.css.erb => application.scss.erb} | 31 ++++++-- app/assets/stylesheets/clean.css.erb | 36 +++++---- app/assets/stylesheets/mobile.scss.erb | 2 +- app/views/layouts/_lowermapelements.html.erb | 1 + .../src/Metamaps/GlobalUI/ImportDialog.js | 36 +++++++++ frontend/src/Metamaps/GlobalUI/index.js | 4 +- frontend/src/Metamaps/Import.js | 1 + frontend/src/Metamaps/Map/index.js | 14 +++- frontend/src/Metamaps/PasteInput.js | 27 ++++--- frontend/src/Metamaps/index.js | 3 +- frontend/src/components/ImportDialogBox.js | 75 ++++++++++++++++++ package.json | 1 + 15 files changed, 195 insertions(+), 37 deletions(-) create mode 100644 app/assets/images/import-example.png create mode 100644 app/assets/images/import.png rename app/assets/stylesheets/{application.css.erb => application.scss.erb} (99%) create mode 100644 frontend/src/Metamaps/GlobalUI/ImportDialog.js create mode 100644 frontend/src/components/ImportDialogBox.js diff --git a/app/assets/images/import-example.png b/app/assets/images/import-example.png new file mode 100644 index 0000000000000000000000000000000000000000..3f013d58ce972f7236cd3d49f9aab47458cc299f GIT binary patch literal 57176 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 literal 0 HcmV?d00001 diff --git a/app/assets/images/import.png b/app/assets/images/import.png new file mode 100644 index 0000000000000000000000000000000000000000..5c66e984a88e7f7f8408c44bf4eb999428cee9c8 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdzwj^(N7l!{JxM1({$v_d#0*}aI z1_o|n5N2eUHAey{$X?><>&pIsi;YD~aiT-hSD?^YPZ!6Kh{JEM8G0Rd6gl!RKgmIu z+mXdHQSk{!n2^W(;u)8`KJZnE@=v>T%BvvvnwwCEV!c@6@>`D%{(ie>`{89%7A<v? zTForOGvUb=Nry>>&phKM=|ou+^jWi9_e^8A{y9B;hRTkkQu8eiv&mOzFWtlc_5h3a zoGZ@^4$GWYnS7Ap)!Y3An-@Qk3oo^2D@^EGZuLF3>PYIOivkS%J8mq_>&+~Ev%~vG zw#s_@=Rv26`ycUr$Y;D%@3NNh*l(7z8@K3g_{Cx6aK4f60^>f$*Y8xGBo%vZ0(y+W M)78&qol`;+0AW~si2wiq literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index e8f3a25b..916d175a 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -14,6 +14,7 @@ Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' Metamaps.Erb['topic_description_signifier.png'] = '<%= asset_path('topic_description_signifier.png') %>' Metamaps.Erb['topic_link_signifier.png'] = '<%= asset_path('topic_link_signifier.png') %>' Metamaps.Erb['synapse16.png'] = '<%= asset_path('synapse16.png') %>' +Metamaps.Erb['import-example.png'] = '<%= asset_path('import-example.png') %>' Metamaps.Erb['sounds/MM_sounds.mp3'] = '<%= asset_path 'sounds/MM_sounds.mp3' %>' Metamaps.Erb['sounds/MM_sounds.ogg'] = '<%= asset_path 'sounds/MM_sounds.ogg' %>' Metamaps.Metacodes = <%= Metacode.all.to_json.gsub(%r[(icon.*?)(\"},)], '\1?purple=stupid\2').html_safe %> diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.scss.erb similarity index 99% rename from app/assets/stylesheets/application.css.erb rename to app/assets/stylesheets/application.scss.erb index 97276b9f..e4e7762e 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -1526,9 +1526,8 @@ h3.filterBox { background-image: url(<%= asset_data_uri('permissions32_sprite.png') %>); } /* map info box */ -/* map info box */ -.wrapper div.mapInfoBox { +.wrapper .mapInfoBox { display: none; position: absolute; bottom: 40px; @@ -1536,12 +1535,34 @@ h3.filterBox { background-color: #424242; color: #F5F5F5; border-radius: 2px; + box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0px 3px 3px rgba(0,0,0,0.16); + text-align: center; + font-style: normal; +} +.import-dialog{ + button { + margin: 1em 0.5em; + } + .import-blue-button { + display: inline-block; + box-sizing: border-box; + margin: 0.75em; + padding: 0.75em; + height: 3em; + background-color: #AAB0FB; + border-radius: 0.3em; + color: white; + cursor: pointer; + } + .fileupload { + width: 75%; + text-align: center; + } +} +.wrapper .mapInfoBox { width: 360px; min-height: 300px; padding: 0; - font-style: normal; - text-align: center; - box-shadow: 0 3px 3px rgba(0,0,0,0.23), 0px 3px 3px rgba(0,0,0,0.16); } .requestTitle { display: none; diff --git a/app/assets/stylesheets/clean.css.erb b/app/assets/stylesheets/clean.css.erb index b41f14ca..deb2719f 100644 --- a/app/assets/stylesheets/clean.css.erb +++ b/app/assets/stylesheets/clean.css.erb @@ -188,7 +188,7 @@ .upperRightIcon { width: 32px; height: 32px; - background-image: url(<%= asset_data_uri('topright_sprite.png') %>); + background-image: url(<%= asset_path('topright_sprite.png') %>); background-repeat: no-repeat; cursor: pointer; } @@ -325,7 +325,7 @@ } .fullWidthWrapper.withPartners { - background: url(<%= asset_data_uri('homepage_bg_fade.png') %>) no-repeat center -300px; + background: url(<%= asset_path('homepage_bg_fade.png') %>) no-repeat center -300px; } .homeWrapper.homePartners { padding: 64px 0 280px; @@ -364,7 +364,7 @@ cursor: pointer; } .openCheatsheet { - background-image: url(<%= asset_data_uri('help_sprite.png') %>); + background-image: url(<%= asset_path('help_sprite.png') %>); background-repeat:no-repeat; } .openCheatsheet:hover { @@ -373,7 +373,7 @@ .mapInfoIcon { position: relative; top: 56px; /* puts it just offscreen */ - background-image: url(<%= asset_data_uri('mapinfo_sprite.png') %>); + background-image: url(<%= asset_path('mapinfo_sprite.png') %>); background-repeat:no-repeat; } .mapInfoIcon:hover { @@ -382,8 +382,14 @@ .mapPage .mapInfoIcon { top: 0; } +.importDialog { + background-image: url(<%= asset_path('import.png') %>); + background-position: 0 0; + background-repeat: no-repeat; + width: 32px; +} .starMap { - background-image: url(<%= asset_data_uri('starmap_sprite.png') %>); + background-image: url(<%= asset_path('starmap_sprite.png') %>); background-position: 0 0; background-repeat: no-repeat; width: 32px; @@ -437,7 +443,7 @@ .takeScreenshot { margin-bottom: 5px; border-radius: 2px; - background-image: url(<%= asset_data_uri 'screenshot_sprite.png' %>); + background-image: url(<%= asset_path 'screenshot_sprite.png' %>); display: none; } .takeScreenshot:hover { @@ -450,7 +456,7 @@ .zoomExtents { margin-bottom:5px; border-radius: 2px; - background-image: url(<%= asset_data_uri('extents_sprite.png') %>); + background-image: url(<%= asset_path('extents_sprite.png') %>); } .zoomExtents:hover { @@ -458,7 +464,7 @@ } .zoomExtents:hover .tooltips, .zoomIn:hover .tooltips, .zoomOut:hover .tooltips, .takeScreenshot:hover .tooltips, .sidebarFilterIcon:hover .tooltipsUnder, .sidebarForkIcon:hover .tooltipsUnder, .addMap:hover .tooltipsUnder, .authenticated .sidebarAccountIcon:hover .tooltipsUnder, - .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { + .mapInfoIcon:hover .tooltipsAbove, .openCheatsheet:hover .tooltipsAbove, .chat-button:hover .tooltips, importDialog:hover .tooltipsAbove, .starMap:hover .tooltipsAbove, .openMetacodeSwitcher:hover .tooltipsAbove, .pinCarousel:not(.isPinned):hover .tooltipsAbove.helpPin, .pinCarousel.isPinned:hover .tooltipsAbove.helpUnpin { display: block; } @@ -623,7 +629,7 @@ } .zoomIn { - background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); + background-image: url(<%= asset_path('zoom_sprite.png') %>); background-position: 0 /…0; border-top-left-radius: 2px; border-top-right-radius: 2px; @@ -632,7 +638,7 @@ background-position: -32px 0; } .zoomOut { - background-image: url(<%= asset_data_uri('zoom_sprite.png') %>); + background-image: url(<%= asset_path('zoom_sprite.png') %>); background-position:0 -32px; border-bottom-left-radius: 2px; border-bottom-right-radius: 2px; @@ -740,23 +746,23 @@ left:5px; } .exploreMapsCenter .myMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -32px 0; } .exploreMapsCenter .sharedMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -128px 0; } .exploreMapsCenter .activeMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: 0 0; } .exploreMapsCenter .featuredMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } .exploreMapsCenter .starredMaps .exploreMapsIcon { - background-image: url(<%= asset_data_uri 'exploremaps_sprite.png' %>); + background-image: url(<%= asset_path 'exploremaps_sprite.png' %>); background-position: -96px 0; } .myMaps:hover .exploreMapsIcon, .myMaps.active .exploreMapsIcon { diff --git a/app/assets/stylesheets/mobile.scss.erb b/app/assets/stylesheets/mobile.scss.erb index cf416e37..a646fea4 100644 --- a/app/assets/stylesheets/mobile.scss.erb +++ b/app/assets/stylesheets/mobile.scss.erb @@ -56,7 +56,7 @@ width: 100%; } - .wrapper div.mapInfoBox { + .wrapper .mapInfoBox { position: fixed; top: 50px; right: 0px; diff --git a/app/views/layouts/_lowermapelements.html.erb b/app/views/layouts/_lowermapelements.html.erb index fe120219..b8b7f868 100644 --- a/app/views/layouts/_lowermapelements.html.erb +++ b/app/views/layouts/_lowermapelements.html.erb @@ -8,6 +8,7 @@ <div class="infoAndHelp"> <%= render :partial => 'maps/mapinfobox' %> + <div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox"><div class="tooltipsAbove">Import data</div></div> <% starred = current_user && @map && current_user.starred_map?(@map) starClass = starred ? 'starred' : '' tooltip = starred ? 'Star' : 'Unstar' %> diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js new file mode 100644 index 00000000..3671cd90 --- /dev/null +++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js @@ -0,0 +1,36 @@ +import React from 'react' +import ReactDOM from 'react-dom' +import outdent from 'outdent' + +import ImportDialogBox from '../../components/ImportDialogBox' + +import PasteInput from '../PasteInput' + +const ImportDialog = { + openLightbox: null, + closeLightbox: null, + + init: function(serverData, openLightbox, closeLightbox) { + const self = ImportDialog + self.openLightbox = openLightbox + self.closeLightbox = closeLightbox + + $('#lightbox_content').append($(outdent` + <div class="lightboxContent" id="import-dialog-lightbox"> + <div class="importDialogWrapper" /> + </div> + `)) + ReactDOM.render(React.createElement(ImportDialogBox, { + onFileAdded: PasteInput.handleFile, + exampleImageUrl: serverData['import-example.png'], + }), $('.importDialogWrapper').get(0)) + }, + show: function() { + self.openLightbox('import-dialog') + }, + hide: function() { + self.closeLightbox('import-dialog') + } +} + +export default ImportDialog diff --git a/frontend/src/Metamaps/GlobalUI/index.js b/frontend/src/Metamaps/GlobalUI/index.js index 3b16375e..4eb29bb7 100644 --- a/frontend/src/Metamaps/GlobalUI/index.js +++ b/frontend/src/Metamaps/GlobalUI/index.js @@ -6,6 +6,7 @@ import Create from '../Create' import Search from './Search' import CreateMap from './CreateMap' import Account from './Account' +import ImportDialog from './ImportDialog' /* * Metamaps.Backbone @@ -21,6 +22,7 @@ const GlobalUI = { self.Search.init() self.CreateMap.init() self.Account.init() + self.ImportDialog.init(Metamaps.Erb, self.openLightbox, self.closeLightbox) if ($('#toast').html().trim()) self.notifyUser($('#toast').html()) @@ -141,5 +143,5 @@ const GlobalUI = { } } -export { Search, CreateMap, Account } +export { Search, CreateMap, Account, ImportDialog } export default GlobalUI diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index f70a1290..05d762e5 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -411,6 +411,7 @@ const Import = { newKey = newKey.replace(/\s/g, '') // remove whitespace if (newKey === 'url') newKey = 'link' if (newKey === 'title') newKey = 'name' + if (newKey === 'label') newKey = 'desc' if (newKey === 'description') newKey = 'desc' if (newKey === 'direction') newKey = 'category' return newKey diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 4c2f78bb..7d7322fc 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,6 +1,8 @@ /* global Metamaps, $ */ import outdent from 'outdent' +import React from 'react' +import ReactDOM from 'react-dom' import Active from '../Active' import AutoLayout from '../AutoLayout' @@ -40,6 +42,12 @@ const Map = { init: function () { var self = Map + // prevent right clicks on the main canvas, so as to not get in the way of our right clicks + $('#wrapper').on('contextmenu', function (e) { + return false + }) + + $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() @@ -52,7 +60,7 @@ const Map = { GlobalUI.CreateMap.emptyForkMapForm = $('#fork_map').html() self.updateStar() - self.InfoBox.init() + InfoBox.init() CheatSheet.init() $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) @@ -102,7 +110,7 @@ const Map = { Selected.reset() // set the proper mapinfobox content - Map.InfoBox.load() + InfoBox.load() // these three update the actual filter box with the right list items Filter.checkMetacodes() @@ -132,7 +140,7 @@ const Map = { Create.newTopic.hide(true) // true means force (and override pinned) Create.newSynapse.hide() Filter.close() - Map.InfoBox.close() + InfoBox.close() Realtime.endActiveMap() } }, diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 6f1cc03f..51d4a933 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -21,16 +21,7 @@ const PasteInput = { e.preventDefault(); var coords = Util.pixelsToCoords({ x: e.clientX, y: e.clientY }) if (e.dataTransfer.files.length > 0) { - var fileReader = new window.FileReader() - fileReader.readAsText(e.dataTransfer.files[0]) - fileReader.onload = function(e) { - var text = e.currentTarget.result - if (text.substring(0,5) === '<?xml') { - // assume this is a macOS .webloc link - text = text.replace(/[\s\S]*<string>(.*)<\/string>[\s\S]*/m, '$1') - } - self.handle(text, coords) - } + self.handleFile(e.dataTransfer.files[0], coords) } // OMG import bookmarks 😍 if (e.dataTransfer.items.length > 0) { @@ -52,7 +43,21 @@ const PasteInput = { }) }, - handle: function(text, coords) { + handleFile: (file, coords = null) => { + var self = PasteInput + var fileReader = new FileReader() + fileReader.readAsText(file) + fileReader.onload = function(e) { + var text = e.currentTarget.result + if (text.substring(0,5) === '<?xml') { + // assume this is a macOS .webloc link + text = text.replace(/[\s\S]*<string>(.*)<\/string>[\s\S]*/m, '$1') + } + self.handle(text, coords) + } + }, + + handle: function(text, coords = null) { var self = PasteInput if (text.match(self.URL_REGEX)) { diff --git a/frontend/src/Metamaps/index.js b/frontend/src/Metamaps/index.js index d179713e..44bbfdb6 100644 --- a/frontend/src/Metamaps/index.js +++ b/frontend/src/Metamaps/index.js @@ -10,7 +10,7 @@ import Create from './Create' import Debug from './Debug' import Filter from './Filter' import GlobalUI, { - Search, CreateMap, Account as GlobalUI_Account + Search, CreateMap, ImportDialog, Account as GlobalUI_Account } from './GlobalUI' import Import from './Import' import JIT from './JIT' @@ -47,6 +47,7 @@ Metamaps.GlobalUI = GlobalUI Metamaps.GlobalUI.Search = Search Metamaps.GlobalUI.CreateMap = CreateMap Metamaps.GlobalUI.Account = GlobalUI_Account +Metamaps.GlobalUI.ImportDialog = ImportDialog Metamaps.Import = Import Metamaps.JIT = JIT Metamaps.Listeners = Listeners diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js new file mode 100644 index 00000000..4d113ccc --- /dev/null +++ b/frontend/src/components/ImportDialogBox.js @@ -0,0 +1,75 @@ +import React, { PropTypes, Component } from 'react' +import Dropzone from 'react-dropzone' + +class ImportDialogBox extends Component { + constructor(props) { + super(props) + + this.state = { + showImportInstructions: false + } + } + + handleExport = format => () => { + window.open(`${window.location.pathname}/export.${format}`, '_blank') + } + + handleFile = (files, e) => { + // for some reason it uploads twice, so we need this debouncer + this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) + if (!this.debouncer) { + this.props.onFileAdded(files[0]) + } + } + + toggleShowInstructions = e => { + this.setState({ + showImportInstructions: !this.state.showImportInstructions + }) + } + + render = () => { + return ( + <div className="import-dialog"> + <h3>EXPORT</h3> + <div className="import-blue-button" onClick={this.handleExport('csv')}> + Export as CSV + </div> + <div className="import-blue-button" onClick={this.handleExport('json')}> + Export as JSON + </div> + <h3>IMPORT</h3> + <p>To upload a file, drop it here:</p> + <Dropzone onDropAccepted={this.handleFile} + className="import-blue-button fileupload" + > + Drop files here! + </Dropzone> + <p> + <a onClick={this.toggleShowInstructions} style={{ textDecoration: 'underline', cursor: 'pointer' }}> + Show/hide import instructions + </a> + </p> + {!this.state.showImportInstructions ? null : (<div> + <p> + You can import topics and synapses by uploading a spreadsheet here. + The file should be in comma-separated format (when you save, change the + filetype from .xls to .csv). + </p> + <img src={this.props.exampleImageUrl} style={{ maxWidth: '75%', float: 'right', margin: '1em' }}/> + <p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p> + <p> </p> + <p> * There are many valid import formats. Try exporting a map to see what columns you can include in your import data. You can also copy-paste from Excel to import, or import JSON.</p> + <p> * If you are importing a list of links, you can use a Link column in place of the Name column.</p> + </div>)} + </div> + ) + } +} + +ImportDialogBox.propTypes = { + onFileAdded: PropTypes.func, + exampleImageUrl: PropTypes.string +} + +export default ImportDialogBox diff --git a/package.json b/package.json index c882fdd0..82cd120d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "outdent": "0.2.1", "react": "15.3.2", "react-dom": "15.3.2", + "react-dropzone": "3.6.0", "socket.io": "0.9.12", "webpack": "1.13.2" }, From 38c323a18a719c31eee879389aedd72195ddf7ab Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 6 Oct 2016 16:22:09 +0800 Subject: [PATCH 175/306] global lightbox css changes --- app/assets/stylesheets/application.scss.erb | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index e4e7762e..2797cbeb 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -2049,17 +2049,17 @@ and it won't be important on password protected instances */ left: 0; width: 100%; height: 100%; - position: fixed; + position: absolute; z-index: 1000000; display: none; } #lightbox_main { width: 800px; - height: auto; margin: 0 auto; z-index: 2; position: relative; - top: 50%; + top: 5vh; + height: 90vh; background-color: transparent; color: black; } @@ -2098,8 +2098,11 @@ and it won't be important on password protected instances */ background-position: center center; } #lightbox_content { - width: 552px; - height: 434px; + width: 800px; + height: 500px; + max-height: 90vh; + box-sizing: border-box; + overflow-y: auto; background-color: #e0e0e0; padding: 64px 124px 64px 124px; box-shadow: 0px 6px 3px rgba(0, 0, 0, 0.23), 10px 10px 10px rgba(0, 0, 0, 0.19); From a79d6a824cc4b6e5d376419bdc746861003fcf36 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:07:46 -0400 Subject: [PATCH 176/306] dont do async: false (#731) * dont do async: false * account for case where callback isn't provided --- frontend/src/Metamaps/AutoLayout.js | 5 +- frontend/src/Metamaps/Backbone/index.js | 40 ----------- frontend/src/Metamaps/Import.js | 4 +- frontend/src/Metamaps/Realtime.js | 1 - frontend/src/Metamaps/Synapse.js | 60 +++++++---------- frontend/src/Metamaps/Topic.js | 88 ++++++++++--------------- 6 files changed, 61 insertions(+), 137 deletions(-) diff --git a/frontend/src/Metamaps/AutoLayout.js b/frontend/src/Metamaps/AutoLayout.js index f3e91440..1408ba62 100644 --- a/frontend/src/Metamaps/AutoLayout.js +++ b/frontend/src/Metamaps/AutoLayout.js @@ -49,7 +49,7 @@ const AutoLayout = { } } - if (opts.map && self.coordsTaken(nextX, nextY, opts.map)) { + if (opts.mappings && self.coordsTaken(nextX, nextY, opts.mappings)) { // check if the coordinate is already taken on the current map return self.getNextCoord(opts) } else { @@ -59,8 +59,7 @@ const AutoLayout = { } } }, - coordsTaken: function (x, y, map) { - const mappings = map.getMappings() + coordsTaken: function (x, y, mappings) { if (mappings.findWhere({ xloc: x, yloc: y })) { return true } else { diff --git a/frontend/src/Metamaps/Backbone/index.js b/frontend/src/Metamaps/Backbone/index.js index 1994c483..389d7dcf 100644 --- a/frontend/src/Metamaps/Backbone/index.js +++ b/frontend/src/Metamaps/Backbone/index.js @@ -85,46 +85,6 @@ _Backbone.Map = Backbone.Model.extend({ getUser: function () { return Mapper.get(this.get('user_id')) }, - fetchContained: function () { - var bb = _Backbone - var that = this - var start = function (data) { - that.set('mappers', new bb.MapperCollection(data.mappers)) - that.set('topics', new bb.TopicCollection(data.topics)) - that.set('synapses', new bb.SynapseCollection(data.synapses)) - that.set('mappings', new bb.MappingCollection(data.mappings)) - } - - $.ajax({ - url: '/maps/' + this.id + '/contains.json', - success: start, - async: false - }) - }, - getTopics: function () { - if (!this.get('topics')) { - this.fetchContained() - } - return this.get('topics') - }, - getSynapses: function () { - if (!this.get('synapses')) { - this.fetchContained() - } - return this.get('synapses') - }, - getMappings: function () { - if (!this.get('mappings')) { - this.fetchContained() - } - return this.get('mappings') - }, - getMappers: function () { - if (!this.get('mappers')) { - this.fetchContained() - } - return this.get('mappers') - }, updateView: function () { var map = Active.Map var isActiveMap = this.id === map.id diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index f70a1290..f3e872ec 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -227,7 +227,7 @@ const Import = { parsedTopics.forEach(function (topic) { let coords = { x: topic.x, y: topic.y } if (!coords.x || !coords.y) { - coords = AutoLayout.getNextCoord({ map: Active.Map }) + coords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) } if (!topic.name && topic.link || @@ -353,7 +353,7 @@ const Import = { handleURL: function (url, opts = {}) { let coords = opts.coords if (!coords || coords.x === undefined || coords.y === undefined) { - coords = AutoLayout.getNextCoord({ map: Active.Map }) + coords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) } const name = opts.name || 'Link' diff --git a/frontend/src/Metamaps/Realtime.js b/frontend/src/Metamaps/Realtime.js index 6522d460..f905bb84 100644 --- a/frontend/src/Metamaps/Realtime.js +++ b/frontend/src/Metamaps/Realtime.js @@ -981,7 +981,6 @@ const Realtime = { else if (!couldEditBefore && canEditNow) { Map.canEditNow() } else { - model.fetchContained() model.trigger('changeByOther') } } diff --git a/frontend/src/Metamaps/Synapse.js b/frontend/src/Metamaps/Synapse.js index 400cb0b0..8253d6ba 100644 --- a/frontend/src/Metamaps/Synapse.js +++ b/frontend/src/Metamaps/Synapse.js @@ -19,35 +19,21 @@ import Visualize from './Visualize' * - Metamaps.Topics */ +const noOp = () => {} const Synapse = { // this function is to retrieve a synapse JSON object from the database // @param id = the id of the synapse to retrieve - get: function (id, callback) { + get: function (id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it if (Metamaps.Synapses.get(id) == undefined) { - if (!callback) { - var e = $.ajax({ - url: '/synapses/' + id + '.json', - async: false - }) - Metamaps.Synapses.add($.parseJSON(e.responseText)) - return Metamaps.Synapses.get(id) - } else { - return $.ajax({ - url: '/synapses/' + id + '.json', - success: function (data) { - Metamaps.Synapses.add(data) - callback(Metamaps.Synapses.get(id)) - } - }) - } - } else { - if (!callback) { - return Metamaps.Synapses.get(id) - } else { - return callback(Metamaps.Synapses.get(id)) - } - } + $.ajax({ + url: '/synapses/' + id + '.json', + success: function (data) { + Metamaps.Synapses.add(data) + callback(Metamaps.Synapses.get(id)) + } + }) + } else callback(Metamaps.Synapses.get(id)) }, /* * @@ -152,21 +138,19 @@ const Synapse = { node1, node2 - var synapse = self.get(id) - - var mapping = new Metamaps.Backbone.Mapping({ - mappable_type: 'Synapse', - mappable_id: synapse.id, + self.get(id, synapse => { + var mapping = new Metamaps.Backbone.Mapping({ + mappable_type: 'Synapse', + mappable_id: synapse.id, + }) + Metamaps.Mappings.add(mapping) + topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) + node1 = topic1.get('node') + topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) + node2 = topic2.get('node') + Create.newSynapse.hide() + self.renderSynapse(mapping, synapse, node1, node2, true) }) - Metamaps.Mappings.add(mapping) - - topic1 = Metamaps.Topics.get(Create.newSynapse.topic1id) - node1 = topic1.get('node') - topic2 = Metamaps.Topics.get(Create.newSynapse.topic2id) - node2 = topic2.get('node') - Create.newSynapse.hide() - - self.renderSynapse(mapping, synapse, node1, node2, true) } } diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 34e2bb64..5be6cc57 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -28,37 +28,21 @@ import Visualize from './Visualize' * - Metamaps.Synapses * - Metamaps.Topics */ - +const noOp = () => {} const Topic = { // this function is to retrieve a topic JSON object from the database // @param id = the id of the topic to retrieve - get: function (id, callback) { + get: function (id, callback = noOp) { // if the desired topic is not yet in the local topic repository, fetch it if (Metamaps.Topics.get(id) == undefined) { - // console.log("Ajax call!") - if (!callback) { - var e = $.ajax({ - url: '/topics/' + id + '.json', - async: false - }) - Metamaps.Topics.add($.parseJSON(e.responseText)) - return Metamaps.Topics.get(id) - } else { - return $.ajax({ - url: '/topics/' + id + '.json', - success: function (data) { - Metamaps.Topics.add(data) - callback(Metamaps.Topics.get(id)) - } - }) - } - } else { - if (!callback) { - return Metamaps.Topics.get(id) - } else { - return callback(Metamaps.Topics.get(id)) - } - } + $.ajax({ + url: '/topics/' + id + '.json', + success: function (data) { + Metamaps.Topics.add(data) + callback(Metamaps.Topics.get(id)) + } + }) + } else callback(Metamaps.Topics.get(id)) }, launch: function (id) { var bb = Metamaps.Backbone @@ -192,7 +176,7 @@ const Topic = { }, // opts is additional options in a hash - // TODO: move createNewInDB and permitCerateSYnapseAfter into opts + // TODO: move createNewInDB and permitCreateSynapseAfter into opts renderTopic: function (mapping, topic, createNewInDB, permitCreateSynapseAfter, opts = {}) { var self = Topic @@ -335,7 +319,7 @@ const Topic = { Metamaps.Topics.add(topic) if (Create.newTopic.pinned) { - var nextCoords = AutoLayout.getNextCoord() + var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) } var mapping = new Metamaps.Backbone.Mapping({ xloc: nextCoords ? nextCoords.x : Create.newTopic.x, @@ -357,40 +341,38 @@ const Topic = { Create.newTopic.hide() - var topic = self.get(id) + self.get(id, (topic) => { + if (Create.newTopic.pinned) { + var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + } + var mapping = new Metamaps.Backbone.Mapping({ + xloc: nextCoords ? nextCoords.x : Create.newTopic.x, + yloc: nextCoords ? nextCoords.y : Create.newTopic.y, + mappable_type: 'Topic', + mappable_id: topic.id, + }) + Metamaps.Mappings.add(mapping) - if (Create.newTopic.pinned) { - var nextCoords = AutoLayout.getNextCoord() - } - var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords ? nextCoords.x : Create.newTopic.x, - yloc: nextCoords ? nextCoords.y : Create.newTopic.y, - mappable_type: 'Topic', - mappable_id: topic.id, + self.renderTopic(mapping, topic, true, true) }) - Metamaps.Mappings.add(mapping) - - self.renderTopic(mapping, topic, true, true) }, getTopicFromSearch: function (event, id) { var self = Topic $(document).trigger(Map.events.editedByActiveMapper) - var topic = self.get(id) - - var nextCoords = AutoLayout.getNextCoord() - var mapping = new Metamaps.Backbone.Mapping({ - xloc: nextCoords.x, - yloc: nextCoords.y, - mappable_type: 'Topic', - mappable_id: topic.id, + self.get(id, (topic) => { + var nextCoords = AutoLayout.getNextCoord({ mappings: Metamaps.Mappings }) + var mapping = new Metamaps.Backbone.Mapping({ + xloc: nextCoords.x, + yloc: nextCoords.y, + mappable_type: 'Topic', + mappable_id: topic.id, + }) + Metamaps.Mappings.add(mapping) + self.renderTopic(mapping, topic, true, true) + GlobalUI.notifyUser('Topic was added to your map!') }) - Metamaps.Mappings.add(mapping) - - self.renderTopic(mapping, topic, true, true) - - GlobalUI.notifyUser('Topic was added to your map!') event.stopPropagation() event.preventDefault() From 85dcad928f4aa03ef12c004d43216488cf8ab3d5 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:12:01 -0400 Subject: [PATCH 177/306] enable pulling in of references to maps through typeahead (#636) --- .../javascripts/src/Metamaps.Erb.js.erb | 1 + app/controllers/topics_controller.rb | 19 ++++++++---- app/helpers/topics_helper.rb | 18 ++++++----- app/models/topic.rb | 10 ++++++ frontend/src/Metamaps/Create.js | 9 +++++- frontend/src/Metamaps/Topic.js | 31 +++++++++++++++++++ frontend/src/Metamaps/TopicCard.js | 11 +++++-- 7 files changed, 82 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/src/Metamaps.Erb.js.erb b/app/assets/javascripts/src/Metamaps.Erb.js.erb index e8f3a25b..75535f82 100644 --- a/app/assets/javascripts/src/Metamaps.Erb.js.erb +++ b/app/assets/javascripts/src/Metamaps.Erb.js.erb @@ -8,6 +8,7 @@ window.Metamaps = window.Metamaps || {} Metamaps.Erb = {} Metamaps.Erb['REALTIME_SERVER'] = '<%= ENV['REALTIME_SERVER'] %>' +Metamaps.Erb['RAILS_ENV'] = '<%= ENV['RAILS_ENV'] %>' Metamaps.Erb['junto_spinner_darkgrey.gif'] = '<%= asset_path('junto_spinner_darkgrey.gif') %>' Metamaps.Erb['user.png'] = '<%= asset_path('user.png') %>' Metamaps.Erb['icons/wildcard.png'] = '<%= asset_path('icons/wildcard.png') %>' diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index ce430f43..986266be 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -10,12 +10,19 @@ class TopicsController < ApplicationController # GET /topics/autocomplete_topic def autocomplete_topic term = params[:term] - @topics = if term && !term.empty? - policy_scope(Topic.where('LOWER("name") like ?', term.downcase + '%')).order('"name"') - else - [] - end - render json: autocomplete_array_json(@topics) + if term && !term.empty? + @topics = policy_scope(Topic.where('LOWER("name") like ?', term.downcase + '%')).order('"name"') + @mapTopics = @topics.select { |t| t.metacode.name == 'Metamap' } + # prioritize topics which point to maps, over maps + @exclude = @mapTopics.length > 0 ? @mapTopics.map(&:name) : [''] + @maps = policy_scope(Map.where('LOWER("name") like ? AND name NOT IN (?)', term.downcase + '%', @exclude)).order('"name"') + else + @topics = [] + @maps = [] + end + @all= @topics.concat(@maps).sort { |a, b| a.name <=> b.name } + + render json: autocomplete_array_json(@all) end # GET topics/:id diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index e1a1d179..926e51b1 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -3,21 +3,23 @@ module TopicsHelper ## this one is for building our custom JSON autocomplete format for typeahead def autocomplete_array_json(topics) topics.map do |t| + is_map = t.is_a?(Map) { id: t.id, label: t.name, value: t.name, description: t.desc ? t.desc&.truncate(70) : '', # make this return matched results - type: t.metacode.name, - typeImageURL: t.metacode.icon, - permission: t.permission, - mapCount: t.maps.count, - synapseCount: t.synapses.count, originator: t.user.name, originatorImage: t.user.image.url(:thirtytwo), - rtype: :topic, - inmaps: t.inmaps, - inmapsLinks: t.inmapsLinks + permission: t.permission, + + rtype: is_map ? 'map' : 'topic', + inmaps: is_map ? [] : t.inmaps, + inmapsLinks: is_map ? [] : t.inmapsLinks + type: is_map ? metamapsMetacode.name : t.metacode.name, + typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, + mapCount: is_map ? 0 : t.maps.count, + synapseCount: is_map ? 0 : t.synapses.count, } end end diff --git a/app/models/topic.rb b/app/models/topic.rb index 7d83ecac..c3028cc4 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -15,6 +15,8 @@ class Topic < ApplicationRecord belongs_to :metacode + before_create :create_metamap? + validates :permission, presence: true validates :permission, inclusion: { in: Perm::ISSIONS.map(&:to_s) } @@ -128,4 +130,12 @@ class Topic < ApplicationRecord def mk_permission Perm.short(permission) end + + protected + def create_metamap? + if link == '' and metacode.name == 'Metamap' + @map = Map.create({ name: name, permission: permission, desc: '', arranged: true, user_id: user_id }) + self.link = Rails.application.routes.url_helpers.map_url(:host => ENV['MAILER_DEFAULT_URL'], :id => @map.id) + end + end end diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index 87b91540..e52024be 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -194,7 +194,14 @@ const Create = { // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { - Topic.getTopicFromAutocomplete(datum.id) + if (datum.rtype === 'topic') { + Topic.getTopicFromAutocomplete(datum.id) + } else if (datum.rtype === 'map') { + Topic.getMapFromAutocomplete({ + id: datum.id, + name: datum.label + }) + } }) // initialize metacode spinner and then hide it diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 5be6cc57..0de5526a 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -356,6 +356,37 @@ const Topic = { self.renderTopic(mapping, topic, true, true) }) }, + getMapFromAutocomplete: function (data) { + var self = Metamaps.Topic + + // hide the 'double-click to add a topic' message + Metamaps.GlobalUI.hideDiv('#instructions') + + $(document).trigger(Metamaps.Map.events.editedByActiveMapper) + + var metacode = Metamaps.Metacodes.findWhere({ name: 'Metamap' }) + + var topic = new Metamaps.Backbone.Topic({ + name: data.name, + metacode_id: metacode.id, + defer_to_map_id: Metamaps.Active.Map.id, + link: window.location.origin + '/maps/' + data.id + }) + Metamaps.Topics.add(topic) + + var mapping = new Metamaps.Backbone.Mapping({ + xloc: Metamaps.Create.newTopic.x, + yloc: Metamaps.Create.newTopic.y, + mappable_id: topic.cid, + mappable_type: 'Topic', + }) + Metamaps.Mappings.add(mapping) + + // these can't happen until the value is retrieved, which happens in the line above + Metamaps.Create.newTopic.hide() + + self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database + }, getTopicFromSearch: function (event, id) { var self = Topic diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 0b2d1497..40c51fbd 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -135,9 +135,13 @@ const TopicCard = { loader.setRange(0.9); // default is 1.3 loader.show() // Hidden by default var e = embedly('card', document.getElementById('embedlyLink')) - if (!e) { + if (!e && Metamaps.Erb.RAILS_ENV != 'development') { self.handleInvalidLink() } + else if (!e) { + $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() + $('#embedlyLinkLoader').hide() + } } }, 100) } @@ -154,8 +158,11 @@ const TopicCard = { loader.show() // Hidden by default var e = embedly('card', document.getElementById('embedlyLink')) self.showLinkRemover() - if (!e) { + if (!e && Metamaps.Erb.RAILS_ENV != 'development') { self.handleInvalidLink() + } else if (!e) { + $('#embedlyLink').attr('target', '_blank').html(topic.get('link')).show() + $('#embedlyLinkLoader').hide() } } From a56c4eb110e16891f1c4b9abf98f5d355b6a7919 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:27:18 -0400 Subject: [PATCH 178/306] missing comma --- app/helpers/topics_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 926e51b1..8ef9647a 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -15,7 +15,7 @@ module TopicsHelper rtype: is_map ? 'map' : 'topic', inmaps: is_map ? [] : t.inmaps, - inmapsLinks: is_map ? [] : t.inmapsLinks + inmapsLinks: is_map ? [] : t.inmapsLinks, type: is_map ? metamapsMetacode.name : t.metacode.name, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, mapCount: is_map ? 0 : t.maps.count, From e72ae5df94dd785bb19165b50e240345949b64d0 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 09:33:10 -0400 Subject: [PATCH 179/306] another issue from the maps in maps branch --- app/controllers/topics_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 986266be..9f280d0b 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -20,7 +20,7 @@ class TopicsController < ApplicationController @topics = [] @maps = [] end - @all= @topics.concat(@maps).sort { |a, b| a.name <=> b.name } + @all= @topics.to_a.concat(@maps.to_a).sort { |a, b| a.name <=> b.name } render json: autocomplete_array_json(@all) end From b52523e7be054ce6339a8893e6f6d826bfaffd93 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 10:32:06 -0400 Subject: [PATCH 180/306] one more maps in maps error --- app/helpers/topics_helper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/helpers/topics_helper.rb b/app/helpers/topics_helper.rb index 8ef9647a..fa16c358 100644 --- a/app/helpers/topics_helper.rb +++ b/app/helpers/topics_helper.rb @@ -4,6 +4,7 @@ module TopicsHelper def autocomplete_array_json(topics) topics.map do |t| is_map = t.is_a?(Map) + metamapMetacode = Metacode.find_by_name('Metamap') { id: t.id, label: t.name, @@ -16,7 +17,7 @@ module TopicsHelper rtype: is_map ? 'map' : 'topic', inmaps: is_map ? [] : t.inmaps, inmapsLinks: is_map ? [] : t.inmapsLinks, - type: is_map ? metamapsMetacode.name : t.metacode.name, + type: is_map ? metamapMetacode.name : t.metacode.name, typeImageURL: is_map ? metamapMetacode.icon : t.metacode.icon, mapCount: is_map ? 0 : t.maps.count, synapseCount: is_map ? 0 : t.synapses.count, From 658f102a4eb73149c1cb6fa3d53bf4333c212b02 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 10:37:01 -0400 Subject: [PATCH 181/306] fixes #720 double topic create when pinned (#732) --- frontend/src/Metamaps/Create.js | 8 +++++--- frontend/src/Metamaps/Topic.js | 20 +++++++++++++------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/frontend/src/Metamaps/Create.js b/frontend/src/Metamaps/Create.js index e52024be..088622e3 100644 --- a/frontend/src/Metamaps/Create.js +++ b/frontend/src/Metamaps/Create.js @@ -194,6 +194,7 @@ const Create = { // tell the autocomplete to submit the form with the topic you clicked on if you pick from the autocomplete $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { + Create.newTopic.beingCreated = false if (datum.rtype === 'topic') { Topic.getTopicFromAutocomplete(datum.id) } else if (datum.rtype === 'map') { @@ -235,19 +236,20 @@ const Create = { GlobalUI.hideDiv('#instructions') }, hide: function (force) { - if (Create.newTopic.beingCreated === false) return if (force || !Create.newTopic.pinned) { $('#new_topic').fadeOut('fast') - Create.newTopic.beingCreated = false } if (force) { $('.pinCarousel').removeClass('isPinned') Create.newTopic.pinned = false } - $('#topic_name').typeahead('val', '') if (Metamaps.Topics.length === 0) { GlobalUI.showDiv('#instructions') } + Create.newTopic.beingCreated = false + }, + reset: function () { + $('#topic_name').typeahead('val', '') } }, newSynapse: { diff --git a/frontend/src/Metamaps/Topic.js b/frontend/src/Metamaps/Topic.js index 0de5526a..ddf20840 100644 --- a/frontend/src/Metamaps/Topic.js +++ b/frontend/src/Metamaps/Topic.js @@ -330,16 +330,21 @@ const Topic = { Metamaps.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above - Create.newTopic.hide() + if (!Create.newTopic.pinned) Create.newTopic.hide() + Create.newTopic.reset() self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database }, getTopicFromAutocomplete: function (id) { var self = Topic + // hide the 'double-click to add a topic' message + GlobalUI.hideDiv('#instructions') + $(document).trigger(Map.events.editedByActiveMapper) - Create.newTopic.hide() + if (!Create.newTopic.pinned) Create.newTopic.hide() + Create.newTopic.reset() self.get(id, (topic) => { if (Create.newTopic.pinned) { @@ -354,18 +359,16 @@ const Topic = { Metamaps.Mappings.add(mapping) self.renderTopic(mapping, topic, true, true) + // this blocked the enterKeyHandler from creating a new topic as well + if (Create.newTopic.pinned) Create.newTopic.beingCreated = true }) }, getMapFromAutocomplete: function (data) { var self = Metamaps.Topic - // hide the 'double-click to add a topic' message - Metamaps.GlobalUI.hideDiv('#instructions') - $(document).trigger(Metamaps.Map.events.editedByActiveMapper) var metacode = Metamaps.Metacodes.findWhere({ name: 'Metamap' }) - var topic = new Metamaps.Backbone.Topic({ name: data.name, metacode_id: metacode.id, @@ -383,9 +386,12 @@ const Topic = { Metamaps.Mappings.add(mapping) // these can't happen until the value is retrieved, which happens in the line above - Metamaps.Create.newTopic.hide() + if (!Create.newTopic.pinned) Create.newTopic.hide() + Create.newTopic.reset() self.renderTopic(mapping, topic, true, true) // this function also includes the creation of the topic in the database + // this blocked the enterKeyHandler from creating a new topic as well + if (Create.newTopic.pinned) Create.newTopic.beingCreated = true }, getTopicFromSearch: function (event, id) { var self = Topic From 97d2868fadaae6819c4b432322cf09c31ae5ebbf Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 10:49:49 -0400 Subject: [PATCH 182/306] dont pan while using arrow keys during creation fixes #721 (#733) --- frontend/src/Metamaps/Listeners.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index f78a030b..5861e15e 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -16,6 +16,7 @@ const Listeners = { $(document).on('keydown', function (e) { if (!(Active.Map || Active.Topic)) return + const creatingTopic = e.target.id === 'topic_name' switch (e.which) { case 13: // if enter key is pressed // prevent topic creation if sending a message @@ -28,16 +29,16 @@ const Listeners = { JIT.escKeyHandler() break case 37: // if Left arrow key is pressed - Visualize.mGraph.canvas.translate(-20, 0) + if (!creatingTopic) Visualize.mGraph.canvas.translate(-20, 0) break case 38: // if Up arrow key is pressed - Visualize.mGraph.canvas.translate(0, -20) + if (!creatingTopic) Visualize.mGraph.canvas.translate(0, -20) break case 39: // if Right arrow key is pressed - Visualize.mGraph.canvas.translate(20, 0) + if (!creatingTopic) Visualize.mGraph.canvas.translate(20, 0) break case 40: // if Down arrow key is pressed - Visualize.mGraph.canvas.translate(0, 20) + if (!creatingTopic) Visualize.mGraph.canvas.translate(0, 20) break case 65: // if a or A is pressed if (e.ctrlKey) { From 0aeb6caadb20312ac906e9b585df8ed726b8d35c Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 7 Oct 2016 00:33:16 +0000 Subject: [PATCH 183/306] Makes it so that resizing the browser window doesn't change the user's location on the map --- frontend/src/Metamaps/Listeners.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 5861e15e..08aa9cee 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -7,6 +7,7 @@ import Mobile from './Mobile' import Realtime from './Realtime' import Selected from './Selected' import Topic from './Topic' +import Util from './Util' import Visualize from './Visualize' import { Search } from './GlobalUI' @@ -122,7 +123,22 @@ const Listeners = { }) $(window).resize(function () { + var canvas = Visualize.mGraph.canvas, + scaleX = canvas.scaleOffsetX, + scaleY = canvas.scaleOffsetY, + centrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, + centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); + if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + + canvas.scale(scaleX,scaleY); + var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, + newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); + + canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() }) From b978247785fd4e918a9c93548eba042b23033891 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 7 Oct 2016 00:51:52 +0000 Subject: [PATCH 184/306] Put all the code within the if statement --- frontend/src/Metamaps/Listeners.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 08aa9cee..bd73e59b 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -123,21 +123,23 @@ const Listeners = { }) $(window).resize(function () { - var canvas = Visualize.mGraph.canvas, + if (Visualize && Visualize.mGraph){ + var canvas = Visualize.mGraph.canvas, scaleX = canvas.scaleOffsetX, scaleY = canvas.scaleOffsetY, centrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); - - if (Visualize && Visualize.mGraph) Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) - - canvas.scale(scaleX,scaleY); - var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, - newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, - newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); - - canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + + Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + + canvas.scale(scaleX,scaleY); + var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, + newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); + + canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + } if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() From 86a6e92bc3ea74a6fb45e3d3a46fecf5598d5426 Mon Sep 17 00:00:00 2001 From: Connor Turland <connorturland@gmail.com> Date: Thu, 6 Oct 2016 23:45:17 -0400 Subject: [PATCH 185/306] dont show private maps in global collection (#734) * dont show private maps in global collection * Update explore_controller.rb * Update main_controller.rb --- app/controllers/explore_controller.rb | 2 +- app/controllers/main_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/explore_controller.rb b/app/controllers/explore_controller.rb index dc4c2de9..2e713213 100644 --- a/app/controllers/explore_controller.rb +++ b/app/controllers/explore_controller.rb @@ -9,7 +9,7 @@ class ExploreController < ApplicationController # GET /explore/active def active - @maps = map_scope(Map.where.not(name: 'Untitled Map')) + @maps = map_scope(Map.where.not(name: 'Untitled Map').where.not(permission: 'private')) respond_to do |format| format.html do diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index 7df4e366..38d9458c 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -8,7 +8,7 @@ class MainController < ApplicationController respond_to do |format| format.html do if authenticated? - @maps = policy_scope(Map).where.not(name: 'Untitled Map') + @maps = policy_scope(Map).where.not(name: 'Untitled Map').where.not(permission: 'private') .order(updated_at: :desc).page(1).per(20) render 'explore/active' else From 08f89ee630b35e44625ff4da929664df8c4a2933 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Thu, 6 Oct 2016 23:56:39 -0400 Subject: [PATCH 186/306] Update Listeners.js --- frontend/src/Metamaps/Listeners.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index bd73e59b..48e50a69 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -124,6 +124,7 @@ const Listeners = { $(window).resize(function () { if (Visualize && Visualize.mGraph){ + //Find the current canvas scale and map-coordinate at the centre of the user's screen var canvas = Visualize.mGraph.canvas, scaleX = canvas.scaleOffsetX, scaleY = canvas.scaleOffsetY, @@ -131,8 +132,10 @@ const Listeners = { centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); + //Resize the canvas to fill the new indow size, based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen canvas.scale(scaleX,scaleY); var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, From 3e4ff59a82fd089707c6fa826d49a05fcb447654 Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Thu, 6 Oct 2016 23:58:57 -0400 Subject: [PATCH 187/306] Update Listeners.js --- frontend/src/Metamaps/Listeners.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 48e50a69..cc5621cb 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -132,7 +132,7 @@ const Listeners = { centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); - //Resize the canvas to fill the new indow size, based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 + //Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen From 2b036bfb4eb5cb771403fca8250089fb07321b6e Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 7 Oct 2016 14:03:48 +0800 Subject: [PATCH 188/306] all Ctrl shortcuts now also work with Meta (Cmd on OSX) --- frontend/src/Metamaps/Listeners.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index 5861e15e..cf3365f3 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -41,7 +41,7 @@ const Listeners = { if (!creatingTopic) Visualize.mGraph.canvas.translate(0, 20) break case 65: // if a or A is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { Control.deselectAllNodes() Control.deselectAllEdges() @@ -55,13 +55,13 @@ const Listeners = { break case 68: // if d or D is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { e.preventDefault() Control.deleteSelected() } break case 69: // if e or E is pressed - if (e.ctrlKey && Active.Map) { + if ((e.ctrlKey || e.metaKey) && Active.Map) { e.preventDefault() JIT.zoomExtents(null, Visualize.mGraph.canvas) break @@ -79,14 +79,14 @@ const Listeners = { } break case 72: // if h or H is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { e.preventDefault() Control.hideSelectedNodes() Control.hideSelectedEdges() } break case 77: // if m or M is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { e.preventDefault() Control.removeSelectedNodes() Control.removeSelectedEdges() @@ -111,7 +111,7 @@ const Listeners = { } break case 191: // if / is pressed - if (e.ctrlKey) { + if (e.ctrlKey || e.metaKey) { Search.focus() } break From b6da38e29e143bd05a85db910a0284f184b0361b Mon Sep 17 00:00:00 2001 From: Robert Best <chessscholar@gmail.com> Date: Fri, 7 Oct 2016 02:36:41 -0400 Subject: [PATCH 189/306] Update Listeners.js Simplified based on Connor's suggestion about usage of variables. --- frontend/src/Metamaps/Listeners.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index cc5621cb..ce14dc9f 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -128,17 +128,17 @@ const Listeners = { var canvas = Visualize.mGraph.canvas, scaleX = canvas.scaleOffsetX, scaleY = canvas.scaleOffsetY, - centrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, - centrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + centrePixX = canvas.canvases[0].size.width / 2, + centrePixY = canvas.canvases[0].size.height / 2, centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); //Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 - Visualize.mGraph.canvas.resize($(window).width(), $(window).height()) + canvas.resize($(window).width(), $(window).height()) //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen canvas.scale(scaleX,scaleY); - var newCentrePixX = Visualize.mGraph.canvas.canvases[0].size.width / 2, - newCentrePixY = Visualize.mGraph.canvas.canvases[0].size.height / 2, + var newCentrePixX = canvas.canvases[0].size.width / 2, + newCentrePixY = canvas.canvases[0].size.height / 2, newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); From 42bb2cd86a60c463e8f32e56fbc4744f44f276c0 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:16:37 +0800 Subject: [PATCH 190/306] look and feel updates --- app/assets/images/import-example.png | Bin 57176 -> 45339 bytes app/assets/stylesheets/application.scss.erb | 6 ++++++ app/views/layouts/_lowermapelements.html.erb | 15 +++++++-------- app/views/layouts/_upperelements.html.erb | 4 ++++ frontend/src/components/ImportDialogBox.js | 4 ++-- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/app/assets/images/import-example.png b/app/assets/images/import-example.png index 3f013d58ce972f7236cd3d49f9aab47458cc299f..02d59dcd6024aef449b485ea0e3485013e435328 100644 GIT binary patch literal 45339 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%0AIjRdYjq~X9 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 Xgkc+nVc6;eAfK<|@?w=Dh5`Qv9sg9Y literal 57176 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 diff --git a/app/assets/stylesheets/application.scss.erb b/app/assets/stylesheets/application.scss.erb index 2797cbeb..5961c633 100644 --- a/app/assets/stylesheets/application.scss.erb +++ b/app/assets/stylesheets/application.scss.erb @@ -1555,8 +1555,14 @@ h3.filterBox { cursor: pointer; } .fileupload { + box-sizing: border-box; + margin: 0.75em; + padding: 0.75em; + height: 3em; + border: 3px dashed #AAB0FB; width: 75%; text-align: center; + cursor: pointer; } } .wrapper .mapInfoBox { diff --git a/app/views/layouts/_lowermapelements.html.erb b/app/views/layouts/_lowermapelements.html.erb index b8b7f868..82ec71f2 100644 --- a/app/views/layouts/_lowermapelements.html.erb +++ b/app/views/layouts/_lowermapelements.html.erb @@ -8,12 +8,11 @@ <div class="infoAndHelp"> <%= render :partial => 'maps/mapinfobox' %> - <div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox"><div class="tooltipsAbove">Import data</div></div> - <% starred = current_user && @map && current_user.starred_map?(@map) - starClass = starred ? 'starred' : '' - tooltip = starred ? 'Star' : 'Unstar' %> - <div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div> - <div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div> - <div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div> - <div class="clearfloat"></div> + <% starred = current_user && @map && current_user.starred_map?(@map) + starClass = starred ? 'starred' : '' + tooltip = starred ? 'Star' : 'Unstar' %> + <div class="starMap infoElement mapElement <%= starClass %>"><div class="tooltipsAbove"><%= tooltip %></div></div> + <div class="mapInfoIcon infoElement mapElement"><div class="tooltipsAbove">Map Info</div></div> + <div class="openCheatsheet openLightbox infoElement mapElement" data-open="cheatsheet"><div class="tooltipsAbove">Help</div></div> + <div class="clearfloat"></div> </div> diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index e393f767..c2344643 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -19,6 +19,10 @@ <div class="upperRightUI"> <div class="mapElement upperRightEl upperRightMapButtons"> + <div class="importDialog infoElement mapElement openLightbox" data-open="import-dialog-lightbox"> + <div class="tooltipsAbove">Import data</div> + </div> + <!-- filtering --> <div class="sidebarFilter upperRightEl"> <div class="sidebarFilterIcon upperRightIcon"><div class="tooltipsUnder">Filter</div></div> diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index 4d113ccc..9851fd07 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -41,7 +41,7 @@ class ImportDialogBox extends Component { <h3>IMPORT</h3> <p>To upload a file, drop it here:</p> <Dropzone onDropAccepted={this.handleFile} - className="import-blue-button fileupload" + className="fileupload" > Drop files here! </Dropzone> @@ -56,7 +56,7 @@ class ImportDialogBox extends Component { The file should be in comma-separated format (when you save, change the filetype from .xls to .csv). </p> - <img src={this.props.exampleImageUrl} style={{ maxWidth: '75%', float: 'right', margin: '1em' }}/> + <img src={this.props.exampleImageUrl} style={{ width: '100%' }} /> <p style={{ marginTop: '1em' }}>You can choose which columns to include in your data. Topics must have a name field. Synapses must have Topic 1 and Topic 2.</p> <p> </p> <p> * There are many valid import formats. Try exporting a map to see what columns you can include in your import data. You can also copy-paste from Excel to import, or import JSON.</p> From 20da1ef39f9480cccdd38d43f3aeb36cff639b50 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:21:04 +0800 Subject: [PATCH 191/306] fiddle with import icon --- app/assets/images/import.png | Bin 320 -> 325 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/import.png b/app/assets/images/import.png index 5c66e984a88e7f7f8408c44bf4eb999428cee9c8..29b5b89653aba363907399e286a688f8e6e4a6b6 100644 GIT binary patch delta 292 zcmV+<0o(q-0>uK5Dt`a~0002_L%V+f000SaNLh0L01FcU01FcV0GgZ_00007bV*G` z2jB_^5EDAKGOB6-007cSL_t(o!|m2FN<&c)h2ifc4>Yk5M358~LY819g1zf7rIp|w z%vM}jN-HbDYx4pr648*yMZD8p7?^+N%p7h?DQd<@<!AtV1Aiou<fsZj66i2b@{;7X zh6E?LMsSg2wpTO1Im5p5;0g<T;20~s<F%CXHuMB0xW~9J4l~Sgmt-;sz()6<xZII; zgYzWaP!pKqsR!8=k2p&*8Uo+}r$5MB^h<i!CEOy(a{Vr)JU1(D0Dlr7?ML+Q0lIIT qZh7cp+uX8A&>eiBNzeeQ1AGCiFC^s10uZDC0000<MNUMnLSTXtPj)B( delta 287 zcmV+)0pR|{0>A>0Du4d~{{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G` z2jB_@1S%nsK!WuE007NNL_t(o!|m2FO2a`E#NlsZKobi=1W92b<Oo(0NS||<(n|0S zdKC|r(#lG3ZPrE-h#`L#VW-;S!TasJx5K8CqNPR}s{<SikbmT~89<UT=1E?XELu2l zjynX`NoEH-`Il#yB)G*IpE$z?A9yRJybtZb1S_1><}kw?%OsP*0P5|(aI>T29#?fg z2614Dr#kmtdc<Xt(GUO+xY(m^jcIl4xJ&qJB(Fc`Qp$6W;tud{0n&d&tpUdS1kJIu lEwK6sIzZ2X4j9k@z5(m*B;;aqNwNR{002ovPDHLkV1jroc^&`& From fc044294f168ea4243ac56f6e4db06c749606e8d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 7 Oct 2016 17:23:11 +0800 Subject: [PATCH 192/306] add markdown to topic cards --- app/views/layouts/_templates.html.erb | 2 +- frontend/src/Metamaps/TopicCard.js | 17 +++++++++++------ frontend/src/Metamaps/Util.js | 5 +++++ package.json | 4 +++- webpack.config.js | 3 +++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index ff41c7dc..59a5ceb8 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -220,7 +220,7 @@ </div> <div class="scroll"> <div class="desc"> - <span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea">{{desc}}</span> + <span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea" data-original-content="{{desc_markdown}}">{{{desc_html}}}</span> <div class="clearfloat"></div> </div> </div> diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 40c51fbd..84892450 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -273,10 +273,14 @@ const TopicCard = { topic.trigger('saved') }) + // this is for all subsequent renders after in-place editing the desc field $(showCard).find('.best_in_place_desc').bind('ajax:success', function () { - this.innerHTML = this.innerHTML.replace(/\r/g, '') - var desc = $(this).html() === $(this).data('nil') ? '' : $(this).html() + var desc = $(this).html() === $(this).data('nil') + ? '' + : $(this).text() topic.set('desc', desc) + $(this).data('bestInPlaceEditor').original_content = desc + this.innerHTML = Util.mdToHTML(desc) topic.trigger('saved') }) } @@ -397,8 +401,6 @@ const TopicCard = { } else { } - var desc_nil = 'Click to add description...' - nodeValues.attachmentsHidden = '' if (topic.get('link') && topic.get('link') !== '') { nodeValues.embeds = '<a href="' + topic.get('link') + '" id="embedlyLink" target="_blank" data-card-description="0">' @@ -454,8 +456,11 @@ const TopicCard = { nodeValues.date = topic.getDate() // the code for this is stored in /views/main/_metacodeOptions.html.erb nodeValues.metacode_select = $('#metacodeOptions').html() - nodeValues.desc_nil = desc_nil - nodeValues.desc = (topic.get('desc') == '' && authorized) ? desc_nil : topic.get('desc') + nodeValues.desc_nil = 'Click to add description...' + nodeValues.desc_markdown = (topic.get('desc') === '' && authorized) + ? desc_nil + : topic.get('desc') + nodeValues.desc_html = Util.mdToHTML(nodeValues.desc_markdown) return nodeValues } } diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 9eb715de..f1f8b39c 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -1,3 +1,5 @@ +import { Parser, HtmlRenderer } from 'commonmark' + import Visualize from './Visualize' const Util = { @@ -119,6 +121,9 @@ const Util = { }, checkURLisYoutubeVideo: function (url) { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) + }, + mdToHTML: text => { + return new HtmlRenderer().render(new Parser().parse(text)) } } diff --git a/package.json b/package.json index c882fdd0..29e37af3 100644 --- a/package.json +++ b/package.json @@ -26,14 +26,16 @@ "babel-preset-es2015": "6.14.0", "babel-preset-react": "6.11.1", "backbone": "1.0.0", - "underscore": "1.4.4", + "commonmark": "0.26.0", "csv-parse": "1.1.7", + "json-loader": "0.5.4", "lodash": "4.16.1", "node-uuid": "1.4.7", "outdent": "0.2.1", "react": "15.3.2", "react-dom": "15.3.2", "socket.io": "0.9.12", + "underscore": "1.4.4", "webpack": "1.13.2" }, "devDependencies": { diff --git a/webpack.config.js b/webpack.config.js index 91498abd..644ff002 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -21,6 +21,9 @@ const config = module.exports = { plugins, devtool, module: { + preLoaders: [ + { test: /\.json$/, loader: 'json' } + ], loaders: [ { test: /\.(js|jsx)?$/, From 0085ce71e6e7d43861fc0f00341e5c3d9093ee5d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Fri, 7 Oct 2016 17:26:20 +0800 Subject: [PATCH 193/306] upgrade to best in place 3.0.0 alpha --- app/assets/javascripts/lib/best_in_place.js | 685 +++++++++++++++++ app/assets/javascripts/lib/bip.js | 780 -------------------- app/views/layouts/_templates.html.erb | 13 +- app/views/maps/_mapinfobox.html.erb | 4 +- frontend/src/Metamaps/Listeners.js | 1 - frontend/src/Metamaps/Map/InfoBox.js | 25 +- frontend/src/Metamaps/SynapseCard.js | 11 +- frontend/src/Metamaps/TopicCard.js | 9 +- 8 files changed, 728 insertions(+), 800 deletions(-) create mode 100644 app/assets/javascripts/lib/best_in_place.js delete mode 100644 app/assets/javascripts/lib/bip.js diff --git a/app/assets/javascripts/lib/best_in_place.js b/app/assets/javascripts/lib/best_in_place.js new file mode 100644 index 00000000..5000103f --- /dev/null +++ b/app/assets/javascripts/lib/best_in_place.js @@ -0,0 +1,685 @@ +/* + * BestInPlace (for jQuery) + * version: 3.0.0.alpha (2014) + * + * By Bernat Farrero based on the work of Jan Varwig. + * Examples at http://bernatfarrero.com + * + * Licensed under the MIT: + * http://www.opensource.org/licenses/mit-license.php + * + * @requires jQuery + * + * Usage: + * + * Attention. + * The format of the JSON object given to the select inputs is the following: + * [["key", "value"],["key", "value"]] + * The format of the JSON object given to the checkbox inputs is the following: + * ["falseValue", "trueValue"] + + */ +//= require jquery.autosize + +function BestInPlaceEditor(e) { + 'use strict'; + this.element = e; + this.initOptions(); + this.bindForm(); + this.initPlaceHolder(); + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); +} + +BestInPlaceEditor.prototype = { + // Public Interface Functions ////////////////////////////////////////////// + + activate: function () { + 'use strict'; + var to_display; + if (this.isPlaceHolder()) { + to_display = ""; + } else if (this.original_content) { + to_display = this.original_content; + } else { + switch (this.formType) { + case 'input': + case 'textarea': + if (this.display_raw) { + to_display = this.element.html().replace(/&/gi, '&'); + } + else { + var value = this.element.data('bipValue'); + if (typeof value === 'undefined') { + to_display = ''; + } else if (typeof value === 'string') { + to_display = this.element.data('bipValue').replace(/&/gi, '&'); + } else { + to_display = this.element.data('bipValue'); + } + } + break; + case 'select': + to_display = this.element.html(); + + } + } + + this.oldValue = this.isPlaceHolder() ? "" : this.element.html(); + this.display_value = to_display; + jQuery(this.activator).unbind("click", this.clickHandler); + this.activateForm(); + this.element.trigger(jQuery.Event("best_in_place:activate")); + }, + + abort: function () { + 'use strict'; + this.activateText(this.oldValue); + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:abort")); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + }, + + abortIfConfirm: function () { + 'use strict'; + if (!this.useConfirm) { + this.abort(); + return; + } + + if (confirm(BestInPlaceEditor.defaults.locales[''].confirmMessage)) { + this.abort(); + } + }, + + update: function () { + 'use strict'; + var editor = this, + value = this.getValue(); + + // Avoid request if no change is made + if (this.formType in {"input": 1, "textarea": 1} && value === this.oldValue) { + this.abort(); + return true; + } + + editor.ajax({ + "type": this.requestMethod(), + "dataType": BestInPlaceEditor.defaults.ajaxDataType, + "data": editor.requestData(), + "success": function (data, status, xhr) { + editor.loadSuccessCallback(data, status, xhr); + }, + "error": function (request, error) { + editor.loadErrorCallback(request, error); + } + }); + + + switch (this.formType) { + case "select": + this.previousCollectionValue = value; + + // search for the text for the span + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + break; + + case "checkbox": + $.each(this.values, function(index, arr){ if (String(arr[0]) === String(value)) editor.element.html(arr[1]); }); + break; + + default: + if (value !== "") { + if (this.display_raw) { + editor.element.html(value); + } else { + editor.element.text(value); + } + } else { + editor.element.html(this.placeHolder); + } + } + + editor.element.data('bipValue', value); + editor.element.attr('data-bip-value', value); + + editor.element.trigger(jQuery.Event("best_in_place:update")); + + + }, + + activateForm: function () { + 'use strict'; + alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); + }, + + activateText: function (value) { + 'use strict'; + this.element.html(value); + if (this.isPlaceHolder()) { + this.element.html(this.placeHolder); + } + }, + + // Helper Functions //////////////////////////////////////////////////////// + + initOptions: function () { + // Try parent supplied info + 'use strict'; + var self = this; + self.element.parents().each(function () { + var $parent = jQuery(this); + self.url = self.url || $parent.data("bipUrl"); + self.activator = self.activator || $parent.data("bipActivator"); + self.okButton = self.okButton || $parent.data("bipOkButton"); + self.okButtonClass = self.okButtonClass || $parent.data("bipOkButtonClass"); + self.cancelButton = self.cancelButton || $parent.data("bipCancelButton"); + self.cancelButtonClass = self.cancelButtonClass || $parent.data("bipCancelButtonClass"); + self.skipBlur = self.skipBlur || $parent.data("bipSkipBlur"); + }); + + // Load own attributes (overrides all others) + self.url = self.element.data("bipUrl") || self.url || document.location.pathname; + self.collection = self.element.data("bipCollection") || self.collection; + self.formType = self.element.data("bipType") || "input"; + self.objectName = self.element.data("bipObject") || self.objectName; + self.attributeName = self.element.data("bipAttribute") || self.attributeName; + self.activator = self.element.data("bipActivator") || self.element; + self.okButton = self.element.data("bipOkButton") || self.okButton; + self.okButtonClass = self.element.data("bipOkButtonClass") || self.okButtonClass || BestInPlaceEditor.defaults.okButtonClass; + self.cancelButton = self.element.data("bipCancelButton") || self.cancelButton; + self.cancelButtonClass = self.element.data("bipCancelButtonClass") || self.cancelButtonClass || BestInPlaceEditor.defaults.cancelButtonClass; + self.skipBlur = self.element.data("bipSkipBlur") || self.skipBlur || BestInPlaceEditor.defaults.skipBlur; + self.isNewObject = self.element.data("bipNewObject"); + self.dataExtraPayload = self.element.data("bipExtraPayload"); + + // Fix for default values of 0 + if (self.element.data("bipPlaceholder") == null) { + self.placeHolder = BestInPlaceEditor.defaults.locales[''].placeHolder; + } else { + self.placeHolder = self.element.data("bipPlaceholder"); + } + + self.inner_class = self.element.data("bipInnerClass"); + self.html_attrs = self.element.data("bipHtmlAttrs"); + self.original_content = self.element.data("bipOriginalContent") || self.original_content; + + // if set the input won't be satinized + self.display_raw = self.element.data("bip-raw"); + + self.useConfirm = self.element.data("bip-confirm"); + + if (self.formType === "select" || self.formType === "checkbox") { + self.values = self.collection; + self.collectionValue = self.element.data("bipValue") || self.collectionValue; + } + }, + + bindForm: function () { + 'use strict'; + this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; + this.getValue = BestInPlaceEditor.forms[this.formType].getValue; + }, + + + initPlaceHolder: function () { + 'use strict'; + // TODO add placeholder for select and checkbox + if (this.element.html() === "") { + this.element.addClass('bip-placeholder'); + this.element.html(this.placeHolder); + } + }, + + isPlaceHolder: function () { + 'use strict'; + // TODO: It only work when form is deactivated. + // Condition will fail when form is activated + return this.element.html() === "" || this.element.html() === this.placeHolder; + }, + + getValue: function () { + 'use strict'; + alert(BestInPlaceEditor.defaults.locales[''].uninitializedForm); + }, + + // Trim and Strips HTML from text + sanitizeValue: function (s) { + 'use strict'; + return jQuery.trim(s); + }, + + requestMethod: function() { + 'use strict'; + return this.isNewObject ? 'post' : BestInPlaceEditor.defaults.ajaxMethod; + }, + + /* Generate the data sent in the POST request */ + requestData: function () { + 'use strict'; + // To prevent xss attacks, a csrf token must be defined as a meta attribute + var csrf_token = jQuery('meta[name=csrf-token]').attr('content'), + csrf_param = jQuery('meta[name=csrf-param]').attr('content'); + + var data = {} + data['_method'] = this.requestMethod() + + data[this.objectName] = this.dataExtraPayload || {} + + data[this.objectName][this.attributeName] = this.getValue() + + if (csrf_param !== undefined && csrf_token !== undefined) { + data[csrf_param] = csrf_token + } + return jQuery.param(data); + }, + + ajax: function (options) { + 'use strict'; + options.url = this.url; + options.beforeSend = function (xhr) { + xhr.setRequestHeader("Accept", "application/json"); + }; + return jQuery.ajax(options); + }, + + // Handlers //////////////////////////////////////////////////////////////// + + loadSuccessCallback: function (data, status, xhr) { + 'use strict'; + data = jQuery.trim(data); + //Update original content with current text. + if (this.display_raw) { + this.original_content = this.element.html(); + } else { + this.original_content = this.element.text(); + } + + if (data && data !== "") { + var response = jQuery.parseJSON(data); + if (response !== null && response.hasOwnProperty("display_as")) { + this.element.data('bip-original-content', this.element.text()); + this.element.html(response.display_as); + } + if (this.isNewObject && response && response[this.objectName]) { + if (response[this.objectName]["id"]) { + this.isNewObject = false + this.url += "/" + response[this.objectName]["id"] // in REST a POST /thing url should become PUT /thing/123 + } + } + } + this.element.toggleClass('bip-placeholder', this.isPlaceHolder()); + + this.element.trigger(jQuery.Event("best_in_place:success"), [data, status, xhr]); + this.element.trigger(jQuery.Event("ajax:success"), [data, status, xhr]); + + // Binding back after being clicked + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + + if (this.collectionValue !== null && this.formType === "select") { + this.collectionValue = this.previousCollectionValue; + this.previousCollectionValue = null; + } + }, + + loadErrorCallback: function (request, error) { + 'use strict'; + this.activateText(this.oldValue); + + this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); + this.element.trigger(jQuery.Event("ajax:error"), request, error); + + // Binding back after being clicked + jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); + this.element.trigger(jQuery.Event("best_in_place:deactivate")); + }, + + clickHandler: function (event) { + 'use strict'; + event.preventDefault(); + event.data.editor.activate(); + }, + + setHtmlAttributes: function () { + 'use strict'; + var formField = this.element.find(this.formType); + + if (this.html_attrs) { + var attrs = this.html_attrs; + $.each(attrs, function (key, val) { + formField.attr(key, val); + }); + } + }, + + placeButtons: function (output, field) { + 'use strict'; + if (field.okButton) { + output.append( + jQuery(document.createElement('input')) + .attr('type', 'submit') + .attr('class', field.okButtonClass) + .attr('value', field.okButton) + ); + } + if (field.cancelButton) { + output.append( + jQuery(document.createElement('input')) + .attr('type', 'button') + .attr('class', field.cancelButtonClass) + .attr('value', field.cancelButton) + ); + } + } +}; + + +// Button cases: +// If no buttons, then blur saves, ESC cancels +// If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) +// If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels +// If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels +BestInPlaceEditor.forms = { + "input": { + activateForm: function () { + 'use strict'; + var output = jQuery(document.createElement('form')) + .addClass('form_in_place') + .attr('action', 'javascript:void(0);') + .attr('style', 'display:inline'); + var input_elt = jQuery(document.createElement('input')) + .attr('type', 'text') + .attr('name', this.attributeName) + .val(this.display_value); + + // Add class to form input + if (this.inner_class) { + input_elt.addClass(this.inner_class); + } + + output.append(input_elt); + this.placeButtons(output, this); + + this.element.html(output); + this.setHtmlAttributes(); + + this.element.find("input[type='text']")[0].select(); + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); + if (this.cancelButton) { + this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); + } + if (!this.okButton) { + this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); + } + this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); + this.blurTimer = null; + this.userClicked = false; + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("input").val()); + }, + + // When buttons are present, use a timer on the blur event to give precedence to clicks + inputBlurHandler: function (event) { + 'use strict'; + if (event.data.editor.okButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.abort(); + } + }, 500); + } else { + if (event.data.editor.cancelButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.update(); + } + }, 500); + } else { + event.data.editor.update(); + } + } + }, + + submitHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.update(); + }, + + cancelButtonHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.abort(); + event.stopPropagation(); // Without this, click isn't handled + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abort(); + event.stopImmediatePropagation(); + } + } + }, + + "select": { + activateForm: function () { + 'use strict'; + var output = jQuery(document.createElement('form')) + .attr('action', 'javascript:void(0)') + .attr('style', 'display:inline'), + selected = '', + select_elt = jQuery(document.createElement('select')) + .attr('class', this.inner_class !== null ? this.inner_class : ''), + currentCollectionValue = this.collectionValue, + key, value, + a = this.values; + + $.each(a, function(index, arr){ + key = arr[0]; + value = arr[1]; + var option_elt = jQuery(document.createElement('option')) + .val(key) + .html(value); + + if (currentCollectionValue) { + if (String(key) === String(currentCollectionValue)) option_elt.attr('selected', 'selected'); + } + select_elt.append(option_elt); + }); + output.append(select_elt); + + this.element.html(output); + this.setHtmlAttributes(); + this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); + this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); + this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); + this.element.find("select")[0].focus(); + + // automatically click on the select so you + // don't have to click twice + try { + var e = document.createEvent("MouseEvents"); + e.initMouseEvent("mousedown", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); + this.element.find("select")[0].dispatchEvent(e); + } + catch(e) { + // browser doesn't support this, e.g. IE8 + } + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("select").val()); + }, + + blurHandler: function (event) { + 'use strict'; + event.data.editor.update(); + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abort(); + } + } + }, + + "checkbox": { + activateForm: function () { + 'use strict'; + this.collectionValue = !this.getValue(); + this.setHtmlAttributes(); + this.update(); + }, + + getValue: function () { + 'use strict'; + return this.collectionValue; + } + }, + + "textarea": { + activateForm: function () { + 'use strict'; + // grab width and height of text + var width = this.element.css('width'); + var height = this.element.css('height'); + + // construct form + var output = jQuery(document.createElement('form')) + .addClass('form_in_place') + .attr('action', 'javascript:void(0);') + .attr('style', 'display:inline'); + var textarea_elt = jQuery(document.createElement('textarea')) + .attr('name', this.attributeName) + .val(this.sanitizeValue(this.display_value)); + + if (this.inner_class !== null) { + textarea_elt.addClass(this.inner_class); + } + + output.append(textarea_elt); + + this.placeButtons(output, this); + + this.element.html(output); + this.setHtmlAttributes(); + + // set width and height of textarea + jQuery(this.element.find("textarea")[0]).css({'min-width': width, 'min-height': height}); + jQuery(this.element.find("textarea")[0]).autosize(); + + this.element.find("textarea")[0].focus(); + this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); + + if (this.cancelButton) { + this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); + } + + if (!this.skipBlur) { + this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); + } + this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); + this.blurTimer = null; + this.userClicked = false; + }, + + getValue: function () { + 'use strict'; + return this.sanitizeValue(this.element.find("textarea").val()); + }, + + // When buttons are present, use a timer on the blur event to give precedence to clicks + blurHandler: function (event) { + 'use strict'; + if (event.data.editor.okButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.abortIfConfirm(); + } + }, 500); + } else { + if (event.data.editor.cancelButton) { + event.data.editor.blurTimer = setTimeout(function () { + if (!event.data.editor.userClicked) { + event.data.editor.update(); + } + }, 500); + } else { + event.data.editor.update(); + } + } + }, + + submitHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.update(); + }, + + cancelButtonHandler: function (event) { + 'use strict'; + event.data.editor.userClicked = true; + clearTimeout(event.data.editor.blurTimer); + event.data.editor.abortIfConfirm(); + event.stopPropagation(); // Without this, click isn't handled + }, + + keyupHandler: function (event) { + 'use strict'; + if (event.keyCode === 27) { + event.data.editor.abortIfConfirm(); + } + } + } +}; + +BestInPlaceEditor.defaults = { + locales: {}, + ajaxMethod: "put", //TODO Change to patch when support to 3.2 is dropped + ajaxDataType: 'text', + okButtonClass: '', + cancelButtonClass: '', + skipBlur: false +}; + +// Default locale +BestInPlaceEditor.defaults.locales[''] = { + confirmMessage: "Are you sure you want to discard your changes?", + uninitializedForm: "The form was not properly initialized. getValue is unbound", + placeHolder: '-' +}; + +jQuery.fn.best_in_place = function () { + 'use strict'; + function setBestInPlace(element) { + if (!element.data('bestInPlaceEditor')) { + element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); + return true; + } + } + + jQuery(this.context).delegate(this.selector, 'click', function () { + var el = jQuery(this); + if (setBestInPlace(el)) { + el.click(); + } + }); + + this.each(function () { + setBestInPlace(jQuery(this)); + }); + + return this; +}; + + + diff --git a/app/assets/javascripts/lib/bip.js b/app/assets/javascripts/lib/bip.js deleted file mode 100644 index 1d575fef..00000000 --- a/app/assets/javascripts/lib/bip.js +++ /dev/null @@ -1,780 +0,0 @@ -/* - BestInPlace (for jQuery) - version: 0.1.0 (01/01/2011) - @requires jQuery >= v1.4 - @requires jQuery.purr to display pop-up windows - - By Bernat Farrero based on the work of Jan Varwig. - Examples at http://bernatfarrero.com - - Licensed under the MIT: - http://www.opensource.org/licenses/mit-license.php - - Usage: - - Attention. - The format of the JSON object given to the select inputs is the following: - [["key", "value"],["key", "value"]] - The format of the JSON object given to the checkbox inputs is the following: - ["falseValue", "trueValue"] -*/ - - -function BestInPlaceEditor(e) { - this.element = e; - this.initOptions(); - this.bindForm(); - this.initNil(); - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); -} - -BestInPlaceEditor.prototype = { - // Public Interface Functions ////////////////////////////////////////////// - - activate : function() { - var to_display = ""; - if (this.isNil()) { - to_display = ""; - } - else if (this.original_content) { - to_display = this.original_content; - } - else { - if (this.sanitize) { - to_display = this.element.text(); - } else { - to_display = this.element.html(); - } - } - - this.oldValue = this.isNil() ? "" : this.element.html(); - this.display_value = to_display; - jQuery(this.activator).unbind("click", this.clickHandler); - this.activateForm(); - this.element.trigger(jQuery.Event("best_in_place:activate")); - }, - - abort : function() { - this.activateText(this.oldValue); - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:abort")); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - }, - - abortIfConfirm : function () { - if (!this.useConfirm) { - this.abort(); - return; - } - - if (confirm("Are you sure you want to discard your changes?")) { - this.abort(); - } - }, - - update : function() { - var editor = this; - if (this.formType in {"input":1, "textarea":1} && this.getValue() == this.oldValue) - { // Avoid request if no change is made - this.abort(); - return true; - } - editor.ajax({ - "type" : "post", - "dataType" : "text", - "data" : editor.requestData(), - "success" : function(data){ editor.loadSuccessCallback(data); }, - "error" : function(request, error){ editor.loadErrorCallback(request, error); } - }); - if (this.formType == "select") { - var value = this.getValue(); - this.previousCollectionValue = value; - - jQuery.each(this.values, function(i, v) { - if (value == v[0]) { - editor.element.html(v[1]); - } - } - ); - } else if (this.formType == "checkbox") { - editor.element.html(this.getValue() ? this.values[1] : this.values[0]); - } else { - if (this.getValue() !== "") { - editor.element.text(this.getValue()); - } else { - editor.element.html(this.nil); - } - } - editor.element.trigger(jQuery.Event("best_in_place:update")); - }, - - activateForm : function() { - alert("The form was not properly initialized. activateForm is unbound"); - }, - - activateText : function(value){ - this.element.html(value); - if(this.isNil()) this.element.html(this.nil); - }, - - // Helper Functions //////////////////////////////////////////////////////// - - initOptions : function() { - // Try parent supplied info - var self = this; - self.element.parents().each(function(){ - $parent = jQuery(this); - self.url = self.url || $parent.attr("data-url"); - self.collection = self.collection || $parent.attr("data-collection"); - self.formType = self.formType || $parent.attr("data-type"); - self.objectName = self.objectName || $parent.attr("data-object"); - self.attributeName = self.attributeName || $parent.attr("data-attribute"); - self.activator = self.activator || $parent.attr("data-activator"); - self.okButton = self.okButton || $parent.attr("data-ok-button"); - self.okButtonClass = self.okButtonClass || $parent.attr("data-ok-button-class"); - self.cancelButton = self.cancelButton || $parent.attr("data-cancel-button"); - self.cancelButtonClass = self.cancelButtonClass || $parent.attr("data-cancel-button-class"); - self.nil = self.nil || $parent.attr("data-nil"); - self.inner_class = self.inner_class || $parent.attr("data-inner-class"); - self.html_attrs = self.html_attrs || $parent.attr("data-html-attrs"); - self.original_content = self.original_content || $parent.attr("data-original-content"); - self.collectionValue = self.collectionValue || $parent.attr("data-value"); - }); - - // Try Rails-id based if parents did not explicitly supply something - self.element.parents().each(function(){ - var res = this.id.match(/^(\w+)_(\d+)$/i); - if (res) { - self.objectName = self.objectName || res[1]; - } - }); - - // Load own attributes (overrides all others) - self.url = self.element.attr("data-url") || self.url || document.location.pathname; - self.collection = self.element.attr("data-collection") || self.collection; - self.formType = self.element.attr("data-type") || self.formtype || "input"; - self.objectName = self.element.attr("data-object") || self.objectName; - self.attributeName = self.element.attr("data-attribute") || self.attributeName; - self.activator = self.element.attr("data-activator") || self.element; - self.okButton = self.element.attr("data-ok-button") || self.okButton; - self.okButtonClass = self.element.attr("data-ok-button-class") || self.okButtonClass || ""; - self.cancelButton = self.element.attr("data-cancel-button") || self.cancelButton; - self.cancelButtonClass = self.element.attr("data-cancel-button-class") || self.cancelButtonClass || ""; - self.nil = self.element.attr("data-nil") || self.nil || "—"; - self.inner_class = self.element.attr("data-inner-class") || self.inner_class || null; - self.html_attrs = self.element.attr("data-html-attrs") || self.html_attrs; - self.original_content = self.element.attr("data-original-content") || self.original_content; - self.collectionValue = self.element.attr("data-value") || self.collectionValue; - - if (!self.element.attr("data-sanitize")) { - self.sanitize = true; - } - else { - self.sanitize = (self.element.attr("data-sanitize") == "true"); - } - - if (!self.element.attr("data-use-confirm")) { - self.useConfirm = true; - } else { - self.useConfirm = (self.element.attr("data-use-confirm") != "false"); - } - - if ((self.formType == "select" || self.formType == "checkbox") && self.collection !== null) - { - self.values = jQuery.parseJSON(self.collection); - } - - }, - - bindForm : function() { - this.activateForm = BestInPlaceEditor.forms[this.formType].activateForm; - this.getValue = BestInPlaceEditor.forms[this.formType].getValue; - }, - - initNil: function() { - if (this.element.html() === "") - { - this.element.html(this.nil); - } - }, - - isNil: function() { - // TODO: It only work when form is deactivated. - // Condition will fail when form is activated - return this.element.html() === "" || this.element.html() === this.nil; - }, - - getValue : function() { - alert("The form was not properly initialized. getValue is unbound"); - }, - - // Trim and Strips HTML from text - sanitizeValue : function(s) { - return jQuery.trim(s); - }, - - /* Generate the data sent in the POST request */ - requestData : function() { - // To prevent xss attacks, a csrf token must be defined as a meta attribute - csrf_token = jQuery('meta[name=csrf-token]').attr('content'); - csrf_param = jQuery('meta[name=csrf-param]').attr('content'); - - var data = "_method=put"; - data += "&" + this.objectName + '[' + this.attributeName + ']=' + encodeURIComponent(this.getValue()); - - if (csrf_param !== undefined && csrf_token !== undefined) { - data += "&" + csrf_param + "=" + encodeURIComponent(csrf_token); - } - return data; - }, - - ajax : function(options) { - options.url = this.url; - options.beforeSend = function(xhr){ xhr.setRequestHeader("Accept", "application/json"); }; - return jQuery.ajax(options); - }, - - // Handlers //////////////////////////////////////////////////////////////// - - loadSuccessCallback : function(data) { - data = jQuery.trim(data); - - if(data && data!=""){ - var response = jQuery.parseJSON(jQuery.trim(data)); - if (response !== null && response.hasOwnProperty("display_as")) { - this.element.attr("data-original-content", this.element.text()); - this.original_content = this.element.text(); - this.element.html(response["display_as"]); - } - - this.element.trigger(jQuery.Event("best_in_place:success"), data); - this.element.trigger(jQuery.Event("ajax:success"), data); - } else { - this.element.trigger(jQuery.Event("best_in_place:success")); - this.element.trigger(jQuery.Event("ajax:success")); - } - - // Binding back after being clicked - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - - if (this.collectionValue !== null && this.formType == "select") { - this.collectionValue = this.previousCollectionValue; - this.previousCollectionValue = null; - } - }, - - loadErrorCallback : function(request, error) { - this.activateText(this.oldValue); - - this.element.trigger(jQuery.Event("best_in_place:error"), [request, error]); - this.element.trigger(jQuery.Event("ajax:error"), request, error); - - // Binding back after being clicked - jQuery(this.activator).bind('click', {editor: this}, this.clickHandler); - this.element.trigger(jQuery.Event("best_in_place:deactivate")); - }, - - clickHandler : function(event) { - event.preventDefault(); - event.data.editor.activate(); - }, - - setHtmlAttributes : function() { - var formField = this.element.find(this.formType); - - if(this.html_attrs){ - var attrs = jQuery.parseJSON(this.html_attrs); - for(var key in attrs){ - formField.attr(key, attrs[key]); - } - } - } -}; - - -// Button cases: -// If no buttons, then blur saves, ESC cancels -// If just Cancel button, then blur saves, ESC or clicking Cancel cancels (careful of blur event!) -// If just OK button, then clicking OK saves (careful of blur event!), ESC or blur cancels -// If both buttons, then clicking OK saves, ESC or clicking Cancel or blur cancels -BestInPlaceEditor.forms = { - "input" : { - activateForm : function() { - var output = jQuery(document.createElement('form')) - .addClass('form_in_place') - .attr('action', 'javascript:void(0);') - .attr('style', 'display:inline'); - var input_elt = jQuery(document.createElement('input')) - .attr('type', 'text') - .attr('name', this.attributeName) - .val(this.display_value); - if(this.inner_class !== null) { - input_elt.addClass(this.inner_class); - } - output.append(input_elt); - if(this.okButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'submit') - .attr('class', this.okButtonClass) - .attr('value', this.okButton) - ) - } - if(this.cancelButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'button') - .attr('class', this.cancelButtonClass) - .attr('value', this.cancelButton) - ) - } - - this.element.html(output); - this.setHtmlAttributes(); - // START METAMAPS CODE - //this.element.find("input[type='text']")[0].select(); - this.element.find("input[type='text']")[0].focus(); - // END METAMAPS CODE - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); - if (this.cancelButton) { - this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.input.cancelButtonHandler); - } - this.element.find("input[type='text']").bind('blur', {editor: this}, BestInPlaceEditor.forms.input.inputBlurHandler); - // START METAMAPS CODE - this.element.find("input[type='text']").bind('keydown', {editor: this}, BestInPlaceEditor.forms.input.keydownHandler); - // END METAMAPS CODE - this.element.find("input[type='text']").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); - this.blurTimer = null; - this.userClicked = false; - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("input").val()); - }, - - // When buttons are present, use a timer on the blur event to give precedence to clicks - inputBlurHandler : function(event) { - if (event.data.editor.okButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.abort(); - } - }, 500); - } else { - if (event.data.editor.cancelButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.update(); - } - }, 500); - } else { - event.data.editor.update(); - } - } - }, - - submitHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.update(); - }, - - cancelButtonHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.abort(); - event.stopPropagation(); // Without this, click isn't handled - }, - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abort(); - } - // START METAMAPS CODE - else if (event.keyCode == 13 && !event.shiftKey) { - event.data.editor.update(); - } - // END METAMAPS CODE - } - }, - - "date" : { - activateForm : function() { - var that = this, - output = jQuery(document.createElement('form')) - .addClass('form_in_place') - .attr('action', 'javascript:void(0);') - .attr('style', 'display:inline'), - input_elt = jQuery(document.createElement('input')) - .attr('type', 'text') - .attr('name', this.attributeName) - .attr('value', this.sanitizeValue(this.display_value)); - if(this.inner_class !== null) { - input_elt.addClass(this.inner_class); - } - output.append(input_elt) - - this.element.html(output); - this.setHtmlAttributes(); - this.element.find('input')[0].select(); - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.input.submitHandler); - this.element.find("input").bind('keyup', {editor: this}, BestInPlaceEditor.forms.input.keyupHandler); - - this.element.find('input') - .datepicker({ - onClose: function() { - that.update(); - } - }) - .datepicker('show'); - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("input").val()); - }, - - submitHandler : function(event) { - event.data.editor.update(); - }, - - // START METAMAPS CODE - keydownHandler : function(event) { - if (event.keyCode == 13 && !event.shiftKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }, - // END METAMAPS CODE - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abort(); - } - } - }, - - "select" : { - activateForm : function() { - var output = jQuery(document.createElement('form')) - .attr('action', 'javascript:void(0)') - .attr('style', 'display:inline'); - selected = '', - oldValue = this.oldValue, - select_elt = jQuery(document.createElement('select')) - .attr('class', this.inned_class !== null ? this.inner_class : '' ), - currentCollectionValue = this.collectionValue; - - jQuery.each(this.values, function (index, value) { - var option_elt = jQuery(document.createElement('option')) - // .attr('value', value[0]) - .val(value[0]) - .html(value[1]); - if(value[0] == currentCollectionValue) { - option_elt.attr('selected', 'selected'); - } - select_elt.append(option_elt); - }); - output.append(select_elt); - - this.element.html(output); - this.setHtmlAttributes(); - this.element.find("select").bind('change', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); - this.element.find("select").bind('blur', {editor: this}, BestInPlaceEditor.forms.select.blurHandler); - this.element.find("select").bind('keyup', {editor: this}, BestInPlaceEditor.forms.select.keyupHandler); - this.element.find("select")[0].focus(); - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("select").val()); - // return this.element.find("select").val(); - }, - - blurHandler : function(event) { - event.data.editor.update(); - }, - - keyupHandler : function(event) { - if (event.keyCode == 27) event.data.editor.abort(); - } - }, - - "checkbox" : { - activateForm : function() { - this.collectionValue = !this.getValue(); - this.setHtmlAttributes(); - this.update(); - }, - - getValue : function() { - return this.collectionValue; - } - }, - - "textarea" : { - activateForm : function() { - // grab width and height of text - width = this.element.css('width'); - height = this.element.css('height'); - - // construct form - var output = jQuery(document.createElement('form')) - .attr('action', 'javascript:void(0)') - .attr('style', 'display:inline') - .append(jQuery(document.createElement('textarea')) - .val(this.sanitizeValue(this.display_value))); - if(this.okButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'submit') - .attr('value', this.okButton) - ); - } - if(this.cancelButton) { - output.append( - jQuery(document.createElement('input')) - .attr('type', 'button') - .attr('value', this.cancelButton) - ) - } - - this.element.html(output); - this.setHtmlAttributes(); - - // set width and height of textarea - jQuery(this.element.find("textarea")[0]).css({ 'min-width': width, 'min-height': height }); - jQuery(this.element.find("textarea")[0]).elastic(); - - this.element.find("textarea")[0].focus(); - this.element.find("form").bind('submit', {editor: this}, BestInPlaceEditor.forms.textarea.submitHandler); - if (this.cancelButton) { - this.element.find("input[type='button']").bind('click', {editor: this}, BestInPlaceEditor.forms.textarea.cancelButtonHandler); - } - this.element.find("textarea").bind('blur', {editor: this}, BestInPlaceEditor.forms.textarea.blurHandler); - // START METAMAPS CODE - this.element.find("textarea").bind('keydown', {editor: this}, BestInPlaceEditor.forms.textarea.keydownHandler); - // END METAMAPS CODE - this.element.find("textarea").bind('keyup', {editor: this}, BestInPlaceEditor.forms.textarea.keyupHandler); - this.blurTimer = null; - this.userClicked = false; - }, - - getValue : function() { - return this.sanitizeValue(this.element.find("textarea").val()); - }, - - // When buttons are present, use a timer on the blur event to give precedence to clicks - blurHandler : function(event) { - if (event.data.editor.okButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.abortIfConfirm(); - } - }, 500); - } else { - if (event.data.editor.cancelButton) { - event.data.editor.blurTimer = setTimeout(function () { - if (!event.data.editor.userClicked) { - event.data.editor.update(); - } - }, 500); - } else { - event.data.editor.update(); - } - } - }, - - submitHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.update(); - }, - - cancelButtonHandler : function(event) { - event.data.editor.userClicked = true; - clearTimeout(event.data.editor.blurTimer); - event.data.editor.abortIfConfirm(); - event.stopPropagation(); // Without this, click isn't handled - }, - - // START METAMAPS CODE - keydownHandler : function(event) { - if (event.keyCode == 13 && !event.shiftKey) { - event.preventDefault(); - event.stopPropagation(); - return false; - } - }, - // END METAMAPS CODE - - keyupHandler : function(event) { - if (event.keyCode == 27) { - event.data.editor.abortIfConfirm(); - } - // START METAMAPS CODE - else if (event.keyCode == 13 && !event.shiftKey) { - event.data.editor.update(); - } - // END METAMAPS CODE - } - } -}; - -jQuery.fn.best_in_place = function() { - - function setBestInPlace(element) { - if (!element.data('bestInPlaceEditor')) { - element.data('bestInPlaceEditor', new BestInPlaceEditor(element)); - return true; - } - } - - jQuery(this.context).delegate(this.selector, 'click', function () { - var el = jQuery(this); - if (setBestInPlace(el)) - el.click(); - }); - - this.each(function () { - setBestInPlace(jQuery(this)); - }); - - return this; -}; - - - -/** -* @name Elastic -* @descripton Elastic is Jquery plugin that grow and shrink your textareas automaticliy -* @version 1.6.5 -* @requires Jquery 1.2.6+ -* -* @author Jan Jarfalk -* @author-email jan.jarfalk@unwrongest.com -* @author-website http://www.unwrongest.com -* -* @licens MIT License - http://www.opensource.org/licenses/mit-license.php -*/ - -(function(jQuery){ - if (typeof jQuery.fn.elastic !== 'undefined') return; - - jQuery.fn.extend({ - elastic: function() { - // We will create a div clone of the textarea - // by copying these attributes from the textarea to the div. - var mimics = [ - 'paddingTop', - 'paddingRight', - 'paddingBottom', - 'paddingLeft', - 'fontSize', - 'lineHeight', - 'fontFamily', - 'width', - 'fontWeight']; - - return this.each( function() { - - // Elastic only works on textareas - if ( this.type != 'textarea' ) { - return false; - } - - var $textarea = jQuery(this), - $twin = jQuery('<div />').css({'position': 'absolute','display':'none','word-wrap':'break-word'}), - lineHeight = parseInt($textarea.css('line-height'),10) || parseInt($textarea.css('font-size'),'10'), - minheight = parseInt($textarea.css('height'),10) || lineHeight*3, - maxheight = parseInt($textarea.css('max-height'),10) || Number.MAX_VALUE, - goalheight = 0, - i = 0; - - // Opera returns max-height of -1 if not set - if (maxheight < 0) { maxheight = Number.MAX_VALUE; } - - // Append the twin to the DOM - // We are going to meassure the height of this, not the textarea. - $twin.appendTo($textarea.parent()); - - // Copy the essential styles (mimics) from the textarea to the twin - i = mimics.length; - while(i--){ - $twin.css(mimics[i].toString(),$textarea.css(mimics[i].toString())); - } - - - // Sets a given height and overflow state on the textarea - function setHeightAndOverflow(height, overflow){ - curratedHeight = Math.floor(parseInt(height,10)); - if($textarea.height() != curratedHeight){ - $textarea.css({'height': curratedHeight + 'px','overflow':overflow}); - - } - } - - - // This function will update the height of the textarea if necessary - function update() { - - // Get curated content from the textarea. - var textareaContent = $textarea.val().replace(/&/g,'&').replace(/ /g, ' ').replace(/<|>/g, '>').replace(/\n/g, '<br />'); - - // Compare curated content with curated twin. - var twinContent = $twin.html().replace(/<br>/ig,'<br />'); - - if(textareaContent+' ' != twinContent){ - - // Add an extra white space so new rows are added when you are at the end of a row. - $twin.html(textareaContent+' '); - - // Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height - if(Math.abs($twin.height() + lineHeight - $textarea.height()) > 3){ - - var goalheight = $twin.height()+lineHeight; - if(goalheight >= maxheight) { - setHeightAndOverflow(maxheight,'auto'); - } else if(goalheight <= minheight) { - setHeightAndOverflow(minheight,'hidden'); - } else { - setHeightAndOverflow(goalheight,'hidden'); - } - - } - - } - - } - - // Hide scrollbars - $textarea.css({'overflow':'hidden'}); - - // Update textarea size on keyup, change, cut and paste - $textarea.bind('keyup change cut paste', function(){ - update(); - }); - - // Compact textarea on blur - // Lets animate this.... - $textarea.bind('blur',function(){ - if($twin.height() < maxheight){ - if($twin.height() > minheight) { - $textarea.height($twin.height()); - } else { - $textarea.height(minheight); - } - } - }); - - // And this line is to catch the browser paste event - $textarea.on("input paste", function(e){ setTimeout( update, 250); }); - - // Run update once when elastic is initialized - update(); - - }); - - } - }); -})(jQuery); diff --git a/app/views/layouts/_templates.html.erb b/app/views/layouts/_templates.html.erb index 59a5ceb8..921a7d88 100644 --- a/app/views/layouts/_templates.html.erb +++ b/app/views/layouts/_templates.html.erb @@ -183,11 +183,12 @@ <span class="title"> <div class="titleWrapper" id="titleActivator"> <span class="best_in_place best_in_place_name" - data-url="/topics/{{id}}" - data-object="topic" - data-attribute="name" - data-activator="#titleActivator" - data-type="textarea">{{name}}</span> + data-bip-url="/topics/{{id}}" + data-bip-object="topic" + data-bip-attribute="name" + data-bip-activator="#titleActivator" + data-bip-value="{{name}}" + data-bip-type="textarea">{{name}}</span> </div> </span> <div class="links"> @@ -220,7 +221,7 @@ </div> <div class="scroll"> <div class="desc"> - <span class="best_in_place best_in_place_desc" data-url="/topics/{{id}}" data-object="topic" data-nil="{{desc_nil}}" data-attribute="desc" data-type="textarea" data-original-content="{{desc_markdown}}">{{{desc_html}}}</span> + <span class="best_in_place best_in_place_desc" data-bip-url="/topics/{{id}}" data-bip-object="topic" data-bip-nil="{{desc_nil}}" data-bip-attribute="desc" data-bip-type="textarea" data-bip-value="{{desc_markdown}}">{{{desc_html}}}</span> <div class="clearfloat"></div> </div> </div> diff --git a/app/views/maps/_mapinfobox.html.erb b/app/views/maps/_mapinfobox.html.erb index 5a158d2d..8e6b2dba 100644 --- a/app/views/maps/_mapinfobox.html.erb +++ b/app/views/maps/_mapinfobox.html.erb @@ -16,7 +16,7 @@ <% if @map %> <div class="mapInfoName" id="mapInfoName"> <% if policy(@map).update? %> - <span class="best_in_place best_in_place_name" id="best_in_place_map_<%= @map.id %>_name" data-url="/maps/<%= @map.id %>" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName"><%= @map.name %></span> + <span class="best_in_place best_in_place_name" id="best_in_place_map_<%= @map.id %>_name" data-bip-url="/maps/<%= @map.id %>" data-bip-object="map" data-bip-attribute="name" data-bip-type="textarea" data-bip-activator="#mapInfoName" data-bip-value="<%= @map.name %>"><%= @map.name %></span> <% else %> <%= @map.name %> <% end %> @@ -67,7 +67,7 @@ <div class="mapInfoDesc" id="mapInfoDesc"> <% if policy(@map).update? %> - <span class="best_in_place best_in_place_desc" id="best_in_place_map_<%= @map.id %>_desc" data-url="/maps/<%= @map.id %>" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc"><%= @map.desc %></span> + <span class="best_in_place best_in_place_desc" id="best_in_place_map_<%= @map.id %>_desc" data-bip-url="/maps/<%= @map.id %>" data-bip-object="map" data-bip-attribute="desc" data-bip-nil="Click to add description..." data-bip-type="textarea" data-bip-activator="#mapInfoDesc" data-bip-value="<%= @map.desc %>"><%= @map.desc %></span> <% else %> <%= @map.desc %> <% end %> diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index cf3365f3..db78323d 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -23,7 +23,6 @@ const Listeners = { if (e.target.className !== 'chat-input') { JIT.enterKeyHandler() } - e.preventDefault() break case 27: // if esc key is pressed JIT.escKeyHandler() diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index 0d3a5c5f..ddfd72c3 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -1,5 +1,7 @@ /* global Metamaps, $, Hogan, Bloodhound, Countable */ +import outdent from 'outdent' + import Active from '../Active' import GlobalUI from '../GlobalUI' import Router from '../Router' @@ -19,8 +21,27 @@ const InfoBox = { changing: false, selectingPermission: false, changePermissionText: "<div class='tooltips'>As the creator, you can change the permission of this map, and the permission of all the topics and synapses you have authority to change will change as well.</div>", - nameHTML: '<span class="best_in_place best_in_place_name" id="best_in_place_map_{{id}}_name" data-url="/maps/{{id}}" data-object="map" data-attribute="name" data-type="textarea" data-activator="#mapInfoName">{{name}}</span>', - descHTML: '<span class="best_in_place best_in_place_desc" id="best_in_place_map_{{id}}_desc" data-url="/maps/{{id}}" data-object="map" data-attribute="desc" data-nil="Click to add description..." data-type="textarea" data-activator="#mapInfoDesc">{{desc}}</span>', + nameHTML: outdent` + <span class="best_in_place best_in_place_name" + id="best_in_place_map_{{id}}_name" + data-bip-url="/maps/{{id}}" + data-bip-object="map" + data-bip-attribute="name" + data-bip-type="textarea" + data-bip-activator="#mapInfoName" + data-bip-value="{{name}}" + >{{name}}</span>`, + descHTML: outdent` + <span class="best_in_place best_in_place_desc" + id="best_in_place_map_{{id}}_desc" + data-bip-url="/maps/{{id}}" + data-bip-object="map" + data-bip-attribute="desc" + data-bip-nil="Click to add description..." + data-bip-type="textarea" + data-bip-activator="#mapInfoDesc" + data-bip-value="{{desc}}" + >{{desc}}</span>`, init: function () { var self = InfoBox diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index 8203657d..b1810dac 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -80,11 +80,12 @@ const SynapseCard = { // desc editing form $('#editSynUpperBar').append('<div id="edit_synapse_desc"></div>') $('#edit_synapse_desc').attr('class', 'best_in_place best_in_place_desc') - $('#edit_synapse_desc').attr('data-object', 'synapse') - $('#edit_synapse_desc').attr('data-attribute', 'desc') - $('#edit_synapse_desc').attr('data-type', 'textarea') - $('#edit_synapse_desc').attr('data-nil', data_nil) - $('#edit_synapse_desc').attr('data-url', '/synapses/' + synapse.id) + $('#edit_synapse_desc').attr('data-bip-object', 'synapse') + $('#edit_synapse_desc').attr('data-bip-attribute', 'desc') + $('#edit_synapse_desc').attr('data-bip-type', 'textarea') + $('#edit_synapse_desc').attr('data-bip-nil', data_nil) + $('#edit_synapse_desc').attr('data-bip-url', '/synapses/' + synapse.id) + $('#edit_synapse_desc').attr('data-bip-value', synapse.get('desc')) $('#edit_synapse_desc').html(synapse.get('desc')) // if edge data is blank or just whitespace, populate it with data_nil diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index 84892450..f74cf18f 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -274,12 +274,13 @@ const TopicCard = { }) // this is for all subsequent renders after in-place editing the desc field - $(showCard).find('.best_in_place_desc').bind('ajax:success', function () { - var desc = $(this).html() === $(this).data('nil') + 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('bestInPlaceEditor').original_content = desc + $(this).data('bip-value', desc) this.innerHTML = Util.mdToHTML(desc) topic.trigger('saved') }) @@ -458,7 +459,7 @@ const TopicCard = { nodeValues.metacode_select = $('#metacodeOptions').html() nodeValues.desc_nil = 'Click to add description...' nodeValues.desc_markdown = (topic.get('desc') === '' && authorized) - ? desc_nil + ? nodeValues.desc_nil : topic.get('desc') nodeValues.desc_html = Util.mdToHTML(nodeValues.desc_markdown) return nodeValues From f77562937177e3b9daa92139f226db1a2e32801d Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:03:50 +0800 Subject: [PATCH 194/306] showCard .desc css for ul and a tags --- app/assets/stylesheets/base.css.erb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 5b0fcf84..1e5d68e8 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -143,6 +143,15 @@ margin-top:5px; } +.CardOnGraph .desc ul { + margin-left: 1em; + +} +.CardOnGraph .desc a:hover { + text-decoration: underline; + opacity: 0.9; +} + .CardOnGraph .best_in_place_desc { display:block; margin-top:2px; From 7eacda2ae7c175ba1113ad2de01ae9e9be9f8118 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 00:31:32 +0800 Subject: [PATCH 195/306] code style --- .eslintrc.js | 7 ++++++ .../src/Metamaps/GlobalUI/ImportDialog.js | 14 ++++++----- frontend/src/Metamaps/Map/index.js | 3 --- frontend/src/Metamaps/PasteInput.js | 2 +- frontend/src/components/ImportDialogBox.js | 3 ++- frontend/src/components/Maps/Header.js | 24 +++++++++---------- frontend/src/components/Maps/MapCard.js | 10 ++++---- frontend/src/components/Maps/MapperCard.js | 16 ++++++------- frontend/src/components/Maps/index.js | 12 +++++----- 9 files changed, 49 insertions(+), 42 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index aa594fa7..1222f4a1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,6 +1,11 @@ module.exports = { "sourceType": "module", "parser": "babel-eslint", + "parserOptions": { + "ecmaFeatures": { + "jsx": true + } + }, "extends": "standard", "installedESLint": true, "env": { @@ -13,6 +18,8 @@ module.exports = { "react" ], "rules": { + "react/jsx-uses-react": [2], + "react/jsx-uses-vars": [2], "yoda": [2, "never", { "exceptRange": true }] } } diff --git a/frontend/src/Metamaps/GlobalUI/ImportDialog.js b/frontend/src/Metamaps/GlobalUI/ImportDialog.js index 3671cd90..96f9524f 100644 --- a/frontend/src/Metamaps/GlobalUI/ImportDialog.js +++ b/frontend/src/Metamaps/GlobalUI/ImportDialog.js @@ -1,3 +1,5 @@ +/* global $ */ + import React from 'react' import ReactDOM from 'react-dom' import outdent from 'outdent' @@ -10,7 +12,7 @@ const ImportDialog = { openLightbox: null, closeLightbox: null, - init: function(serverData, openLightbox, closeLightbox) { + init: function (serverData, openLightbox, closeLightbox) { const self = ImportDialog self.openLightbox = openLightbox self.closeLightbox = closeLightbox @@ -22,14 +24,14 @@ const ImportDialog = { `)) ReactDOM.render(React.createElement(ImportDialogBox, { onFileAdded: PasteInput.handleFile, - exampleImageUrl: serverData['import-example.png'], + exampleImageUrl: serverData['import-example.png'] }), $('.importDialogWrapper').get(0)) }, - show: function() { - self.openLightbox('import-dialog') + show: function () { + ImportDialog.openLightbox('import-dialog') }, - hide: function() { - self.closeLightbox('import-dialog') + hide: function () { + ImportDialog.closeLightbox('import-dialog') } } diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 7d7322fc..43f04a30 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -1,8 +1,6 @@ /* global Metamaps, $ */ import outdent from 'outdent' -import React from 'react' -import ReactDOM from 'react-dom' import Active from '../Active' import AutoLayout from '../AutoLayout' @@ -47,7 +45,6 @@ const Map = { return false }) - $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() else self.star() diff --git a/frontend/src/Metamaps/PasteInput.js b/frontend/src/Metamaps/PasteInput.js index 51d4a933..f0425032 100644 --- a/frontend/src/Metamaps/PasteInput.js +++ b/frontend/src/Metamaps/PasteInput.js @@ -45,7 +45,7 @@ const PasteInput = { handleFile: (file, coords = null) => { var self = PasteInput - var fileReader = new FileReader() + var fileReader = new window.FileReader() fileReader.readAsText(file) fileReader.onload = function(e) { var text = e.currentTarget.result diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index 9851fd07..bfb60235 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -2,7 +2,7 @@ import React, { PropTypes, Component } from 'react' import Dropzone from 'react-dropzone' class ImportDialogBox extends Component { - constructor(props) { + constructor (props) { super(props) this.state = { @@ -16,6 +16,7 @@ class ImportDialogBox extends Component { handleFile = (files, e) => { // for some reason it uploads twice, so we need this debouncer + // eslint-disable-next-line no-return-assign this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) if (!this.debouncer) { this.props.onFileAdded(files[0]) diff --git a/frontend/src/components/Maps/Header.js b/frontend/src/components/Maps/Header.js index ee4184d5..d323c0d5 100644 --- a/frontend/src/components/Maps/Header.js +++ b/frontend/src/components/Maps/Header.js @@ -21,15 +21,15 @@ class Header extends Component { const { signedIn, section } = this.props const activeClass = (title) => { - let forClass = "exploreMapsButton" - forClass += " " + title + "Maps" - if (title == "my" && section == "mine" || - title == section) forClass += " active" + let forClass = 'exploreMapsButton' + forClass += ' ' + title + 'Maps' + if (title === 'my' && section === 'mine' || + title === section) forClass += ' active' return forClass } - const explore = section == "mine" || section == "active" || section == "starred" || section == "shared" || section == "featured" - const mapper = section == "mapper" + const explore = section === 'mine' || section === 'active' || section === 'starred' || section === 'shared' || section === 'featured' + const mapper = section === 'mapper' return ( <div id="exploreMapsHeader"> @@ -38,31 +38,31 @@ class Header extends Component { <div className="exploreMapsCenter"> <MapLink show={signedIn && explore} href="/explore/mine" - linkClass={activeClass("my")} + linkClass={activeClass('my')} data-router="true" text="My Maps" /> <MapLink show={signedIn && explore} href="/explore/shared" - linkClass={activeClass("shared")} + linkClass={activeClass('shared')} data-router="true" text="Shared With Me" /> <MapLink show={signedIn && explore} href="/explore/starred" - linkClass={activeClass("starred")} + linkClass={activeClass('starred')} data-router="true" text="Starred By Me" /> <MapLink show={explore} - href={signedIn ? "/" : "/explore/active"} - linkClass={activeClass("active")} + href={signedIn ? '/' : '/explore/active'} + linkClass={activeClass('active')} data-router="true" text="Global" /> <MapLink show={!signedIn && explore} href="/explore/featured" - linkClass={activeClass("featured")} + linkClass={activeClass('featured')} data-router="true" text="Featured Maps" /> diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index bf1416fc..e31ede18 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -3,7 +3,7 @@ import React, { Component, PropTypes } from 'react' class MapCard extends Component { render = () => { const { map, currentUser } = this.props - + function capitalize (string) { return string.charAt(0).toUpperCase() + string.slice(1) } @@ -16,12 +16,12 @@ class MapCard extends Component { const truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : '' const truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : '' const editPermission = map.authorizeToEdit(currentUser) ? 'canEdit' : 'cannotEdit' - + return ( <div className="map" id={ map.id }> <a href={ '/maps/' + map.id } data-router="true"> <div className={ 'permission ' + editPermission }> - <div className="mapCard"> + <div className="mapCard"> <span className="title" title={ map.get('name') }> { truncatedName } </span> @@ -46,7 +46,7 @@ class MapCard extends Component { { map.get('topic_count') } </span> { map.get('topic_count') === 1 ? ' topic' : ' topics' } - </div> + </div> <div className="metadataSection mapPermission"> { map.get('permission') ? capitalize(map.get('permission')) : 'Commons' } </div> @@ -57,7 +57,7 @@ class MapCard extends Component { { map.get('synapse_count') === 1 ? ' synapse' : ' synapses' } </div> <div className="clearfloat"></div> - </div> + </div> </div> </div> </a> diff --git a/frontend/src/components/Maps/MapperCard.js b/frontend/src/components/Maps/MapperCard.js index e2f4cb33..dbb06cbc 100644 --- a/frontend/src/components/Maps/MapperCard.js +++ b/frontend/src/components/Maps/MapperCard.js @@ -3,14 +3,14 @@ import React, { Component, PropTypes } from 'react' class MapperCard extends Component { render = () => { const { user } = this.props - + return ( <div className="mapper"> - <div className="mapperCard"> + <div className="mapperCard"> <div className="mapperImage"> <img src={ user.image } width="96" height="96" /> - </div> - <div className="mapperName" title={ user.name }> + </div> + <div className="mapperName" title={ user.name }> { user.name } </div> <div className="mapperInfo"> @@ -19,10 +19,10 @@ class MapperCard extends Component { </div> <div className="mapperMetadata"> <div className="metadataSection metadataMaps"><div>{ user.numMaps }</div>maps</div> - <div className="metadataSection metadataTopics"><div>{ user.numTopics }</div>topics</div> - <div className="metadataSection metadataSynapses"><div>{ user.numSynapses }</div>synapses</div> - <div className="clearfloat"></div> - </div> + <div className="metadataSection metadataTopics"><div>{ user.numTopics }</div>topics</div> + <div className="metadataSection metadataSynapses"><div>{ user.numSynapses }</div>synapses</div> + <div className="clearfloat"></div> + </div> </div> </div> ) diff --git a/frontend/src/components/Maps/index.js b/frontend/src/components/Maps/index.js index 2c3e8ba1..22a41d3e 100644 --- a/frontend/src/components/Maps/index.js +++ b/frontend/src/components/Maps/index.js @@ -9,12 +9,11 @@ class Maps extends Component { const { maps, currentUser, section, displayStyle, user, moreToLoad, loadMore } = this.props let mapElements - if (displayStyle == 'grid') { + if (displayStyle === 'grid') { mapElements = maps.models.map(function (map) { return <MapCard key={ map.id } map={ map } currentUser={ currentUser } /> }) - } - else if (displayStyle == 'list') { + } else if (displayStyle === 'list') { mapElements = maps.models.map(function (map) { return <MapListItem key={ map.id } map={ map } /> }) @@ -28,9 +27,10 @@ class Maps extends Component { { currentUser && !user ? <div className="map newMap"><a href="/maps/new"><div className="newMapImage"></div><span>Create new map...</span></a></div> : null } { mapElements } <div className='clearfloat'></div> - { moreToLoad ? - [<button className="button loadMore" onClick={ loadMore }>load more</button>, <div className='clearfloat'></div>] - : null } + {!moreToLoad ? null : [ + <button className="button loadMore" onClick={ loadMore }>load more</button>, + <div className='clearfloat'></div> + ]} </div> </div> <Header signedIn={ !!currentUser } From 129e3db9465cbb46182c38acbc3f5af97a5a0a30 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 12:26:08 +0800 Subject: [PATCH 196/306] redirect to root_path if you get a 403 --- app/controllers/application_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 5dea17b5..eddf510d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base def handle_unauthorized if authenticated? - head :forbidden # TODO: make this better + redirect_to root_path, notice: "You don't have permission to see that page." else redirect_to new_user_session_path, notice: 'Try signing in to do that.' end From be6a2401b657fcccf4bf962c781df0456bed4a89 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 13:42:25 +0800 Subject: [PATCH 197/306] fix spec. not sure how this should work --- spec/controllers/maps_controller_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/maps_controller_spec.rb b/spec/controllers/maps_controller_spec.rb index 0f053dd9..800ab4dc 100644 --- a/spec/controllers/maps_controller_spec.rb +++ b/spec/controllers/maps_controller_spec.rb @@ -79,8 +79,8 @@ RSpec.describe MapsController, type: :controller do id: unowned_map.to_param } end.to change(Map, :count).by(0) - expect(response.body).to eq '' - expect(response.status).to eq 403 + expect(response.headers['Location']).to eq(request.base_url + root_path) + expect(response.status).to eq 302 end it 'deletes owned map' do From 2c64b67abdb86ae23cfc775a02cd70019bc8ed10 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 12:42:17 +0800 Subject: [PATCH 198/306] return 404s for all unmatched api routes --- app/controllers/api/v1/deprecated_controller.rb | 2 +- app/controllers/api/v2/restful_controller.rb | 5 +++++ config/routes.rb | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index b9e07214..aa67d6b1 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -4,7 +4,7 @@ module Api class DeprecatedController < ApplicationController # rubocop:disable Style/MethodMissing def method_missing - render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' } + render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' }, status: :gone end # rubocop:enable Style/MethodMissing end diff --git a/app/controllers/api/v2/restful_controller.rb b/app/controllers/api/v2/restful_controller.rb index 5d8f81b3..b64682f3 100644 --- a/app/controllers/api/v2/restful_controller.rb +++ b/app/controllers/api/v2/restful_controller.rb @@ -29,6 +29,11 @@ module Api head :no_content end + def catch_404 + skip_authorization + render json: { error: '404 Not found' }, status: :not_found + end + private def accessible_records diff --git a/config/routes.rb b/config/routes.rb index 05fe5845..62728fe7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -77,6 +77,7 @@ Metamaps::Application.routes.draw do resources :users, only: [:index, :show] do get :current, on: :collection end + match '*path', to: 'restful#catch_404', via: :all end namespace :v1, path: '/v1' do # api v1 routes all lead to a deprecation error method @@ -88,7 +89,9 @@ Metamaps::Application.routes.draw do resources :tokens, only: [:create, :destroy] do get :my_tokens, on: :collection end + match '*path', to: 'deprecated#method_missing', via: :all end + match '*path', to: 'v2/restful#catch_404', via: :all end devise_for :users, skip: :sessions, controllers: { From 9513087bbddcefdf86c46e8d3284781ff6674228 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 14:00:29 +0800 Subject: [PATCH 199/306] remove unnecessary api v1 code --- app/controllers/api/v1/deprecated_controller.rb | 8 ++++---- app/controllers/api/v1/mappings_controller.rb | 7 ------- app/controllers/api/v1/maps_controller.rb | 7 ------- app/controllers/api/v1/synapses_controller.rb | 7 ------- app/controllers/api/v1/tokens_controller.rb | 7 ------- app/controllers/api/v1/topics_controller.rb | 7 ------- config/routes.rb | 12 ++---------- 7 files changed, 6 insertions(+), 49 deletions(-) delete mode 100644 app/controllers/api/v1/mappings_controller.rb delete mode 100644 app/controllers/api/v1/maps_controller.rb delete mode 100644 app/controllers/api/v1/synapses_controller.rb delete mode 100644 app/controllers/api/v1/tokens_controller.rb delete mode 100644 app/controllers/api/v1/topics_controller.rb diff --git a/app/controllers/api/v1/deprecated_controller.rb b/app/controllers/api/v1/deprecated_controller.rb index aa67d6b1..3269a1a8 100644 --- a/app/controllers/api/v1/deprecated_controller.rb +++ b/app/controllers/api/v1/deprecated_controller.rb @@ -2,11 +2,11 @@ module Api module V1 class DeprecatedController < ApplicationController - # rubocop:disable Style/MethodMissing - def method_missing - render json: { error: '/api/v1 is deprecated! Please use /api/v2 instead.' }, status: :gone + def deprecated + render json: { + error: '/api/v1 has been deprecated! Please use /api/v2 instead.' + }, status: :gone end - # rubocop:enable Style/MethodMissing end end end diff --git a/app/controllers/api/v1/mappings_controller.rb b/app/controllers/api/v1/mappings_controller.rb deleted file mode 100644 index 8ba6e704..00000000 --- a/app/controllers/api/v1/mappings_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class MappingsController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/maps_controller.rb b/app/controllers/api/v1/maps_controller.rb deleted file mode 100644 index 0ff6f472..00000000 --- a/app/controllers/api/v1/maps_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class MapsController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/synapses_controller.rb b/app/controllers/api/v1/synapses_controller.rb deleted file mode 100644 index 32522e52..00000000 --- a/app/controllers/api/v1/synapses_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class SynapsesController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/tokens_controller.rb b/app/controllers/api/v1/tokens_controller.rb deleted file mode 100644 index 9df2094a..00000000 --- a/app/controllers/api/v1/tokens_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class TokensController < DeprecatedController - end - end -end diff --git a/app/controllers/api/v1/topics_controller.rb b/app/controllers/api/v1/topics_controller.rb deleted file mode 100644 index d316bfa8..00000000 --- a/app/controllers/api/v1/topics_controller.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true -module Api - module V1 - class TopicsController < DeprecatedController - end - end -end diff --git a/config/routes.rb b/config/routes.rb index 62728fe7..b5078d86 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -80,16 +80,8 @@ Metamaps::Application.routes.draw do match '*path', to: 'restful#catch_404', via: :all end namespace :v1, path: '/v1' do - # api v1 routes all lead to a deprecation error method - # see app/controllers/api/v1/deprecated_controller.rb - resources :maps, only: [:create, :show, :update, :destroy] - resources :synapses, only: [:create, :show, :update, :destroy] - resources :topics, only: [:create, :show, :update, :destroy] - resources :mappings, only: [:create, :show, :update, :destroy] - resources :tokens, only: [:create, :destroy] do - get :my_tokens, on: :collection - end - match '*path', to: 'deprecated#method_missing', via: :all + root to: 'deprecated#deprecated', via: :all + match '*path', to: 'deprecated#deprecated', via: :all end match '*path', to: 'v2/restful#catch_404', via: :all end From fe1c57b458ce6fb04b1821e098f7f304b1f47a6b Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sat, 8 Oct 2016 16:42:34 +0800 Subject: [PATCH 200/306] further updates - make Enter update bip fields whaaat --- app/assets/javascripts/lib/jquery.purr.js | 180 ---------------------- app/assets/stylesheets/base.css.erb | 1 + frontend/src/Metamaps/Map/InfoBox.js | 7 + frontend/src/Metamaps/SynapseCard.js | 6 + frontend/src/Metamaps/TopicCard.js | 13 ++ 5 files changed, 27 insertions(+), 180 deletions(-) delete mode 100644 app/assets/javascripts/lib/jquery.purr.js diff --git a/app/assets/javascripts/lib/jquery.purr.js b/app/assets/javascripts/lib/jquery.purr.js deleted file mode 100644 index 1972165b..00000000 --- a/app/assets/javascripts/lib/jquery.purr.js +++ /dev/null @@ -1,180 +0,0 @@ -/** - * jquery.purr.js - * Copyright (c) 2008 Net Perspective (net-perspective.com) - * Licensed under the MIT License (http://www.opensource.org/licenses/mit-license.php) - * - * @author R.A. Ray - * @projectDescription jQuery plugin for dynamically displaying unobtrusive messages in the browser. Mimics the behavior of the MacOS program "Growl." - * @version 0.1.0 - * - * @requires jquery.js (tested with 1.2.6) - * - * @param fadeInSpeed int - Duration of fade in animation in miliseconds - * default: 500 - * @param fadeOutSpeed int - Duration of fade out animationin miliseconds - default: 500 - * @param removeTimer int - Timeout, in miliseconds, before notice is removed once it is the top non-sticky notice in the list - default: 4000 - * @param isSticky bool - Whether the notice should fade out on its own or wait to be manually closed - default: false - * @param usingTransparentPNG bool - Whether or not the notice is using transparent .png images in its styling - default: false - */ - -( function( $ ) { - - $.purr = function ( notice, options ) - { - // Convert notice to a jQuery object - notice = $( notice ); - - // Add a class to denote the notice as not sticky - if ( !options.isSticky ) - { - notice.addClass( 'not-sticky' ); - }; - - // Get the container element from the page - var cont = document.getElementById( 'purr-container' ); - - // If the container doesn't yet exist, we need to create it - if ( !cont ) - { - cont = '<div id="purr-container"></div>'; - } - - // Convert cont to a jQuery object - cont = $( cont ); - - // Add the container to the page - $( 'body' ).append( cont ); - - notify(); - - function notify () - { - // Set up the close button - var close = document.createElement( 'a' ); - $( close ).attr( - { - className: 'close', - href: '#close', - innerHTML: 'Close' - } - ) - .appendTo( notice ) - .click( function () - { - removeNotice(); - - return false; - } - ); - - // Add the notice to the page and keep it hidden initially - notice.appendTo( cont ) - .hide(); - - if ( jQuery.browser.msie && options.usingTransparentPNG ) - { - // IE7 and earlier can't handle the combination of opacity and transparent pngs, so if we're using transparent pngs in our - // notice style, we'll just skip the fading in. - notice.show(); - } - else - { - //Fade in the notice we just added - notice.fadeIn( options.fadeInSpeed ); - } - - // Set up the removal interval for the added notice if that notice is not a sticky - if ( !options.isSticky ) - { - var topSpotInt = setInterval( function () - { - // Check to see if our notice is the first non-sticky notice in the list - if ( notice.prevAll( '.not-sticky' ).length == 0 ) - { - // Stop checking once the condition is met - clearInterval( topSpotInt ); - - // Call the close action after the timeout set in options - setTimeout( function () - { - removeNotice(); - }, options.removeTimer - ); - } - }, 200 ); - } - } - - function removeNotice () - { - // IE7 and earlier can't handle the combination of opacity and transparent pngs, so if we're using transparent pngs in our - // notice style, we'll just skip the fading out. - if ( jQuery.browser.msie && options.usingTransparentPNG ) - { - notice.css( { opacity: 0 } ) - .animate( - { - height: '0px' - }, - { - duration: options.fadeOutSpeed, - complete: function () - { - notice.remove(); - } - } - ); - } - else - { - // Fade the object out before reducing its height to produce the sliding effect - notice.animate( - { - opacity: '0' - }, - { - duration: options.fadeOutSpeed, - complete: function () - { - notice.animate( - { - height: '0px' - }, - { - duration: options.fadeOutSpeed, - complete: function () - { - notice.remove(); - } - } - ); - } - } - ); - } - }; - }; - - $.fn.purr = function ( options ) - { - options = options || {}; - options.fadeInSpeed = options.fadeInSpeed || 500; - options.fadeOutSpeed = options.fadeOutSpeed || 500; - options.removeTimer = options.removeTimer || 4000; - options.isSticky = options.isSticky || false; - options.usingTransparentPNG = options.usingTransparentPNG || false; - - this.each( function() - { - new $.purr( this, options ); - } - ); - - return this; - }; -})( jQuery ); - diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 1e5d68e8..6cfb6b57 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -143,6 +143,7 @@ margin-top:5px; } +.CardOnGraph .desc ol, .CardOnGraph .desc ul { margin-left: 1em; diff --git a/frontend/src/Metamaps/Map/InfoBox.js b/frontend/src/Metamaps/Map/InfoBox.js index ddfd72c3..79fa6c4d 100644 --- a/frontend/src/Metamaps/Map/InfoBox.js +++ b/frontend/src/Metamaps/Map/InfoBox.js @@ -173,6 +173,13 @@ const InfoBox = { Active.Map.trigger('saved') }) + $('.mapInfoDesc .best_in_place_desc, .mapInfoName .best_in_place_name').unbind('keypress').keypress(function(e) { + const ENTER = 13 + if (e.which === ENTER) { + $(this).data('bestInPlaceEditor').update() + } + }) + $('.yourMap .mapPermission').unbind().click(self.onPermissionClick) // .yourMap in the unbind/bind is just a namespace for the events // not a reference to the class .yourMap on the .mapInfoBox diff --git a/frontend/src/Metamaps/SynapseCard.js b/frontend/src/Metamaps/SynapseCard.js index b1810dac..303b98cf 100644 --- a/frontend/src/Metamaps/SynapseCard.js +++ b/frontend/src/Metamaps/SynapseCard.js @@ -97,6 +97,12 @@ const SynapseCard = { } } + $('#edit_synapse_desc').keypress(function (e) { + const ENTER = 13 + if (e.which === ENTER) { + $(this).data('bestInPlaceEditor').update() + } + }) $('#edit_synapse_desc').bind('ajax:success', function () { var desc = $(this).html() if (desc == data_nil) { diff --git a/frontend/src/Metamaps/TopicCard.js b/frontend/src/Metamaps/TopicCard.js index f74cf18f..0956c07b 100644 --- a/frontend/src/Metamaps/TopicCard.js +++ b/frontend/src/Metamaps/TopicCard.js @@ -265,6 +265,12 @@ const TopicCard = { 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 () { @@ -284,6 +290,13 @@ const TopicCard = { 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) { From ba9e26bc05384a03e711dc5d443621138070a916 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 9 Oct 2016 10:20:17 +0800 Subject: [PATCH 201/306] enable xss filtering and smart quote replacement in markdown --- frontend/src/Metamaps/Util.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index f1f8b39c..2e21f4e5 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -123,7 +123,9 @@ const Util = { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) }, mdToHTML: text => { - return new HtmlRenderer().render(new Parser().parse(text)) + // use safe: true to filter xss + return new HtmlRenderer({ safe: true, smart: true }) + .render(new Parser().parse(text)) } } From 8b1d85c3ca3b964109cf83b0b94d742cbb3e85b2 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Sun, 9 Oct 2016 10:24:13 +0800 Subject: [PATCH 202/306] actually the smart option is dumb --- frontend/src/Metamaps/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 2e21f4e5..32730a6f 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -124,7 +124,7 @@ const Util = { }, mdToHTML: text => { // use safe: true to filter xss - return new HtmlRenderer({ safe: true, smart: true }) + return new HtmlRenderer({ safe: true }) .render(new Parser().parse(text)) } } From 6e6d33abbe4cfbd305269ccd3b8c812a8b963a3f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 10 Oct 2016 12:12:42 +0800 Subject: [PATCH 203/306] fix screenshot no file error --- frontend/src/Metamaps/Map/index.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 43f04a30..48be9def 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -326,8 +326,6 @@ const Map = { node.visited = !T }) - var imageData = canvas.canvas.toDataURL() - var map = Active.Map var today = new Date() @@ -347,7 +345,7 @@ const Map = { var downloadMessage = outdent` Captured map screenshot! - <a href="${imageData.encodedImage}" download="${filename}">DOWNLOAD</a>` + <a href="${canvas.canvas.toDataURL()}" download="${filename}">DOWNLOAD</a>` GlobalUI.notifyUser(downloadMessage) canvas.canvas.toBlob(imageBlob => { From 858ca66d69ed9e43f4d3a4ce6be288508cf4efa1 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Mon, 10 Oct 2016 17:20:43 +0800 Subject: [PATCH 204/306] eslint updates --- frontend/src/Metamaps/Listeners.js | 42 +++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/frontend/src/Metamaps/Listeners.js b/frontend/src/Metamaps/Listeners.js index ce14dc9f..00238e90 100644 --- a/frontend/src/Metamaps/Listeners.js +++ b/frontend/src/Metamaps/Listeners.js @@ -123,36 +123,36 @@ const Listeners = { }) $(window).resize(function () { - if (Visualize && Visualize.mGraph){ - //Find the current canvas scale and map-coordinate at the centre of the user's screen - var canvas = Visualize.mGraph.canvas, - scaleX = canvas.scaleOffsetX, - scaleY = canvas.scaleOffsetY, - centrePixX = canvas.canvases[0].size.width / 2, - centrePixY = canvas.canvases[0].size.height / 2, - centreCoords = Util.pixelsToCoords({x:centrePixX ,y:centrePixY}); - - //Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 + if (Visualize && Visualize.mGraph) { + // Find the current canvas scale and map-coordinate at the centre of the user's screen + let canvas = Visualize.mGraph.canvas + const scaleX = canvas.scaleOffsetX + const scaleY = canvas.scaleOffsetY + const centrePixX = canvas.canvases[0].size.width / 2 + const centrePixY = canvas.canvases[0].size.height / 2 + const centreCoords = Util.pixelsToCoords({ x: centrePixX, y: centrePixY }) + + // Resize the canvas to fill the new window size. Based on how JIT works, this also resets the map back to scale 1 and tranlations = 0 canvas.resize($(window).width(), $(window).height()) - - //Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen - canvas.scale(scaleX,scaleY); - var newCentrePixX = canvas.canvases[0].size.width / 2, - newCentrePixY = canvas.canvases[0].size.height / 2, - newCentreCoords = Util.pixelsToCoords({x:newCentrePixX ,y:newCentrePixY}); - - canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y); + + // Return the map to the original scale, and then put the previous central map-coordinate back to the centre of user's newly resized screen + canvas.scale(scaleX, scaleY) + const newCentrePixX = canvas.canvases[0].size.width / 2 + const newCentrePixY = canvas.canvases[0].size.height / 2 + const newCentreCoords = Util.pixelsToCoords({ x: newCentrePixX, y: newCentrePixY }) + + canvas.translate(newCentreCoords.x - centreCoords.x, newCentreCoords.y - centreCoords.y) } - + if (Active.Map && Realtime.inConversation) Realtime.positionVideos() Mobile.resizeTitle() }) }, - centerAndReveal: function(nodes, opts) { + centerAndReveal: function (nodes, opts) { if (nodes.length < 1) return var node = nodes[nodes.length - 1] if (opts.center && opts.reveal) { - Topic.centerOn(node.id, function() { + Topic.centerOn(node.id, function () { Topic.fetchRelatives(nodes) }) } else if (opts.center) { From 3051723bcf27ad0adcfbe7864c1e166da8d6efa9 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Wed, 12 Oct 2016 00:08:31 +0800 Subject: [PATCH 205/306] [WIP] add markdown getting started page to api docs (#752) * add markdown getting started page to api docs. TODO section 3 * Update getting-started.md --- doc/api/api.raml | 6 +++ doc/api/pages/getting-started.md | 81 ++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 doc/api/pages/getting-started.md diff --git a/doc/api/api.raml b/doc/api/api.raml index 6ffa29f1..b45fe0c5 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -4,6 +4,12 @@ title: Metamaps version: v2.0 baseUri: https://metamaps.cc/api/v2 mediaType: application/json +protocols: [ HTTPS ] +documentation: + - title: Getting Started + content: !include pages/getting-started.md + - title: Endpoints + content: "" securitySchemes: oauth_2_0: !include securitySchemes/oauth_2_0.raml diff --git a/doc/api/pages/getting-started.md b/doc/api/pages/getting-started.md new file mode 100644 index 00000000..889168a4 --- /dev/null +++ b/doc/api/pages/getting-started.md @@ -0,0 +1,81 @@ +[Skip ahead to the endpoints.](#endpoints) + +There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. + +### 1. Cookie-based authentication + +One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI. + +To run a POST or DELETE request, you can use the Fetch API. See the example in the next section. + +### 2. Token-based authentication + +If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means. + +To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. + +``` +fetch('/api/v2/tokens', { + method: 'GET', + credentials: 'same-origin' // needed to use the cookie-based auth +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: + +``` +fetch('/api/v2/tokens', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. + +### 3. OAuth 2 Authentication + +We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) + +The first step is to register your client app. + +#### Registering the client + +Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now). + +#### Requesting authorization + +To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL: + +``` +http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code +``` + +Once you are there, you should sign in and click on `Authorize`. +You will then see a response that contains your "authorization code", which you need to exchange for an access token. + +#### Requesting the access token + +To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch` + +```javascript +fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) + +# The response will be like +{ + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" +} +``` + +You can now make requests to the API with the access token returned. From 62c489cba7ad6c1003b33d6c8eb67cfd36d7065f Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 13 Oct 2016 00:22:38 +0800 Subject: [PATCH 206/306] suggesting api doc updates (#756) --- doc/api/api.raml | 2 +- doc/api/pages/getting-started.md | 15 ++++++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/doc/api/api.raml b/doc/api/api.raml index b45fe0c5..4473a6dd 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -1,7 +1,7 @@ #%RAML 1.0 --- title: Metamaps -version: v2.0 +version: 2.0 baseUri: https://metamaps.cc/api/v2 mediaType: application/json protocols: [ HTTPS ] diff --git a/doc/api/pages/getting-started.md b/doc/api/pages/getting-started.md index 889168a4..c620e888 100644 --- a/doc/api/pages/getting-started.md +++ b/doc/api/pages/getting-started.md @@ -1,6 +1,6 @@ [Skip ahead to the endpoints.](#endpoints) -There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. +There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. ### 1. Cookie-based authentication @@ -14,7 +14,7 @@ If you are logged into the API via another means, you can create a token. Once y To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. -``` +```javascript fetch('/api/v2/tokens', { method: 'GET', credentials: 'same-origin' // needed to use the cookie-based auth @@ -25,13 +25,15 @@ fetch('/api/v2/tokens', { If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: -``` +```javascript fetch('/api/v2/tokens', { method: 'POST', credentials: 'same-origin' }).then(response => { return response.json() -}).then(console.log).catch(console.error) +}).then(payload => { + console.log(payload) +}).catch(console.error) ``` `payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. @@ -68,8 +70,11 @@ fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET }).then(response => { return response.json() }).then(console.log).catch(console.error) +``` -# The response will be like +The response will look like + +```json { "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", "token_type": "bearer", From 7eae8deacbaed64394dd3071274f46cd1632da20 Mon Sep 17 00:00:00 2001 From: Devin Howard <devin@callysto.com> Date: Thu, 13 Oct 2016 01:54:43 +0800 Subject: [PATCH 207/306] revamp HTML template a bit for api docs (#757) * my_tokens endpoint moved to normal index * remove secured_by from metacodes/users * ch ch ch changes * mess with template * fix securedBy * convenience open * gross authentication notes at the top of every endpoint * better ordering * move login tutorials into security tab * oauth tutorial * getting closer * remove unneeded Endpoints header * ok looks OK --- .env.swp | Bin 0 -> 12288 bytes app/controllers/api/v2/tokens_controller.rb | 6 - bin/build-apidocs.sh | 10 +- config/routes.rb | 4 +- doc/api/api.raml | 6 +- doc/api/apis/metacodes.raml | 2 + doc/api/apis/tokens.raml | 17 +- doc/api/apis/users.raml | 18 +- doc/api/pages/cookie_tutorial.md | 3 + doc/api/pages/getting-started.md | 86 +----- doc/api/pages/oauth_2_0_tutorial.md | 41 +++ doc/api/pages/token_tutorial.md | 25 ++ doc/api/securitySchemes/cookie.raml | 3 + doc/api/securitySchemes/oauth_2_0.raml | 3 +- doc/api/securitySchemes/token.raml | 3 + doc/api/templates/item.nunjucks | 61 ++++ doc/api/templates/resource.nunjucks | 314 ++++++++++++++++++++ doc/api/templates/template.nunjucks | 232 +++++++++++++++ 18 files changed, 718 insertions(+), 116 deletions(-) create mode 100644 .env.swp create mode 100644 doc/api/pages/cookie_tutorial.md create mode 100644 doc/api/pages/oauth_2_0_tutorial.md create mode 100644 doc/api/pages/token_tutorial.md create mode 100644 doc/api/securitySchemes/cookie.raml create mode 100644 doc/api/securitySchemes/token.raml create mode 100644 doc/api/templates/item.nunjucks create mode 100644 doc/api/templates/resource.nunjucks create mode 100644 doc/api/templates/template.nunjucks diff --git a/.env.swp b/.env.swp new file mode 100644 index 0000000000000000000000000000000000000000..e737a79b5a0d3866f6b3955a4495bd58f83cdc9c GIT binary patch literal 12288 zcmeI2&yU+g6vthJprw>naIc2ukX_Ma$G`0r2C|9U4V%QPIN7CiD>ELCH_>|SU^{IW zDv;VDapTwvN5qXA5>opQ@CP6+2!VP++&FXLIh&;WLo6WmK&7$tjpH}(y?OKTjI*1J zk2jiaq*oOIjtheDrTfF?#Z!Xt?el^#_Q!EPE*efgXcEUsnv9Yt8E2JUmXlpF%7P@P zv?7^`s)+I=!6*9(mu@BLPBtQxEmc{3^W@yTl@s6uo)&>~f-H%+$X2@aI(qHZ{?kg# z^Kb&304Kl+Z~~kFC%_4C0-V4zKp-ET5<Z1GpPSY?`5pk?!&d;4V|Jci@)IY(32*|O z04Kl+Z~~kFC%_4C0-OLRzzIBq1V|tV4_*|6d#9mz`2BzO|NrCjg77`)8_)yL$Dofu z70~am2*M-KL(oOg{c{ixQbA`yXFz|vEC@e>z5{&?`U>;~=r)Lf)<Gu70R4GZ5Pk;z z1bPH|2>Kk<1ziTc0Q%)6*pmSI4D<m=2b~7}dIs#Ed!W0ZHs}KAJm@zV(|u4ew!6jn z_`wNq0-OLRzzJ{y{~H3O5<+M@Wg}Ecn^`hIm34G!;q5mqyR)&luvHCNP6t~HcebuC zR<GA8?|V1is4i?R-r2gbcnRH^MkWw5Z7hsglIEx~MsJ~<?T(O8LSd33g2wUiLzJWe zOId)TVV0vLM6e4H-6UzY4Ev%E*YclADT2dc9H4x_5Y&$7I89lcM|)@(qjAQHQ_)67 zBe)#K+h`fVpF_7tMX}xf$~Et5-E&%vcCFQN>RzkaZuY#ZU1zhm>RqYTdrsF9VW;Ll z1hi}KS(+hfrU_fUPWH8ANyVmOh)4ZStUxnPbf5HZVpSDY`6Nf^+%RMXYe$_{?b>>Q z!C86NuJ@)G{(k%u_bDal+jCn^&$V<#)sGGTpzAy#K33(#?Mm0Sr@2(sl=NeRo_IAo zaEtbP6b((0jt!>gtl1q)2^HDEAvK9)sCpoV7#kR?L}tFBnbNV`y?)2GFe+s%M|pz8 z;)TV;j~KdIhbf)qFjY}H_V>^>Nqw@-5KW?p(R`T1D9guTC=`=^di&j0t-j{j9dFZK zS#_K>uitG!YYAlxrYpNm*=Q0NC;2cOQc_%GMV|y^5urOUacQz!OxRn@A0)|6(fcfS zySMK3UAx<<we2Hjux{AhBgS>7+dFEix$dUZZ5%N-oOZ3*5lRQluH&uQ*Pa+^o37{D z^{(CXYW2G9y2qQJ*uCb#9%OH&Utfd3{lXz<3EdhD=>SmzYjrOfm!k;93?39fVsa-2 zY!{736HjOcKVVHl1`9l5<R&W?H$?goiFYo;qb6cmHp!DR6tGd0>=g(hXc%WXi7C4b z@8%DH=`dh;-b&52t+J%))F6fuYO<-pVv#~c^{I?S%t#P~Skj3f5??h!s;g$8F-bOQ zF|9&FWy}OifJI*<KEx|D023v8Xi!rUWrAsFih&#$fh?;y#HJ=nGLtBw)Tb1ik`k6@ z3$tNwG+kH>`<$Ubs~qYgRWzNck`_vGpoxkv``9ovjgmmo4TGv!@>u}0n--O0jp;ID z8Z{wG^))GEVyFeG6cXJhhG=TOEMplKMj(Z<51A#M5v*&bOjU_d21WT;)G>zq&|7(q zZCV`6l$PCv8aoZUYn5|GcHfz8MXypiAIsFQVPC2BTfJf-Sml}D{+9Ja+u5tsga>z; zRfowuMc1yidd+qLq?q1Td64I$Y<X$v-{o0WF%}QhzcQJC^C~cw<~=o5ykfOjWmS<6 mSyvqwtWiQqG)S`iP{4F8S!G!i4+YH@sZ}0941C~bW#KPuW{ao* literal 0 HcmV?d00001 diff --git a/app/controllers/api/v2/tokens_controller.rb b/app/controllers/api/v2/tokens_controller.rb index 1170945f..e0474e25 100644 --- a/app/controllers/api/v2/tokens_controller.rb +++ b/app/controllers/api/v2/tokens_controller.rb @@ -18,12 +18,6 @@ module Api create_action respond_with_resource end - - def my_tokens - authorize resource_class - instantiate_collection - respond_with_collection - end end end end diff --git a/bin/build-apidocs.sh b/bin/build-apidocs.sh index be85012c..28931b2f 100755 --- a/bin/build-apidocs.sh +++ b/bin/build-apidocs.sh @@ -2,8 +2,16 @@ # Note: you need to run `npm install` before using this script or raml2html won't be installed +OLD_DIR=$(pwd) +cd $(dirname $0)/.. + if [[ ! -x ./node_modules/.bin/raml2html ]]; then npm install fi -./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html +./node_modules/.bin/raml2html -i ./doc/api/api.raml -o ./public/api/index.html -t doc/api/templates/template.nunjucks +if [[ -x $(which open) ]]; then + open public/api/index.html +fi + +cd $OLD_DIR diff --git a/config/routes.rb b/config/routes.rb index b5078d86..79db8599 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -70,9 +70,7 @@ Metamaps::Application.routes.draw do delete :stars, to: 'stars#destroy', on: :member end resources :synapses, only: [:index, :create, :show, :update, :destroy] - resources :tokens, only: [:create, :destroy] do - get :my_tokens, on: :collection - end + resources :tokens, only: [:index, :create, :destroy] resources :topics, only: [:index, :create, :show, :update, :destroy] resources :users, only: [:index, :show] do get :current, on: :collection diff --git a/doc/api/api.raml b/doc/api/api.raml index 4473a6dd..8703aae9 100644 --- a/doc/api/api.raml +++ b/doc/api/api.raml @@ -8,12 +8,12 @@ protocols: [ HTTPS ] documentation: - title: Getting Started content: !include pages/getting-started.md - - title: Endpoints - content: "" securitySchemes: + cookie: !include securitySchemes/cookie.raml + token: !include securitySchemes/token.raml oauth_2_0: !include securitySchemes/oauth_2_0.raml -securedBy: [ oauth_2_0 ] +securedBy: [ cookie, token, oauth_2_0 ] traits: pageable: !include traits/pageable.raml diff --git a/doc/api/apis/metacodes.raml b/doc/api/apis/metacodes.raml index 37cbd17a..877e1835 100644 --- a/doc/api/apis/metacodes.raml +++ b/doc/api/apis/metacodes.raml @@ -1,4 +1,5 @@ #type: collection +securedBy: [ null, cookie, token, oauth_2_0 ] get: is: [ searchable: { searchFields: "name" }, orderable, pageable ] responses: @@ -7,6 +8,7 @@ get: application/json: example: !include ../examples/metacodes.json /{id}: + securedBy: [ null, cookie, token, oauth_2_0 ] #type: item get: responses: diff --git a/doc/api/apis/tokens.raml b/doc/api/apis/tokens.raml index ef7a8379..5d4eb191 100644 --- a/doc/api/apis/tokens.raml +++ b/doc/api/apis/tokens.raml @@ -1,4 +1,13 @@ #type: collection +get: + description: | + A list of the current user's tokens. + is: [ searchable: { searchFields: description }, pageable, orderable ] + responses: + 200: + body: + application/json: + example: !include ../examples/tokens.json post: body: application/json: @@ -11,14 +20,6 @@ post: body: application/json: example: !include ../examples/token.json -/my_tokens: - get: - is: [ searchable: { searchFields: description }, pageable, orderable ] - responses: - 200: - body: - application/json: - example: !include ../examples/tokens.json /{id}: #type: item delete: diff --git a/doc/api/apis/users.raml b/doc/api/apis/users.raml index 7f421059..1d37bc0d 100644 --- a/doc/api/apis/users.raml +++ b/doc/api/apis/users.raml @@ -1,4 +1,5 @@ #type: collection +securedBy: [ null, cookie, token, oauth_2_0 ] get: is: [ searchable: { searchFields: "name" }, orderable, pageable ] responses: @@ -6,6 +7,15 @@ get: body: application/json: example: !include ../examples/users.json +/{id}: + #type: item + securedBy: [ null, cookie, token, oauth_2_0 ] + get: + responses: + 200: + body: + application/json: + example: !include ../examples/user.json /current: #type: item get: @@ -14,11 +24,3 @@ get: body: application/json: example: !include ../examples/current_user.json -/{id}: - #type: item - get: - responses: - 200: - body: - application/json: - example: !include ../examples/user.json diff --git a/doc/api/pages/cookie_tutorial.md b/doc/api/pages/cookie_tutorial.md new file mode 100644 index 00000000..9481b919 --- /dev/null +++ b/doc/api/pages/cookie_tutorial.md @@ -0,0 +1,3 @@ +One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI. + +To run a POST or DELETE request, you can use the Fetch API. See the example in the next section. diff --git a/doc/api/pages/getting-started.md b/doc/api/pages/getting-started.md index c620e888..429e6971 100644 --- a/doc/api/pages/getting-started.md +++ b/doc/api/pages/getting-started.md @@ -1,86 +1,2 @@ -[Skip ahead to the endpoints.](#endpoints) +There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. Check the security tab of any of the endpoints above for instructions on logging in. -There are three ways to log in: cookie-based authentication, token-based authentication, or OAuth 2. If you're testing the API or making simple scripts, cookie-based or token-based is the best. If you're developing and app and want users to be able to log into Metamaps inside your app, you'll be able to use the OAuth 2 mechanism. - -### 1. Cookie-based authentication - -One way to access the API is through your browser. Log into metamaps.cc normally, then browse manually to https://metamaps.cc/api/v2/user/current. You should see a JSON description of your own user object in the database. You can browse any GET endpoint by simply going to that URL and appending query parameters in the URI. - -To run a POST or DELETE request, you can use the Fetch API. See the example in the next section. - -### 2. Token-based authentication - -If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means. - -To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. - -```javascript -fetch('/api/v2/tokens', { - method: 'GET', - credentials: 'same-origin' // needed to use the cookie-based auth -}).then(response => { - return response.json() -}).then(console.log).catch(console.error) -``` - -If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: - -```javascript -fetch('/api/v2/tokens', { - method: 'POST', - credentials: 'same-origin' -}).then(response => { - return response.json() -}).then(payload => { - console.log(payload) -}).catch(console.error) -``` - -`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. - -### 3. OAuth 2 Authentication - -We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) - -The first step is to register your client app. - -#### Registering the client - -Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now). - -#### Requesting authorization - -To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL: - -``` -http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code -``` - -Once you are there, you should sign in and click on `Authorize`. -You will then see a response that contains your "authorization code", which you need to exchange for an access token. - -#### Requesting the access token - -To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch` - -```javascript -fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', { - method: 'POST', - credentials: 'same-origin' -}).then(response => { - return response.json() -}).then(console.log).catch(console.error) -``` - -The response will look like - -```json -{ - "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", - "token_type": "bearer", - "expires_in": 7200, - "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" -} -``` - -You can now make requests to the API with the access token returned. diff --git a/doc/api/pages/oauth_2_0_tutorial.md b/doc/api/pages/oauth_2_0_tutorial.md new file mode 100644 index 00000000..e419a621 --- /dev/null +++ b/doc/api/pages/oauth_2_0_tutorial.md @@ -0,0 +1,41 @@ +We use a flow for Oauth 2 authentication called Authorization Code. It basically consists of an exchange of an `authorization` token for an `access token`. For more detailed info, check out the [RFC spec here](http://tools.ietf.org/html/rfc6749#section-4.1) + +The first step is to register your client app. + +#### Registering the client + +Set up a new client in `/oauth/applications/new`. For testing purposes, you should fill in the redirect URI field with `urn:ietf:wg:oauth:2.0:oob`. This will tell it to display the authorization code instead of redirecting to a client application (that you don't have now). + +#### Requesting authorization + +To request the authorization token, you should visit the `/oauth/authorize` endpoint. You can do that either by clicking in the link to the authorization page in the app details or by visiting manually the URL: + +``` +http://metamaps.cc/oauth/authorize?client_id=YOUR_CLIENT_ID&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code +``` + +Once you are there, you should sign in and click on `Authorize`. +You will then see a response that contains your "authorization code", which you need to exchange for an access token. + +#### Requesting the access token + +To request the access token, you should use the returned code and exchange it for an access token. To do that you can use any HTTP client. Here's an example with `fetch` + +```javascript +fetch('https://metamaps.cc/oauth/token?client_id=THE_ID&client_secret=THE_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=urn:ietf:wg:oauth:2.0:oob', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) + +# The response will be like +{ + "access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54", + "token_type": "bearer", + "expires_in": 7200, + "refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1" +} +``` + +You can now make requests to the API with the access token returned. diff --git a/doc/api/pages/token_tutorial.md b/doc/api/pages/token_tutorial.md new file mode 100644 index 00000000..3a46582a --- /dev/null +++ b/doc/api/pages/token_tutorial.md @@ -0,0 +1,25 @@ +If you are logged into the API via another means, you can create a token. Once you have this token, you can append it to a request. For example, opening a private window in your browser and browsing to `https://metamaps.cc/api/v2/user/current?token=...token here...` would show you your current user, even without logging in by another means. + +To get a list of your current tokens, you can log in using cookie-based authentication and run the following fetch request in your browser console (assuming the current tab is on some page within the `metamaps.cc` website. + +``` +fetch('/api/v2/tokens', { + method: 'GET', + credentials: 'same-origin' // needed to use the cookie-based auth +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +If this is your first time accessing the API, this list wil be empty. You can create a token using a similar method: + +``` +fetch('/api/v2/tokens', { + method: 'POST', + credentials: 'same-origin' +}).then(response => { + return response.json() +}).then(console.log).catch(console.error) +``` + +`payload.data.token` will contain a string which you can use to append to requests to access the API from anywhere. diff --git a/doc/api/securitySchemes/cookie.raml b/doc/api/securitySchemes/cookie.raml new file mode 100644 index 00000000..fb1041b1 --- /dev/null +++ b/doc/api/securitySchemes/cookie.raml @@ -0,0 +1,3 @@ +description: !include ../pages/cookie_tutorial.md +type: x-cookie +displayName: Secured by cookie-based authentication diff --git a/doc/api/securitySchemes/oauth_2_0.raml b/doc/api/securitySchemes/oauth_2_0.raml index b271e03a..3a84e293 100644 --- a/doc/api/securitySchemes/oauth_2_0.raml +++ b/doc/api/securitySchemes/oauth_2_0.raml @@ -1,5 +1,4 @@ -description: | - OAuth 2.0 implementation +description: !include ../pages/oauth_2_0_tutorial.md type: OAuth 2.0 settings: authorizationUri: https://metamaps.cc/api/v2/oauth/authorize diff --git a/doc/api/securitySchemes/token.raml b/doc/api/securitySchemes/token.raml new file mode 100644 index 00000000..f83e1177 --- /dev/null +++ b/doc/api/securitySchemes/token.raml @@ -0,0 +1,3 @@ +description: !include ../pages/token_tutorial.md +type: x-token +displayName: Secured by token-based authentication diff --git a/doc/api/templates/item.nunjucks b/doc/api/templates/item.nunjucks new file mode 100644 index 00000000..044c24d9 --- /dev/null +++ b/doc/api/templates/item.nunjucks @@ -0,0 +1,61 @@ +<li> + {% if item.displayName %} + <strong>{{ item.displayName }}</strong>: + {% else %} + <strong>{{ item.key }}</strong>: + {% endif %} + + {% if not item.structuredValue %} + <em> + {%- if item.required -%}required {% endif -%} + ( + {%- if item.enum -%} + {%- if item.enum.length === 1 -%} + {{ item.enum.join(', ') }} + {%- else -%} + one of {{ item.enum.join(', ') }} + {%- endif -%} + {%- else -%} + {{ item.type }} + {%- endif -%} + + {%- if item.default or item.default == 0 or item.default == false %} - default: {{ item.default }}{%- endif -%} + {%- if item.repeat %} - repeat: {{ item.repeat }}{%- endif -%} + {%- if item.type == 'string' -%} + {%- if item.minLength or item.minLength == 0 %} - minLength: {{ item.minLength }}{%- endif -%} + {%- if item.maxLength or item.maxLength == 0 %} - maxLength: {{ item.maxLength }}{%- endif -%} + {%- else -%} + {%- if item.minimum or item.minimum == 0 %} - minimum: {{ item.minimum }}{%- endif -%} + {%- if item.maximum or item.maximum == 0 %} - maximum: {{ item.maximum }}{%- endif -%} + {%- endif -%} + {%- if item.pattern %} - pattern: {{ item.pattern }}{%- endif -%} + ) + </em> + {% endif %} + +{% markdown %} +{{ item.description }} +{% endmarkdown %} + +{# + {% if item.type %} + <p><strong>Type</strong>:</p> + <pre><code>{{ item.type | escape }}</code></pre> + {% endif %} +#} + + {% if item.examples.length %} + <p><strong>Examples</strong>:</p> + {% for example in item.examples %} + {% if item.type == 'string' %} + <pre>{{ example | escape }}</pre> + {% else %} + <pre><code>{{ example | escape }}</code></pre> + {% endif %} + {% endfor %} + {% endif %} + + {% if item.structuredValue %} + <pre><code>{{ item.structuredValue | dump }}</code></pre> + {% endif %} +</li> diff --git a/doc/api/templates/resource.nunjucks b/doc/api/templates/resource.nunjucks new file mode 100644 index 00000000..6bdaf2a6 --- /dev/null +++ b/doc/api/templates/resource.nunjucks @@ -0,0 +1,314 @@ +{% if (resource.methods or (resource.description and resource.parentUrl)) %} + <div class="panel panel-white"> + <div class="panel-heading"> + <h4 class="panel-title"> + <a class="collapsed" data-toggle="collapse" href="#panel_{{ resource.uniqueId }}"> + <span class="parent">{{ resource.parentUrl }}</span>{{ resource.relativeUri }} + </a> + + <span class="methods"> + {% for method in resource.methods %} + <a href="#{{ resource.uniqueId }}_{{ method.method }}"><!-- modal shown by hashchange event --> + <span class="badge badge_{{ method.method }}">{{ method.method }} + {% if method.securedBy.length %} + {% if method.securedBy | first == null %} + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + {% endif %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> + {% endif %} + </span> + </a> + {% endfor %} + </span> + </h4> + </div> + + <div id="panel_{{ resource.uniqueId }}" class="panel-collapse collapse"> + <div class="panel-body"> + {% if resource.parentUrl %} + {% if resource.description %} + <div class="resource-description"> +{% markdown %} +{{ resource.description }} +{% endmarkdown %} + </div> + {% endif %} + {% endif %} + + <div class="list-group"> + {% for method in resource.methods %} + <div onclick="window.location.href = '#{{ resource.uniqueId }}_{{ method.method }}'" class="list-group-item"> + <span class="badge badge_{{ method.method }}"> + {{ method.method }} + {% if method.securedBy.length %} + {% if method.securedBy | first == null %} + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + {% endif %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> + {% endif %} + </span> + <div class="method_description"> +{% markdown %} +{{ method.description}} +{% endmarkdown %} + </div> + <div class="clearfix"></div> + </div> + {% endfor %} + </div> + </div> + </div> + + {% for method in resource.methods %} + <div class="modal fade" tabindex="0" id="{{ resource.uniqueId }}_{{ method.method }}"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> + <h4 class="modal-title" id="myModalLabel"> + <span class="badge badge_{{ method.method }}"> + {{ method.method }} + {% if method.securedBy.length %} + {% if method.securedBy | first == null %} + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + {% endif %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> + {% endif %} + </span> + <span class="parent">{{ resource.parentUrl }}</span>{{ resource.relativeUri }} + </h4> + </div> + + <div class="modal-body"> + {% if method.description %} + <div class="alert alert-info"> +{% markdown %} +{{ method.description}} +{% endmarkdown %} + </div> + {% endif %} + + <!-- Nav tabs --> + <ul class="nav nav-tabs"> + {% if method.allUriParameters.length or method.queryString or method.queryParameters or method.headers or method.body %} + <li class="active"> + <a href="#{{ resource.uniqueId }}_{{ method.method }}_request" data-toggle="tab">Request</a> + </li> + {% endif %} + + {% if method.responses %} + <li{% + if not method.allUriParameters.length and not method.queryParameters + and not method.queryString + and not method.headers and not method.body + %} class="active"{% + endif + %}> + <a href="#{{ resource.uniqueId }}_{{ method.method }}_response" data-toggle="tab">Response</a> + </li> + {% endif %} + + {% if method.securedBy.length %} + <li> + <a href="#{{ resource.uniqueId }}_{{ method.method }}_securedby" data-toggle="tab">Security</a> + </li> + {% endif %} + </ul> + + <!-- Tab panes --> + <div class="tab-content"> + {% if method.allUriParameters.length or method.queryString or method.queryParameters or method.headers or method.body %} + <div class="tab-pane active" id="{{ resource.uniqueId }}_{{ method.method }}_request"> + {% if resource.allUriParameters.length %} + <h3>URI Parameters</h3> + <ul> + {% for item in resource.allUriParameters %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.annotations.length %} + <h3>Annotations</h3> + <ul> + {% for item in method.annotations %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in method.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.queryString and method.queryString.properties.length %} + <h3>Query String</h3> + <ul> + {% for item in method.queryString.properties %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.queryParameters.length %} + <h3>Query Parameters</h3> + <ul> + {% for item in method.queryParameters %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if method.body %} + <h3>Body</h3> + {% for b in method.body %} + <p><strong>Type: {{ b.key }}</strong></p> + +{# + {% if b.type %} + <p><strong>Type</strong>:</p> + <pre><code>{{ b.type | escape }}</code></pre> + {% endif %} +#} + + {% if b.properties.length %} + <strong>Properties</strong> + <ul> + {% for item in b.properties %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if b.examples.length %} + <p><strong>Examples</strong>:</p> + {% for example in b.examples %} + <pre><code>{{ example | escape }}</code></pre> + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + </div> + {% endif %} + + {% if method.responses %} + <div class="tab-pane{% + if not method.allUriParameters.length and not method.queryParameters.length + and not method.queryString + and not method.headers.length and not method.body.length + %} active{% + endif + %}" id="{{ resource.uniqueId }}_{{ method.method }}_response"> + {% for response in method.responses %} + <h2>HTTP status code <a href="http://httpstatus.es/{{ response.code }}" target="_blank">{{ response.code }}</a></h2> +{% markdown %} +{{ response.description}} +{% endmarkdown %} + + {% if response.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in response.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if response.body.length %} + <h3>Body</h3> + {% for b in response.body %} + <p><strong>Type: {{ b.key }}</strong></p> + +{# + {% if b.type %} + <p><strong>Type</strong>:</p> + <pre><code>{{ b.type | escape }}</code></pre> + {% endif %} +#} + + {% if b.properties.length %} + <strong>Properties</strong> + <ul> + {% for item in b.properties %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% if b.examples.length %} + <p><strong>Examples</strong>:</p> + {% for example in b.examples %} + <pre><code>{{ example | escape }}</code></pre> + {% endfor %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + </div> + {% endif %} + + {% if method.securedBy.length %} + <div class="tab-pane" id="{{ resource.uniqueId }}_{{ method.method }}_securedby"> + {% for securedBy in method.securedBy %} + {% if securedBy == null %} + <div class="alert alert-info"> + <span class="glyphicon glyphicon-transfer" title="Authentication not required"></span> + This route can be accessed anonymously.</h1> + </div> + {% else %} + {% set securityScheme = securitySchemes[securedBy] %} + <div class="alert alert-warning"> + {% set securedByScopes = renderSecuredBy(securedBy) %} + <span class="glyphicon glyphicon-lock" title="Authentication required"></span> Secured by {{ securedByScopes }} + {% set securityScheme = securitySchemes[securedBy] %} + {% if securityScheme.description %} +{% markdown %} +{{ securityScheme.description }} +{% endmarkdown %} + {% endif %} + + {% if securityScheme.describedBy.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in securityScheme.describedBy.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + + {% for response in securityScheme.describedBy.responses.length %} + <h2>HTTP status code <a href="http://httpstatus.es/{{ response.code }}" target="_blank">{{ response.code }}</a></h2> +{% markdown %} +{{ response.description}} +{% endmarkdown %} + {% if response.headers.length %} + <h3>Headers</h3> + <ul> + {% for item in response.headers %} + {% include "./item.nunjucks" %} + {% endfor %} + </ul> + {% endif %} + {% endfor %} + </div> + {% endif %} + {% endfor %} + </div> + {% endif %} + </div> + </div> + </div> + </div> + </div> + {% endfor %} + </div> +{% endif %} + +{% for resource in resource.resources %} + {% include "./resource.nunjucks" %} +{% endfor %} diff --git a/doc/api/templates/template.nunjucks b/doc/api/templates/template.nunjucks new file mode 100644 index 00000000..6911f44e --- /dev/null +++ b/doc/api/templates/template.nunjucks @@ -0,0 +1,232 @@ +<!DOCTYPE HTML> +<html> + <head> + <title>{{ title }} API documentation + + + + + + + + + + + + + + + + +
+
+
+ + + {% for resource in resources %} +
+
+

{% if resource.displayName %}{{ resource.displayName}}{% else %}{{ resource.relativeUri }}{% endif %}

+
+ +
+ {% if resource.description %} +
+{% markdown %} +{{ resource.description }} +{% endmarkdown %} +
+ {% endif %} + +
+ {% include "./resource.nunjucks" %} +
+
+
+ {% endfor %} + + {% for chapter in documentation %} +

{{ chapter.title }}

+{% markdown %} +{{ chapter.content }} +{% endmarkdown %} + {% endfor %} +
+ + +
+
+ + From b2a4acc99d7b5524e88bae6731946754546bdf40 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 14:23:55 +0800 Subject: [PATCH 208/306] make default category explicit in import.js --- frontend/src/Metamaps/Import.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 868843bf..6f25d21b 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -333,8 +333,8 @@ const Import = { } // if var synapse = new Metamaps.Backbone.Synapse({ - desc: desc || "", - category: category, + desc: desc || '' + category: category || 'from-to', permission: permission, topic1_id: topic1.id, topic2_id: topic2.id From 6e03132f1b16d6b0109d7f19c65b916778f4015f Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 14:51:58 +0800 Subject: [PATCH 209/306] fix spec --- spec/api/v2/tokens_api_spec.rb | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/spec/api/v2/tokens_api_spec.rb b/spec/api/v2/tokens_api_spec.rb index cd424ba0..c9ec77ed 100644 --- a/spec/api/v2/tokens_api_spec.rb +++ b/spec/api/v2/tokens_api_spec.rb @@ -6,13 +6,14 @@ RSpec.describe 'tokens API', type: :request do let(:auth_token) { create(:token, user: user).token } let(:token) { create(:token, user: user) } - it 'GET /api/v2/tokens/my_tokens' do + it 'GET /api/v2/tokens' do create_list(:token, 5, user: user) - get '/api/v2/tokens/my_tokens', params: { access_token: auth_token } + get '/api/v2/tokens', params: { access_token: auth_token } expect(response).to have_http_status(:success) expect(response).to match_json_schema(:tokens) - expect(Token.count).to eq 6 # 5 + the extra auth token; let(:token) wasn't used + # 5 + the auth_token; let(:token) wasn't used + expect(Token.count).to eq 6 end it 'POST /api/v2/tokens' do From 0e7e649f56975666637bb22ce3e90655dd86aec1 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Wed, 12 Oct 2016 22:01:42 +0800 Subject: [PATCH 210/306] don't need coffeescript, tunemygc fails on Windows --- Gemfile | 1 - Gemfile.lock | 10 +--------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 7f34c12e..8fae0328 100644 --- a/Gemfile +++ b/Gemfile @@ -28,7 +28,6 @@ gem 'snorlax' gem 'uservoice-ruby' # asset stuff -gem 'coffee-rails' gem 'jquery-rails' gem 'jquery-ui-rails' gem 'sass-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 350585ae..5af227db 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,13 +70,6 @@ GEM cocaine (0.5.8) climate_control (>= 0.0.3, < 1.0) coderay (1.1.1) - coffee-rails (4.2.1) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.2.x) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.10.0) concurrent-ruby (1.0.2) debug_inspector (0.0.2) delayed_job (4.1.2) @@ -282,7 +275,6 @@ DEPENDENCIES better_errors binding_of_caller brakeman - coffee-rails delayed_job delayed_job_active_record devise @@ -321,4 +313,4 @@ RUBY VERSION ruby 2.3.0p0 BUNDLED WITH - 1.13.2 + 1.13.3 From 6f3c74b7f1866ff1eb97f5400afe39e89847c7e3 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 15:21:27 +0800 Subject: [PATCH 211/306] token policy fix --- app/policies/token_policy.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/policies/token_policy.rb b/app/policies/token_policy.rb index cd9a5ab7..e96e4db8 100644 --- a/app/policies/token_policy.rb +++ b/app/policies/token_policy.rb @@ -10,11 +10,11 @@ class TokenPolicy < ApplicationPolicy end end - def create? + def index? user.present? end - def my_tokens? + def create? user.present? end From fc2849824f192ba75f59604ccdcfcafc3b9ded33 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 16:48:46 +0800 Subject: [PATCH 212/306] fix js syntax error --- frontend/src/Metamaps/Import.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/Metamaps/Import.js b/frontend/src/Metamaps/Import.js index 6f25d21b..f1539274 100644 --- a/frontend/src/Metamaps/Import.js +++ b/frontend/src/Metamaps/Import.js @@ -296,8 +296,8 @@ const Import = { metacode_id: metacode.id, permission: topic_permission, defer_to_map_id: defer_to_map_id, - desc: desc || "", - link: link || "", + desc: desc || '', + link: link || '', calculated_permission: Active.Map.get('permission') }) Metamaps.Topics.add(topic) @@ -333,7 +333,7 @@ const Import = { } // if var synapse = new Metamaps.Backbone.Synapse({ - desc: desc || '' + desc: desc || '', category: category || 'from-to', permission: permission, topic1_id: topic1.id, From 407ac1f29ce56b08094b090e0527d826dc5716c0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 15:31:45 +0800 Subject: [PATCH 213/306] more simplecov groups --- .simplecov | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.simplecov b/.simplecov index b81ebfeb..efee8860 100644 --- a/.simplecov +++ b/.simplecov @@ -1,3 +1,7 @@ if ENV['COVERAGE'] == 'on' - SimpleCov.start 'rails' + SimpleCov.start 'rails' do + add_group 'Policies', 'app/policies' + add_group 'Services', 'app/services' + add_group 'Serializers', 'app/serializers' + end end From 26a8cddd1494969a784c88c93908267132361bf7 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Thu, 13 Oct 2016 15:39:13 +0800 Subject: [PATCH 214/306] mailer spec --- config/environments/test.rb | 1 + spec/mailers/map_mailer_spec.rb | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 spec/mailers/map_mailer_spec.rb diff --git a/config/environments/test.rb b/config/environments/test.rb index 5f0b1ee2..d85899fd 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,6 +30,7 @@ Metamaps::Application.configure do # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.default_url_options = { host: 'localhost:3000' } # Print deprecation notices to the stderr config.active_support.deprecation = :stderr diff --git a/spec/mailers/map_mailer_spec.rb b/spec/mailers/map_mailer_spec.rb new file mode 100644 index 00000000..d4da119e --- /dev/null +++ b/spec/mailers/map_mailer_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +RSpec.describe MapMailer, type: :mailer do + let(:map) { create(:map) } + let(:inviter) { create(:user) } + let(:invitee) { create(:user) } + describe 'invite_to_edit_email' do + let(:mail) { described_class.invite_to_edit_email(map, inviter, invitee) } + + it { expect(mail.from).to eq ['team@metamaps.cc'] } + it { expect(mail.to).to eq [invitee.email] } + it { expect(mail.subject).to match map.name } + it { expect(mail.body.encoded).to match inviter.name } + it { expect(mail.body.encoded).to match map.name } + it { expect(mail.body.encoded).to match map_url(map) } + end +end From 8180a8cc7149b9a57644a6b0748eed330c2d5c84 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Fri, 14 Oct 2016 14:29:16 +0800 Subject: [PATCH 215/306] fix file upload box --- frontend/src/components/ImportDialogBox.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/ImportDialogBox.js b/frontend/src/components/ImportDialogBox.js index bfb60235..43825421 100644 --- a/frontend/src/components/ImportDialogBox.js +++ b/frontend/src/components/ImportDialogBox.js @@ -15,12 +15,13 @@ class ImportDialogBox extends Component { } handleFile = (files, e) => { - // for some reason it uploads twice, so we need this debouncer - // eslint-disable-next-line no-return-assign - this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) - if (!this.debouncer) { - this.props.onFileAdded(files[0]) - } + // // for some reason it uploads twice, so we need this debouncer + // // eslint-disable-next-line no-return-assign + // this.debouncer = this.debouncer || window.setTimeout(() => this.debouncer = null, 10) + // if (!this.debouncer) { + // this.props.onFileAdded(files[0]) + // } + this.props.onFileAdded(files[0]) } toggleShowInstructions = e => { From 4602ded8a414e6db1adbd857e19ceb4211020ffc Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 16 Oct 2016 20:22:00 -0400 Subject: [PATCH 216/306] access requests (#762) * start on access requests * set up access requests further * set default values for approved and answered --- app/assets/images/view-only.png | Bin 0 -> 421 bytes .../stylesheets/request_access.scss.erb | 98 ++++++++++++++++++ app/controllers/access_controller.rb | 98 ++++++++++++++++++ app/controllers/application_controller.rb | 17 +-- app/controllers/maps_controller.rb | 24 +---- .../users/registrations_controller.rb | 11 +- app/mailers/map_mailer.rb | 9 +- app/models/access_request.rb | 18 ++++ app/models/map.rb | 5 +- app/policies/map_policy.rb | 28 ++++- app/views/layouts/_upperelements.html.erb | 17 +++ .../map_mailer/access_request_email.html.erb | 23 ++++ .../map_mailer/access_request_email.text.erb | 10 ++ .../map_mailer/invite_to_edit_email.html.erb | 2 +- .../map_mailer/invite_to_edit_email.text.erb | 2 +- app/views/maps/request_access.html.erb | 37 +++++++ config/environments/development.rb | 2 +- config/routes.rb | 40 ++++--- .../20161013162214_create_access_requests.rb | 12 +++ db/schema.rb | 15 ++- frontend/src/Metamaps/Map/index.js | 24 +++++ spec/mailers/previews/map_mailer_preview.rb | 5 + spec/models/access_request_spec.rb | 5 + 23 files changed, 448 insertions(+), 54 deletions(-) create mode 100644 app/assets/images/view-only.png create mode 100644 app/assets/stylesheets/request_access.scss.erb create mode 100644 app/controllers/access_controller.rb create mode 100644 app/models/access_request.rb create mode 100644 app/views/map_mailer/access_request_email.html.erb create mode 100644 app/views/map_mailer/access_request_email.text.erb create mode 100644 app/views/maps/request_access.html.erb create mode 100644 db/migrate/20161013162214_create_access_requests.rb create mode 100644 spec/models/access_request_spec.rb diff --git a/app/assets/images/view-only.png b/app/assets/images/view-only.png new file mode 100644 index 0000000000000000000000000000000000000000..a4cc262f325e05c2d81f4ff16bd7d8b47a1d50e7 GIT binary patch literal 421 zcmV;W0b2fvP)yO$#(=1Q>kd7oxxfWY!yv!(zxpkj&fWs zW`malWe0}2-P_&w_U*p6nT&^rhsR%pwKfY*fYac-lIC*Uc*FTg7*+w8~p z*0^?j0N%jJE7aI{u>XbQz*-^^!6M5rdJOG??=tcR*1;lJkk-|2G>+q&VEH#^9Domz zUDC+D P00000NkvXXu0mjf2`9d; literal 0 HcmV?d00001 diff --git a/app/assets/stylesheets/request_access.scss.erb b/app/assets/stylesheets/request_access.scss.erb new file mode 100644 index 00000000..19ae2792 --- /dev/null +++ b/app/assets/stylesheets/request_access.scss.erb @@ -0,0 +1,98 @@ +.viewOnly { + float: left; + margin-left: 16px; + display: none; + height: 32px; + border: 1px solid #BDBDBD; + border-radius: 2px; + background-color: #424242; + color: #FFF; + font-size: 14px; + line-height: 32px; + + &.isViewOnly { + display: block; + } + + .eyeball { + background: url('<%= asset_path('view-only.png') %>') no-repeat 4px 0; + padding-left: 40px; + border-right: #747474; + padding-right: 10px; + display: inline-block; + } + + .requestNotice { + display: none; + padding: 0 8px; + } + + .requestAccess { + background-color: #a354cd; + &:hover { + background-color: #9150bc; + } + cursor: pointer; + } + + .requestPending { + background-color: #4fc059; + } + + .requestNotAccepted { + background-color: #c04f4f; + } + + &.sendRequest .requestAccess { + display: inline-block; + } + &.sentRequest .requestPending { + display: inline-block; + } + &.requestDenied .requestNotAccepted { + display: inline-block; + } +} + +.request_access { + position: absolute; + width: 90%; + margin: 0 5%; + + .monkey { + width: 250px; + height: 250px; + border: 6px solid #424242; + border-radius: 125px; + background: url(https://s3.amazonaws.com/metamaps-assets/site/monkeyselfie.jpg) no-repeat; + background-position: 50% 20%; + background-size: 100%; + margin: 80px auto 20px auto; + } + + .explainer_text { + padding: 0 20% 0 20%; + font-size: 24px; + line-height: 30px; + margin-bottom: 20px; + text-align: center; + } + + .make_request { + background-color: #a354cd; + display: block; + width: 220px; + height: 14px; + padding: 16px 0; + margin-bottom: 16px; + text-align: center; + border-radius: 2px; + font-size: 14px; + box-shadow: 0px 1px 1.5px rgba(0,0,0,0.12), 0 1px 1px rgba(0,0,0,0.24); + margin: 0 auto 20px auto; + text-decoration: none; + color: #FFFFFF !important; + cursor: pointer; + } + +} diff --git a/app/controllers/access_controller.rb b/app/controllers/access_controller.rb new file mode 100644 index 00000000..302e9385 --- /dev/null +++ b/app/controllers/access_controller.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true +class AccessController < ApplicationController + before_action :require_user, only: [:access, :access_request, :approve_access, :approve_access_post, + :deny_access, :deny_access_post, :request_access] + before_action :set_map, only: [:access, :access_request, :approve_access, :approve_access_post, + :deny_access, :deny_access_post, :request_access] + after_action :verify_authorized + + + # GET maps/:id/request_access + def request_access + @map = nil + respond_to do |format| + format.html do + render 'maps/request_access' + end + end + end + + # POST maps/:id/access_request + def access_request + request = AccessRequest.create(user: current_user, map: @map) + # what about push notification to map owner? + MapMailer.access_request_email(request, @map).deliver_later + + respond_to do |format| + format.json do + head :ok + end + end + end + + # POST maps/:id/access + def access + user_ids = params[:access] || [] + + @map.add_new_collaborators(user_ids).each do |user_id| + # add_new_collaborators returns array of added users, + # who we then send an email to + MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later + end + @map.remove_old_collaborators(user_ids) + + respond_to do |format| + format.json do + head :ok + end + end + end + + # GET maps/:id/approve_access/:request_id + def approve_access + request = AccessRequest.find(params[:request_id]) + request.approve() + respond_to do |format| + format.html { redirect_to map_path(@map), notice: 'Request was approved' } + end + end + + # GET maps/:id/deny_access/:request_id + def deny_access + request = AccessRequest.find(params[:request_id]) + request.deny() + respond_to do |format| + format.html { redirect_to map_path(@map), notice: 'Request was turned down' } + end + end + + # POST maps/:id/approve_access/:request_id + def approve_access_post + request = AccessRequest.find(params[:request_id]) + request.approve() + respond_to do |format| + format.json do + head :ok + end + end + end + + # POST maps/:id/deny_access/:request_id + def deny_access_post + request = AccessRequest.find(params[:request_id]) + request.deny() + respond_to do |format| + format.json do + head :ok + end + end + end + + private + + def set_map + @map = Map.find(params[:id]) + authorize @map + end + +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index eddf510d..6138fa31 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -22,21 +22,26 @@ class ApplicationController < ActionController::Base helper_method :admin? def after_sign_in_path_for(resource) - sign_in_url = url_for(action: 'new', controller: 'sessions', only_path: false) + sign_in_url = new_user_session_url + sign_up_url = new_user_registration_url + stored = stored_location_for(User) - if request.referer == sign_in_url + if stored + stored + elsif request.referer.include?(sign_in_url) || request.referer.include?(sign_up_url) super - elsif params[:uv_login] == '1' - 'http://support.metamaps.cc/login_success?sso=' + current_sso_token else - stored_location_for(resource) || request.referer || root_path + request.referer || root_path end end def handle_unauthorized - if authenticated? + if authenticated? and params[:controller] == 'maps' and params[:action] == 'show' + redirect_to request_access_map_path(params[:id]) + elsif authenticated? redirect_to root_path, notice: "You don't have permission to see that page." else + store_location_for(resource, request.fullpath) redirect_to new_user_session_path, notice: 'Try signing in to do that.' end end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index cdbbd900..7044d424 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true class MapsController < ApplicationController - before_action :require_user, only: [:create, :update, :destroy, :access, :events] - before_action :set_map, only: [:show, :update, :destroy, :access, :contains, - :events, :export] + before_action :require_user, only: [:create, :update, :destroy, :events] + before_action :set_map, only: [:show, :update, :destroy, :contains, :events, :export] after_action :verify_authorized # GET maps/:id @@ -16,6 +15,7 @@ class MapsController < ApplicationController @allmappings = policy_scope(@map.mappings) @allmessages = @map.messages.sort_by(&:created_at) @allstars = @map.stars + @allrequests = @map.access_requests end format.json { render json: @map } format.csv { redirect_to action: :export, format: :csv } @@ -80,24 +80,6 @@ class MapsController < ApplicationController end end - # POST maps/:id/access - def access - user_ids = params[:access] || [] - - @map.add_new_collaborators(user_ids).each do |user_id| - # add_new_collaborators returns array of added users, - # who we then send an email to - MapMailer.invite_to_edit_email(@map, current_user, User.find(user_id)).deliver_later - end - @map.remove_old_collaborators(user_ids) - - respond_to do |format| - format.json do - render json: { message: 'Successfully altered edit permissions' } - end - end - end - # GET maps/:id/contains def contains respond_to do |format| diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 21cd9666..e472152e 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -2,19 +2,22 @@ class Users::RegistrationsController < Devise::RegistrationsController before_action :configure_sign_up_params, only: [:create] before_action :configure_account_update_params, only: [:update] + after_action :store_location, only: [:new] protected - def after_sign_up_path_for(resource) - signed_in_root_path(resource) - end - def after_update_path_for(resource) signed_in_root_path(resource) end private + def store_location + if params[:redirect_to] + store_location_for(User, params[:redirect_to]) + end + end + def configure_sign_up_params devise_parameter_sanitizer.permit(:sign_up, keys: [:name, :joinedwithcode]) end diff --git a/app/mailers/map_mailer.rb b/app/mailers/map_mailer.rb index e70d0b82..f6865ecd 100644 --- a/app/mailers/map_mailer.rb +++ b/app/mailers/map_mailer.rb @@ -2,10 +2,17 @@ class MapMailer < ApplicationMailer default from: 'team@metamaps.cc' + def access_request_email(request, map) + @request = request + @map = map + subject = @map.name + ' - request to edit' + mail(to: @map.user.email, subject: subject) + end + def invite_to_edit_email(map, inviter, invitee) @inviter = inviter @map = map - subject = @map.name + ' - Invitation to edit' + subject = @map.name + ' - invitation to edit' mail(to: invitee.email, subject: subject) end end diff --git a/app/models/access_request.rb b/app/models/access_request.rb new file mode 100644 index 00000000..185a04f0 --- /dev/null +++ b/app/models/access_request.rb @@ -0,0 +1,18 @@ +class AccessRequest < ApplicationRecord + belongs_to :user + belongs_to :map + + def approve + self.approved = true + self.answered = true + self.save + UserMap.create(user: self.user, map: self.map) + MapMailer.invite_to_edit_email(self.map, self.map.user, self.user).deliver_later + end + + def deny + self.approved = false + self.answered = true + self.save + end +end diff --git a/app/models/map.rb b/app/models/map.rb index a8e9c866..cdd6b333 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -9,6 +9,7 @@ class Map < ApplicationRecord has_many :messages, as: :resource, dependent: :destroy has_many :stars + has_many :access_requests, dependent: :destroy has_many :user_maps, dependent: :destroy has_many :collaborators, through: :user_maps, source: :user @@ -102,7 +103,8 @@ class Map < ApplicationRecord mappers: contributors, collaborators: editors, messages: messages.sort_by(&:created_at), - stars: stars + stars: stars, + requests: access_requests } end @@ -122,6 +124,7 @@ class Map < ApplicationRecord removed = current_collaborators.map(&:id).map do |old_user_id| next nil if user_ids.include?(old_user_id) user_maps.where(user_id: old_user_id).find_each(&:destroy) + access_requests.where(user_id: old_user_id).find_each(&:destroy) old_user_id end removed.compact diff --git a/app/policies/map_policy.rb b/app/policies/map_policy.rb index 9999a055..f670f59e 100644 --- a/app/policies/map_policy.rb +++ b/app/policies/map_policy.rb @@ -37,10 +37,36 @@ class MapPolicy < ApplicationPolicy end def access? - # note that this is to edit who can access the map + # this is for the map creator to bulk change who can access the map user.present? && record.user == user end + def request_access? + # this is to access the page where you can request access to a map + user.present? + end + + def access_request? + # this is to actually request access + user.present? + end + + def approve_access? + record.user == user + end + + def deny_access? + approve_access? + end + + def approve_access_post? + approve_access? + end + + def deny_access_post? + approve_access? + end + def contains? show? end diff --git a/app/views/layouts/_upperelements.html.erb b/app/views/layouts/_upperelements.html.erb index c2344643..1d26ced9 100644 --- a/app/views/layouts/_upperelements.html.erb +++ b/app/views/layouts/_upperelements.html.erb @@ -14,6 +14,23 @@
+ + <% request = current_user && @map && @allrequests.find{|a| a.user == current_user} + className = (@map and not policy(@map).update?) ? 'isViewOnly ' : '' + if @map + className += 'sendRequest' if not request + className += 'sentRequest' if request and not request.answered + className += 'requestDenied' if request and request.answered and not request.approved + end %> + +
+
View Only
+ <% if current_user %> +
Request Access
+
Request Pending
+
Request Not Accepted
+ <% end %> +
diff --git a/app/views/map_mailer/access_request_email.html.erb b/app/views/map_mailer/access_request_email.html.erb new file mode 100644 index 00000000..0445c6da --- /dev/null +++ b/app/views/map_mailer/access_request_email.html.erb @@ -0,0 +1,23 @@ + + + + + + + +
+ <% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> + +

<%= @request.user.name %> is requesting access to collaboratively edit the following metamap:

+ +

<%= @map.name %>

+ +

<%= link_to "Grant", approve_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %> +

<%= link_to "Deny", deny_access_map_url(id: @map.id, request_id: @request.id), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #DB5D5D;" %>

+ + <%= link_to 'Open in Metamaps', map_url(@map), target: "_blank", style: button_style %> + +

Make sense with Metamaps

+
+ + diff --git a/app/views/map_mailer/access_request_email.text.erb b/app/views/map_mailer/access_request_email.text.erb new file mode 100644 index 00000000..0c5b07dd --- /dev/null +++ b/app/views/map_mailer/access_request_email.text.erb @@ -0,0 +1,10 @@ +<%= @request.user.name %> has requested to collaboratively edit the following metamap: + +<%= @map.name %> [<%= map_url(@map) %>] + +Approve Request [<%= approve_access_map_url(id: @map.id, request_id: @request.id) %>] +Deny Request [<%= deny_access_map_url(id: @map.id, request_id: @request.id) %>] + +Make sense with Metamaps + + diff --git a/app/views/map_mailer/invite_to_edit_email.html.erb b/app/views/map_mailer/invite_to_edit_email.html.erb index 1a8b80c2..73067c48 100644 --- a/app/views/map_mailer/invite_to_edit_email.html.erb +++ b/app/views/map_mailer/invite_to_edit_email.html.erb @@ -8,7 +8,7 @@
<% button_style = "background-color:#4fc059;border-radius:2px;color:white;display:inline-block;font-family:Roboto,Arial,Helvetica,sans-serif;font-size:12px;font-weight:bold;min-height:29px;line-height:29px;min-width:54px;outline:0px;padding:0 8px;text-align:center;text-decoration:none" %> -

<%= @inviter.name %> has invited you to collaboratively edit the following metamap:

+

<%= @inviter.name %> has invited you to collaboratively edit the following map:

<%= link_to @map.name, map_url(@map), target: "_blank", style: "font-size: 18px; text-decoration: none; color: #4fc059;" %>

<% if @map.desc %>

<%= @map.desc %>

diff --git a/app/views/map_mailer/invite_to_edit_email.text.erb b/app/views/map_mailer/invite_to_edit_email.text.erb index 62bd2c90..80eecfed 100644 --- a/app/views/map_mailer/invite_to_edit_email.text.erb +++ b/app/views/map_mailer/invite_to_edit_email.text.erb @@ -1,4 +1,4 @@ -<%= @inviter.name %> has invited you to collaboratively edit the following metamap: +<%= @inviter.name %> has invited you to collaboratively edit the following map: <%= @map.name %> [<%= map_url(@map) %>] diff --git a/app/views/maps/request_access.html.erb b/app/views/maps/request_access.html.erb new file mode 100644 index 00000000..cf8aadb4 --- /dev/null +++ b/app/views/maps/request_access.html.erb @@ -0,0 +1,37 @@ +<%# +# @file +# Code to request access to a map +# /maps/:id/request_access +#%> + +<% content_for :title, 'Request Access | Metamaps' %> +<% content_for :mobile_title, 'Request Access' %> + +
+
+
+
+ Hmmm. This map is private, but you can request to edit it from the map creator. +
+
REQUEST ACCESS
+
+
+ + diff --git a/config/environments/development.rb b/config/environments/development.rb index 5449e5e8..38741a18 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -32,7 +32,7 @@ Rails.application.configure do # Print deprecation notices to the Rails logger config.active_support.deprecation = :log - config.action_mailer.preview_path = '/vagrant/spec/mailers/previews' + config.action_mailer.preview_path = "#{Rails.root}/spec/mailers/previews" # Expands the lines which load the assets config.assets.debug = false diff --git a/config/routes.rb b/config/routes.rb index 79db8599..e20f600f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,9 +19,17 @@ Metamaps::Application.routes.draw do get :export post 'events/:event', action: :events get :contains - post :access, default: { format: :json } - post :star, to: 'stars#create', defaults: { format: :json } - post :unstar, to: 'stars#destroy', defaults: { format: :json } + + get :request_access, to: 'access#request_access' + get 'approve_access/:request_id', to: 'access#approve_access', as: :approve_access + get 'deny_access/:request_id', to: 'access#deny_access', as: :deny_access + post :access_request, to: 'access#access_request', default: { format: :json } + post 'approve_access/:request_id', to: 'access#approve_access_post', default: { format: :json } + post 'deny_access/:request_id', to: 'access#deny_access_post', default: { format: :json } + post :access, to: 'access#access', default: { format: :json } + + post :star, to: 'stars#create', default: { format: :json } + post :unstar, to: 'stars#destroy', default: { format: :json } end end @@ -54,6 +62,19 @@ Metamaps::Application.routes.draw do end end + devise_for :users, skip: :sessions, controllers: { + registrations: 'users/registrations', + passwords: 'users/passwords', + sessions: 'devise/sessions' + } + + devise_scope :user do + get 'login' => 'devise/sessions#new', :as => :new_user_session + post 'login' => 'devise/sessions#create', :as => :user_session + get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session + get 'join' => 'devise/registrations#new', :as => :new_user_registration_path + end + resources :users, except: [:index, :destroy] do member do get :details @@ -84,19 +105,6 @@ Metamaps::Application.routes.draw do match '*path', to: 'v2/restful#catch_404', via: :all end - devise_for :users, skip: :sessions, controllers: { - registrations: 'users/registrations', - passwords: 'users/passwords', - sessions: 'devise/sessions' - } - - devise_scope :user do - get 'login' => 'devise/sessions#new', :as => :new_user_session - post 'login' => 'devise/sessions#create', :as => :user_session - get 'logout' => 'devise/sessions#destroy', :as => :destroy_user_session - get 'join' => 'devise/registrations#new', :as => :new_user_registration_path - end - namespace :hacks do get 'load_url_title' end diff --git a/db/migrate/20161013162214_create_access_requests.rb b/db/migrate/20161013162214_create_access_requests.rb new file mode 100644 index 00000000..248ad005 --- /dev/null +++ b/db/migrate/20161013162214_create_access_requests.rb @@ -0,0 +1,12 @@ +class CreateAccessRequests < ActiveRecord::Migration[5.0] + def change + create_table :access_requests do |t| + t.references :user, foreign_key: true + t.boolean :approved, default: false + t.boolean :answered, default: false + t.references :map, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 0bfa7f1a..9807c3e0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,11 +10,22 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20160928022635) do +ActiveRecord::Schema.define(version: 20161013162214) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" + create_table "access_requests", force: :cascade do |t| + t.integer "user_id" + t.boolean "approved" + t.boolean "answered" + t.integer "map_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["map_id"], name: "index_access_requests_on_map_id", using: :btree + t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree + end + create_table "delayed_jobs", force: :cascade do |t| t.integer "priority", default: 0, null: false t.integer "attempts", default: 0, null: false @@ -268,5 +279,7 @@ ActiveRecord::Schema.define(version: 20160928022635) do t.index ["hookable_type", "hookable_id"], name: "index_webhooks_on_hookable_type_and_hookable_id", using: :btree end + add_foreign_key "access_requests", "maps" + add_foreign_key "access_requests", "users" add_foreign_key "tokens", "users" end diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index 48be9def..e5f50633 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -60,8 +60,28 @@ const Map = { InfoBox.init() CheatSheet.init() + $('.viewOnly .requestAccess').click(self.requestAccess) + $(document).on(Map.events.editedByActiveMapper, self.editedByActiveMapper) }, + requestAccess: function () { + $('.viewOnly').removeClass('sendRequest').addClass('sentRequest') + const mapId = Active.Map.id + $.post({ + url: `/maps/${mapId}/access_request` + }) + GlobalUI.notifyUser('Map creator will be notified of your request') + }, + setAccessRequest: function (requests, activeMapper) { + let className = 'isViewOnly ' + if (activeMapper) { + const request = _.find(requests, r => r.user_id === activeMapper.id) + if (!request) className += 'sendRequest' + else if (request && !request.answered) className += 'sentRequest' + else if (request && request.answered && !request.approved) className += 'requestDenied' + } + $('.viewOnly').removeClass('sendRequest sentRequest requestDenied').addClass(className) + }, launch: function (id) { var bb = Metamaps.Backbone var start = function (data) { @@ -84,6 +104,9 @@ const Map = { if (map.authorizeToEdit(mapper)) { $('.wrapper').addClass('canEditMap') } + else { + Map.setAccessRequest(data.requests, mapper) + } // add class to .wrapper for specifying if the map can // be collaborated on @@ -139,6 +162,7 @@ const Map = { Filter.close() InfoBox.close() Realtime.endActiveMap() + $('.viewOnly').removeClass('isViewOnly') } }, updateStar: function () { diff --git a/spec/mailers/previews/map_mailer_preview.rb b/spec/mailers/previews/map_mailer_preview.rb index 96d07c07..17ea7671 100644 --- a/spec/mailers/previews/map_mailer_preview.rb +++ b/spec/mailers/previews/map_mailer_preview.rb @@ -4,4 +4,9 @@ class MapMailerPreview < ActionMailer::Preview def invite_to_edit_email MapMailer.invite_to_edit_email(Map.first, User.first, User.second) end + + def access_request_email + request = AccessRequest.first + MapMailer.access_request_email(request, request.map) + end end diff --git a/spec/models/access_request_spec.rb b/spec/models/access_request_spec.rb new file mode 100644 index 00000000..4119eaa6 --- /dev/null +++ b/spec/models/access_request_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe AccessRequest, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From e46aa54ba35c8f9623252b88b229bfb43ffd7e0b Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 10:39:46 +0800 Subject: [PATCH 217/306] schema update --- db/schema.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/db/schema.rb b/db/schema.rb index 9807c3e0..850f59a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -17,11 +17,11 @@ ActiveRecord::Schema.define(version: 20161013162214) do create_table "access_requests", force: :cascade do |t| t.integer "user_id" - t.boolean "approved" - t.boolean "answered" + t.boolean "approved", default: false + t.boolean "answered", default: false t.integer "map_id" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false t.index ["map_id"], name: "index_access_requests_on_map_id", using: :btree t.index ["user_id"], name: "index_access_requests_on_user_id", using: :btree end From c113253fc56ced7fb8693624d680e1fab4f840b0 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 10:41:56 +0800 Subject: [PATCH 218/306] add scripts from default rails install --- bin/bundle | 3 +++ bin/rails | 4 ++++ bin/rake | 4 ++++ 3 files changed, 11 insertions(+) create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..66e9889e --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..07396602 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../config/application', __dir__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..17240489 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run From 179849b639bfdfdb4bf0f9a4bf175a1b05b99771 Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 11:42:11 +0800 Subject: [PATCH 219/306] remove check-canvas-support.js --- .../javascripts/src/check-canvas-support.js | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 app/assets/javascripts/src/check-canvas-support.js diff --git a/app/assets/javascripts/src/check-canvas-support.js b/app/assets/javascripts/src/check-canvas-support.js deleted file mode 100644 index 90afdde1..00000000 --- a/app/assets/javascripts/src/check-canvas-support.js +++ /dev/null @@ -1,15 +0,0 @@ -// TODO document this user agent function -var labelType, useGradients, nativeTextSupport, animate -;(function () { - var ua = navigator.userAgent, - iStuff = ua.match(/iPhone/i) || ua.match(/iPad/i), - typeOfCanvas = typeof HTMLCanvasElement, - nativeCanvasSupport = (typeOfCanvas == 'object' || typeOfCanvas == 'function'), - textSupport = nativeCanvasSupport && (typeof document.createElement('canvas').getContext('2d').fillText == 'function') - // I'm setting this based on the fact that ExCanvas provides text support for IE - // and that as of today iPhone/iPad current text support is lame - labelType = (!nativeCanvasSupport || (textSupport && !iStuff)) ? 'Native' : 'HTML' - nativeTextSupport = labelType == 'Native' - useGradients = nativeCanvasSupport - animate = !(iStuff || !nativeCanvasSupport) -})() From 332bb2ec0898e16269817c5b6c9700d3bf8cc2a5 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 16 Oct 2016 23:46:55 -0400 Subject: [PATCH 220/306] Map Card changes (#769) * map card rewrite underway * star count * css fix --- app/assets/stylesheets/base.css.erb | 152 +----------------------- app/assets/stylesheets/mapcard.scss.erb | 144 ++++++++++++++++++++++ app/models/map.rb | 12 +- frontend/src/components/Maps/MapCard.js | 61 +++++----- 4 files changed, 185 insertions(+), 184 deletions(-) create mode 100644 app/assets/stylesheets/mapcard.scss.erb diff --git a/app/assets/stylesheets/base.css.erb b/app/assets/stylesheets/base.css.erb index 6cfb6b57..aa2c96db 100644 --- a/app/assets/stylesheets/base.css.erb +++ b/app/assets/stylesheets/base.css.erb @@ -17,7 +17,6 @@ } - #center-container { position:relative; height:100%; @@ -592,10 +591,10 @@ background-color: #E0E0E0; position: relative; } -.CardOnGraph .hoverForTip:hover .tip, .mapCard .hoverForTip:hover .tip, #mapContribs:hover .tip { +.CardOnGraph .hoverForTip:hover .tip, #mapContribs:hover .tip { display:block; } -.CardOnGraph .tip, .mapCard .tip { +.CardOnGraph .tip { display:none; position: absolute; background: black; @@ -952,154 +951,7 @@ font-family: 'din-regular', helvetica, sans-serif; background-position: 0 -24px; } -/* Map Cards */ -.map { - display:inline-block; - width:220px; - height:340px; - font-size: 12px; - text-align: left; - overflow: visible; - background: #e8e8e8; - border-radius:2px; - margin:16px 16px 16px 19px; - box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); -} -.map:hover { - background: #dcdcdc; -} -.map.newMap { - float: left; - position: relative; -} -.map.newMap a { - height: 340px; - display: block; - position: relative; -} -.newMap .newMapImage { - display: block; - width: 72px; - height: 72px; - background-image: url("<%= asset_data_uri('newmap_sprite.png') %>"); - background-repeat: no-repeat; - background-position: 0 0; - position: absolute; - left: 50%; - margin-left: -36px; - top: 50%; - margin-top: -36px; -} -.map:hover .newMapImage { - background-position: 0 -72px; -} -.newMap span { - font-family: 'din-regular', sans-serif; - font-size: 18px; - line-height: 22px; - text-align: center; - display: block; - padding-top: 220px; -} - -.mapCard { - display: -webkit-box; /* OLD - iOS 6-, Safari 3.1-6 */ - display: -moz-box; /* OLD - Firefox 19- (buggy but mostly works) */ - display: -ms-flexbox; /* TWEENER - IE 10 */ - display: -webkit-flex; /* NEW - Chrome */ - display: flex; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - -webkit-box-orient: vertical; - -moz-box-orient: vertical; - -webkit-box-direction: normal; - -moz-box-direction: normal; - -ms-flex-direction: column; - -webkit-flex-direction: column; - flex-direction: column; - position:relative; - width:100%; - height:308px; - padding: 16px 0; - color: #424242; -} - -.mapCard .title { - word-wrap: break-word; - font-size:18px; - line-height:22px; - height: 44px; - display:block; - padding: 0 16px; - text-align: center; - -webkit-box-flex: none; /* OLD - iOS 6-, Safari 3.1-6 */ - -moz-box-flex: none; /* OLD - Firefox 19- */ - -webkit-flex: none; /* Chrome */ - -ms-flex: none; /* IE 10 */ - flex: none; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - font-family: 'din-regular', sans-serif; -} - -.mapCard .mapScreenshot { - width: 188px; - height: 126px; - padding: 8px 16px; -} -.mapCard .mapScreenshot img { - width: 188px; - height: 126px; - border-radius: 2px; -} - -.mapCard .scroll { - display:block; - -webkit-box-flex: 1; /* OLD - iOS 6-, Safari 3.1-6 */ - -moz-box-flex: 1; /* OLD - Firefox 19- */ - -webkit-flex: 1; /* Chrome */ - -ms-flex: 1; /* IE 10 */ - flex: 1; /* NEW, Spec - Opera 12.1, Firefox 20+ */ - padding:0 16px 8px; - font-family: helvetica, sans-serif; - font-style: italic; - font-size: 12px; - word-wrap: break-word; -} -.mCS_no_scrollbar { - padding-right: 5px; -} - -.mapCard .mapMetadata { - font-family: 'din-regular', sans-serif; - font-size: 12px; - position:relative; - border-top: 1px solid #BDBDBD; - -webkit-box-flex: none; /* OLD - iOS 6-, Safari 3.1-6 */ - -moz-box-flex: none; /* OLD - Firefox 19- */ - -webkit-flex: none; /* Chrome */ - -ms-flex: none; /* IE 10 */ - flex: none; /* NEW, Spec - Opera 12.1, Firefox 20+ */ -} - -.mapCard .metadataSection { - padding: 8px 16px 0 16px; - width: 78px; - float: left; -} - -.mapPermission { - font-family: 'din-medium', sans-serif; -} -.cCountColor { - font-family: 'din-medium', sans-serif; - color: #DB5D5D; -} -.tCountColor { - font-family: 'din-medium', sans-serif; - color: #4FC059; -} -.sCountColor { - font-family: 'din-medium', sans-serif; - color: #DAB539; -} /* mapper card */ diff --git a/app/assets/stylesheets/mapcard.scss.erb b/app/assets/stylesheets/mapcard.scss.erb new file mode 100644 index 00000000..529257ed --- /dev/null +++ b/app/assets/stylesheets/mapcard.scss.erb @@ -0,0 +1,144 @@ +/* Map Cards */ + +.map { + display:inline-block; + width:220px; + height:340px; + font-size: 12px; + text-align: left; + overflow: visible; + background: #e8e8e8; + border-radius:2px; + margin:16px 16px 16px 19px; + box-shadow: 0px 3px 3px rgba(0,0,0,0.23), 0 3px 3px rgba(0,0,0,0.16); +} +.map.newMap { + float: left; + position: relative; +} +.map.newMap:hover { + background: #dcdcdc; +} +.map.newMap a { + height: 340px; + display: block; + position: relative; +} +.newMap .newMapImage { + display: block; + width: 72px; + height: 72px; + background-image: url("<%= asset_data_uri('newmap_sprite.png') %>"); + background-repeat: no-repeat; + background-position: 0 0; + position: absolute; + left: 50%; + margin-left: -36px; + top: 50%; + margin-top: -36px; +} +.map:hover .newMapImage { + background-position: 0 -72px; +} +.newMap span { + font-family: 'din-regular', sans-serif; + font-size: 18px; + line-height: 22px; + text-align: center; + display: block; + padding-top: 220px; +} + +.mapCard { + position:relative; + width:100%; + height:308px; + padding: 0 0 16px 0; + color: #424242; + +.mapScreenshot { + width: 100%; + height: 220px; +} + +.mapScreenshot img { + width: 100%; +} + +.title { + word-wrap: break-word; + font-size:18px; + line-height:22px; + height: 71px; + display:table; + padding: 0 16px; + font-family: 'din-regular', sans-serif; + margin: 0 auto; + + .innerTitle { + display: table-cell; + vertical-align: middle; + text-align: center; + } +} + +.creatorAndPerm { + padding: 8px; +} + +.creatorImage { + display: inline-block; + border-radius: 16px; + vertical-align: middle; + width: 32px; + height: 32px; +} + +span.creatorName { + margin-left: 8px; +} + + +.scroll { + display:block; + font-family: helvetica, sans-serif; + font-size: 12px; + word-wrap: break-word; + text-align: center; + margin-top: 16px; +} + +&:hover .mainContent { + filter: blur(2px); +} + +&:hover .mapMetadata { + display: block; +} + +.mapMetadata { + display: none; + position: absolute; + top: 0; + left: 0; + padding: 40px 20px 0; + height: 300px; + font-family: 'din-regular', sans-serif; + font-size: 12px; + color: #FFF; + background: -moz-linear-gradient(top, rgba(0,0,0,0.65) 0%, rgba(0,0,0,0.43) 81%, rgba(0,0,0,0) 100%); + background: -webkit-linear-gradient(top, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); + background: linear-gradient(to bottom, rgba(0,0,0,0.65) 0%,rgba(0,0,0,0.43) 81%,rgba(0,0,0,0) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#a6000000', endColorstr='#00000000',GradientType=0 ); +} + +.metadataSection { + padding: 16px 0; + width: 90px; + float: left; + font-family: 'din-medium', sans-serif; + text-align: center; +} + +} + diff --git a/app/models/map.rb b/app/models/map.rb index cdd6b333..3f5a0a16 100644 --- a/app/models/map.rb +++ b/app/models/map.rb @@ -19,10 +19,10 @@ class Map < ApplicationRecord # This method associates the attribute ":image" with a file attachment has_attached_file :screenshot, styles: { - thumb: ['188x126#', :png] + thumb: ['220x220#', :png] #:full => ['940x630#', :png] }, - default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-white.png' + default_url: 'https://s3.amazonaws.com/metamaps-assets/site/missing-map-square.png' validates :name, presence: true validates :arranged, inclusion: { in: [true, false] } @@ -59,13 +59,17 @@ class Map < ApplicationRecord delegate :name, to: :user, prefix: true def user_image - user.image.url + user.image.url(:thirtytwo) end def contributor_count contributors.length end + def star_count + stars.length + end + def collaborator_ids collaborators.map(&:id) end @@ -87,7 +91,7 @@ class Map < ApplicationRecord end def as_json(_options = {}) - json = super(methods: [:user_name, :user_image, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) + json = super(methods: [:user_name, :user_image, :star_count, :topic_count, :synapse_count, :contributor_count, :collaborator_ids, :screenshot_url], except: [:screenshot_content_type, :screenshot_file_size, :screenshot_file_name, :screenshot_updated_at]) json[:created_at_clean] = created_at_str json[:updated_at_clean] = updated_at_str json diff --git a/frontend/src/components/Maps/MapCard.js b/frontend/src/components/Maps/MapCard.js index e31ede18..3a1557ee 100644 --- a/frontend/src/components/Maps/MapCard.js +++ b/frontend/src/components/Maps/MapCard.js @@ -12,7 +12,7 @@ class MapCard extends Component { const d = map.get('desc') const maxNameLength = 32 - const maxDescLength = 118 + const maxDescLength = 236 const truncatedName = n ? (n.length > maxNameLength ? n.substring(0, maxNameLength) + '...' : n) : '' const truncatedDesc = d ? (d.length > maxDescLength ? d.substring(0, maxDescLength) + '...' : d) : '' const editPermission = map.authorizeToEdit(currentUser) ? 'canEdit' : 'cannotEdit' @@ -21,42 +21,43 @@ class MapCard extends Component {
-
- - { truncatedName } - -
- -
-
-
- { truncatedDesc } -
+
+
+
+ +
+
+
{ truncatedName }
+
+
+ + { map.get('user_name') }
-
- - { map.get('contributor_count') } - - { map.get('contributor_count') === 1 ? ' contributor' : ' contributors' } +
+ { map.get('contributor_count') }
+ { map.get('contributor_count') === 1 ? 'contributor' : 'contributors' }
-
- - { map.get('topic_count') } - - { map.get('topic_count') === 1 ? ' topic' : ' topics' } -
-
- { map.get('permission') ? capitalize(map.get('permission')) : 'Commons' } +
+ { map.get('topic_count') }
+ { map.get('topic_count') === 1 ? 'topic' : 'topics' }
-
- - { map.get('synapse_count') } - - { map.get('synapse_count') === 1 ? ' synapse' : ' synapses' } +
+ { map.get('star_count') }
+ { map.get('star_count') === 1 ? 'star' : 'stars' } +
+
+ { map.get('synapse_count') }
+ { map.get('synapse_count') === 1 ? 'synapse' : 'synapses' }
+
+
+ { truncatedDesc } +
+
+
From c0955d7c5ea9592ecfac6b624bc6ca6a518617ba Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 17 Oct 2016 01:20:48 -0400 Subject: [PATCH 221/306] multiple policy issues (#771) * multiple policy errors * make some things more explicit --- app/models/user.rb | 5 +++++ app/policies/mapping_policy.rb | 12 +++++++----- app/policies/message_policy.rb | 12 +++++++----- app/policies/synapse_policy.rb | 3 +-- app/policies/topic_policy.rb | 2 +- 5 files changed, 21 insertions(+), 13 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 4f679c1b..52d6ef09 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -65,6 +65,11 @@ class User < ApplicationRecord json end + def all_accessible_maps + #TODO: is there a way to keep this an ActiveRecord relation? + maps + shared_maps + end + def recentMetacodes array = [] self.topics.sort{|a,b| b.created_at <=> a.created_at }.each do |t| diff --git a/app/policies/mapping_policy.rb b/app/policies/mapping_policy.rb index efcb798b..6cdb7e9b 100644 --- a/app/policies/mapping_policy.rb +++ b/app/policies/mapping_policy.rb @@ -8,11 +8,13 @@ class MappingPolicy < ApplicationPolicy # a private topic, since you can't see the private topic anyways visible = %w(public commons) permission = 'maps.permission IN (?)' - if user - scope.joins(:map).where(permission, visible).or(scope.joins(:map).where(user_id: user.id)) - else - scope.joins(:map).where(permission, visible) - end + return scope.joins(:map).where(permission, visible) unless user + + # if this is getting changed, the policy_scope for messages should also be changed + # as it is based entirely on the map to which it belongs + scope.joins(:map).where(permission, visible) + .or(scope.joins(:map).where('maps.id IN (?)', user.shared_maps.map(&:id))) + .or(scope.joins(:map).where('maps.user_id = ?', user.id)) end end diff --git a/app/policies/message_policy.rb b/app/policies/message_policy.rb index f35a2895..c32e29ed 100644 --- a/app/policies/message_policy.rb +++ b/app/policies/message_policy.rb @@ -4,11 +4,13 @@ class MessagePolicy < ApplicationPolicy def resolve visible = %w(public commons) permission = 'maps.permission IN (?)' - if user - scope.joins(:maps).where(permission + ' OR maps.user_id = ?', visible, user.id) - else - scope.where(permission, visible) - end + return scope.joins(:map).where(permission, visible) unless user + + # if this is getting changed, the policy_scope for mappings should also be changed + # as it is based entirely on the map to which it belongs + scope.joins(:map).where(permission, visible) + .or(scope.joins(:map).where('maps.id IN (?)', user.shared_maps.map(&:id))) + .or(scope.joins(:map).where('maps.user_id = ?', user.id)) end end diff --git a/app/policies/synapse_policy.rb b/app/policies/synapse_policy.rb index eae820b3..f3d2c997 100644 --- a/app/policies/synapse_policy.rb +++ b/app/policies/synapse_policy.rb @@ -3,11 +3,10 @@ class SynapsePolicy < ApplicationPolicy class Scope < Scope def resolve visible = %w(public commons) - return scope.where(permission: visible) unless user scope.where(permission: visible) - .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end diff --git a/app/policies/topic_policy.rb b/app/policies/topic_policy.rb index 7bcf585c..cf091662 100644 --- a/app/policies/topic_policy.rb +++ b/app/policies/topic_policy.rb @@ -6,7 +6,7 @@ class TopicPolicy < ApplicationPolicy return scope.where(permission: visible) unless user scope.where(permission: visible) - .or(scope.where(defer_to_map_id: user.shared_maps.map(&:id))) + .or(scope.where.not(defer_to_map_id: nil).where(defer_to_map_id: user.all_accessible_maps.map(&:id))) .or(scope.where(user_id: user.id)) end end From 0ee1b3284a97487b5a113778078ddc3323cb80ed Mon Sep 17 00:00:00 2001 From: Devin Howard Date: Mon, 17 Oct 2016 13:47:42 +0800 Subject: [PATCH 222/306] fix check-canvas-support require --- app/assets/javascripts/application.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index df086157..051edc8b 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -16,4 +16,3 @@ //= require_directory ./lib //= require ./src/Metamaps.Erb //= require ./webpacked/metamaps.bundle -//= require ./src/check-canvas-support From 517cfcb9132c300ba749557a40543d18aa8c2c8f Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Mon, 17 Oct 2016 10:39:08 -0400 Subject: [PATCH 223/306] remove static lib files in favor of npm ones (#773) * remove static lib files in favor of npm ones * update howler to work correctly * patch npm modules to not use window --- .../javascripts/lib/attachMediaStream.js | 39 - app/assets/javascripts/lib/howler.js | 1353 --- .../javascripts/lib/simplewebrtc.bundle.js | 9808 ----------------- frontend/src/Metamaps/Realtime.js | 18 +- frontend/src/Metamaps/Views/ChatView.js | 5 +- frontend/src/Metamaps/Views/Room.js | 3 +- package.json | 5 + 7 files changed, 20 insertions(+), 11211 deletions(-) delete mode 100644 app/assets/javascripts/lib/attachMediaStream.js delete mode 100644 app/assets/javascripts/lib/howler.js delete mode 100644 app/assets/javascripts/lib/simplewebrtc.bundle.js diff --git a/app/assets/javascripts/lib/attachMediaStream.js b/app/assets/javascripts/lib/attachMediaStream.js deleted file mode 100644 index de3feef5..00000000 --- a/app/assets/javascripts/lib/attachMediaStream.js +++ /dev/null @@ -1,39 +0,0 @@ -var attachMediaStream = function (stream, el, options) { - var URL = window.URL; - var opts = { - autoplay: true, - mirror: false, - muted: false - }; - var element = el || document.createElement('video'); - var item; - - if (options) { - for (item in options) { - opts[item] = options[item]; - } - } - - if (opts.autoplay) element.autoplay = 'autoplay'; - if (opts.muted) element.muted = true; - if (opts.mirror) { - ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { - var styleName = prefix ? prefix + 'Transform' : 'transform'; - element.style[styleName] = 'scaleX(-1)'; - }); - } - - // this first one should work most everywhere now - // but we have a few fallbacks just in case. - if (URL && URL.createObjectURL) { - element.src = URL.createObjectURL(stream); - } else if (element.srcObject) { - element.srcObject = stream; - } else if (element.mozSrcObject) { - element.mozSrcObject = stream; - } else { - return false; - } - - return element; - }; \ No newline at end of file diff --git a/app/assets/javascripts/lib/howler.js b/app/assets/javascripts/lib/howler.js deleted file mode 100644 index f393b3b1..00000000 --- a/app/assets/javascripts/lib/howler.js +++ /dev/null @@ -1,1353 +0,0 @@ -/*! - * howler.js v1.1.26 - * howlerjs.com - * - * (c) 2013-2015, James Simpson of GoldFire Studios - * goldfirestudios.com - * - * MIT License - */ - -(function() { - // setup - var cache = {}; - - // setup the audio context - var ctx = null, - usingWebAudio = true, - noAudio = false; - try { - if (typeof AudioContext !== 'undefined') { - ctx = new AudioContext(); - } else if (typeof webkitAudioContext !== 'undefined') { - ctx = new webkitAudioContext(); - } else { - usingWebAudio = false; - } - } catch(e) { - usingWebAudio = false; - } - - if (!usingWebAudio) { - if (typeof Audio !== 'undefined') { - try { - new Audio(); - } catch(e) { - noAudio = true; - } - } else { - noAudio = true; - } - } - - // create a master gain node - if (usingWebAudio) { - var masterGain = (typeof ctx.createGain === 'undefined') ? ctx.createGainNode() : ctx.createGain(); - masterGain.gain.value = 1; - masterGain.connect(ctx.destination); - } - - // create global controller - var HowlerGlobal = function(codecs) { - this._volume = 1; - this._muted = false; - this.usingWebAudio = usingWebAudio; - this.ctx = ctx; - this.noAudio = noAudio; - this._howls = []; - this._codecs = codecs; - this.iOSAutoEnable = true; - }; - HowlerGlobal.prototype = { - /** - * Get/set the global volume for all sounds. - * @param {Float} vol Volume from 0.0 to 1.0. - * @return {Howler/Float} Returns self or current volume. - */ - volume: function(vol) { - var self = this; - - // make sure volume is a number - vol = parseFloat(vol); - - if (vol >= 0 && vol <= 1) { - self._volume = vol; - - if (usingWebAudio) { - masterGain.gain.value = vol; - } - - // loop through cache and change volume of all nodes that are using HTML5 Audio - for (var key in self._howls) { - if (self._howls.hasOwnProperty(key) && self._howls[key]._webAudio === false) { - // loop through the audio nodes - for (var i=0; i 0) ? node._pos : self._sprite[sprite][0] / 1000; - - // determine how long to play for - var duration = 0; - if (self._webAudio) { - duration = self._sprite[sprite][1] / 1000 - node._pos; - if (node._pos > 0) { - pos = self._sprite[sprite][0] / 1000 + pos; - } - } else { - duration = self._sprite[sprite][1] / 1000 - (pos - self._sprite[sprite][0] / 1000); - } - - // determine if this sound should be looped - var loop = !!(self._loop || self._sprite[sprite][2]); - - // set timer to fire the 'onend' event - var soundId = (typeof callback === 'string') ? callback : Math.round(Date.now() * Math.random()) + '', - timerId; - (function() { - var data = { - id: soundId, - sprite: sprite, - loop: loop - }; - timerId = setTimeout(function() { - // if looping, restart the track - if (!self._webAudio && loop) { - self.stop(data.id).play(sprite, data.id); - } - - // set web audio node to paused at end - if (self._webAudio && !loop) { - self._nodeById(data.id).paused = true; - self._nodeById(data.id)._pos = 0; - - // clear the end timer - self._clearEndTimer(data.id); - } - - // end the track if it is HTML audio and a sprite - if (!self._webAudio && !loop) { - self.stop(data.id); - } - - // fire ended event - self.on('end', soundId); - }, duration * 1000); - - // store the reference to the timer - self._onendTimer.push({timer: timerId, id: data.id}); - })(); - - if (self._webAudio) { - var loopStart = self._sprite[sprite][0] / 1000, - loopEnd = self._sprite[sprite][1] / 1000; - - // set the play id to this node and load into context - node.id = soundId; - node.paused = false; - refreshBuffer(self, [loop, loopStart, loopEnd], soundId); - self._playStart = ctx.currentTime; - node.gain.value = self._volume; - - if (typeof node.bufferSource.start === 'undefined') { - loop ? node.bufferSource.noteGrainOn(0, pos, 86400) : node.bufferSource.noteGrainOn(0, pos, duration); - } else { - loop ? node.bufferSource.start(0, pos, 86400) : node.bufferSource.start(0, pos, duration); - } - } else { - if (node.readyState === 4 || !node.readyState && navigator.isCocoonJS) { - node.readyState = 4; - node.id = soundId; - node.currentTime = pos; - node.muted = Howler._muted || node.muted; - node.volume = self._volume * Howler.volume(); - setTimeout(function() { node.play(); }, 0); - } else { - self._clearEndTimer(soundId); - - (function(){ - var sound = self, - playSprite = sprite, - fn = callback, - newNode = node; - var listener = function() { - sound.play(playSprite, fn); - - // clear the event listener - newNode.removeEventListener('canplaythrough', listener, false); - }; - newNode.addEventListener('canplaythrough', listener, false); - })(); - - return self; - } - } - - // fire the play event and send the soundId back in the callback - self.on('play'); - if (typeof callback === 'function') callback(soundId); - - return self; - }); - - return self; - }, - - /** - * Pause playback and save the current position. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - pause: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.pause(id); - }); - - return self; - } - - // clear 'onend' timer - self._clearEndTimer(id); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - activeNode._pos = self.pos(null, id); - - if (self._webAudio) { - // make sure the sound has been created - if (!activeNode.bufferSource || activeNode.paused) { - return self; - } - - activeNode.paused = true; - if (typeof activeNode.bufferSource.stop === 'undefined') { - activeNode.bufferSource.noteOff(0); - } else { - activeNode.bufferSource.stop(0); - } - } else { - activeNode.pause(); - } - } - - self.on('pause'); - - return self; - }, - - /** - * Stop playback and reset to start. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - stop: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.stop(id); - }); - - return self; - } - - // clear 'onend' timer - self._clearEndTimer(id); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - activeNode._pos = 0; - - if (self._webAudio) { - // make sure the sound has been created - if (!activeNode.bufferSource || activeNode.paused) { - return self; - } - - activeNode.paused = true; - - if (typeof activeNode.bufferSource.stop === 'undefined') { - activeNode.bufferSource.noteOff(0); - } else { - activeNode.bufferSource.stop(0); - } - } else if (!isNaN(activeNode.duration)) { - activeNode.pause(); - activeNode.currentTime = 0; - } - } - - return self; - }, - - /** - * Mute this sound. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - mute: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.mute(id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = 0; - } else { - activeNode.muted = true; - } - } - - return self; - }, - - /** - * Unmute this sound. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - unmute: function(id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.unmute(id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = self._volume; - } else { - activeNode.muted = false; - } - } - - return self; - }, - - /** - * Get/set volume of this sound. - * @param {Float} vol Volume from 0.0 to 1.0. - * @param {String} id (optional) The play instance ID. - * @return {Howl/Float} Returns self or current volume. - */ - volume: function(vol, id) { - var self = this; - - // make sure volume is a number - vol = parseFloat(vol); - - if (vol >= 0 && vol <= 1) { - self._volume = vol; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('play', function() { - self.volume(vol, id); - }); - - return self; - } - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (self._webAudio) { - activeNode.gain.value = vol; - } else { - activeNode.volume = vol * Howler.volume(); - } - } - - return self; - } else { - return self._volume; - } - }, - - /** - * Get/set whether to loop the sound. - * @param {Boolean} loop To loop or not to loop, that is the question. - * @return {Howl/Boolean} Returns self or current looping value. - */ - loop: function(loop) { - var self = this; - - if (typeof loop === 'boolean') { - self._loop = loop; - - return self; - } else { - return self._loop; - } - }, - - /** - * Get/set sound sprite definition. - * @param {Object} sprite Example: {spriteName: [offset, duration, loop]} - * @param {Integer} offset Where to begin playback in milliseconds - * @param {Integer} duration How long to play in milliseconds - * @param {Boolean} loop (optional) Set true to loop this sprite - * @return {Howl} Returns current sprite sheet or self. - */ - sprite: function(sprite) { - var self = this; - - if (typeof sprite === 'object') { - self._sprite = sprite; - - return self; - } else { - return self._sprite; - } - }, - - /** - * Get/set the position of playback. - * @param {Float} pos The position to move current playback to. - * @param {String} id (optional) The play instance ID. - * @return {Howl/Float} Returns self or current playback position. - */ - pos: function(pos, id) { - var self = this; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('load', function() { - self.pos(pos); - }); - - return typeof pos === 'number' ? self : self._pos || 0; - } - - // make sure we are dealing with a number for pos - pos = parseFloat(pos); - - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - if (pos >= 0) { - self.pause(id); - activeNode._pos = pos; - self.play(activeNode._sprite, id); - - return self; - } else { - return self._webAudio ? activeNode._pos + (ctx.currentTime - self._playStart) : activeNode.currentTime; - } - } else if (pos >= 0) { - return self; - } else { - // find the first inactive node to return the pos for - for (var i=0; i= 0 || x < 0) { - if (self._webAudio) { - var activeNode = (id) ? self._nodeById(id) : self._activeNode(); - if (activeNode) { - self._pos3d = [x, y, z]; - activeNode.panner.setPosition(x, y, z); - activeNode.panner.panningModel = self._model || 'HRTF'; - } - } - } else { - return self._pos3d; - } - - return self; - }, - - /** - * Fade a currently playing sound between two volumes. - * @param {Number} from The volume to fade from (0.0 to 1.0). - * @param {Number} to The volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback (optional) Fired when the fade is complete. - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - fade: function(from, to, len, callback, id) { - var self = this, - diff = Math.abs(from - to), - dir = from > to ? 'down' : 'up', - steps = diff / 0.01, - stepTime = len / steps; - - // if the sound hasn't been loaded, add it to the event queue - if (!self._loaded) { - self.on('load', function() { - self.fade(from, to, len, callback, id); - }); - - return self; - } - - // set the volume to the start position - self.volume(from, id); - - for (var i=1; i<=steps; i++) { - (function() { - var change = self._volume + (dir === 'up' ? 0.01 : -0.01) * i, - vol = Math.round(1000 * change) / 1000, - toVol = to; - - setTimeout(function() { - self.volume(vol, id); - - if (vol === toVol) { - if (callback) callback(); - } - }, stepTime * i); - })(); - } - }, - - /** - * [DEPRECATED] Fade in the current sound. - * @param {Float} to Volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback - * @return {Howl} - */ - fadeIn: function(to, len, callback) { - return this.volume(0).play().fade(0, to, len, callback); - }, - - /** - * [DEPRECATED] Fade out the current sound and pause when finished. - * @param {Float} to Volume to fade to (0.0 to 1.0). - * @param {Number} len Time in milliseconds to fade. - * @param {Function} callback - * @param {String} id (optional) The play instance ID. - * @return {Howl} - */ - fadeOut: function(to, len, callback, id) { - var self = this; - - return self.fade(self._volume, to, len, function() { - if (callback) callback(); - self.pause(id); - - // fire ended event - self.on('end'); - }, id); - }, - - /** - * Get an audio node by ID. - * @return {Howl} Audio node. - */ - _nodeById: function(id) { - var self = this, - node = self._audioNode[0]; - - // find the node with this ID - for (var i=0; i=0; i--) { - if (inactive <= 5) { - break; - } - - if (self._audioNode[i].paused) { - // disconnect the audio source if using Web Audio - if (self._webAudio) { - self._audioNode[i].disconnect(0); - } - - inactive--; - self._audioNode.splice(i, 1); - } - } - }, - - /** - * Clear 'onend' timeout before it ends. - * @param {String} soundId The play instance ID. - */ - _clearEndTimer: function(soundId) { - var self = this, - index = 0; - - // loop through the timers to find the one associated with this sound - for (var i=0; i= 0) { - Howler._howls.splice(index, 1); - } - - // delete this sound from the cache - delete cache[self._src]; - self = null; - } - - }; - - // only define these functions when using WebAudio - if (usingWebAudio) { - - /** - * Buffer a sound from URL (or from cache) and decode to audio source (Web Audio API). - * @param {Object} obj The Howl object for the sound to load. - * @param {String} url The path to the sound file. - */ - var loadBuffer = function(obj, url) { - // check if the buffer has already been cached - if (url in cache) { - // set the duration from the cache - obj._duration = cache[url].duration; - - // load the sound into this object - loadSound(obj); - return; - } - - if (/^data:[^;]+;base64,/.test(url)) { - // Decode base64 data-URIs because some browsers cannot load data-URIs with XMLHttpRequest. - var data = atob(url.split(',')[1]); - var dataView = new Uint8Array(data.length); - for (var i=0; i= 26) || - (window.navigator.userAgent.match('Firefox') && parseInt(window.navigator.userAgent.match(/Firefox\/(.*)/)[1], 10) >= 33)); -var AudioContext = window.AudioContext || window.webkitAudioContext; -var videoEl = document.createElement('video'); -var supportVp8 = videoEl && videoEl.canPlayType && videoEl.canPlayType('video/webm; codecs="vp8", vorbis') === "probably"; -var getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia || navigator.mozGetUserMedia; - -// export support flags and constructors.prototype && PC -module.exports = { - prefix: prefix, - support: !!PC && supportVp8 && !!getUserMedia, - // new support style - supportRTCPeerConnection: !!PC, - supportVp8: supportVp8, - supportGetUserMedia: !!getUserMedia, - supportDataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), - supportWebAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - supportMediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - supportScreenSharing: !!screenSharing, - // old deprecated style. Dont use this anymore - dataChannel: !!(PC && PC.prototype && PC.prototype.createDataChannel), - webAudio: !!(AudioContext && AudioContext.prototype.createMediaStreamSource), - mediaStream: !!(MediaStream && MediaStream.prototype.removeTrack), - screenSharing: !!screenSharing, - // constructors - AudioContext: AudioContext, - PeerConnection: PC, - SessionDescription: SessionDescription, - IceCandidate: IceCandidate, - MediaStream: MediaStream, - getUserMedia: getUserMedia -}; - -},{}],6:[function(require,module,exports){ -module.exports = function (stream, el, options) { - var URL = window.URL; - var opts = { - autoplay: true, - mirror: false, - muted: false - }; - var element = el || document.createElement('video'); - var item; - - if (options) { - for (item in options) { - opts[item] = options[item]; - } - } - - if (opts.autoplay) element.autoplay = 'autoplay'; - if (opts.muted) element.muted = true; - if (opts.mirror) { - ['', 'moz', 'webkit', 'o', 'ms'].forEach(function (prefix) { - var styleName = prefix ? prefix + 'Transform' : 'transform'; - element.style[styleName] = 'scaleX(-1)'; - }); - } - - // this first one should work most everywhere now - // but we have a few fallbacks just in case. - if (URL && URL.createObjectURL) { - element.src = URL.createObjectURL(stream); - } else if (element.srcObject) { - element.srcObject = stream; - } else if (element.mozSrcObject) { - element.mozSrcObject = stream; - } else { - return false; - } - - return element; -}; - -},{}],7:[function(require,module,exports){ -var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","); -var l = methods.length; -var fn = function () {}; -var mockconsole = {}; - -while (l--) { - mockconsole[methods[l]] = fn; -} - -module.exports = mockconsole; - -},{}],2:[function(require,module,exports){ -var io = require('socket.io-client'); - -function SocketIoConnection(config) { - this.connection = io.connect(config.url, config.socketio); -} - -SocketIoConnection.prototype.on = function (ev, fn) { - this.connection.on(ev, fn); -}; - -SocketIoConnection.prototype.emit = function () { - this.connection.emit.apply(this.connection, arguments); -}; - -SocketIoConnection.prototype.getSessionid = function () { - return this.connection.socket.sessionid; -}; - -SocketIoConnection.prototype.disconnect = function () { - return this.connection.disconnect(); -}; - -module.exports = SocketIoConnection; - -},{"socket.io-client":8}],8:[function(require,module,exports){ -/*! Socket.IO.js build:0.9.16, development. Copyright(c) 2011 LearnBoost MIT Licensed */ - -var io = ('undefined' === typeof module ? {} : module.exports); -(function() { - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, global) { - - /** - * IO namespace. - * - * @namespace - */ - - var io = exports; - - /** - * Socket.IO version - * - * @api public - */ - - io.version = '0.9.16'; - - /** - * Protocol implemented. - * - * @api public - */ - - io.protocol = 1; - - /** - * Available transports, these will be populated with the available transports - * - * @api public - */ - - io.transports = []; - - /** - * Keep track of jsonp callbacks. - * - * @api private - */ - - io.j = []; - - /** - * Keep track of our io.Sockets - * - * @api private - */ - io.sockets = {}; - - - /** - * Manages connections to hosts. - * - * @param {String} uri - * @Param {Boolean} force creation of new socket (defaults to false) - * @api public - */ - - io.connect = function (host, details) { - var uri = io.util.parseUri(host) - , uuri - , socket; - - if (global && global.location) { - uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); - uri.host = uri.host || (global.document - ? global.document.domain : global.location.hostname); - uri.port = uri.port || global.location.port; - } - - uuri = io.util.uniqueUri(uri); - - var options = { - host: uri.host - , secure: 'https' == uri.protocol - , port: uri.port || ('https' == uri.protocol ? 443 : 80) - , query: uri.query || '' - }; - - io.util.merge(options, details); - - if (options['force new connection'] || !io.sockets[uuri]) { - socket = new io.Socket(options); - } - - if (!options['force new connection'] && socket) { - io.sockets[uuri] = socket; - } - - socket = socket || io.sockets[uuri]; - - // if path is different from '' or / - return socket.of(uri.path.length > 1 ? uri.path : ''); - }; - -})('object' === typeof module ? module.exports : (this.io = {}), this); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, global) { - - /** - * Utilities namespace. - * - * @namespace - */ - - var util = exports.util = {}; - - /** - * Parses an URI - * - * @author Steven Levithan (MIT license) - * @api public - */ - - var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; - - var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', - 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', - 'anchor']; - - util.parseUri = function (str) { - var m = re.exec(str || '') - , uri = {} - , i = 14; - - while (i--) { - uri[parts[i]] = m[i] || ''; - } - - return uri; - }; - - /** - * Produces a unique url that identifies a Socket.IO connection. - * - * @param {Object} uri - * @api public - */ - - util.uniqueUri = function (uri) { - var protocol = uri.protocol - , host = uri.host - , port = uri.port; - - if ('document' in global) { - host = host || document.domain; - port = port || (protocol == 'https' - && document.location.protocol !== 'https:' ? 443 : document.location.port); - } else { - host = host || 'localhost'; - - if (!port && protocol == 'https') { - port = 443; - } - } - - return (protocol || 'http') + '://' + host + ':' + (port || 80); - }; - - /** - * Mergest 2 query strings in to once unique query string - * - * @param {String} base - * @param {String} addition - * @api public - */ - - util.query = function (base, addition) { - var query = util.chunkQuery(base || '') - , components = []; - - util.merge(query, util.chunkQuery(addition || '')); - for (var part in query) { - if (query.hasOwnProperty(part)) { - components.push(part + '=' + query[part]); - } - } - - return components.length ? '?' + components.join('&') : ''; - }; - - /** - * Transforms a querystring in to an object - * - * @param {String} qs - * @api public - */ - - util.chunkQuery = function (qs) { - var query = {} - , params = qs.split('&') - , i = 0 - , l = params.length - , kv; - - for (; i < l; ++i) { - kv = params[i].split('='); - if (kv[0]) { - query[kv[0]] = kv[1]; - } - } - - return query; - }; - - /** - * Executes the given function when the page is loaded. - * - * io.util.load(function () { console.log('page loaded'); }); - * - * @param {Function} fn - * @api public - */ - - var pageLoaded = false; - - util.load = function (fn) { - if ('document' in global && document.readyState === 'complete' || pageLoaded) { - return fn(); - } - - util.on(global, 'load', fn, false); - }; - - /** - * Adds an event. - * - * @api private - */ - - util.on = function (element, event, fn, capture) { - if (element.attachEvent) { - element.attachEvent('on' + event, fn); - } else if (element.addEventListener) { - element.addEventListener(event, fn, capture); - } - }; - - /** - * Generates the correct `XMLHttpRequest` for regular and cross domain requests. - * - * @param {Boolean} [xdomain] Create a request that can be used cross domain. - * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest. - * @api private - */ - - util.request = function (xdomain) { - - if (xdomain && 'undefined' != typeof XDomainRequest && !util.ua.hasCORS) { - return new XDomainRequest(); - } - - if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { - return new XMLHttpRequest(); - } - - if (!xdomain) { - try { - return new window[(['Active'].concat('Object').join('X'))]('Microsoft.XMLHTTP'); - } catch(e) { } - } - - return null; - }; - - /** - * XHR based transport constructor. - * - * @constructor - * @api public - */ - - /** - * Change the internal pageLoaded value. - */ - - if ('undefined' != typeof window) { - util.load(function () { - pageLoaded = true; - }); - } - - /** - * Defers a function to ensure a spinner is not displayed by the browser - * - * @param {Function} fn - * @api public - */ - - util.defer = function (fn) { - if (!util.ua.webkit || 'undefined' != typeof importScripts) { - return fn(); - } - - util.load(function () { - setTimeout(fn, 100); - }); - }; - - /** - * Merges two objects. - * - * @api public - */ - - util.merge = function merge (target, additional, deep, lastseen) { - var seen = lastseen || [] - , depth = typeof deep == 'undefined' ? 2 : deep - , prop; - - for (prop in additional) { - if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) { - if (typeof target[prop] !== 'object' || !depth) { - target[prop] = additional[prop]; - seen.push(additional[prop]); - } else { - util.merge(target[prop], additional[prop], depth - 1, seen); - } - } - } - - return target; - }; - - /** - * Merges prototypes from objects - * - * @api public - */ - - util.mixin = function (ctor, ctor2) { - util.merge(ctor.prototype, ctor2.prototype); - }; - - /** - * Shortcut for prototypical and static inheritance. - * - * @api private - */ - - util.inherit = function (ctor, ctor2) { - function f() {}; - f.prototype = ctor2.prototype; - ctor.prototype = new f; - }; - - /** - * Checks if the given object is an Array. - * - * io.util.isArray([]); // true - * io.util.isArray({}); // false - * - * @param Object obj - * @api public - */ - - util.isArray = Array.isArray || function (obj) { - return Object.prototype.toString.call(obj) === '[object Array]'; - }; - - /** - * Intersects values of two arrays into a third - * - * @api public - */ - - util.intersect = function (arr, arr2) { - var ret = [] - , longest = arr.length > arr2.length ? arr : arr2 - , shortest = arr.length > arr2.length ? arr2 : arr; - - for (var i = 0, l = shortest.length; i < l; i++) { - if (~util.indexOf(longest, shortest[i])) - ret.push(shortest[i]); - } - - return ret; - }; - - /** - * Array indexOf compatibility. - * - * @see bit.ly/a5Dxa2 - * @api public - */ - - util.indexOf = function (arr, o, i) { - - for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; - i < j && arr[i] !== o; i++) {} - - return j <= i ? -1 : i; - }; - - /** - * Converts enumerables to array. - * - * @api public - */ - - util.toArray = function (enu) { - var arr = []; - - for (var i = 0, l = enu.length; i < l; i++) - arr.push(enu[i]); - - return arr; - }; - - /** - * UA / engines detection namespace. - * - * @namespace - */ - - util.ua = {}; - - /** - * Whether the UA supports CORS for XHR. - * - * @api public - */ - - util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { - try { - var a = new XMLHttpRequest(); - } catch (e) { - return false; - } - - return a.withCredentials != undefined; - })(); - - /** - * Detect webkit. - * - * @api public - */ - - util.ua.webkit = 'undefined' != typeof navigator - && /webkit/i.test(navigator.userAgent); - - /** - * Detect iPad/iPhone/iPod. - * - * @api public - */ - - util.ua.iDevice = 'undefined' != typeof navigator - && /iPad|iPhone|iPod/i.test(navigator.userAgent); - -})('undefined' != typeof io ? io : module.exports, this); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.EventEmitter = EventEmitter; - - /** - * Event emitter constructor. - * - * @api public. - */ - - function EventEmitter () {}; - - /** - * Adds a listener - * - * @api public - */ - - EventEmitter.prototype.on = function (name, fn) { - if (!this.$events) { - this.$events = {}; - } - - if (!this.$events[name]) { - this.$events[name] = fn; - } else if (io.util.isArray(this.$events[name])) { - this.$events[name].push(fn); - } else { - this.$events[name] = [this.$events[name], fn]; - } - - return this; - }; - - EventEmitter.prototype.addListener = EventEmitter.prototype.on; - - /** - * Adds a volatile listener. - * - * @api public - */ - - EventEmitter.prototype.once = function (name, fn) { - var self = this; - - function on () { - self.removeListener(name, on); - fn.apply(this, arguments); - }; - - on.listener = fn; - this.on(name, on); - - return this; - }; - - /** - * Removes a listener. - * - * @api public - */ - - EventEmitter.prototype.removeListener = function (name, fn) { - if (this.$events && this.$events[name]) { - var list = this.$events[name]; - - if (io.util.isArray(list)) { - var pos = -1; - - for (var i = 0, l = list.length; i < l; i++) { - if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { - pos = i; - break; - } - } - - if (pos < 0) { - return this; - } - - list.splice(pos, 1); - - if (!list.length) { - delete this.$events[name]; - } - } else if (list === fn || (list.listener && list.listener === fn)) { - delete this.$events[name]; - } - } - - return this; - }; - - /** - * Removes all listeners for an event. - * - * @api public - */ - - EventEmitter.prototype.removeAllListeners = function (name) { - if (name === undefined) { - this.$events = {}; - return this; - } - - if (this.$events && this.$events[name]) { - this.$events[name] = null; - } - - return this; - }; - - /** - * Gets all listeners for a certain event. - * - * @api publci - */ - - EventEmitter.prototype.listeners = function (name) { - if (!this.$events) { - this.$events = {}; - } - - if (!this.$events[name]) { - this.$events[name] = []; - } - - if (!io.util.isArray(this.$events[name])) { - this.$events[name] = [this.$events[name]]; - } - - return this.$events[name]; - }; - - /** - * Emits an event. - * - * @api public - */ - - EventEmitter.prototype.emit = function (name) { - if (!this.$events) { - return false; - } - - var handler = this.$events[name]; - - if (!handler) { - return false; - } - - var args = Array.prototype.slice.call(arguments, 1); - - if ('function' == typeof handler) { - handler.apply(this, args); - } else if (io.util.isArray(handler)) { - var listeners = handler.slice(); - - for (var i = 0, l = listeners.length; i < l; i++) { - listeners[i].apply(this, args); - } - } else { - return false; - } - - return true; - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -/** - * Based on JSON2 (http://www.JSON.org/js.html). - */ - -(function (exports, nativeJSON) { - "use strict"; - - // use native JSON if it's available - if (nativeJSON && nativeJSON.parse){ - return exports.JSON = { - parse: nativeJSON.parse - , stringify: nativeJSON.stringify - }; - } - - var JSON = exports.JSON = {}; - - function f(n) { - // Format integers to have at least two digits. - return n < 10 ? '0' + n : n; - } - - function date(d, key) { - return isFinite(d.valueOf()) ? - d.getUTCFullYear() + '-' + - f(d.getUTCMonth() + 1) + '-' + - f(d.getUTCDate()) + 'T' + - f(d.getUTCHours()) + ':' + - f(d.getUTCMinutes()) + ':' + - f(d.getUTCSeconds()) + 'Z' : null; - }; - - var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, - gap, - indent, - meta = { // table of character substitutions - '\b': '\\b', - '\t': '\\t', - '\n': '\\n', - '\f': '\\f', - '\r': '\\r', - '"' : '\\"', - '\\': '\\\\' - }, - rep; - - - function quote(string) { - -// If the string contains no control characters, no quote characters, and no -// backslash characters, then we can safely slap some quotes around it. -// Otherwise we must also replace the offending characters with safe escape -// sequences. - - escapable.lastIndex = 0; - return escapable.test(string) ? '"' + string.replace(escapable, function (a) { - var c = meta[a]; - return typeof c === 'string' ? c : - '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }) + '"' : '"' + string + '"'; - } - - - function str(key, holder) { - -// Produce a string from holder[key]. - - var i, // The loop counter. - k, // The member key. - v, // The member value. - length, - mind = gap, - partial, - value = holder[key]; - -// If the value has a toJSON method, call it to obtain a replacement value. - - if (value instanceof Date) { - value = date(key); - } - -// If we were called with a replacer function, then call the replacer to -// obtain a replacement value. - - if (typeof rep === 'function') { - value = rep.call(holder, key, value); - } - -// What happens next depends on the value's type. - - switch (typeof value) { - case 'string': - return quote(value); - - case 'number': - -// JSON numbers must be finite. Encode non-finite numbers as null. - - return isFinite(value) ? String(value) : 'null'; - - case 'boolean': - case 'null': - -// If the value is a boolean or null, convert it to a string. Note: -// typeof null does not produce 'null'. The case is included here in -// the remote chance that this gets fixed someday. - - return String(value); - -// If the type is 'object', we might be dealing with an object or an array or -// null. - - case 'object': - -// Due to a specification blunder in ECMAScript, typeof null is 'object', -// so watch out for that case. - - if (!value) { - return 'null'; - } - -// Make an array to hold the partial results of stringifying this object value. - - gap += indent; - partial = []; - -// Is the value an array? - - if (Object.prototype.toString.apply(value) === '[object Array]') { - -// The value is an array. Stringify every element. Use null as a placeholder -// for non-JSON values. - - length = value.length; - for (i = 0; i < length; i += 1) { - partial[i] = str(i, value) || 'null'; - } - -// Join all of the elements together, separated with commas, and wrap them in -// brackets. - - v = partial.length === 0 ? '[]' : gap ? - '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : - '[' + partial.join(',') + ']'; - gap = mind; - return v; - } - -// If the replacer is an array, use it to select the members to be stringified. - - if (rep && typeof rep === 'object') { - length = rep.length; - for (i = 0; i < length; i += 1) { - if (typeof rep[i] === 'string') { - k = rep[i]; - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } else { - -// Otherwise, iterate through all of the keys in the object. - - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = str(k, value); - if (v) { - partial.push(quote(k) + (gap ? ': ' : ':') + v); - } - } - } - } - -// Join all of the member texts together, separated with commas, -// and wrap them in braces. - - v = partial.length === 0 ? '{}' : gap ? - '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : - '{' + partial.join(',') + '}'; - gap = mind; - return v; - } - } - -// If the JSON object does not yet have a stringify method, give it one. - - JSON.stringify = function (value, replacer, space) { - -// The stringify method takes a value and an optional replacer, and an optional -// space parameter, and returns a JSON text. The replacer can be a function -// that can replace values, or an array of strings that will select the keys. -// A default replacer method can be provided. Use of the space parameter can -// produce text that is more easily readable. - - var i; - gap = ''; - indent = ''; - -// If the space parameter is a number, make an indent string containing that -// many spaces. - - if (typeof space === 'number') { - for (i = 0; i < space; i += 1) { - indent += ' '; - } - -// If the space parameter is a string, it will be used as the indent string. - - } else if (typeof space === 'string') { - indent = space; - } - -// If there is a replacer, it must be a function or an array. -// Otherwise, throw an error. - - rep = replacer; - if (replacer && typeof replacer !== 'function' && - (typeof replacer !== 'object' || - typeof replacer.length !== 'number')) { - throw new Error('JSON.stringify'); - } - -// Make a fake root object containing our value under the key of ''. -// Return the result of stringifying the value. - - return str('', {'': value}); - }; - -// If the JSON object does not yet have a parse method, give it one. - - JSON.parse = function (text, reviver) { - // The parse method takes a text and an optional reviver function, and returns - // a JavaScript value if the text is a valid JSON text. - - var j; - - function walk(holder, key) { - - // The walk method is used to recursively walk the resulting structure so - // that modifications can be made. - - var k, v, value = holder[key]; - if (value && typeof value === 'object') { - for (k in value) { - if (Object.prototype.hasOwnProperty.call(value, k)) { - v = walk(value, k); - if (v !== undefined) { - value[k] = v; - } else { - delete value[k]; - } - } - } - } - return reviver.call(holder, key, value); - } - - - // Parsing happens in four stages. In the first stage, we replace certain - // Unicode characters with escape sequences. JavaScript handles many characters - // incorrectly, either silently deleting them, or treating them as line endings. - - text = String(text); - cx.lastIndex = 0; - if (cx.test(text)) { - text = text.replace(cx, function (a) { - return '\\u' + - ('0000' + a.charCodeAt(0).toString(16)).slice(-4); - }); - } - - // In the second stage, we run the text against regular expressions that look - // for non-JSON patterns. We are especially concerned with '()' and 'new' - // because they can cause invocation, and '=' because it can cause mutation. - // But just to be safe, we want to reject all unexpected forms. - - // We split the second stage into 4 regexp operations in order to work around - // crippling inefficiencies in IE's and Safari's regexp engines. First we - // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we - // replace all simple value tokens with ']' characters. Third, we delete all - // open brackets that follow a colon or comma or that begin the text. Finally, - // we look to see that the remaining characters are only whitespace or ']' or - // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. - - if (/^[\],:{}\s]*$/ - .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') - .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') - .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - - // In the third stage we use the eval function to compile the text into a - // JavaScript structure. The '{' operator is subject to a syntactic ambiguity - // in JavaScript: it can begin a block or an object literal. We wrap the text - // in parens to eliminate the ambiguity. - - j = eval('(' + text + ')'); - - // In the optional fourth stage, we recursively walk the new structure, passing - // each name/value pair to a reviver function for possible transformation. - - return typeof reviver === 'function' ? - walk({'': j}, '') : j; - } - - // If the text is not JSON parseable, then a SyntaxError is thrown. - - throw new SyntaxError('JSON.parse'); - }; - -})( - 'undefined' != typeof io ? io : module.exports - , typeof JSON !== 'undefined' ? JSON : undefined -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Parser namespace. - * - * @namespace - */ - - var parser = exports.parser = {}; - - /** - * Packet types. - */ - - var packets = parser.packets = [ - 'disconnect' - , 'connect' - , 'heartbeat' - , 'message' - , 'json' - , 'event' - , 'ack' - , 'error' - , 'noop' - ]; - - /** - * Errors reasons. - */ - - var reasons = parser.reasons = [ - 'transport not supported' - , 'client not handshaken' - , 'unauthorized' - ]; - - /** - * Errors advice. - */ - - var advice = parser.advice = [ - 'reconnect' - ]; - - /** - * Shortcuts. - */ - - var JSON = io.JSON - , indexOf = io.util.indexOf; - - /** - * Encodes a packet. - * - * @api private - */ - - parser.encodePacket = function (packet) { - var type = indexOf(packets, packet.type) - , id = packet.id || '' - , endpoint = packet.endpoint || '' - , ack = packet.ack - , data = null; - - switch (packet.type) { - case 'error': - var reason = packet.reason ? indexOf(reasons, packet.reason) : '' - , adv = packet.advice ? indexOf(advice, packet.advice) : ''; - - if (reason !== '' || adv !== '') - data = reason + (adv !== '' ? ('+' + adv) : ''); - - break; - - case 'message': - if (packet.data !== '') - data = packet.data; - break; - - case 'event': - var ev = { name: packet.name }; - - if (packet.args && packet.args.length) { - ev.args = packet.args; - } - - data = JSON.stringify(ev); - break; - - case 'json': - data = JSON.stringify(packet.data); - break; - - case 'connect': - if (packet.qs) - data = packet.qs; - break; - - case 'ack': - data = packet.ackId - + (packet.args && packet.args.length - ? '+' + JSON.stringify(packet.args) : ''); - break; - } - - // construct packet with required fragments - var encoded = [ - type - , id + (ack == 'data' ? '+' : '') - , endpoint - ]; - - // data fragment is optional - if (data !== null && data !== undefined) - encoded.push(data); - - return encoded.join(':'); - }; - - /** - * Encodes multiple messages (payload). - * - * @param {Array} messages - * @api private - */ - - parser.encodePayload = function (packets) { - var decoded = ''; - - if (packets.length == 1) - return packets[0]; - - for (var i = 0, l = packets.length; i < l; i++) { - var packet = packets[i]; - decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]; - } - - return decoded; - }; - - /** - * Decodes a packet - * - * @api private - */ - - var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; - - parser.decodePacket = function (data) { - var pieces = data.match(regexp); - - if (!pieces) return {}; - - var id = pieces[2] || '' - , data = pieces[5] || '' - , packet = { - type: packets[pieces[1]] - , endpoint: pieces[4] || '' - }; - - // whether we need to acknowledge the packet - if (id) { - packet.id = id; - if (pieces[3]) - packet.ack = 'data'; - else - packet.ack = true; - } - - // handle different packet types - switch (packet.type) { - case 'error': - var pieces = data.split('+'); - packet.reason = reasons[pieces[0]] || ''; - packet.advice = advice[pieces[1]] || ''; - break; - - case 'message': - packet.data = data || ''; - break; - - case 'event': - try { - var opts = JSON.parse(data); - packet.name = opts.name; - packet.args = opts.args; - } catch (e) { } - - packet.args = packet.args || []; - break; - - case 'json': - try { - packet.data = JSON.parse(data); - } catch (e) { } - break; - - case 'connect': - packet.qs = data || ''; - break; - - case 'ack': - var pieces = data.match(/^([0-9]+)(\+)?(.*)/); - if (pieces) { - packet.ackId = pieces[1]; - packet.args = []; - - if (pieces[3]) { - try { - packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; - } catch (e) { } - } - } - break; - - case 'disconnect': - case 'heartbeat': - break; - }; - - return packet; - }; - - /** - * Decodes data payload. Detects multiple messages - * - * @return {Array} messages - * @api public - */ - - parser.decodePayload = function (data) { - // IE doesn't like data[i] for unicode chars, charAt works fine - if (data.charAt(0) == '\ufffd') { - var ret = []; - - for (var i = 1, length = ''; i < data.length; i++) { - if (data.charAt(i) == '\ufffd') { - ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length))); - i += Number(length) + 1; - length = ''; - } else { - length += data.charAt(i); - } - } - - return ret; - } else { - return [parser.decodePacket(data)]; - } - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.Transport = Transport; - - /** - * This is the transport template for all supported transport methods. - * - * @constructor - * @api public - */ - - function Transport (socket, sessid) { - this.socket = socket; - this.sessid = sessid; - }; - - /** - * Apply EventEmitter mixin. - */ - - io.util.mixin(Transport, io.EventEmitter); - - - /** - * Indicates whether heartbeats is enabled for this transport - * - * @api private - */ - - Transport.prototype.heartbeats = function () { - return true; - }; - - /** - * Handles the response from the server. When a new response is received - * it will automatically update the timeout, decode the message and - * forwards the response to the onMessage function for further processing. - * - * @param {String} data Response from the server. - * @api private - */ - - Transport.prototype.onData = function (data) { - this.clearCloseTimeout(); - - // If the connection in currently open (or in a reopening state) reset the close - // timeout since we have just received data. This check is necessary so - // that we don't reset the timeout on an explicitly disconnected connection. - if (this.socket.connected || this.socket.connecting || this.socket.reconnecting) { - this.setCloseTimeout(); - } - - if (data !== '') { - // todo: we should only do decodePayload for xhr transports - var msgs = io.parser.decodePayload(data); - - if (msgs && msgs.length) { - for (var i = 0, l = msgs.length; i < l; i++) { - this.onPacket(msgs[i]); - } - } - } - - return this; - }; - - /** - * Handles packets. - * - * @api private - */ - - Transport.prototype.onPacket = function (packet) { - this.socket.setHeartbeatTimeout(); - - if (packet.type == 'heartbeat') { - return this.onHeartbeat(); - } - - if (packet.type == 'connect' && packet.endpoint == '') { - this.onConnect(); - } - - if (packet.type == 'error' && packet.advice == 'reconnect') { - this.isOpen = false; - } - - this.socket.onPacket(packet); - - return this; - }; - - /** - * Sets close timeout - * - * @api private - */ - - Transport.prototype.setCloseTimeout = function () { - if (!this.closeTimeout) { - var self = this; - - this.closeTimeout = setTimeout(function () { - self.onDisconnect(); - }, this.socket.closeTimeout); - } - }; - - /** - * Called when transport disconnects. - * - * @api private - */ - - Transport.prototype.onDisconnect = function () { - if (this.isOpen) this.close(); - this.clearTimeouts(); - this.socket.onDisconnect(); - return this; - }; - - /** - * Called when transport connects - * - * @api private - */ - - Transport.prototype.onConnect = function () { - this.socket.onConnect(); - return this; - }; - - /** - * Clears close timeout - * - * @api private - */ - - Transport.prototype.clearCloseTimeout = function () { - if (this.closeTimeout) { - clearTimeout(this.closeTimeout); - this.closeTimeout = null; - } - }; - - /** - * Clear timeouts - * - * @api private - */ - - Transport.prototype.clearTimeouts = function () { - this.clearCloseTimeout(); - - if (this.reopenTimeout) { - clearTimeout(this.reopenTimeout); - } - }; - - /** - * Sends a packet - * - * @param {Object} packet object. - * @api private - */ - - Transport.prototype.packet = function (packet) { - this.send(io.parser.encodePacket(packet)); - }; - - /** - * Send the received heartbeat message back to server. So the server - * knows we are still connected. - * - * @param {String} heartbeat Heartbeat response from the server. - * @api private - */ - - Transport.prototype.onHeartbeat = function (heartbeat) { - this.packet({ type: 'heartbeat' }); - }; - - /** - * Called when the transport opens. - * - * @api private - */ - - Transport.prototype.onOpen = function () { - this.isOpen = true; - this.clearCloseTimeout(); - this.socket.onOpen(); - }; - - /** - * Notifies the base when the connection with the Socket.IO server - * has been disconnected. - * - * @api private - */ - - Transport.prototype.onClose = function () { - var self = this; - - /* FIXME: reopen delay causing a infinit loop - this.reopenTimeout = setTimeout(function () { - self.open(); - }, this.socket.options['reopen delay']);*/ - - this.isOpen = false; - this.socket.onClose(); - this.onDisconnect(); - }; - - /** - * Generates a connection url based on the Socket.IO URL Protocol. - * See for more details. - * - * @returns {String} Connection url - * @api private - */ - - Transport.prototype.prepareUrl = function () { - var options = this.socket.options; - - return this.scheme() + '://' - + options.host + ':' + options.port + '/' - + options.resource + '/' + io.protocol - + '/' + this.name + '/' + this.sessid; - }; - - /** - * Checks if the transport is ready to start a connection. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ - - Transport.prototype.ready = function (socket, fn) { - fn.call(this); - }; -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports.Socket = Socket; - - /** - * Create a new `Socket.IO client` which can establish a persistent - * connection with a Socket.IO enabled server. - * - * @api public - */ - - function Socket (options) { - this.options = { - port: 80 - , secure: false - , document: 'document' in global ? document : false - , resource: 'socket.io' - , transports: io.transports - , 'connect timeout': 10000 - , 'try multiple transports': true - , 'reconnect': true - , 'reconnection delay': 500 - , 'reconnection limit': Infinity - , 'reopen delay': 3000 - , 'max reconnection attempts': 10 - , 'sync disconnect on unload': false - , 'auto connect': true - , 'flash policy port': 10843 - , 'manualFlush': false - }; - - io.util.merge(this.options, options); - - this.connected = false; - this.open = false; - this.connecting = false; - this.reconnecting = false; - this.namespaces = {}; - this.buffer = []; - this.doBuffer = false; - - if (this.options['sync disconnect on unload'] && - (!this.isXDomain() || io.util.ua.hasCORS)) { - var self = this; - io.util.on(global, 'beforeunload', function () { - self.disconnectSync(); - }, false); - } - - if (this.options['auto connect']) { - this.connect(); - } -}; - - /** - * Apply EventEmitter mixin. - */ - - io.util.mixin(Socket, io.EventEmitter); - - /** - * Returns a namespace listener/emitter for this socket - * - * @api public - */ - - Socket.prototype.of = function (name) { - if (!this.namespaces[name]) { - this.namespaces[name] = new io.SocketNamespace(this, name); - - if (name !== '') { - this.namespaces[name].packet({ type: 'connect' }); - } - } - - return this.namespaces[name]; - }; - - /** - * Emits the given event to the Socket and all namespaces - * - * @api private - */ - - Socket.prototype.publish = function () { - this.emit.apply(this, arguments); - - var nsp; - - for (var i in this.namespaces) { - if (this.namespaces.hasOwnProperty(i)) { - nsp = this.of(i); - nsp.$emit.apply(nsp, arguments); - } - } - }; - - /** - * Performs the handshake - * - * @api private - */ - - function empty () { }; - - Socket.prototype.handshake = function (fn) { - var self = this - , options = this.options; - - function complete (data) { - if (data instanceof Error) { - self.connecting = false; - self.onError(data.message); - } else { - fn.apply(null, data.split(':')); - } - }; - - var url = [ - 'http' + (options.secure ? 's' : '') + ':/' - , options.host + ':' + options.port - , options.resource - , io.protocol - , io.util.query(this.options.query, 't=' + +new Date) - ].join('/'); - - if (this.isXDomain() && !io.util.ua.hasCORS) { - var insertAt = document.getElementsByTagName('script')[0] - , script = document.createElement('script'); - - script.src = url + '&jsonp=' + io.j.length; - insertAt.parentNode.insertBefore(script, insertAt); - - io.j.push(function (data) { - complete(data); - script.parentNode.removeChild(script); - }); - } else { - var xhr = io.util.request(); - - xhr.open('GET', url, true); - if (this.isXDomain()) { - xhr.withCredentials = true; - } - xhr.onreadystatechange = function () { - if (xhr.readyState == 4) { - xhr.onreadystatechange = empty; - - if (xhr.status == 200) { - complete(xhr.responseText); - } else if (xhr.status == 403) { - self.onError(xhr.responseText); - } else { - self.connecting = false; - !self.reconnecting && self.onError(xhr.responseText); - } - } - }; - xhr.send(null); - } - }; - - /** - * Find an available transport based on the options supplied in the constructor. - * - * @api private - */ - - Socket.prototype.getTransport = function (override) { - var transports = override || this.transports, match; - - for (var i = 0, transport; transport = transports[i]; i++) { - if (io.Transport[transport] - && io.Transport[transport].check(this) - && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) { - return new io.Transport[transport](this, this.sessionid); - } - } - - return null; - }; - - /** - * Connects to the server. - * - * @param {Function} [fn] Callback. - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.connect = function (fn) { - if (this.connecting) { - return this; - } - - var self = this; - self.connecting = true; - - this.handshake(function (sid, heartbeat, close, transports) { - self.sessionid = sid; - self.closeTimeout = close * 1000; - self.heartbeatTimeout = heartbeat * 1000; - if(!self.transports) - self.transports = self.origTransports = (transports ? io.util.intersect( - transports.split(',') - , self.options.transports - ) : self.options.transports); - - self.setHeartbeatTimeout(); - - function connect (transports){ - if (self.transport) self.transport.clearTimeouts(); - - self.transport = self.getTransport(transports); - if (!self.transport) return self.publish('connect_failed'); - - // once the transport is ready - self.transport.ready(self, function () { - self.connecting = true; - self.publish('connecting', self.transport.name); - self.transport.open(); - - if (self.options['connect timeout']) { - self.connectTimeoutTimer = setTimeout(function () { - if (!self.connected) { - self.connecting = false; - - if (self.options['try multiple transports']) { - var remaining = self.transports; - - while (remaining.length > 0 && remaining.splice(0,1)[0] != - self.transport.name) {} - - if (remaining.length){ - connect(remaining); - } else { - self.publish('connect_failed'); - } - } - } - }, self.options['connect timeout']); - } - }); - } - - connect(self.transports); - - self.once('connect', function (){ - clearTimeout(self.connectTimeoutTimer); - - fn && typeof fn == 'function' && fn(); - }); - }); - - return this; - }; - - /** - * Clears and sets a new heartbeat timeout using the value given by the - * server during the handshake. - * - * @api private - */ - - Socket.prototype.setHeartbeatTimeout = function () { - clearTimeout(this.heartbeatTimeoutTimer); - if(this.transport && !this.transport.heartbeats()) return; - - var self = this; - this.heartbeatTimeoutTimer = setTimeout(function () { - self.transport.onClose(); - }, this.heartbeatTimeout); - }; - - /** - * Sends a message. - * - * @param {Object} data packet. - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.packet = function (data) { - if (this.connected && !this.doBuffer) { - this.transport.packet(data); - } else { - this.buffer.push(data); - } - - return this; - }; - - /** - * Sets buffer state - * - * @api private - */ - - Socket.prototype.setBuffer = function (v) { - this.doBuffer = v; - - if (!v && this.connected && this.buffer.length) { - if (!this.options['manualFlush']) { - this.flushBuffer(); - } - } - }; - - /** - * Flushes the buffer data over the wire. - * To be invoked manually when 'manualFlush' is set to true. - * - * @api public - */ - - Socket.prototype.flushBuffer = function() { - this.transport.payload(this.buffer); - this.buffer = []; - }; - - - /** - * Disconnect the established connect. - * - * @returns {io.Socket} - * @api public - */ - - Socket.prototype.disconnect = function () { - if (this.connected || this.connecting) { - if (this.open) { - this.of('').packet({ type: 'disconnect' }); - } - - // handle disconnection immediately - this.onDisconnect('booted'); - } - - return this; - }; - - /** - * Disconnects the socket with a sync XHR. - * - * @api private - */ - - Socket.prototype.disconnectSync = function () { - // ensure disconnection - var xhr = io.util.request(); - var uri = [ - 'http' + (this.options.secure ? 's' : '') + ':/' - , this.options.host + ':' + this.options.port - , this.options.resource - , io.protocol - , '' - , this.sessionid - ].join('/') + '/?disconnect=1'; - - xhr.open('GET', uri, false); - xhr.send(null); - - // handle disconnection immediately - this.onDisconnect('booted'); - }; - - /** - * Check if we need to use cross domain enabled transports. Cross domain would - * be a different port or different domain name. - * - * @returns {Boolean} - * @api private - */ - - Socket.prototype.isXDomain = function () { - - var port = global.location.port || - ('https:' == global.location.protocol ? 443 : 80); - - return this.options.host !== global.location.hostname - || this.options.port != port; - }; - - /** - * Called upon handshake. - * - * @api private - */ - - Socket.prototype.onConnect = function () { - if (!this.connected) { - this.connected = true; - this.connecting = false; - if (!this.doBuffer) { - // make sure to flush the buffer - this.setBuffer(false); - } - this.emit('connect'); - } - }; - - /** - * Called when the transport opens - * - * @api private - */ - - Socket.prototype.onOpen = function () { - this.open = true; - }; - - /** - * Called when the transport closes. - * - * @api private - */ - - Socket.prototype.onClose = function () { - this.open = false; - clearTimeout(this.heartbeatTimeoutTimer); - }; - - /** - * Called when the transport first opens a connection - * - * @param text - */ - - Socket.prototype.onPacket = function (packet) { - this.of(packet.endpoint).onPacket(packet); - }; - - /** - * Handles an error. - * - * @api private - */ - - Socket.prototype.onError = function (err) { - if (err && err.advice) { - if (err.advice === 'reconnect' && (this.connected || this.connecting)) { - this.disconnect(); - if (this.options.reconnect) { - this.reconnect(); - } - } - } - - this.publish('error', err && err.reason ? err.reason : err); - }; - - /** - * Called when the transport disconnects. - * - * @api private - */ - - Socket.prototype.onDisconnect = function (reason) { - var wasConnected = this.connected - , wasConnecting = this.connecting; - - this.connected = false; - this.connecting = false; - this.open = false; - - if (wasConnected || wasConnecting) { - this.transport.close(); - this.transport.clearTimeouts(); - if (wasConnected) { - this.publish('disconnect', reason); - - if ('booted' != reason && this.options.reconnect && !this.reconnecting) { - this.reconnect(); - } - } - } - }; - - /** - * Called upon reconnection. - * - * @api private - */ - - Socket.prototype.reconnect = function () { - this.reconnecting = true; - this.reconnectionAttempts = 0; - this.reconnectionDelay = this.options['reconnection delay']; - - var self = this - , maxAttempts = this.options['max reconnection attempts'] - , tryMultiple = this.options['try multiple transports'] - , limit = this.options['reconnection limit']; - - function reset () { - if (self.connected) { - for (var i in self.namespaces) { - if (self.namespaces.hasOwnProperty(i) && '' !== i) { - self.namespaces[i].packet({ type: 'connect' }); - } - } - self.publish('reconnect', self.transport.name, self.reconnectionAttempts); - } - - clearTimeout(self.reconnectionTimer); - - self.removeListener('connect_failed', maybeReconnect); - self.removeListener('connect', maybeReconnect); - - self.reconnecting = false; - - delete self.reconnectionAttempts; - delete self.reconnectionDelay; - delete self.reconnectionTimer; - delete self.redoTransports; - - self.options['try multiple transports'] = tryMultiple; - }; - - function maybeReconnect () { - if (!self.reconnecting) { - return; - } - - if (self.connected) { - return reset(); - }; - - if (self.connecting && self.reconnecting) { - return self.reconnectionTimer = setTimeout(maybeReconnect, 1000); - } - - if (self.reconnectionAttempts++ >= maxAttempts) { - if (!self.redoTransports) { - self.on('connect_failed', maybeReconnect); - self.options['try multiple transports'] = true; - self.transports = self.origTransports; - self.transport = self.getTransport(); - self.redoTransports = true; - self.connect(); - } else { - self.publish('reconnect_failed'); - reset(); - } - } else { - if (self.reconnectionDelay < limit) { - self.reconnectionDelay *= 2; // exponential back off - } - - self.connect(); - self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts); - self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay); - } - }; - - this.options['try multiple transports'] = false; - this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay); - - this.on('connect', maybeReconnect); - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.SocketNamespace = SocketNamespace; - - /** - * Socket namespace constructor. - * - * @constructor - * @api public - */ - - function SocketNamespace (socket, name) { - this.socket = socket; - this.name = name || ''; - this.flags = {}; - this.json = new Flag(this, 'json'); - this.ackPackets = 0; - this.acks = {}; - }; - - /** - * Apply EventEmitter mixin. - */ - - io.util.mixin(SocketNamespace, io.EventEmitter); - - /** - * Copies emit since we override it - * - * @api private - */ - - SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit; - - /** - * Creates a new namespace, by proxying the request to the socket. This - * allows us to use the synax as we do on the server. - * - * @api public - */ - - SocketNamespace.prototype.of = function () { - return this.socket.of.apply(this.socket, arguments); - }; - - /** - * Sends a packet. - * - * @api private - */ - - SocketNamespace.prototype.packet = function (packet) { - packet.endpoint = this.name; - this.socket.packet(packet); - this.flags = {}; - return this; - }; - - /** - * Sends a message - * - * @api public - */ - - SocketNamespace.prototype.send = function (data, fn) { - var packet = { - type: this.flags.json ? 'json' : 'message' - , data: data - }; - - if ('function' == typeof fn) { - packet.id = ++this.ackPackets; - packet.ack = true; - this.acks[packet.id] = fn; - } - - return this.packet(packet); - }; - - /** - * Emits an event - * - * @api public - */ - - SocketNamespace.prototype.emit = function (name) { - var args = Array.prototype.slice.call(arguments, 1) - , lastArg = args[args.length - 1] - , packet = { - type: 'event' - , name: name - }; - - if ('function' == typeof lastArg) { - packet.id = ++this.ackPackets; - packet.ack = 'data'; - this.acks[packet.id] = lastArg; - args = args.slice(0, args.length - 1); - } - - packet.args = args; - - return this.packet(packet); - }; - - /** - * Disconnects the namespace - * - * @api private - */ - - SocketNamespace.prototype.disconnect = function () { - if (this.name === '') { - this.socket.disconnect(); - } else { - this.packet({ type: 'disconnect' }); - this.$emit('disconnect'); - } - - return this; - }; - - /** - * Handles a packet - * - * @api private - */ - - SocketNamespace.prototype.onPacket = function (packet) { - var self = this; - - function ack () { - self.packet({ - type: 'ack' - , args: io.util.toArray(arguments) - , ackId: packet.id - }); - }; - - switch (packet.type) { - case 'connect': - this.$emit('connect'); - break; - - case 'disconnect': - if (this.name === '') { - this.socket.onDisconnect(packet.reason || 'booted'); - } else { - this.$emit('disconnect', packet.reason); - } - break; - - case 'message': - case 'json': - var params = ['message', packet.data]; - - if (packet.ack == 'data') { - params.push(ack); - } else if (packet.ack) { - this.packet({ type: 'ack', ackId: packet.id }); - } - - this.$emit.apply(this, params); - break; - - case 'event': - var params = [packet.name].concat(packet.args); - - if (packet.ack == 'data') - params.push(ack); - - this.$emit.apply(this, params); - break; - - case 'ack': - if (this.acks[packet.ackId]) { - this.acks[packet.ackId].apply(this, packet.args); - delete this.acks[packet.ackId]; - } - break; - - case 'error': - if (packet.advice){ - this.socket.onError(packet); - } else { - if (packet.reason == 'unauthorized') { - this.$emit('connect_failed', packet.reason); - } else { - this.$emit('error', packet.reason); - } - } - break; - } - }; - - /** - * Flag interface. - * - * @api private - */ - - function Flag (nsp, name) { - this.namespace = nsp; - this.name = name; - }; - - /** - * Send a message - * - * @api public - */ - - Flag.prototype.send = function () { - this.namespace.flags[this.name] = true; - this.namespace.send.apply(this.namespace, arguments); - }; - - /** - * Emit an event - * - * @api public - */ - - Flag.prototype.emit = function () { - this.namespace.flags[this.name] = true; - this.namespace.emit.apply(this.namespace, arguments); - }; - -})( - 'undefined' != typeof io ? io : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports.websocket = WS; - - /** - * The WebSocket transport uses the HTML5 WebSocket API to establish an - * persistent connection with the Socket.IO server. This transport will also - * be inherited by the FlashSocket fallback as it provides a API compatible - * polyfill for the WebSockets. - * - * @constructor - * @extends {io.Transport} - * @api public - */ - - function WS (socket) { - io.Transport.apply(this, arguments); - }; - - /** - * Inherits from Transport. - */ - - io.util.inherit(WS, io.Transport); - - /** - * Transport name - * - * @api public - */ - - WS.prototype.name = 'websocket'; - - /** - * Initializes a new `WebSocket` connection with the Socket.IO server. We attach - * all the appropriate listeners to handle the responses from the server. - * - * @returns {Transport} - * @api public - */ - - WS.prototype.open = function () { - var query = io.util.query(this.socket.options.query) - , self = this - , Socket - - - if (!Socket) { - Socket = global.MozWebSocket || global.WebSocket; - } - - this.websocket = new Socket(this.prepareUrl() + query); - - this.websocket.onopen = function () { - self.onOpen(); - self.socket.setBuffer(false); - }; - this.websocket.onmessage = function (ev) { - self.onData(ev.data); - }; - this.websocket.onclose = function () { - self.onClose(); - self.socket.setBuffer(true); - }; - this.websocket.onerror = function (e) { - self.onError(e); - }; - - return this; - }; - - /** - * Send a message to the Socket.IO server. The message will automatically be - * encoded in the correct message format. - * - * @returns {Transport} - * @api public - */ - - // Do to a bug in the current IDevices browser, we need to wrap the send in a - // setTimeout, when they resume from sleeping the browser will crash if - // we don't allow the browser time to detect the socket has been closed - if (io.util.ua.iDevice) { - WS.prototype.send = function (data) { - var self = this; - setTimeout(function() { - self.websocket.send(data); - },0); - return this; - }; - } else { - WS.prototype.send = function (data) { - this.websocket.send(data); - return this; - }; - } - - /** - * Payload - * - * @api private - */ - - WS.prototype.payload = function (arr) { - for (var i = 0, l = arr.length; i < l; i++) { - this.packet(arr[i]); - } - return this; - }; - - /** - * Disconnect the established `WebSocket` connection. - * - * @returns {Transport} - * @api public - */ - - WS.prototype.close = function () { - this.websocket.close(); - return this; - }; - - /** - * Handle the errors that `WebSocket` might be giving when we - * are attempting to connect or send messages. - * - * @param {Error} e The error. - * @api private - */ - - WS.prototype.onError = function (e) { - this.socket.onError(e); - }; - - /** - * Returns the appropriate scheme for the URI generation. - * - * @api private - */ - WS.prototype.scheme = function () { - return this.socket.options.secure ? 'wss' : 'ws'; - }; - - /** - * Checks if the browser has support for native `WebSockets` and that - * it's not the polyfill created for the FlashSocket transport. - * - * @return {Boolean} - * @api public - */ - - WS.check = function () { - return ('WebSocket' in global && !('__addTask' in WebSocket)) - || 'MozWebSocket' in global; - }; - - /** - * Check if the `WebSocket` transport support cross domain communications. - * - * @returns {Boolean} - * @api public - */ - - WS.xdomainCheck = function () { - return true; - }; - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('websocket'); - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.flashsocket = Flashsocket; - - /** - * The FlashSocket transport. This is a API wrapper for the HTML5 WebSocket - * specification. It uses a .swf file to communicate with the server. If you want - * to serve the .swf file from a other server than where the Socket.IO script is - * coming from you need to use the insecure version of the .swf. More information - * about this can be found on the github page. - * - * @constructor - * @extends {io.Transport.websocket} - * @api public - */ - - function Flashsocket () { - io.Transport.websocket.apply(this, arguments); - }; - - /** - * Inherits from Transport. - */ - - io.util.inherit(Flashsocket, io.Transport.websocket); - - /** - * Transport name - * - * @api public - */ - - Flashsocket.prototype.name = 'flashsocket'; - - /** - * Disconnect the established `FlashSocket` connection. This is done by adding a - * new task to the FlashSocket. The rest will be handled off by the `WebSocket` - * transport. - * - * @returns {Transport} - * @api public - */ - - Flashsocket.prototype.open = function () { - var self = this - , args = arguments; - - WebSocket.__addTask(function () { - io.Transport.websocket.prototype.open.apply(self, args); - }); - return this; - }; - - /** - * Sends a message to the Socket.IO server. This is done by adding a new - * task to the FlashSocket. The rest will be handled off by the `WebSocket` - * transport. - * - * @returns {Transport} - * @api public - */ - - Flashsocket.prototype.send = function () { - var self = this, args = arguments; - WebSocket.__addTask(function () { - io.Transport.websocket.prototype.send.apply(self, args); - }); - return this; - }; - - /** - * Disconnects the established `FlashSocket` connection. - * - * @returns {Transport} - * @api public - */ - - Flashsocket.prototype.close = function () { - WebSocket.__tasks.length = 0; - io.Transport.websocket.prototype.close.call(this); - return this; - }; - - /** - * The WebSocket fall back needs to append the flash container to the body - * element, so we need to make sure we have access to it. Or defer the call - * until we are sure there is a body element. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ - - Flashsocket.prototype.ready = function (socket, fn) { - function init () { - var options = socket.options - , port = options['flash policy port'] - , path = [ - 'http' + (options.secure ? 's' : '') + ':/' - , options.host + ':' + options.port - , options.resource - , 'static/flashsocket' - , 'WebSocketMain' + (socket.isXDomain() ? 'Insecure' : '') + '.swf' - ]; - - // Only start downloading the swf file when the checked that this browser - // actually supports it - if (!Flashsocket.loaded) { - if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') { - // Set the correct file based on the XDomain settings - WEB_SOCKET_SWF_LOCATION = path.join('/'); - } - - if (port !== 843) { - WebSocket.loadFlashPolicyFile('xmlsocket://' + options.host + ':' + port); - } - - WebSocket.__initialize(); - Flashsocket.loaded = true; - } - - fn.call(self); - } - - var self = this; - if (document.body) return init(); - - io.util.load(init); - }; - - /** - * Check if the FlashSocket transport is supported as it requires that the Adobe - * Flash Player plug-in version `10.0.0` or greater is installed. And also check if - * the polyfill is correctly loaded. - * - * @returns {Boolean} - * @api public - */ - - Flashsocket.check = function () { - if ( - typeof WebSocket == 'undefined' - || !('__initialize' in WebSocket) || !swfobject - ) return false; - - return swfobject.getFlashPlayerVersion().major >= 10; - }; - - /** - * Check if the FlashSocket transport can be used as cross domain / cross origin - * transport. Because we can't see which type (secure or insecure) of .swf is used - * we will just return true. - * - * @returns {Boolean} - * @api public - */ - - Flashsocket.xdomainCheck = function () { - return true; - }; - - /** - * Disable AUTO_INITIALIZATION - */ - - if (typeof window != 'undefined') { - WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; - } - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('flashsocket'); -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); -/* SWFObject v2.2 - is released under the MIT License -*/ -if ('undefined' != typeof window) { -var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O[(['Active'].concat('Object').join('X'))]!=D){try{var ad=new window[(['Active'].concat('Object').join('X'))](W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab -// License: New BSD License -// Reference: http://dev.w3.org/html5/websockets/ -// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol - -(function() { - - if ('undefined' == typeof window || window.WebSocket) return; - - var console = window.console; - if (!console || !console.log || !console.error) { - console = {log: function(){ }, error: function(){ }}; - } - - if (!swfobject.hasFlashPlayerVersion("10.0.0")) { - console.error("Flash Player >= 10.0.0 is required."); - return; - } - if (location.protocol == "file:") { - console.error( - "WARNING: web-socket-js doesn't work in file:///... URL " + - "unless you set Flash Security Settings properly. " + - "Open the page via Web server i.e. http://..."); - } - - /** - * This class represents a faux web socket. - * @param {string} url - * @param {array or string} protocols - * @param {string} proxyHost - * @param {int} proxyPort - * @param {string} headers - */ - WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { - var self = this; - self.__id = WebSocket.__nextId++; - WebSocket.__instances[self.__id] = self; - self.readyState = WebSocket.CONNECTING; - self.bufferedAmount = 0; - self.__events = {}; - if (!protocols) { - protocols = []; - } else if (typeof protocols == "string") { - protocols = [protocols]; - } - // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. - // Otherwise, when onopen fires immediately, onopen is called before it is set. - setTimeout(function() { - WebSocket.__addTask(function() { - WebSocket.__flash.create( - self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); - }); - }, 0); - }; - - /** - * Send data to the web socket. - * @param {string} data The data to send to the socket. - * @return {boolean} True for success, false for failure. - */ - WebSocket.prototype.send = function(data) { - if (this.readyState == WebSocket.CONNECTING) { - throw "INVALID_STATE_ERR: Web Socket connection has not been established"; - } - // We use encodeURIComponent() here, because FABridge doesn't work if - // the argument includes some characters. We don't use escape() here - // because of this: - // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions - // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't - // preserve all Unicode characters either e.g. "\uffff" in Firefox. - // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require - // additional testing. - var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); - if (result < 0) { // success - return true; - } else { - this.bufferedAmount += result; - return false; - } - }; - - /** - * Close this web socket gracefully. - */ - WebSocket.prototype.close = function() { - if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { - return; - } - this.readyState = WebSocket.CLOSING; - WebSocket.__flash.close(this.__id); - }; - - /** - * Implementation of {@link
DOM 2 EventTarget Interface} - * - * @param {string} type - * @param {function} listener - * @param {boolean} useCapture - * @return void - */ - WebSocket.prototype.addEventListener = function(type, listener, useCapture) { - if (!(type in this.__events)) { - this.__events[type] = []; - } - this.__events[type].push(listener); - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {string} type - * @param {function} listener - * @param {boolean} useCapture - * @return void - */ - WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { - if (!(type in this.__events)) return; - var events = this.__events[type]; - for (var i = events.length - 1; i >= 0; --i) { - if (events[i] === listener) { - events.splice(i, 1); - break; - } - } - }; - - /** - * Implementation of {@link DOM 2 EventTarget Interface} - * - * @param {Event} event - * @return void - */ - WebSocket.prototype.dispatchEvent = function(event) { - var events = this.__events[event.type] || []; - for (var i = 0; i < events.length; ++i) { - events[i](event); - } - var handler = this["on" + event.type]; - if (handler) handler(event); - }; - - /** - * Handles an event from Flash. - * @param {Object} flashEvent - */ - WebSocket.prototype.__handleEvent = function(flashEvent) { - if ("readyState" in flashEvent) { - this.readyState = flashEvent.readyState; - } - if ("protocol" in flashEvent) { - this.protocol = flashEvent.protocol; - } - - var jsEvent; - if (flashEvent.type == "open" || flashEvent.type == "error") { - jsEvent = this.__createSimpleEvent(flashEvent.type); - } else if (flashEvent.type == "close") { - // TODO implement jsEvent.wasClean - jsEvent = this.__createSimpleEvent("close"); - } else if (flashEvent.type == "message") { - var data = decodeURIComponent(flashEvent.message); - jsEvent = this.__createMessageEvent("message", data); - } else { - throw "unknown event type: " + flashEvent.type; - } - - this.dispatchEvent(jsEvent); - }; - - WebSocket.prototype.__createSimpleEvent = function(type) { - if (document.createEvent && window.Event) { - var event = document.createEvent("Event"); - event.initEvent(type, false, false); - return event; - } else { - return {type: type, bubbles: false, cancelable: false}; - } - }; - - WebSocket.prototype.__createMessageEvent = function(type, data) { - if (document.createEvent && window.MessageEvent && !window.opera) { - var event = document.createEvent("MessageEvent"); - event.initMessageEvent("message", false, false, data, null, null, window, null); - return event; - } else { - // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. - return {type: type, data: data, bubbles: false, cancelable: false}; - } - }; - - /** - * Define the WebSocket readyState enumeration. - */ - WebSocket.CONNECTING = 0; - WebSocket.OPEN = 1; - WebSocket.CLOSING = 2; - WebSocket.CLOSED = 3; - - WebSocket.__flash = null; - WebSocket.__instances = {}; - WebSocket.__tasks = []; - WebSocket.__nextId = 0; - - /** - * Load a new flash security policy file. - * @param {string} url - */ - WebSocket.loadFlashPolicyFile = function(url){ - WebSocket.__addTask(function() { - WebSocket.__flash.loadManualPolicyFile(url); - }); - }; - - /** - * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. - */ - WebSocket.__initialize = function() { - if (WebSocket.__flash) return; - - if (WebSocket.__swfLocation) { - // For backword compatibility. - window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; - } - if (!window.WEB_SOCKET_SWF_LOCATION) { - console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); - return; - } - var container = document.createElement("div"); - container.id = "webSocketContainer"; - // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents - // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). - // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash - // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is - // the best we can do as far as we know now. - container.style.position = "absolute"; - if (WebSocket.__isFlashLite()) { - container.style.left = "0px"; - container.style.top = "0px"; - } else { - container.style.left = "-100px"; - container.style.top = "-100px"; - } - var holder = document.createElement("div"); - holder.id = "webSocketFlash"; - container.appendChild(holder); - document.body.appendChild(container); - // See this article for hasPriority: - // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html - swfobject.embedSWF( - WEB_SOCKET_SWF_LOCATION, - "webSocketFlash", - "1" /* width */, - "1" /* height */, - "10.0.0" /* SWF version */, - null, - null, - {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, - null, - function(e) { - if (!e.success) { - console.error("[WebSocket] swfobject.embedSWF failed"); - } - }); - }; - - /** - * Called by Flash to notify JS that it's fully loaded and ready - * for communication. - */ - WebSocket.__onFlashInitialized = function() { - // We need to set a timeout here to avoid round-trip calls - // to flash during the initialization process. - setTimeout(function() { - WebSocket.__flash = document.getElementById("webSocketFlash"); - WebSocket.__flash.setCallerUrl(location.href); - WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); - for (var i = 0; i < WebSocket.__tasks.length; ++i) { - WebSocket.__tasks[i](); - } - WebSocket.__tasks = []; - }, 0); - }; - - /** - * Called by Flash to notify WebSockets events are fired. - */ - WebSocket.__onFlashEvent = function() { - setTimeout(function() { - try { - // Gets events using receiveEvents() instead of getting it from event object - // of Flash event. This is to make sure to keep message order. - // It seems sometimes Flash events don't arrive in the same order as they are sent. - var events = WebSocket.__flash.receiveEvents(); - for (var i = 0; i < events.length; ++i) { - WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); - } - } catch (e) { - console.error(e); - } - }, 0); - return true; - }; - - // Called by Flash. - WebSocket.__log = function(message) { - console.log(decodeURIComponent(message)); - }; - - // Called by Flash. - WebSocket.__error = function(message) { - console.error(decodeURIComponent(message)); - }; - - WebSocket.__addTask = function(task) { - if (WebSocket.__flash) { - task(); - } else { - WebSocket.__tasks.push(task); - } - }; - - /** - * Test if the browser is running flash lite. - * @return {boolean} True if flash lite is running, false otherwise. - */ - WebSocket.__isFlashLite = function() { - if (!window.navigator || !window.navigator.mimeTypes) { - return false; - } - var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; - if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { - return false; - } - return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; - }; - - if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { - if (window.addEventListener) { - window.addEventListener("load", function(){ - WebSocket.__initialize(); - }, false); - } else { - window.attachEvent("onload", function(){ - WebSocket.__initialize(); - }); - } - } - -})(); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - * - * @api public - */ - - exports.XHR = XHR; - - /** - * XHR constructor - * - * @costructor - * @api public - */ - - function XHR (socket) { - if (!socket) return; - - io.Transport.apply(this, arguments); - this.sendBuffer = []; - }; - - /** - * Inherits from Transport. - */ - - io.util.inherit(XHR, io.Transport); - - /** - * Establish a connection - * - * @returns {Transport} - * @api public - */ - - XHR.prototype.open = function () { - this.socket.setBuffer(false); - this.onOpen(); - this.get(); - - // we need to make sure the request succeeds since we have no indication - // whether the request opened or not until it succeeded. - this.setCloseTimeout(); - - return this; - }; - - /** - * Check if we need to send data to the Socket.IO server, if we have data in our - * buffer we encode it and forward it to the `post` method. - * - * @api private - */ - - XHR.prototype.payload = function (payload) { - var msgs = []; - - for (var i = 0, l = payload.length; i < l; i++) { - msgs.push(io.parser.encodePacket(payload[i])); - } - - this.send(io.parser.encodePayload(msgs)); - }; - - /** - * Send data to the Socket.IO server. - * - * @param data The message - * @returns {Transport} - * @api public - */ - - XHR.prototype.send = function (data) { - this.post(data); - return this; - }; - - /** - * Posts a encoded message to the Socket.IO server. - * - * @param {String} data A encoded message. - * @api private - */ - - function empty () { }; - - XHR.prototype.post = function (data) { - var self = this; - this.socket.setBuffer(true); - - function stateChange () { - if (this.readyState == 4) { - this.onreadystatechange = empty; - self.posting = false; - - if (this.status == 200){ - self.socket.setBuffer(false); - } else { - self.onClose(); - } - } - } - - function onload () { - this.onload = empty; - self.socket.setBuffer(false); - }; - - this.sendXHR = this.request('POST'); - - if (global.XDomainRequest && this.sendXHR instanceof XDomainRequest) { - this.sendXHR.onload = this.sendXHR.onerror = onload; - } else { - this.sendXHR.onreadystatechange = stateChange; - } - - this.sendXHR.send(data); - }; - - /** - * Disconnects the established `XHR` connection. - * - * @returns {Transport} - * @api public - */ - - XHR.prototype.close = function () { - this.onClose(); - return this; - }; - - /** - * Generates a configured XHR request - * - * @param {String} url The url that needs to be requested. - * @param {String} method The method the request should use. - * @returns {XMLHttpRequest} - * @api private - */ - - XHR.prototype.request = function (method) { - var req = io.util.request(this.socket.isXDomain()) - , query = io.util.query(this.socket.options.query, 't=' + +new Date); - - req.open(method || 'GET', this.prepareUrl() + query, true); - - if (method == 'POST') { - try { - if (req.setRequestHeader) { - req.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); - } else { - // XDomainRequest - req.contentType = 'text/plain'; - } - } catch (e) {} - } - - return req; - }; - - /** - * Returns the scheme to use for the transport URLs. - * - * @api private - */ - - XHR.prototype.scheme = function () { - return this.socket.options.secure ? 'https' : 'http'; - }; - - /** - * Check if the XHR transports are supported - * - * @param {Boolean} xdomain Check if we support cross domain requests. - * @returns {Boolean} - * @api public - */ - - XHR.check = function (socket, xdomain) { - try { - var request = io.util.request(xdomain), - usesXDomReq = (global.XDomainRequest && request instanceof XDomainRequest), - socketProtocol = (socket && socket.options && socket.options.secure ? 'https:' : 'http:'), - isXProtocol = (global.location && socketProtocol != global.location.protocol); - if (request && !(usesXDomReq && isXProtocol)) { - return true; - } - } catch(e) {} - - return false; - }; - - /** - * Check if the XHR transport supports cross domain requests. - * - * @returns {Boolean} - * @api public - */ - - XHR.xdomainCheck = function (socket) { - return XHR.check(socket, true); - }; - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io) { - - /** - * Expose constructor. - */ - - exports.htmlfile = HTMLFile; - - /** - * The HTMLFile transport creates a `forever iframe` based transport - * for Internet Explorer. Regular forever iframe implementations will - * continuously trigger the browsers buzy indicators. If the forever iframe - * is created inside a `htmlfile` these indicators will not be trigged. - * - * @constructor - * @extends {io.Transport.XHR} - * @api public - */ - - function HTMLFile (socket) { - io.Transport.XHR.apply(this, arguments); - }; - - /** - * Inherits from XHR transport. - */ - - io.util.inherit(HTMLFile, io.Transport.XHR); - - /** - * Transport name - * - * @api public - */ - - HTMLFile.prototype.name = 'htmlfile'; - - /** - * Creates a new Ac...eX `htmlfile` with a forever loading iframe - * that can be used to listen to messages. Inside the generated - * `htmlfile` a reference will be made to the HTMLFile transport. - * - * @api private - */ - - HTMLFile.prototype.get = function () { - this.doc = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); - this.doc.open(); - this.doc.write(''); - this.doc.close(); - this.doc.parentWindow.s = this; - - var iframeC = this.doc.createElement('div'); - iframeC.className = 'socketio'; - - this.doc.body.appendChild(iframeC); - this.iframe = this.doc.createElement('iframe'); - - iframeC.appendChild(this.iframe); - - var self = this - , query = io.util.query(this.socket.options.query, 't='+ +new Date); - - this.iframe.src = this.prepareUrl() + query; - - io.util.on(window, 'unload', function () { - self.destroy(); - }); - }; - - /** - * The Socket.IO server will write script tags inside the forever - * iframe, this function will be used as callback for the incoming - * information. - * - * @param {String} data The message - * @param {document} doc Reference to the context - * @api private - */ - - HTMLFile.prototype._ = function (data, doc) { - // unescape all forward slashes. see GH-1251 - data = data.replace(/\\\//g, '/'); - this.onData(data); - try { - var script = doc.getElementsByTagName('script')[0]; - script.parentNode.removeChild(script); - } catch (e) { } - }; - - /** - * Destroy the established connection, iframe and `htmlfile`. - * And calls the `CollectGarbage` function of Internet Explorer - * to release the memory. - * - * @api private - */ - - HTMLFile.prototype.destroy = function () { - if (this.iframe){ - try { - this.iframe.src = 'about:blank'; - } catch(e){} - - this.doc = null; - this.iframe.parentNode.removeChild(this.iframe); - this.iframe = null; - - CollectGarbage(); - } - }; - - /** - * Disconnects the established connection. - * - * @returns {Transport} Chaining. - * @api public - */ - - HTMLFile.prototype.close = function () { - this.destroy(); - return io.Transport.XHR.prototype.close.call(this); - }; - - /** - * Checks if the browser supports this transport. The browser - * must have an `Ac...eXObject` implementation. - * - * @return {Boolean} - * @api public - */ - - HTMLFile.check = function (socket) { - if (typeof window != "undefined" && (['Active'].concat('Object').join('X')) in window){ - try { - var a = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); - return a && io.Transport.XHR.check(socket); - } catch(e){} - } - return false; - }; - - /** - * Check if cross domain requests are supported. - * - * @returns {Boolean} - * @api public - */ - - HTMLFile.xdomainCheck = function () { - // we can probably do handling for sub-domains, we should - // test that it's cross domain but a subdomain here - return false; - }; - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('htmlfile'); - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - - /** - * Expose constructor. - */ - - exports['xhr-polling'] = XHRPolling; - - /** - * The XHR-polling transport uses long polling XHR requests to create a - * "persistent" connection with the server. - * - * @constructor - * @api public - */ - - function XHRPolling () { - io.Transport.XHR.apply(this, arguments); - }; - - /** - * Inherits from XHR transport. - */ - - io.util.inherit(XHRPolling, io.Transport.XHR); - - /** - * Merge the properties from XHR transport - */ - - io.util.merge(XHRPolling, io.Transport.XHR); - - /** - * Transport name - * - * @api public - */ - - XHRPolling.prototype.name = 'xhr-polling'; - - /** - * Indicates whether heartbeats is enabled for this transport - * - * @api private - */ - - XHRPolling.prototype.heartbeats = function () { - return false; - }; - - /** - * Establish a connection, for iPhone and Android this will be done once the page - * is loaded. - * - * @returns {Transport} Chaining. - * @api public - */ - - XHRPolling.prototype.open = function () { - var self = this; - - io.Transport.XHR.prototype.open.call(self); - return false; - }; - - /** - * Starts a XHR request to wait for incoming messages. - * - * @api private - */ - - function empty () {}; - - XHRPolling.prototype.get = function () { - if (!this.isOpen) return; - - var self = this; - - function stateChange () { - if (this.readyState == 4) { - this.onreadystatechange = empty; - - if (this.status == 200) { - self.onData(this.responseText); - self.get(); - } else { - self.onClose(); - } - } - }; - - function onload () { - this.onload = empty; - this.onerror = empty; - self.retryCounter = 1; - self.onData(this.responseText); - self.get(); - }; - - function onerror () { - self.retryCounter ++; - if(!self.retryCounter || self.retryCounter > 3) { - self.onClose(); - } else { - self.get(); - } - }; - - this.xhr = this.request(); - - if (global.XDomainRequest && this.xhr instanceof XDomainRequest) { - this.xhr.onload = onload; - this.xhr.onerror = onerror; - } else { - this.xhr.onreadystatechange = stateChange; - } - - this.xhr.send(null); - }; - - /** - * Handle the unclean close behavior. - * - * @api private - */ - - XHRPolling.prototype.onClose = function () { - io.Transport.XHR.prototype.onClose.call(this); - - if (this.xhr) { - this.xhr.onreadystatechange = this.xhr.onload = this.xhr.onerror = empty; - try { - this.xhr.abort(); - } catch(e){} - this.xhr = null; - } - }; - - /** - * Webkit based browsers show a infinit spinner when you start a XHR request - * before the browsers onload event is called so we need to defer opening of - * the transport until the onload event is called. Wrapping the cb in our - * defer method solve this. - * - * @param {Socket} socket The socket instance that needs a transport - * @param {Function} fn The callback - * @api private - */ - - XHRPolling.prototype.ready = function (socket, fn) { - var self = this; - - io.util.defer(function () { - fn.call(self); - }); - }; - - /** - * Add the transport to your public io.transports array. - * - * @api private - */ - - io.transports.push('xhr-polling'); - -})( - 'undefined' != typeof io ? io.Transport : module.exports - , 'undefined' != typeof io ? io : module.parent.exports - , this -); - -/** - * socket.io - * Copyright(c) 2011 LearnBoost - * MIT Licensed - */ - -(function (exports, io, global) { - /** - * There is a way to hide the loading indicator in Firefox. If you create and - * remove a iframe it will stop showing the current loading indicator. - * Unfortunately we can't feature detect that and UA sniffing is evil. - * - * @api private - */ - - var indicator = global.document && "MozAppearance" in - global.document.documentElement.style; - - /** - * Expose constructor. - */ - - exports['jsonp-polling'] = JSONPPolling; - - /** - * The JSONP transport creates an persistent connection by dynamically - * inserting a script tag in the page. This script tag will receive the - * information of the Socket.IO server. When new information is received - * it creates a new script tag for the new data stream. - * - * @constructor - * @extends {io.Transport.xhr-polling} - * @api public - */ - - function JSONPPolling (socket) { - io.Transport['xhr-polling'].apply(this, arguments); - - this.index = io.j.length; - - var self = this; - - io.j.push(function (msg) { - self._(msg); - }); - }; - - /** - * Inherits from XHR polling transport. - */ - - io.util.inherit(JSONPPolling, io.Transport['xhr-polling']); - - /** - * Transport name - * - * @api public - */ - - JSONPPolling.prototype.name = 'jsonp-polling'; - - /** - * Posts a encoded message to the Socket.IO server using an iframe. - * The iframe is used because script tags can create POST based requests. - * The iframe is positioned outside of the view so the user does not - * notice it's existence. - * - * @param {String} data A encoded message. - * @api private - */ - - JSONPPolling.prototype.post = function (data) { - var self = this - , query = io.util.query( - this.socket.options.query - , 't='+ (+new Date) + '&i=' + this.index - ); - - if (!this.form) { - var form = document.createElement('form') - , area = document.createElement('textarea') - , id = this.iframeId = 'socketio_iframe_' + this.index - , iframe; - - form.className = 'socketio'; - form.style.position = 'absolute'; - form.style.top = '0px'; - form.style.left = '0px'; - form.style.display = 'none'; - form.target = id; - form.method = 'POST'; - form.setAttribute('accept-charset', 'utf-8'); - area.name = 'd'; - form.appendChild(area); - document.body.appendChild(form); - - this.form = form; - this.area = area; - } - - this.form.action = this.prepareUrl() + query; - - function complete () { - initIframe(); - self.socket.setBuffer(false); - }; - - function initIframe () { - if (self.iframe) { - self.form.removeChild(self.iframe); - } - - try { - // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) - iframe = document.createElement(' -
1. GETTING STARTED
-
2. UP YOUR SKILLZ
-
3. ADVANCED MAPPING
From c0b35280f6e47db9103c5b70c4bd47933ff9db0c Mon Sep 17 00:00:00 2001 From: Robert Best Date: Sat, 22 Oct 2016 02:58:13 -0400 Subject: [PATCH 257/306] Middle.mouse.click features (Open contained link & copy text to clipboard) (#792) * changed the code to be based off of the current dev branch * Update JIT.js * Update Util.js * Update JIT.js A few logical operators were replaced with their stricter counterpart. * Update JIT.js * Update index.js * Update Util.js --- frontend/src/Metamaps/JIT.js | 67 ++++++++++++++++++++---------- frontend/src/Metamaps/Map/index.js | 4 ++ frontend/src/Metamaps/Util.js | 12 ++++++ 3 files changed, 61 insertions(+), 22 deletions(-) diff --git a/frontend/src/Metamaps/JIT.js b/frontend/src/Metamaps/JIT.js index 593cf52e..73c1b600 100644 --- a/frontend/src/Metamaps/JIT.js +++ b/frontend/src/Metamaps/JIT.js @@ -21,6 +21,7 @@ import Topic from './Topic' import TopicCard from './TopicCard' import Util from './Util' import Visualize from './Visualize' +import clipboard from 'clipboard-js' /* * Metamaps.Erb @@ -410,14 +411,13 @@ const JIT = { Mouse.boxStartCoordinates = null Mouse.boxEndCoordinates = null } - // console.log('called zoom to box') } if (e.shiftKey) { Visualize.mGraph.busy = false Mouse.boxEndCoordinates = eventInfo.getPos() JIT.selectWithBox(e) - // console.log('called select with box') + return } } @@ -427,13 +427,10 @@ const JIT = { // clicking on a edge, node, or clicking on blank part of canvas? if (node.nodeFrom) { JIT.selectEdgeOnClickHandler(node, e) - // console.log('called selectEdgeOnClickHandler') } else if (node && !node.nodeFrom) { JIT.selectNodeOnClickHandler(node, e) - // console.log('called selectNodeOnClickHandler') } else { JIT.canvasClickHandler(eventInfo.getPos(), e) - // console.log('called canvasClickHandler') } // if }, // Add also a click handler to nodes @@ -1333,6 +1330,9 @@ const JIT = { if (Visualize.mGraph.busy) return const self = JIT + + //Copy topic title to clipboard + if(e.button===1 && e.ctrlKey) clipboard.copy(node.name); // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { @@ -1354,25 +1354,45 @@ const JIT = { // wait a certain length of time, then check again, then run this code setTimeout(function () { if (!JIT.nodeWasDoubleClicked()) { - const nodeAlreadySelected = node.selected - - if (!e.shiftKey) { - Control.deselectAllNodes() - Control.deselectAllEdges() - } - - if (nodeAlreadySelected) { - Control.deselectNode(node) + var nodeAlreadySelected = node.selected + + if(e.button!==1){ + if (!e.shiftKey) { + Control.deselectAllNodes() + Control.deselectAllEdges() + } + + if (nodeAlreadySelected) { + Control.deselectNode(node) + } else { + Control.selectNode(node, e) + } + + // trigger animation to final styles + Visualize.mGraph.fx.animate({ + modes: ['edge-property:lineWidth:color:alpha'], + duration: 500 + }) + Visualize.mGraph.plot() + } else { - Control.selectNode(node, e) + if(!e.ctrlKey){ + var len = Selected.Nodes.length; + + for (let i = 0; i < len; i += 1) { + let n = Selected.Nodes[i]; + let result = Metamaps.Util.openLink(Metamaps.Topics.get(n.id).attributes.link); + + if (!result) { //if link failed to open + break; + } + } + + if(!node.selected){ + Metamaps.Util.openLink(Metamaps.Topics.get(node.id).attributes.link); + } + } } - - // trigger animation to final styles - Visualize.mGraph.fx.animate({ - modes: ['edge-property:lineWidth:color:alpha'], - duration: 500 - }) - Visualize.mGraph.plot() } }, Mouse.DOUBLE_CLICK_TOLERANCE) } @@ -1598,6 +1618,9 @@ const JIT = { if (Visualize.mGraph.busy) return const self = JIT + var synapseText = adj.data.$synapses[0].attributes.desc; + //Copy synapse label to clipboard + if(e.button===1 && e.ctrlKey && synapseText !== "") clipboard.copy(synapseText); // catch right click on mac, which is often like ctrl+click if (navigator.platform.indexOf('Mac') !== -1 && e.ctrlKey) { diff --git a/frontend/src/Metamaps/Map/index.js b/frontend/src/Metamaps/Map/index.js index e5f50633..7aa98cc3 100644 --- a/frontend/src/Metamaps/Map/index.js +++ b/frontend/src/Metamaps/Map/index.js @@ -44,6 +44,10 @@ const Map = { $('#wrapper').on('contextmenu', function (e) { return false }) + + $('#wrapper').mousedown(function (e){ + if(e.button === 1)return false; + }); $('.starMap').click(function () { if ($(this).is('.starred')) self.unstar() diff --git a/frontend/src/Metamaps/Util.js b/frontend/src/Metamaps/Util.js index 445a898c..49797ab4 100644 --- a/frontend/src/Metamaps/Util.js +++ b/frontend/src/Metamaps/Util.js @@ -124,6 +124,18 @@ const Util = { checkURLisYoutubeVideo: function (url) { return (url.match(/^https?:\/\/(?:www\.)?youtube.com\/watch\?(?=[^?]*v=\w+)(?:[^\s?]+)?$/) != null) }, + openLink: function(url){ + var win = (url !== "") ? window.open(url, '_blank') : "empty"; + + if (win) { + //Browser has allowed it to be opened + return true; + } else { + //Browser has blocked it + alert('Please allow popups in order to open the link'); + return false; + } + }, mdToHTML: text => { // use safe: true to filter xss return new HtmlRenderer({ safe: true }) From bc8660c83e79a0e847de7c50123f4cc267538469 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sat, 22 Oct 2016 03:10:09 -0400 Subject: [PATCH 258/306] remove about lightbox in prep for homepage redo and about page --- app/views/layouts/_lightboxes.html.erb | 61 -------------------------- 1 file changed, 61 deletions(-) diff --git a/app/views/layouts/_lightboxes.html.erb b/app/views/layouts/_lightboxes.html.erb index 2e092151..70d62600 100644 --- a/app/views/layouts/_lightboxes.html.erb +++ b/app/views/layouts/_lightboxes.html.erb @@ -7,67 +7,6 @@