]> source.dussan.org Git - redmine.git/commitdiff
Replaces project jump select with custom dropdown (#23310).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 26 Nov 2016 08:16:15 +0000 (08:16 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 26 Nov 2016 08:16:15 +0000 (08:16 +0000)
git-svn-id: http://svn.redmine.org/redmine/trunk@15994 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/controllers/projects_controller.rb
app/helpers/application_helper.rb
app/views/projects/index.js.erb [new file with mode: 0644]
app/views/wiki/show.html.erb
public/javascripts/application.js
public/stylesheets/application.css
public/stylesheets/responsive.css
test/functional/projects_controller_test.rb
test/functional/welcome_controller_test.rb

index 98fc23f5fe739d20c0e5d9708c7da5650fb75507..ad38a15a6140b866233b8759aeac95ce86eec94f 100644 (file)
@@ -45,6 +45,13 @@ class ProjectsController < ApplicationController
         end
         @projects = scope.to_a
       }
+      format.js {
+        if params[:q].present?
+          @projects = Project.visible.like(params[:q]).to_a
+        else
+          @projects = User.current.projects.to_a
+        end
+      }
       format.api  {
         @offset, @limit = api_offset_and_limit
         @project_count = scope.count
index b6874757c6bd0f9b5513dd39ae50dd80ae507952..d4bd6a2f2e583c56bcb8e45f5d5e312e3f2ba3e0 100644 (file)
@@ -330,22 +330,40 @@ module ApplicationHelper
       content_tag 'p', l(:label_no_data), :class => "nodata"
     end
   end
+  # Returns an array of projects that are displayed in the quick-jump box
+  def projects_for_jump_box(user=User.current)
+    if user.logged?
+      user.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
+    else
+      []
+    end
+  end
+
+  def render_projects_for_jump_box(projects, selected=nil)
+    s = ''.html_safe
+    project_tree(projects) do |project, level|
+      padding = level * 16
+      text = content_tag('span', project.name, :style => "padding-left:#{padding}px;")
+      s << link_to(text, project_path(project, :jump => current_menu_item), :title => project.name, :class => (project == selected ? 'selected' : nil))
+    end
+    s
+  end
 
   # Renders the project quick-jump box
   def render_project_jump_box
-    return unless User.current.logged?
-    projects = User.current.projects.active.select(:id, :name, :identifier, :lft, :rgt).to_a
+    projects = projects_for_jump_box(User.current)
     if projects.any?
-      options =
-        ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
-         '<option value="" disabled="disabled">---</option>').html_safe
-
-      options << project_tree_options_for_select(projects, :selected => @project) do |p|
-        { :value => project_path(:id => p, :jump => current_menu_item) }
-      end
-
-      content_tag( :span, nil, :class => 'jump-box-arrow') +
-      select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
+      text = @project.try(:name) || l(:label_jump_to_a_project)
+      trigger = content_tag('span', text, :class => 'drdn-trigger')
+      q = text_field_tag('q', '', :id => 'projects-quick-search', :class => 'autocomplete', :data => {:automcomplete_url => projects_path(:format => 'js')})
+      content = content_tag('div',
+            content_tag('div', q, :class => 'quick-search') + 
+            content_tag('div', render_projects_for_jump_box(projects, @project), :class => 'drdn-items selection'),
+          :class => 'drdn-content'
+        )
+
+      content_tag('span', trigger + content, :id => "project-jump", :class => "drdn")
     end
   end
 
diff --git a/app/views/projects/index.js.erb b/app/views/projects/index.js.erb
new file mode 100644 (file)
index 0000000..b4731fb
--- /dev/null
@@ -0,0 +1,2 @@
+<% s = @projects.any? ? render_projects_for_jump_box(@projects) : content_tag('span', l(:label_no_data)) %>
+$('#project-jump .drdn-items').html('<%= escape_javascript s %>');
index 1cbfcbc656468ecb65d92eda423c8acdb1476df5..4eaa3d62fea0f67bff013f9aa35ca983e2cfff62 100644 (file)
@@ -1,11 +1,13 @@
 <div class="contextual">
-<% if User.current.allowed_to?(:edit_wiki_pages, @project) %>
-<%= link_to l(:label_wiki_page_new), new_project_wiki_page_path(@project), :remote => true, :class => 'icon icon-add' %>
-<% end %>
-<% if @editable %>
-<% if @content.current_version? %>
+  <% if @editable && @content.current_version? %>
   <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :id => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
+  <% end %>
   <%= watcher_link(@page, User.current) %>
+<span class="drdn">
+  <span class="drdn-trigger">Hello</span>
+  <div class="drdn-content drdn-items">
+<% if @editable %>
+<% if @content.current_version? %>
   <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :id => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
   <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :id => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
   <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :id => @page.title}, :class => 'icon icon-move') %>
 <% end %>
 <% end %>
 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :id => @page.title}, :class => 'icon icon-history') %>
+<% if User.current.allowed_to?(:edit_wiki_pages, @project) %>
+<%= link_to l(:label_wiki_page_new), new_project_wiki_page_path(@project), :remote => true, :class => 'icon icon-add' %>
+<% end %>
+  </div>
+</span>
 </div>
 
 <%= wiki_page_breadcrumb(@page) %>
index 3d1ad43cd28cfe0bfa394bae6d46e6efe01a8f06..ad7109e2be6803dca29f3c0ca1a78b72f4f08098 100644 (file)
@@ -572,6 +572,69 @@ function observeSearchfield(fieldId, targetId, url) {
   });
 }
 
+$(document).ready(function(){
+  $(".drdn .autocomplete").val('');
+
+  $(".drdn-trigger").click(function(e){
+    var drdn = $(this).closest(".drdn");
+    if (drdn.hasClass("expanded")) {
+      drdn.removeClass("expanded");
+    } else {
+      $(".drdn").removeClass("expanded");
+      drdn.addClass("expanded");
+      if (!isMobile()) {
+        drdn.find(".autocomplete").focus();
+      }
+      e.stopPropagation();
+    }
+  });
+  $(document).click(function(e){
+    if ($(e.target).closest(".drdn").length < 1) {
+      $(".drdn.expanded").removeClass("expanded");
+    } 
+  });
+
+  observeSearchfield('projects-quick-search', null, $('#projects-quick-search').data('automcomplete-url'));
+
+  $(".drdn-content").keydown(function(event){
+    var items = $(this).find(".drdn-items");
+    var focused = items.find("a:focus");
+    switch (event.which) {
+    case 40: //down
+      if (focused.length > 0) {
+        focused.nextAll("a").first().focus();;
+      } else {
+        items.find("a").first().focus();;
+      }
+      event.preventDefault();
+      break;
+    case 38: //up
+      if (focused.length > 0) {
+        var prev = focused.prevAll("a");
+        if (prev.length > 0) {
+          prev.first().focus();
+        } else {
+          $(this).find(".autocomplete").focus();
+        }
+        event.preventDefault();
+      }
+      break;
+    case 35: //end
+      if (focused.length > 0) {
+        focused.nextAll("a").last().focus();
+        event.preventDefault();
+      }
+      break;
+    case 36: //home
+      if (focused.length > 0) {
+        focused.prevAll("a").last().focus();
+        event.preventDefault();
+      }
+      break;
+    }
+  });
+});
+
 function beforeShowDatePicker(input, inst) {
   var default_date = null;
   switch ($(input).attr("id")) {
index e3cde7c8b2b9fec41241e7ed4b3eaad5f104bf27..459d35fcccaee9bfee1078927e0756e06fa4d936 100644 (file)
@@ -30,7 +30,9 @@ pre, code {font-family: Consolas, Menlo, "Liberation Mono", Courier, monospace;}
 #header a {color:#f8f8f8;}
 #header h1 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap;}
 #header h1 .breadcrumbs { display:block; font-size: .5em; font-weight: normal; }
+
 #quick-search {float:right;}
+#quick-search #q {width:130px; height:24px; box-sizing:border-box; vertical-align:middle; border:1px solid #ccc; border-radius:3px;}
 
 #main-menu {position: absolute;  bottom: 0px;  left:6px; margin-right: -500px; width: 100%;}
 #main-menu ul {margin: 0;  padding: 0; width: 100%; white-space: nowrap;}
@@ -140,6 +142,79 @@ a#toggle-completed-versions {color:#999;}
 
 a.toggle-checkboxes { margin-left: 5px; padding-left: 12px; background: url(../images/toggle_check.png) no-repeat 0% 50%; }
 
+/***** Dropdown *****/
+.drdn {position:relative;}
+.drdn-trigger {
+  width:100%;
+  height:24px;
+  box-sizing:border-box;
+  overflow:hidden;
+  text-overflow:ellipsis;
+  white-space:nowrap;
+  padding:3px 18px 3px 6px;
+  background:#fff url(../images/sort_desc.png) no-repeat 97% 50%;
+  cursor:pointer;
+  user-select:none;
+  -moz-user-select:none;
+  -webkit-user-select:none;
+}
+.drdn-content {
+  display:none;
+  position:absolute;
+  right:0px;
+  top:25px;
+  min-width:100px;
+  background-color:#fff;
+  border:1px solid #ccc;
+  border-radius:4px;
+  color:#555;
+  z-index:99;
+}
+.drdn.expanded .drdn-trigger {background-image:url(../images/sort_asc.png);}
+.drdn.expanded .drdn-content {display:block;}
+
+.drdn-content .quick-search {margin:8px;}
+.drdn-content .autocomplete {box-sizing: border-box; width:100% !important; height:28px;}
+.drdn-content .autocomplete:focus {border-color:#5ad;}
+.drdn-items {max-height:400px; overflow:auto;}
+.quick-search + .drdn-items {border-top:1px solid #ccc;}
+.drdn-items>* {
+  display:block;
+  border:1px solid #fff;
+  color:#555 !important;
+  overflow:hidden;
+  text-overflow: ellipsis;
+  white-space:nowrap;
+  padding:4px 8px;
+}
+.drdn-items>a:hover {text-decoration:none; background-color:#759FCF; color:#fff !important;}
+.drdn-items>*:focus {border:1px dotted #bbb;}
+
+.drdn-items.selection>*:before {
+  content:' ';
+  display:inline-block;
+  line-height:1em;
+  width:1em;
+  height:1em;
+  margin-right:4px;
+  font-weight:bold;
+}
+.drdn-items.selection>*.selected:before {
+  content:"\2713 ";
+}
+.drdn-items>span {color:#999;}
+
+#project-jump.drdn {width:200px;display:inline-block;}
+#project-jump .drdn-trigger {
+  display:inline-block;
+  border-radius:3px;
+  border:1px solid #ccc;
+  margin:0 !important;
+  vertical-align:middle;
+  color:#555;
+}
+#project-jump .drdn-content {width:280px;}
+
 /***** Tables *****/
 table.list, .table-list { border: 1px solid #e4e4e4;  border-collapse: collapse; width: 100%; margin-bottom: 4px; }
 table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
index d81820ccc8e4ce7319bb8d58d90a744821f20871..4845d790f84db4fcaec44ef78f93892b0daabb82 100644 (file)
     background: inherit;
   }
 
-  /* this represents the dropdown arrow to left of the mobile project menu */
-  #header .jump-box-arrow:before {
+  /* styles for combobox within quick-search (#project_quick_jump_box) */
+  #project-jump.drdn {
+    position: absolute;
+    top: 0px;
+    left: 0;
+
+    width: 100%;
+    max-width: 100%;
+    height: 2em;
+    height: 64px;
+    padding: 5px;
+    padding-right: 72px;
+    padding-left: 20px;
+  }
+  #project-jump .drdn-trigger {
+    font-size:1.5em;
+    font-weight:bold;
+    display:block;
+    width:100%;
+    color:#fff;
+    padding-left:24px;
+    background:transparent;
+    height:50px;
+    line-height:40px;
+    border:0;
+  }
+  #project-jump .drdn-trigger:before {
     /* set a font-size in order to achive same result in different themes */
     font-family: Verdana, sans-serif;
-    font-size: 2em;
-    line-height: 64px;
+    font-size: 1.5em;
 
     position: absolute;
     left: 0;
-
-    width: 2em;
-    padding: 0 .5em;
+    padding: 0 8px;
     /* achieve dropdwon arrow by scaling a caret character */
     content: '^';
     -webkit-transform: scale(1,-.8);
 
     opacity: .6;
   }
+  #project-jump.expanded .drdn-trigger:before {
+    -webkit-transform: scale(1,.8);
+        -ms-transform: scale(1,.8);
+            transform: scale(1,.8);
+       padding-top:8px;
+  }
 
-  /* styles for combobox within quick-search (#project_quick_jump_box) */
-  #header #quick-search select {
-    font-size: 1.5em;
-    font-weight: bold;
-    line-height: 1.2;
-
-    position: absolute;
-    top: 15px;
-    left: 0;
-
-    float: left;
-
-    width: 100%;
-    max-width: 100%;
-    height: 2em;
-    height: 35px;
-    padding: 5px;
-    padding-right: 72px;
-    padding-left: 50px;
-
-    text-indent: .01px;
-
-    color: inherit;
-    border: 0;
-    -webkit-border-radius: 0;
-    border-radius: 0;
-    background: none;
-    -webkit-box-shadow: none;
-    box-shadow: none;
-    /* hide default browser arrow */
-    -webkit-appearance: none;
-    -moz-appearance: none;
+  #project-jump .drdn-content {
+       position:absolute;
+    left:0px;
+    top:64px;
+    width:100%;
+    font-size:15px;
+    font-weight:normal;
+  }
+  #project-jump .drdn-content .autocomplete {
+    height:40px;
+       font-size:20px;
+  }
+  #project-jump .drdn-content a {
+    padding:8px;
   }
 
   #header #quick-search form {
index fc261d27713b087504caf4c3b62d0622924c7161..01c68308fbab35e442040918cea06ef94eeaab0d 100644 (file)
@@ -52,6 +52,12 @@ class ProjectsControllerTest < Redmine::ControllerTest
     assert_select 'feed>entry', :count => Project.visible(User.current).count
   end
 
+  def test_index_js
+    xhr :get, :index, :format => 'js', :q => 'coo'
+    assert_response :success
+    assert_equal 'text/javascript', response.content_type
+  end
+
   test "#index by non-admin user with view_time_entries permission should show overall spent time link" do
     @request.session[:user_id] = 3
     get :index
index 004fb674a8a6424562cb26fdf42f89feae9f8e4b..11d23d6322f7b41b2eef5c990eb3b02fa9a79717 100644 (file)
@@ -138,8 +138,8 @@ class WelcomeControllerTest < Redmine::ControllerTest
     @request.session[:user_id] = 2
 
     get :index
-    assert_select "#header select" do
-      assert_select "option", :text => 'Foo & Bar'
+    assert_select "#header #project-jump" do
+      assert_select "a", :text => 'Foo & Bar'
     end
   end