]> source.dussan.org Git - redmine.git/commitdiff
Responsive layout for mobile devices (#19097).
authorJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 7 Nov 2015 13:37:08 +0000 (13:37 +0000)
committerJean-Philippe Lang <jp_lang@yahoo.fr>
Sat, 7 Nov 2015 13:37:08 +0000 (13:37 +0000)
Patch by Felix Gliesche.

git-svn-id: http://svn.redmine.org/redmine/trunk@14817 e93f8b46-1217-0410-a6f0-8f06a7374b81

app/helpers/application_helper.rb
app/views/layouts/base.html.erb
public/javascripts/responsive.js [new file with mode: 0644]
public/stylesheets/responsive.css [new file with mode: 0644]

index c345083b70357f15acaf941b49b5360686c632f2..fce5d6e220d1f2cd40c53f7c46703a5be0637c85 100644 (file)
@@ -340,6 +340,7 @@ module ApplicationHelper
         { :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; }')
     end
   end
@@ -1267,7 +1268,7 @@ module ApplicationHelper
 
   # Returns the javascript tags that are included in the html layout head
   def javascript_heads
-    tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application')
+    tags = javascript_include_tag('jquery-1.11.1-ui-1.11.0-ujs-3.1.4', 'application', 'responsive')
     unless User.current.pref.warn_on_leaving_unsaved == '0'
       tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
     end
index b1ffcaf1323d81892653c6346b299881237d61b0..1477492900957a0a579e26ab182e382ac4662768 100644 (file)
@@ -4,11 +4,12 @@
 <meta charset="utf-8" />
 <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 
 <title><%= html_title %></title>
+<meta name="viewport" content="width=device-width, initial-scale=1">
 <meta name="description" content="<%= Redmine::Info.app_name %>" />
 <meta name="keywords" content="issue,bug,tracker" />
 <%= csrf_meta_tag %>
 <%= favicon %>
-<%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', :media => 'all' %>
+<%= stylesheet_link_tag 'jquery/jquery-ui-1.11.0', 'application', 'responsive', :media => 'all' %>
 <%= stylesheet_link_tag 'rtl', :media => 'all' if l(:direction) == 'rtl' %>
 <%= javascript_heads %>
 <%= heads_for_theme %>
 </head>
 <body class="<%= body_css_classes %>">
 <div id="wrapper">
+
+<div class="flyout-menu js-flyout-menu">
+
+
+    <% if User.current.logged? || !Setting.login_required? %>
+        <div class="flyout-menu__search">
+            <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
+            <%= hidden_field_tag(controller.default_search_scope, 1, :id => nil) if controller.default_search_scope %>
+            <%= label_tag 'flyout-search', '&#9906;'.html_safe, :class => 'search-magnifier search-magnifier--flyout' %>
+            <%= text_field_tag 'q', @question, :id => 'flyout-search', :class => 'small js-search-input', :placeholder => l(:label_search) %>
+            <% end %>
+        </div>
+    <% end %>
+
+    <% if User.current.logged? %>
+        <div class="flyout-menu__avatar <% if !Setting.gravatar_enabled? %>flyout-menu__avatar--no-avatar<% end %>">
+            <% if Setting.gravatar_enabled? %>
+                <%= link_to(avatar(User.current, :size => "80"), user_path(User.current)) %>
+            <% end %>
+            <%= link_to_user(User.current, :format => :username) %>
+        </div>
+    <% end %>
+
+    <% if display_main_menu?(@project) %>
+        <h3><%= l(:label_project) %></h3>
+        <span class="js-project-menu"></span>
+    <% end %>
+
+    <h3><%= l(:label_general) %></h3>
+    <span class="js-general-menu"></span>
+
+    <span class="js-sidebar flyout-menu__sidebar"></span>
+
+    <h3><%= l(:label_profile) %></h3>
+    <span class="js-profile-menu"></span>
+
+</div>
+
 <div id="wrapper2">
 <div id="wrapper3">
 <div id="top-menu">
@@ -29,6 +68,9 @@
 </div>
 
 <div id="header">
+
+    <a href="#" class="mobile-toggle-button js-flyout-menu-toggle-button"></a>
+
     <% if User.current.logged? || !Setting.login_required? %>
     <div id="quick-search">
         <%= form_tag({:controller => 'search', :action => 'index', :id => @project}, :method => :get ) do %>
diff --git a/public/javascripts/responsive.js b/public/javascripts/responsive.js
new file mode 100644 (file)
index 0000000..4c6c3be
--- /dev/null
@@ -0,0 +1,83 @@
+// generic layout specific responsive stuff goes here
+
+function openFlyout() {
+  $('html').addClass('flyout-is-active');
+  $('#wrapper2').on('click', function(e){
+    e.preventDefault();
+    e.stopPropagation();
+    closeFlyout();
+  });
+}
+
+function closeFlyout() {
+  $('html').removeClass('flyout-is-active');
+  $('#wrapper2').off('click');
+}
+
+
+function isMobile() {
+  return $('.js-flyout-menu-toggle-button').is(":visible");
+}
+
+function setupFlyout() {
+  var mobileInit = false,
+    desktopInit = false;
+
+  /* click handler for mobile menu toggle */
+  $('.js-flyout-menu-toggle-button').on('click', function(e) {
+    e.preventDefault();
+    e.stopPropagation();
+    if($('html').hasClass('flyout-is-active')) {
+      closeFlyout();
+    } else {
+      openFlyout();
+    }
+  });
+
+  /* bind resize handler */
+  $(window).resize(function() {
+    initMenu();
+  })
+
+  /* menu init function for dom detaching and appending on mobile / desktop view */
+  function initMenu() {
+
+    var _initMobileMenu = function() {
+      /* only init mobile menu, if it hasn't been done yet */
+      if(!mobileInit) {
+
+        $('#main-menu > ul').detach().appendTo('.js-project-menu');
+        $('#top-menu > ul').detach().appendTo('.js-general-menu');
+        $('#sidebar > *').detach().appendTo('.js-sidebar');
+        $('#account ul').detach().appendTo('.js-profile-menu');
+
+        mobileInit = true;
+        desktopInit = false;
+      }
+    }
+
+    var _initDesktopMenu = function() {
+      if(!desktopInit) {
+
+        $('.js-project-menu > ul').detach().appendTo('#main-menu');
+        $('.js-general-menu ul').detach().appendTo('#top-menu');
+        $('.js-sidebar > *').detach().appendTo('#sidebar');
+        $('.js-profile-menu ul').detach().appendTo('#account');
+
+        desktopInit = true;
+        mobileInit = false;
+      }
+    }
+
+    if(isMobile()) {
+      _initMobileMenu();
+    } else {
+      _initDesktopMenu();
+    }
+  }
+
+  // init menu on page load
+  initMenu();
+}
+
+$(document).ready(setupFlyout);
diff --git a/public/stylesheets/responsive.css b/public/stylesheets/responsive.css
new file mode 100644 (file)
index 0000000..a01d126
--- /dev/null
@@ -0,0 +1,595 @@
+/*----------------------------------------*\
+  RESPONSIVE CSS
+\*----------------------------------------*/
+
+
+/*
+
+    CONTENTS
+
+    A) BASIC MOBILE RESETS
+    B) HEADER & TOP MENUS
+    C) MAIN CONTENT & SIDEBAR
+    D) TOGGLE BUTTON & FLYOUT MENU
+
+*/
+
+
+/* Hide new elements (toggle button and flyout menu) above 900px */
+.mobile-toggle-button,
+.flyout-menu
+{
+    display: none;
+}
+
+/*
+    redmine's body is set to min-width: 900px
+    add first breakpoint here and start adding responsiveness
+*/
+
+@media all and (max-width: 899px)
+{
+    /*----------------------------------------*\
+        A) BASIC MOBILE RESETS
+    \*----------------------------------------*/
+
+    /*
+        apply natural border box, see: http://www.paulirish.com/2012/box-sizing-border-box-ftw/
+        this helps us to better deal with percentages and padding / margin
+    */
+    *,
+    *:before,
+    *:after
+    {
+        -webkit-box-sizing: border-box;
+           -moz-box-sizing: border-box;
+                box-sizing: border-box;
+    }
+
+    body,
+    html
+    {
+        height: 100%;
+        margin: 0;
+        padding: 0;
+    }
+
+    html
+    {
+        overflow-y: auto; /* avoid 2nd scrollbar on desktop */
+    }
+
+    body
+    {
+        overflow-x: hidden; /* hide horizontal overflow */
+
+        min-width: 0; /* reset the min-width of 900px */
+    }
+
+
+    body,
+    input,
+    select,
+    textarea,
+    button
+    {
+        font-size: 14px;  /* Set font-size for standard elements to 14px */
+    }
+
+
+    select
+    {
+        max-width: 100%;  /* prevent long names within select menues from breaking content */
+    }
+
+
+    #wrapper
+    {
+        position: relative;
+
+        max-width: 100%;
+    }
+
+    #wrapper,
+    #wrapper2
+    {
+        margin: 0;
+    }
+
+    /*----------------------------------------*\
+        B) HEADER & TOP MENUS
+    \*----------------------------------------*/
+
+    #header
+    {
+        width: 100%;
+        height: 64px; /* the height of our header on mobile */
+        min-height: 0;
+        margin: 0;
+        padding: 0;
+
+        border: none;
+        background-color: #628db6;
+    }
+
+    /* Hide project name on mobile (project name is still visible in select menu) */
+    #header h1
+    {
+        display: none !important;
+    }
+
+    /* reset #header a color for mobile toggle button */
+    #header a.mobile-toggle-button
+    {
+        color: #f8f8f8;
+    }
+
+
+    /* Hide top-menu and main-menu on mobile, because it's placed in our flyout menu */
+    #top-menu,
+    #header #main-menu
+    {
+        display: none;
+    }
+
+    /* the quick search within header holding search form and #project_quick_jump_box box*/
+    #header #quick-search
+    {
+        float: none;
+        clear: none; /* there are themes which set clear property, this resets it */
+
+        max-width: 100%; /* reset max-width */
+        margin: 0;
+
+        background: inherit;
+    }
+
+    /* this represents the dropdown arrow to left of the mobile project menu */
+    #header .jump-box-arrow: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;
+
+        position: absolute;
+        left: 0;
+
+        width: 2em;
+        padding: 0 .5em;
+        /* achieve dropdwon arrow by scaling a caret character */
+
+        content: '^';
+        -webkit-transform: scale(1,-.8);
+            -ms-transform: scale(1,-.8);
+                transform: scale(1,-.8);
+        text-align: right;
+        pointer-events: none;
+
+        opacity: .6;
+    }
+
+    /* 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;
+    }
+
+    #header #quick-search form
+    {
+        display: none;
+    }
+
+    /*----------------------------------------*\
+        C) MAIN CONTENT & SIDEBAR
+    \*----------------------------------------*/
+
+    #main
+    {
+        padding: 0;
+    }
+
+    #main.nosidebar #content,
+    div#content
+    {
+        width: 100%;
+        min-height: 0; /* reset min-height of #content */
+        margin: 0;
+    }
+
+
+    /* hide sidebar and sidebar switch panel, since it's placed in mobile flyout menu */
+    #sidebar,
+    #sidebar-switch-panel
+    {
+        display: none;
+    }
+
+    .splitcontentleft
+    {
+        width: 100%; /* use full width */
+    }
+
+    .splitcontentright
+    {
+        width: 100%; /* use full width */
+    }
+
+    /*----------------------------------------*\
+        D) TOGGLE BUTTON & FLYOUT MENU
+    \*----------------------------------------*/
+
+    /* Mobile toggle button */
+
+    .mobile-toggle-button
+    {
+        font-size: 42px;
+        line-height: 64px;
+
+        position: relative;
+        z-index: 10;
+
+        display: block; /* remove display: none; of non-mobile version */
+        float: right;
+
+        width: 60px;
+        height: 64px;
+        margin-top: 0;
+
+        text-align: center;
+
+        border-left: 1px solid #ddd;
+    }
+
+    .mobile-toggle-button:hover,
+    .mobile-toggle-button:active
+    {
+        text-decoration: none;
+    }
+
+    .mobile-toggle-button:after
+    {
+        font-family: Verdana, sans-serif;
+
+        display: block;
+
+        margin-top: -3px;
+
+        content: '\2261';
+    }
+
+    /* search magnifier icon */
+    .search-magnifier
+    {
+        font-family: Verdana;
+
+        cursor: pointer;
+        -webkit-transform: rotate(-45deg);
+           -moz-transform: rotate(45deg);
+             -o-transform: rotate(45deg);
+
+        color: #bbb;
+    }
+
+    .search-magnifier--flyout
+    {
+        font-size: 25px;
+        line-height: 54px;
+
+        position: absolute;
+        z-index: 1;
+        left: 12px;
+    }
+
+
+    /* Flyout Menu */
+
+    .flyout-menu
+    {
+        position: absolute;
+        right: -250px;
+
+        display: block; /* remove display: none; of non-mobile version */
+        overflow-x: hidden;
+
+        width: 250px;
+        height: 100%;
+        margin: 0;      /* reset margin for themes that define it */
+        padding: 0;     /* reset padding for themes that define it */
+
+        color: white;
+        background-color: #3e5b76;
+    }
+
+
+    /* avoid zoom on search input focus for ios devices */
+    .flyout-menu input[type='text']
+    {
+        font-size: 16px;
+    }
+
+    .flyout-menu h3
+    {
+        font-size: 11px;
+        line-height: 19px;
+
+        height: 20px;
+        margin: 0;
+        padding: 0;
+
+        letter-spacing: .1em;
+        text-transform: uppercase;
+
+        color: white;
+        border-top: 1px solid #506a83;
+        border-bottom: 1px solid #506a83;
+        background-color: #628db6;
+    }
+
+    .flyout-menu h4
+    {
+        color: white;
+    }
+
+    .flyout-menu h3,
+    .flyout-menu h4,
+    .flyout-menu > p,
+    .flyout-menu > a,
+    .flyout-menu ul li a,
+    .flyout-menu__search,
+    .flyout-menu__sidebar > div,
+    .flyout-menu__sidebar > p,
+    .flyout-menu__sidebar > a,
+    .flyout-menu__sidebar > form,
+    .flyout-menu > div,
+    .flyout-menu > form
+    {
+        padding-left: 8px;
+    }
+
+    .flyout-menu .flyout-menu__avatar
+    {
+        margin-top: -1px; /* move avatar up 1px */
+        padding-left: 0;
+    }
+
+    .flyout-menu__sidebar > form
+    {
+        display: block;
+    }
+
+    .flyout-menu__sidebar > form h3
+    {
+        margin-left: -8px;
+    }
+
+    .flyout-menu__sidebar > form label
+    {
+        display: inline-block;
+
+        margin: 8px 0;
+    }
+
+    .flyout-menu__sidebar > form br  br
+    {
+        display: none;
+    }
+
+    .flyout-menu ul
+    {
+        margin: 0;
+        padding: 0;
+
+        list-style: none;
+    }
+
+    .flyout-menu ul li a
+    {
+        line-height: 40px;
+
+        display: block;
+        overflow: hidden;
+
+        height: 40px;
+
+        white-space: nowrap;
+        text-overflow: ellipsis;
+
+        border-top: 1px solid rgba(255,255,255,.1);
+    }
+
+    .flyout-menu ul li:first-child a
+    {
+        line-height: 39px;
+
+        height: 39px;
+
+        border-top: none;
+    }
+
+    .flyout-menu a
+    {
+        color: white;
+    }
+
+    .flyout-menu ul li a:hover
+    {
+        text-decoration: none;
+    }
+
+    .flyout-menu ul li a.new-object,
+    .new-object ~ .menu-children
+    {
+        display: none;
+    }
+
+    /* Left flyout search container */
+    .flyout-menu__search
+    {
+        line-height: 54px;
+
+        height: 64px;
+        padding-top: 3px;
+        padding-right: 8px;
+    }
+
+    .flyout-menu__search input[type='text']
+    {
+        line-height: 2;
+
+        width: 100%;
+        height: 38px;
+        padding-left: 27px;
+
+        vertical-align: middle;
+
+        border: none;
+        -webkit-border-radius: 3px;
+                border-radius: 3px;
+        background-color: #fff;
+    }
+
+    .flyout-menu__avatar
+    {
+        display: -webkit-box;
+        display: -webkit-flex;
+        display: -ms-flexbox;
+        display:         flex;
+
+        width: 100%;
+
+        border-top: 1px solid rgba(255,255,255,.1);
+    }
+
+
+    .flyout-menu__avatar img.gravatar
+    {
+        width: 40px;
+        height: 40px;
+        padding: 0;
+
+        vertical-align: top;
+
+        border-width: 0;
+    }
+
+    .flyout-menu__avatar a
+    {
+        line-height: 40px;
+
+        height: auto;
+        height: 40px;
+
+        text-decoration: none;
+
+        color: white;
+    }
+
+    /* avatar */
+    .flyout-menu__avatar a:first-child
+    {
+        line-height: 0;
+
+        width: 40px;
+        padding: 0;
+    }
+
+    .flyout-menu__avatar .user
+    {
+        padding-left: 15px;
+    }
+
+    /* user link when no avatar is present */
+    .flyout-menu__avatar--no-avatar a.user
+    {
+        line-height: 40px;
+
+        padding-left: 8px;
+    }
+
+
+    .flyout-is-active body
+    {
+        overflow: hidden; /* for body not to have scrollbars when left flyout menu is active */
+    }
+
+    html.flyout-is-active
+    {
+        overflow: hidden;
+    }
+
+
+    .flyout-is-active  #wrapper
+    {
+        right: 250px; /* when left flyout is active, move body to the right (same amount like flyout-menu's width) */
+
+        height: 100%;
+    }
+
+    .flyout-is-active .mobile-toggle-button:after
+    {
+        content: '\00D7'; /* close glyph */
+    }
+
+    .flyout-is-active #wrapper2
+    {
+
+    /*
+     * only relevant for devices with cursor when flyout it active, in order to show,
+     * that whole wrapper content is clickable and closes flyout menu
+     */
+        cursor: pointer;
+    }
+
+
+    #admin-menu
+    {
+        padding-left: 0;
+    }
+
+    #admin-menu li
+    {
+        padding-bottom: 0;
+    }
+
+    #admin-menu a,
+    #admin-menu a.selected
+    {
+        line-height: 40px;
+
+        padding: 0;
+        padding-left: 32px !important;
+
+        background-position: 8px 50%;
+    }
+}