From 5d04d165908a041f5e7ab3958326dff96f904a75 Mon Sep 17 00:00:00 2001 From: Connor Turland Date: Sun, 18 Sep 2016 15:22:51 +0000 Subject: [PATCH] create MetacodeSelect component --- app/assets/javascripts/lib/cloudcarousel.js | 21 ++- app/assets/javascripts/src/Metamaps.Create.js | 76 +++++++- app/assets/javascripts/src/Metamaps.JIT.js | 2 +- app/assets/stylesheets/application.css.erb | 51 +++++- .../stylesheets/metacode-select.scss.erb | 72 ++++++++ app/controllers/users_controller.rb | 12 +- app/helpers/application_helper.rb | 4 + app/models/user.rb | 22 +++ app/models/user_preference.rb | 3 +- app/views/maps/_newtopic.html.erb | 18 +- config/routes.rb | 1 + frontend/src/components/MetacodeSelect.js | 168 ++++++++++++++++++ frontend/src/index.js | 4 +- 13 files changed, 424 insertions(+), 30 deletions(-) create mode 100644 app/assets/stylesheets/metacode-select.scss.erb create mode 100644 frontend/src/components/MetacodeSelect.js diff --git a/app/assets/javascripts/lib/cloudcarousel.js b/app/assets/javascripts/lib/cloudcarousel.js index b37abfd7..69283044 100644 --- a/app/assets/javascripts/lib/cloudcarousel.js +++ b/app/assets/javascripts/lib/cloudcarousel.js @@ -164,10 +164,12 @@ jQuery.browser = browser; // Add code that makes tab and shift+tab scroll through metacodes $('.new_topic').bind('keydown',this,function(event){ if (event.keyCode == 9 && event.shiftKey) { + $(container).show() event.data.rotate(-1); event.preventDefault(); event.stopPropagation(); } else if (event.keyCode == 9) { + $(container).show() event.data.rotate(1); event.preventDefault(); event.stopPropagation(); @@ -178,12 +180,12 @@ jQuery.browser = browser; if (options.mouseWheel) { // START METAMAPS CODE - $('body').bind('mousewheel',this,function(event, delta) { + /*$('body').bind('mousewheel',this,function(event, delta) { if (Metamaps.Create.newTopic.beingCreated && !Metamaps.Create.isSwitchingSet) { event.data.rotate(delta); return false; } - }); + });*/ // END METAMAPS CODE /* ORIGINAL CODE $(container).bind('mousewheel',this,function(event, delta) { @@ -246,12 +248,10 @@ jQuery.browser = browser; { if ( items[this.frontIndex] === undefined ) { return; } // Images might not have loaded yet. // METAMAPS CODE - Metamaps.Create.newTopic.metacode = $(items[this.frontIndex].image).attr('data-id'); - //$('img.cloudcarousel').css({"background":"none", "width":"","height":""}); - //$(items[this.frontIndex].image).css({"width":"45px","height":"45px"}); + Metamaps.Create.newTopic.setMetacode($(items[this.frontIndex].image).attr('data-id')) // NOT METAMAPS CODE - $(options.titleBox).html( $(items[this.frontIndex].image).attr('title')); - $(options.altBox).html( $(items[this.frontIndex].image).attr('alt')); + //$(options.titleBox).html( $(items[this.frontIndex].image).attr('title')); + //$(options.altBox).html( $(items[this.frontIndex].image).attr('alt')); }; this.go = function() @@ -264,7 +264,10 @@ jQuery.browser = browser; this.stop = function() { clearTimeout(this.controlTimer); - this.controlTimer = 0; + this.controlTimer = 0; + // METAMAPS CODE + $(container).hide() + // END METAMAPS CODE }; @@ -385,7 +388,7 @@ jQuery.browser = browser; } // If all images have valid widths and heights, we can stop checking. clearInterval(this.tt); - this.showFrontText(); + // METAMAPS COMMENT this.showFrontText(); this.autoRotate(); this.updateAll(); diff --git a/app/assets/javascripts/src/Metamaps.Create.js b/app/assets/javascripts/src/Metamaps.Create.js index bb01d129..de8e96f4 100644 --- a/app/assets/javascripts/src/Metamaps.Create.js +++ b/app/assets/javascripts/src/Metamaps.Create.js @@ -1,4 +1,4 @@ -/* global Metamaps, $ */ +/* global Metamaps, $, ReactDOM, React */ /* * Metamaps.Create.js @@ -22,6 +22,8 @@ Metamaps.Create = { newSelectedMetacodeNames: [], selectedMetacodes: [], newSelectedMetacodes: [], + recentMetacodes: [], + mostUsedMetacodes: [], init: function () { var self = Metamaps.Create self.newTopic.init() @@ -82,7 +84,6 @@ Metamaps.Create = { } metacodeModels.sort() - $('#metacodeImg, #metacodeImgTitle').empty() $('#metacodeImg').removeData('cloudcarousel') var newMetacodes = '' metacodeModels.each(function (metacode) { @@ -90,13 +91,11 @@ Metamaps.Create = { }) $('#metacodeImg').empty().append(newMetacodes).CloudCarousel({ - titleBox: $('#metacodeImgTitle'), yRadius: 40, xRadius: 190, xPos: 170, yPos: 40, speed: 0.3, - mouseWheel: true, bringToFront: true }) @@ -147,9 +146,23 @@ Metamaps.Create = { }, newTopic: { init: function () { - $('#topic_name').keyup(function () { + var DOWN_ARROW = 40 + + $('#topic_name').keyup(function (e) { Metamaps.Create.newTopic.name = $(this).val() + if (e.which == DOWN_ARROW && !Metamaps.Create.newTopic.name.length) { + Metamaps.Create.newTopic.openSelector() + } }) + + $('.selectedMetacode').click(function() { + if (Metamaps.Create.newTopic.metacodeSelectorOpen) { + Metamaps.Create.newTopic.hideSelector() + $('#topic_name').focus() + } else Metamaps.Create.newTopic.openSelector() + }) + + Metamaps.Create.newTopic.initSelector() var topicBloodhound = new Bloodhound({ datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'), @@ -183,16 +196,15 @@ Metamaps.Create = { $('#topic_name').bind('typeahead:select', function (event, datum, dataset) { Metamaps.Topic.getTopicFromAutocomplete(datum.id) }) + $('#topic_name').click(function() { Metamaps.Create.newTopic.hideSelector() }) // initialize metacode spinner and then hide it $('#metacodeImg').CloudCarousel({ - titleBox: $('#metacodeImgTitle'), yRadius: 40, xRadius: 190, xPos: 170, yPos: 40, speed: 0.3, - mouseWheel: true, bringToFront: true }) $('.new_topic').hide() @@ -200,10 +212,59 @@ Metamaps.Create = { name: null, newId: 1, beingCreated: false, + metacodeSelectorOpen: false, metacode: null, x: null, y: null, addSynapse: false, + initSelector: function () { + ReactDOM.render( + React.createElement(Metamaps.ReactComponents.MetacodeSelect, { + onClick: function (id) { + Metamaps.Create.newTopic.setMetacode(id) + Metamaps.Create.newTopic.hideSelector() + $('#topic_name').focus() + }, + close: function () { + Metamaps.Create.newTopic.hideSelector() + $('#topic_name').focus() + }, + metacodes: Metamaps.Metacodes.models, + recent: Metamaps.Create.recentMetacodes, + mostUsed: Metamaps.Create.mostUsedMetacodes + }), + document.getElementById('metacodeSelector') + ) + }, + openSelector: function () { + Metamaps.Create.newTopic.initSelector() + $('#metacodeSelector').show() + Metamaps.Create.newTopic.metacodeSelectorOpen = true + $('.metacodeFilterInput').focus() + $('.selectedMetacode').addClass('isBeingSelected') + }, + hideSelector: function () { + ReactDOM.unmountComponentAtNode(document.getElementById('metacodeSelector')) + $('#metacodeSelector').hide() + Metamaps.Create.newTopic.metacodeSelectorOpen = false + $('.selectedMetacode').removeClass('isBeingSelected') + }, + setMetacode: function (id) { + Metamaps.Create.newTopic.metacode = id + var metacode = Metamaps.Metacodes.get(id) + $('.selectedMetacode img').attr('src', metacode.get('icon')) + $('.selectedMetacode span').html(metacode.get('name')) + $.ajax({ + type: 'POST', + dataType: 'json', + url: '/user/update_metacode_focus', + data: { value: id }, + success: function (data) {}, + error: function () { + console.log('failed to save metacode focus') + } + }) + }, open: function () { $('#new_topic').fadeIn('fast', function () { $('#topic_name').focus() @@ -215,6 +276,7 @@ Metamaps.Create = { $('#new_topic').fadeOut('fast') $('#topic_name').typeahead('val', '') Metamaps.Create.newTopic.beingCreated = false + Metamaps.Create.newTopic.hideSelector() } }, newSynapse: { diff --git a/app/assets/javascripts/src/Metamaps.JIT.js b/app/assets/javascripts/src/Metamaps.JIT.js index d5e82081..33a76857 100644 --- a/app/assets/javascripts/src/Metamaps.JIT.js +++ b/app/assets/javascripts/src/Metamaps.JIT.js @@ -706,7 +706,7 @@ Metamaps.JIT = { Metamaps.GlobalUI.CreateMap.submit() } // this is to submit new topic creation - else if (Metamaps.Create.newTopic.beingCreated) { + else if (Metamaps.Create.newTopic.beingCreated && !Metamaps.Create.newTopic.metacodeSelectorOpen) { Metamaps.Topic.createTopicLocally() } // to submit new synapse creation diff --git a/app/assets/stylesheets/application.css.erb b/app/assets/stylesheets/application.css.erb index 5a4d62d3..da964984 100644 --- a/app/assets/stylesheets/application.css.erb +++ b/app/assets/stylesheets/application.css.erb @@ -523,18 +523,48 @@ button.button.btn-no:hover { left: -1000px; display: block; position: absolute; - width: 340px; - margin: -40px 0 0 -35px; z-index: 1; } +.selectedMetacode { + float: left; + background: #FFF; + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; + padding: 5px 10px 5px 6px; + vertical-align: top; + border-right: 1px solid #DDD; + cursor: pointer; +} +.selectedMetacode:hover, .selectedMetacode.isBeingSelected { + background: #EDEDED; +} +.selectedMetacode img { + width: 24px; + height: 24px; + display: inline-block; + vertical-align: middle; +} +.selectedMetacode span { + vertical-align: middle; +} +.selectedMetacode .downArrow { + display: inline-block; + vertical-align: middle; + width: 0; + height: 0; + border-style: solid; + border-width: 8px 6px 0 6px; + border-color: #777 transparent transparent transparent; + margin-left: 2px; + margin-top: 4px; +} + #new_topic .twitter-typeahead { - position: absolute !important; - top: 45px; - left: 41px; z-index: 9999; width: 256px; height: 34px; + float: left; } .new_topic #topic_name, .new_topic .tt-hint { @@ -544,7 +574,8 @@ button.button.btn-no:hover { margin: 0; padding: 10px 6px; border: none; - border-radius: 2px; + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; outline: none; font-size: 14px; line-height: 14px; @@ -555,7 +586,7 @@ button.button.btn-no:hover { color: #BDBDBD; } .openMetacodeSwitcher { - display: block; + display: none; height: 16px; width: 16px; background-image: url(<%= asset_data_uri('metacodesettings_sprite.png') %>); @@ -569,8 +600,14 @@ button.button.btn-no:hover { } #metacodeImg { height: 120px; + width: 380px; + display: none; + position: absolute !important; + top: -30px; + z-index: -1; } #metacodeImgTitle { + display: none; float: left; width: 120px; text-align: center; diff --git a/app/assets/stylesheets/metacode-select.scss.erb b/app/assets/stylesheets/metacode-select.scss.erb new file mode 100644 index 00000000..3e30303a --- /dev/null +++ b/app/assets/stylesheets/metacode-select.scss.erb @@ -0,0 +1,72 @@ +#metacodeSelector { + display: none; +} + +.metacodeSelect { + border-top: 1px solid #DDD; + padding: 0; + + .tabList { + float: left; + background: #FFF; + + div { + border-right: 1px solid #DDD; + border-bottom: 1px solid #DDD; + } + + div:last-child { + border-bottom: none; + } + + div.active { + border-right: none; + background: #EEE; + + .metacodeFilterInput { + background: #EEE; + } + } + + .metacodeFilterInput { + width: 100px; + outline: none; + border: 0; + padding: 8px; + font-size: 14px; + line-height: 14px; + color: #424242; + font-family: 'din-medium', helvetica, sans-serif; + } + + span { + padding: 8px; + display: block; + } + } + + .metacodeList { + float:left; + list-style: none; + background: #FFF; + min-height: 107px; + min-width: 100px; + + li { + padding: 8px; + cursor: pointer; + + &:hover, &.keySelect { + background: #EEE; + } + } + + img { + width: 24px; + height: 24px; + display: inline-block; + vertical-align: middle; + padding-right: 6px; + } + } +} \ No newline at end of file diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 0ea95211..594d93ff 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,5 @@ class UsersController < ApplicationController - before_action :require_user, only: [:edit, :update, :updatemetacodes] + before_action :require_user, only: [:edit, :update, :updatemetacodes, :update_metacode_focus] respond_to :html, :json @@ -91,6 +91,16 @@ class UsersController < ApplicationController format.json { render json: @user } end end + + # PUT /user/update_metacode_focus + def update_metacode_focus + @user = current_user + @user.settings.metacode_focus = params[:value] + @user.save + respond_to do |format| + format.json { render json: { success: "success" }} + end + end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 555a32d2..23a1c942 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,6 +15,10 @@ module ApplicationHelper end @metacodes.sort! { |m1, m2| m2.name.downcase <=> m1.name.downcase }.rotate!(-1) end + + def user_metacode + current_user.settings.metacode_focus ? Metacode.find(current_user.settings.metacode_focus.to_i) || user_metacodes()[0] : user_metacodes()[0] + end def determine_invite_link "#{request.base_url}/join" + (current_user ? "?code=#{current_user.code}" : '') diff --git a/app/models/user.rb b/app/models/user.rb index 1f091499..f24367e1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -63,6 +63,28 @@ class User < ActiveRecord::Base 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/models/user_preference.rb b/app/models/user_preference.rb index a87dc679..0f4e84bd 100644 --- a/app/models/user_preference.rb +++ b/app/models/user_preference.rb @@ -1,5 +1,5 @@ class UserPreference - attr_accessor :metacodes + attr_accessor :metacodes, :metacode_focus def initialize array = [] @@ -8,5 +8,6 @@ class UserPreference array.push(metacode.id.to_s) if metacode end @metacodes = array + @metacode_focus = array[0] end end diff --git a/app/views/maps/_newtopic.html.erb b/app/views/maps/_newtopic.html.erb index e5263d76..2c50686a 100644 --- a/app/views/maps/_newtopic.html.erb +++ b/app/views/maps/_newtopic.html.erb @@ -1,14 +1,20 @@ <%= form_for Topic.new, url: topics_url, remote: true do |form| %>
- <% @metacodes = user_metacodes() %> + <% @metacodes = [user_metacode()].concat(user_metacodes()).uniq %> <% set = get_metacodeset() %> <% @metacodes.each do |metacode| %> <%= metacode.name %> <% end %> -
+ +
+ + <%= user_metacode().name %> +
+
<%= form.text_field :name, :maxlength => 140, :placeholder => "title..." %> -
+
+
<% end %> diff --git a/config/routes.rb b/config/routes.rb index 83f03051..bf2ede0f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -61,5 +61,6 @@ Metamaps::Application.routes.draw do get 'users/:id/details', to: 'users#details', as: :details post 'user/updatemetacodes', to: 'users#updatemetacodes', as: :updatemetacodes + post 'user/update_metacode_focus', to: 'users#update_metacode_focus' resources :users, except: [:index, :destroy] end diff --git a/frontend/src/components/MetacodeSelect.js b/frontend/src/components/MetacodeSelect.js new file mode 100644 index 00000000..e8cde277 --- /dev/null +++ b/frontend/src/components/MetacodeSelect.js @@ -0,0 +1,168 @@ +/* global $ */ + +import React, { Component, PropTypes } from 'react' + +const ENTER_KEY = 13 +const LEFT_ARROW = 37 +const UP_ARROW = 38 +const RIGHT_ARROW = 39 +const DOWN_ARROW = 40 + +const Metacode = (props) => { + const { m, onClick, underCursor } = props + + return ( +
  • onClick(m.id) } className={ underCursor ? 'keySelect' : '' }> + + { m.get('name') } +
  • + ) +} + +class MetacodeSelect extends Component { + + constructor (props) { + super(props) + this.state = { + filterText: '', + activeTab: 0, + selectingSection: true, + underCursor: 0 + } + } + + componentDidMount() { + const self = this + setTimeout(function() { + $(document.body).on('keyup.metacodeSelect', self.handleKeyUp.bind(self)) + }, 10) + } + + componentWillUnmount() { + $(document.body).off('.metacodeSelect') + } + + changeFilterText (e) { + this.setState({ filterText: e.target.value, underCursor: 0 }) + } + + changeDisplay (activeTab) { + this.setState({ activeTab, underCursor: 0 }) + } + + getSelectMetacodes () { + const { metacodes, recent, mostUsed } = this.props + const { filterText, activeTab } = this.state + + let selectMetacodes = [] + if (activeTab == 0) { // search + selectMetacodes = filterText.length > 1 ? metacodes.filter(m => { + return m.get('name').toLowerCase().search(filterText.toLowerCase()) > -1 + }) : [] + } else if (activeTab == 1) { // recent + selectMetacodes = recent.map(id => { + return metacodes.find(m => m.id == id) + }) + } else if (activeTab == 2) { // mostUsed + selectMetacodes = mostUsed.map(id => { + return metacodes.find(m => m.id == id) + }) + } + return selectMetacodes + } + + handleKeyUp (e) { + const { close } = this.props + const { activeTab, underCursor, selectingSection } = this.state + const selectMetacodes = this.getSelectMetacodes() + let nextIndex + + switch (e.which) { + case ENTER_KEY: + if (selectMetacodes.length && !selectingSection) this.resetAndClick(selectMetacodes[underCursor].id) + break + case UP_ARROW: + if (selectingSection && activeTab == 0) { + close() + break + } + else if (selectingSection) { + nextIndex = activeTab - 1 + this.changeDisplay(nextIndex) + break + } + nextIndex = underCursor == 0 ? selectMetacodes.length - 1 : underCursor - 1 + this.setState({ underCursor: nextIndex }) + break + case DOWN_ARROW: + if (selectingSection) { + nextIndex = activeTab == 2 ? 0 : activeTab + 1 + this.changeDisplay(nextIndex) + break + } + nextIndex = underCursor == selectMetacodes.length - 1 ? 0 : underCursor + 1 + this.setState({ underCursor: nextIndex }) + break + case RIGHT_ARROW: + if (selectingSection) this.setState({ selectingSection: false }) + break + case LEFT_ARROW: + if (!selectingSection) this.setState({ selectingSection: true }) + break + } + } + + resetAndClick (id) { + const { onClick } = this.props + this.setState({ filterText: '', underCursor: 0 }) + this.changeDisplay(0) + onClick(id) + } + + render () { + const { onClick, close, recent, mostUsed } = this.props + const { filterText, activeTab, underCursor, selectingSection } = this.state + const selectMetacodes = this.getSelectMetacodes() + return
    +
    +
    { this.changeDisplay(0) }}> + +
    +
    { this.changeDisplay(1) }}> + Recent +
    +
    { this.changeDisplay(2) }}> + Most Used +
    +
    + +
    +
    + } +} + +MetacodeSelect.propTypes = { + onClick: PropTypes.func.isRequired, + close: PropTypes.func.isRequired, + metacodes: PropTypes.array.isRequired, + recent: PropTypes.array.isRequired, + mostUsed: PropTypes.array.isRequired +} + +export default MetacodeSelect + diff --git a/frontend/src/index.js b/frontend/src/index.js index 0556f4c1..4cab8de0 100644 --- a/frontend/src/index.js +++ b/frontend/src/index.js @@ -3,6 +3,7 @@ import ReactDOM from 'react-dom' import Backbone from 'backbone' import _ from 'underscore' import Maps from './components/Maps.js' +import MetacodeSelect from './components/MetacodeSelect.js' // this is optional really, if we import components directly React will be // in the bundle, so we won't need a global reference @@ -14,5 +15,6 @@ window._ = _ window.Metamaps = window.Metamaps || {} window.Metamaps.ReactComponents = { - Maps + Maps, + MetacodeSelect }