Merge pull request #7031 from opf/feature/wp-search-mobile

Global search for mobile devices
pull/7047/head
Henriette Dinger 6 years ago committed by GitHub
commit 35a890e953
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      app/assets/stylesheets/layout/_top_menu.sass
  2. 76
      app/assets/stylesheets/layout/_top_menu_mobile.sass
  3. 2
      app/views/search/_mini_form.html.erb
  4. 1
      config/locales/js-en.yml
  5. 2
      frontend/src/app/angular4-modules.ts
  6. 10
      frontend/src/app/components/resizer/main-menu-toggle.component.ts
  7. 16
      frontend/src/app/components/resizer/main-menu-toggle.service.ts
  8. 11
      frontend/src/app/modules/common/browser/device.service.ts
  9. 43
      frontend/src/app/modules/global_search/global-search-input.component.html
  10. 26
      frontend/src/app/modules/global_search/global-search-input.component.ts
  11. 5
      lib/redmine/menu_manager/top_menu_helper.rb

@ -30,7 +30,8 @@ $hamburger-right: -3px
$hamburger-width: 50px
$search-input-width: 200px
$search-input-width-expanded: 300px
$search-input-width-expanded: 20vw
$search-input-height: 30px
%top-menu-hover-styles
@include varprop(background, header-item-bg-hover-color)
@ -178,6 +179,9 @@ $search-input-width-expanded: 300px
line-height: $header-height
margin: 0 15px
.top-menu-search--back-button
display: none
.top-menu-search--button
position: absolute
right: 2px
@ -185,9 +189,8 @@ $search-input-width-expanded: 300px
@include varprop(color, header-item-font-color)
&:hover
text-decoration: none
.top-menu-search--button.-input-focused
color: $header-search-field-font-color
&.-input-focused
color: $header-search-field-font-color
.top-menu-search--loading
top: $header-height - 11px // display directly under ng-input field
@ -205,26 +208,25 @@ $search-input-width-expanded: 300px
.ng-select-container
background: transparent
border-width: 1px
border-style: solid
@include varprop(border-color, header-item-font-color)
max-height: 32px
min-height: unset
&:hover
@include varprop(border-color, header-item-font-hover-color)
.ng-arrow-wrapper
opacity: 0
display: none
.ng-clear-wrapper
@include varprop(color, header-item-font-color)
top: 1px
width: 30px
right: 25px
text-align: center
.ng-input
top: 0
input
@include varprop(color, header-item-font-color)
height: 30px
height: $search-input-height
cursor: text
.ng-placeholder
@ -232,6 +234,7 @@ $search-input-width-expanded: 300px
&.-expanded
width: $search-input-width-expanded
min-width: 250px
background: white
border-radius: 4px
color: $header-search-field-font-color

@ -26,12 +26,11 @@
// See docs/COPYRIGHT.rdoc for more details.
//++
@include breakpoint(778px down)
.top-menu-search--wrapper
display: none
@include breakpoint(680px down)
$hamburger-right: -3px
$hamburger-width: 50px
$search-input-height-mobile: 36px
#logo
background-color: transparent
@ -67,27 +66,63 @@
.nosidebar &
left: 0px
#account-nav-right
> li
display: none
.top-menu-search
&.-mobile
width: 100vw
margin: 0
padding-right: 15px
&.drop-down,
&:last-child
display: block
.top-menu-search--button
display: none
@include varprop(color, header-item-font-color)
&.last-child
> a
display: block
text-align: center
#global-search-input.-expanded
width: calc(100vw - 50px)
min-width: unset
position: unset
> ul
top: $header-height-mobile
width: 100vw
box-shadow: 1px 1px 4px #cccccc
border: solid 1px rgba(0, 0, 0, 0.2)
.scroll-host
max-height: calc(100vh - #{$header-height})
.ng-clear-wrapper
width: 40px
right: 0
.ng-select-bottom
height: calc(100vh - #{$header-height})
overflow-y: auto
margin: 0
.ng-select-container
height: $search-input-height-mobile !important
border-radius: 4px
li
max-width: none
.ng-input input
height: 36px
.top-menu-search--back-button
display: initial
width: 50px
text-align: center
@include varprop(color, header-item-font-color)
&:hover, &:focus, &:active
text-decoration: none
.top-menu-search--loading
top: $header-height + 1px
.top-menu-search--button.-input-focused
@include varprop(color, header-item-font-color)
#account-nav-right
> li ul
top: $header-height-mobile
width: 100vw
box-shadow: 1px 1px 4px #cccccc
border: solid 1px rgba(0, 0, 0, 0.2)
li
max-width: none
#more-menu.drop-down--modules
// 58 = Width of one menu item
@ -123,4 +158,3 @@
border-left: none !important
.project-menu-autocomplete--results
border-bottom: 2px solid $header-drop-down-border-color !important

@ -26,7 +26,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
See docs/COPYRIGHT.rdoc for more details.
++#%>
<div class="top-menu-search--wrapper hidden-for-mobile">
<div class="top-menu-search--wrapper">
<%= label_tag("q", l(:label_search), class: "hidden-for-sighted") %>
<global-search-input>

@ -829,6 +829,7 @@ en:
global_search:
all_projects: "In all projects"
search: "Search"
close_search: "Close search"
current_project: "In this project"
current_project_and_all_descendants: "In this project + subprojects"
title:

@ -80,6 +80,7 @@ import {BrowserModule} from "@angular/platform-browser";
import {OpenprojectCalendarModule} from "core-app/modules/calendar/openproject-calendar.module";
import {FullCalendarModule} from "ng-fullcalendar";
import {OpenprojectGlobalSearchModule} from "core-app/modules/global_search/openproject-global-search.module";
import {DeviceService} from "core-app/modules/common/browser/device.service";
@NgModule({
imports: [
@ -134,6 +135,7 @@ import {OpenprojectGlobalSearchModule} from "core-app/modules/global_search/open
PaginationService,
OpenProjectFileUploadService,
CurrentProjectService,
DeviceService,
// Split view
CommentService,
// Context menus

@ -33,12 +33,13 @@ import {untilComponentDestroyed} from 'ng2-rx-componentdestroyed';
import {MainMenuResizerComponent} from "core-components/resizer/main-menu-resizer.component";
import {DynamicBootstrapper} from "core-app/globals/dynamic-bootstrapper";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {DeviceService} from "app/modules/common/browser/device.service";
import {Injector} from "@angular/core";
@Component({
selector: 'main-menu-toggle',
template: `
<div *ngIf="this.currentProject.id !== null || toggleService.isMobile" id="main-menu-toggle"
<div *ngIf="this.currentProject.id !== null || this.deviceService.isMobile" id="main-menu-toggle"
aria-haspopup="true"
[attr.title]="toggleTitle"
(accessibleClick)="toggleService.toggleNavigation($event)"
@ -50,18 +51,13 @@ import {Injector} from "@angular/core";
`
})
/*
* Groesse des Menus feststellen
* pruefen, ob kleiner 10 -> Groesse der Sidebar setzen
* collapsed boolean setzen, label im resizer und hamburger icon setzen
*
*/
export class MainMenuToggleComponent implements OnInit, OnDestroy {
toggleTitle:string = "";
currentProject:CurrentProjectService = this.injector.get(CurrentProjectService);
constructor(readonly toggleService:MainMenuToggleService,
readonly cdRef:ChangeDetectorRef,
readonly deviceService:DeviceService,
protected injector:Injector) {
}

@ -30,6 +30,7 @@ import {Injectable} from '@angular/core';
import {BehaviorSubject, fromEvent, Observable, Subscription} from 'rxjs';
import {I18nService} from "core-app/modules/common/i18n/i18n.service";
import {CurrentProjectService} from "core-components/projects/current-project.service";
import {DeviceService} from "app/modules/common/browser/device.service";
import {Injector} from "@angular/core";
@Injectable()
@ -58,7 +59,8 @@ export class MainMenuToggleService {
private resizeSubscription$:Subscription;
constructor(protected I18n:I18nService,
protected injector:Injector) {
protected injector:Injector,
readonly deviceService:DeviceService) {
}
public initializeMenu():void {
@ -78,7 +80,7 @@ export class MainMenuToggleService {
}
// mobile version default: hide menu on initialization
if (this.isMobile) {
if (this.deviceService.isMobile) {
this.closeMenu();
}
}
@ -91,7 +93,7 @@ export class MainMenuToggleService {
}
if (!this.showNavigation) { // sidebar is hidden -> show menu
if (this.isMobile) { // mobile version
if (this.deviceService.isMobile) { // mobile version
this.setWidth(window.innerWidth);
// On mobile the main menu shall close whenever you click outside the menu.
this.setupAutocloseMainMenu();
@ -113,7 +115,7 @@ export class MainMenuToggleService {
}
public closeMenu():void {
if (this.isMobile) {
if (this.deviceService.isMobile) {
this.saveWidth(0);
} else {
this.setWidth(0);
@ -122,7 +124,7 @@ export class MainMenuToggleService {
}
public closeWhenOnMobile():void {
if (this.isMobile) {
if (this.deviceService.isMobile) {
this.closeMenu()
};
}
@ -206,10 +208,6 @@ export class MainMenuToggleService {
}
}
public get isMobile():boolean {
return (window.innerWidth < 680);
}
public get showNavigation():boolean {
return (this.elementWidth > 10);
}

@ -0,0 +1,11 @@
import {Injectable} from '@angular/core';
@Injectable()
export class DeviceService {
public mobileWidthTreshold:number = 680;
public get isMobile():boolean {
return (window.innerWidth < this.mobileWidthTreshold);
}
}

@ -1,4 +1,4 @@
<div class="top-menu-search">
<div class="top-menu-search" [class.-mobile]="mobileSearch">
<div class="top-menu-search--loading ui-autocomplete--loading" style="display: none">
<div class="loading-indicator -small">
<div class="block-1"></div>
@ -8,23 +8,30 @@
<div class="block-5"></div>
</div>
</div>
<ng-select #select
[items]="results"
name="global-search-input"
id="global-search-input"
placeholder="{{text.search}}"
[ngClass]="'top-menu-search--input'"
[class.-expanded]="expanded"
[openOnEnter]="false"
[searchFn]="customSearchFn"
[typeahead]="searchTermChanged$"
(focus)="onFocus()"
(focusout)="onFocusOut()"
(search)="search($event)"
(open)="openCloseMenu(currentValue)"
(close)="select.filterValue = currentValue"
(change)="onChange($event)"
(clear)="clearSearch()">
<a *ngIf="mobileSearch"
(click)="toggleMobileSearch()"
class="top-menu-search--back-button"
href="#"
title="{{text.close_search}}">
<i class="icon-arrow-left1" aria-hidden="true"></i>
</a>
<ng-select #select
[items]="results"
name="global-search-input"
id="global-search-input"
placeholder="{{text.search}}"
[ngClass]="'top-menu-search--input hidden-for-mobile'"
[class.-expanded]="expanded"
[openOnEnter]="false"
[searchFn]="customSearchFn"
[typeahead]="searchTermChanged$"
(focus)="onFocus()"
(focusout)="onFocusOut()"
(search)="search($event)"
(open)="openCloseMenu(currentValue)"
(close)="select.filterValue = currentValue"
(change)="onChange($event)"
(clear)="clearSearch()">
<ng-template ng-option-tmp let-item="item" let-index="index" let-search="searchTerm">
<div *ngIf="!item.id; else workPackageItemTemplate">
<div tabindex="-1" class="global-search--option">

@ -45,6 +45,7 @@ import {CollectionResource} from "app/modules/hal/resources/collection-resource"
import {DynamicCssService} from "app/modules/common/dynamic-css/dynamic-css.service";
import {GlobalSearchService} from "app/modules/global_search/global-search.service";
import {CurrentProjectService} from "app/components/projects/current-project.service";
import {DeviceService} from "app/modules/common/browser/device.service";
import {NgSelectComponent} from "@ng-select/ng-select";
import {debounceTime, distinctUntilChanged} from "rxjs/operators";
import {untilComponentDestroyed} from "ng2-rx-componentdestroyed";
@ -65,6 +66,7 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy {
public expanded:boolean = false;
public results:any[];
public suggestions:any[];
public mobileSearch:boolean = false;
public searchTermChanged$:Subject<string> = new Subject<string>();
@ -77,7 +79,8 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy {
all_projects: this.I18n.t('js.global_search.all_projects'),
current_project: this.I18n.t('js.global_search.current_project'),
current_project_and_all_descendants: this.I18n.t('js.global_search.current_project_and_all_descendants'),
search: this.I18n.t('js.global_search.search') + ' ...'
search: this.I18n.t('js.global_search.search') + ' ...',
close_search: this.I18n.t('js.global_search.close_search')
};
constructor(readonly elementRef:ElementRef,
@ -87,6 +90,7 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy {
readonly dynamicCssService:DynamicCssService,
readonly globalSearchService:GlobalSearchService,
readonly currentProjectService:CurrentProjectService,
readonly deviceService:DeviceService,
readonly cdRef:ChangeDetectorRef) {
}
@ -125,10 +129,22 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy {
// handle click on search button
if (ContainHelpers.insideOrSelf(this.btn.nativeElement, event.target)) {
this.submitNonEmptySearch();
if (this.deviceService.isMobile) {
this.toggleMobileSearch();
// open ng-select menu on default
jQuery('.ng-input input').focus();
} else {
this.submitNonEmptySearch();
}
}
}
// open or close mobile search
public toggleMobileSearch() {
jQuery('.ng-select, #account-nav-right, #account-nav-left, #main-menu-toggle').toggleClass('hidden-for-mobile');
this.mobileSearch = !this.mobileSearch;
}
// load selected item
public onChange($event:any) {
let selectedOption = $event;
@ -162,8 +178,10 @@ export class GlobalSearchInputComponent implements OnInit, OnDestroy {
}
public onFocusOut() {
this.expanded = (this.ngSelectComponent.filterValue.length > 0);
this.ngSelectComponent.isOpen = false;
if (!this.deviceService.isMobile) {
this.expanded = (this.ngSelectComponent.filterValue.length > 0);
this.ngSelectComponent.isOpen = false;
}
}
public clearSearch() {

@ -64,8 +64,9 @@ module Redmine::MenuManager::TopMenuHelper
link = link_to url,
class: 'login',
title: l(:label_login) do
concat(t(:label_login))
concat('<i class="button--dropdown-indicator"></i>'.html_safe)
concat('<span class="button--dropdown-text hidden-for-mobile">'.concat(l(:label_login)).concat('</span>').html_safe)
concat('<i class="button--dropdown-indicator hidden-for-mobile"></i>'.html_safe)
concat('<i class="icon2 icon-user hidden-for-desktop"></i>'.html_safe)
end
render_menu_dropdown(link, menu_item_class: 'drop-down last-child') do

Loading…
Cancel
Save