From 62823442d369705290807fa2a5191a1a67d6ee09 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Lang Date: Sat, 26 Nov 2016 08:16:15 +0000 Subject: [PATCH] Replaces project jump select with custom dropdown (#23310). git-svn-id: http://svn.redmine.org/redmine/trunk@15994 e93f8b46-1217-0410-a6f0-8f06a7374b81 --- app/controllers/projects_controller.rb | 7 ++ app/helpers/application_helper.rb | 42 +++++++--- app/views/projects/index.js.erb | 2 + app/views/wiki/show.html.erb | 17 ++-- public/javascripts/application.js | 63 +++++++++++++++ public/stylesheets/application.css | 75 ++++++++++++++++++ public/stylesheets/responsive.css | 88 ++++++++++++--------- test/functional/projects_controller_test.rb | 6 ++ test/functional/welcome_controller_test.rb | 4 +- 9 files changed, 246 insertions(+), 58 deletions(-) create mode 100644 app/views/projects/index.js.erb diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 98fc23f5f..ad38a15a6 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index b6874757c..d4bd6a2f2 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -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 = - ("" + - '').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 index 000000000..b4731fb09 --- /dev/null +++ b/app/views/projects/index.js.erb @@ -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 %>'); diff --git a/app/views/wiki/show.html.erb b/app/views/wiki/show.html.erb index 1cbfcbc65..4eaa3d62f 100644 --- a/app/views/wiki/show.html.erb +++ b/app/views/wiki/show.html.erb @@ -1,11 +1,13 @@
-<% 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) %> + + Hello +
+<% 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') %> @@ -15,6 +17,11 @@ <% 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 %> +
+
<%= wiki_page_breadcrumb(@page) %> diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 3d1ad43cd..ad7109e2b 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -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")) { diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index e3cde7c8b..459d35fcc 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -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; } diff --git a/public/stylesheets/responsive.css b/public/stylesheets/responsive.css index d81820ccc..4845d790f 100644 --- a/public/stylesheets/responsive.css +++ b/public/stylesheets/responsive.css @@ -125,18 +125,40 @@ 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); @@ -147,39 +169,27 @@ 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 { diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb index fc261d277..01c68308f 100644 --- a/test/functional/projects_controller_test.rb +++ b/test/functional/projects_controller_test.rb @@ -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 diff --git a/test/functional/welcome_controller_test.rb b/test/functional/welcome_controller_test.rb index 004fb674a..11d23d632 100644 --- a/test/functional/welcome_controller_test.rb +++ b/test/functional/welcome_controller_test.rb @@ -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 -- 2.39.5