mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Use focus helpers to keep search container focused
This commit is contained in:
@@ -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})
|
||||
);
|
||||
|
||||
BIN
Binary file not shown.
Generated
-10
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user