Use focus helpers to keep search container focused

This commit is contained in:
Oliver Günther
2018-05-17 10:18:41 +02:00
parent 7c372a200a
commit 269f9416dd
6 changed files with 127 additions and 44 deletions
@@ -0,0 +1,54 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2018 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2017 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See docs/COPYRIGHT.rdoc for more details.
//++
export namespace ContainHelpers {
/**
* Execute the callback when the element is outside
* @param {Element} within
* @param {Function} callback
*/
export function whenOutside(within:Element, callback:Function) {
setTimeout(() => {
if (!insideOrSelf(within, document.activeElement)) {
callback();
}
}, 20);
}
/**
* Return whether the target element is either the same as within, or contained within it.
*
* @param {Element} within
* @param {Element} target
* @returns {boolean}
*/
export function insideOrSelf(within:Element, target:Element):boolean {
return within === target || within.contains(target);
}
}
@@ -1,6 +1,8 @@
<div class="top-menu-search -collapsed" [ngClass]="{'-collapsed': collapsed}">
<div class="top-menu-search -collapsed"
(focusout)="closeWhenFocussedOutside()"
[ngClass]="{'-collapsed': collapsed}">
<input #inputEl type="text" name="q" id="q" size="20" class="top-menu-search--input" placeholder= "{{I18n.t('js.select2.searching')}}">
<a #btn id="top-menu-search-button" class="top-menu-search--button search-form-normal" title="Search" accesskey="4" tabindex="0" (click)="handleClick($event)" >
<i #searchicon class="icon5 icon-search ellipsis" aria-hidden="true"></i>
<a #btn id="top-menu-search-button" class="top-menu-search--button search-form-normal" title="Search" accesskey="4" tabindex="0" (accessibleClick)="handleClick($event)" >
<op-icon icon-classes="icon5 icon-search ellipsis"></op-icon>
</a>
</div>
@@ -26,13 +26,12 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
import {opUiComponentsModule} from '../../angular-modules';
import {ElementRef, ViewChild, HostListener, Component} from '@angular/core';
import {Directive, OnInit, Inject, Input, EventEmitter, Output} from '@angular/core';
import {I18nToken} from '../../angular4-transition-utils';
import {openprojectModule} from '../../angular-modules';
import {Component, ElementRef, HostListener, Inject, OnDestroy, Renderer2, ViewChild} from '@angular/core';
import {I18nToken} from '../../angular4-transition-utils';
import {downgradeComponent} from '@angular/upgrade/static';
import {FocusHelperService} from '../common/focus/focus-helper';
import {ContainHelpers} from "core-components/common/focus/contain-helpers";
@Component({
@@ -40,45 +39,85 @@ import {FocusHelperService} from '../common/focus/focus-helper';
template: require('!!raw-loader!./expandable-search.component.html')
})
export class ExpandableSearchComponent implements OnInit {
export class ExpandableSearchComponent implements OnDestroy {
@ViewChild('inputEl') input:ElementRef;
@ViewChild('btn') btn:ElementRef;
@ViewChild('searchicon') icon:ElementRef;
public collapsed:boolean = true;
public focused:boolean = false;
private unregisterGlobalListener:Function|undefined;
constructor(readonly FocusHelper:FocusHelperService,
@Inject(I18nToken) public I18n:op.I18n) { }
readonly elementRef:ElementRef,
readonly renderer:Renderer2,
@Inject(I18nToken) public I18n:op.I18n) {
}
// detect if click is outside or inside the element
@HostListener('document:click', ['$event']) public handleClick(event:any) {
let clickedEl = event.target;
if (clickedEl === this.input.nativeElement) { return; }
if (clickedEl === this.icon.nativeElement || clickedEl === this.btn.nativeElement) {
event.stopPropagation();
if (this.collapsed) {
// case 1: if collapsed, expand search bar
this.collapsed = false;
this.FocusHelper.focusElement(angular.element(this.input.nativeElement));
@HostListener('click', ['$event'])
public handleClick(event:JQueryEventObject):void {
event.stopPropagation();
event.preventDefault();
} else {
// case 2: if already collapsed and search string is not empty, submit form
if (this.input.nativeElement.value !== '') {
let form = jQuery(clickedEl).closest("form");
form.submit();
} else { this.collapsed = true; }
}
} else {
// if clicked outside the element
this.collapsed = true;
// clear text field for next search
this.input.nativeElement.value = '';
// If search is open, submit form when clicked on icon
if (!this.collapsed && ContainHelpers.insideOrSelf(this.btn.nativeElement, event.target)) {
this.submitNonEmptySearch();
}
if (this.collapsed) {
this.collapsed = false;
this.FocusHelper.focusElement(jQuery(this.input.nativeElement));
this.registerOutsideClick();
}
}
public closeWhenFocussedOutside() {
ContainHelpers.whenOutside(this.elementRef.nativeElement, () => this.close());
return false;
}
ngOnDestroy() {
this.unregister();
}
private registerOutsideClick() {
this.unregisterGlobalListener = this.renderer.listen('document', 'click', () => {
this.close();
});
}
private close() {
this.collapsed = true;
this.searchValue = '';
this.unregister();
}
private unregister() {
if (this.unregisterGlobalListener) {
this.unregisterGlobalListener();
this.unregisterGlobalListener = undefined;
}
}
private submitNonEmptySearch() {
if (this.searchValue !== '') {
jQuery(this.input.nativeElement)
.closest("form")
.submit();
}
}
private get searchValue() {
return this.input.nativeElement.value;
}
private set searchValue(val:string) {
this.input.nativeElement.value = val;
}
}
openprojectModule.directive('expandableSearch',
downgradeComponent({component: ExpandableSearchComponent })
downgradeComponent({component: ExpandableSearchComponent})
);
Binary file not shown.
-10
View File
@@ -365,11 +365,6 @@
"integrity": "sha1-oOHdDqVf137np1fXVTbF6WTIb4E=",
"dev": true
},
"angular2-focus": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/angular2-focus/-/angular2-focus-1.1.2.tgz",
"integrity": "sha512-jsEfWLN0618SqdIA7/SVj2iOTiVRZD8dAod5NGsj8Hs1GVNLsttiwWaU/yKVmrrGRRrCXTiS0Nm73opNt5G6Uw=="
},
"animation-frame-polyfill": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/animation-frame-polyfill/-/animation-frame-polyfill-1.0.1.tgz",
@@ -3857,11 +3852,6 @@
"resolved": "https://registry.npmjs.org/ng-file-upload/-/ng-file-upload-12.2.13.tgz",
"integrity": "sha1-AYAPOHLlJvlTEPhHfpnk8S0NjRQ="
},
"ng-focus-if": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/ng-focus-if/-/ng-focus-if-1.0.7.tgz",
"integrity": "sha1-eZRf1LbJfzKa7I+nykP/pFbkm8c="
},
"ng2-rx-componentdestroyed": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ng2-rx-componentdestroyed/-/ng2-rx-componentdestroyed-2.0.1.tgz",
-2
View File
@@ -66,7 +66,6 @@
"angular-dragula": "~1.2.8",
"angular-elastic": "2.5.0",
"angular-i18n": "~1.3.0",
"angular2-focus": "^1.1.2",
"at.js": "^1.5.3",
"atoa": "^1.0.0",
"autoprefixer": "^6.5.3",
@@ -107,7 +106,6 @@
"mousetrap": "~1.6.0",
"ng-annotate-loader": "^0.2.0",
"ng-file-upload": "^12.2.13",
"ng-focus-if": "^1.0.7",
"ng2-rx-componentdestroyed": "2.0.1",
"ngtemplate-loader": "^0.1.2",
"observable-array": "0.0.4",