AMBARI-22374 Log Search UI: button with caret doesn't toggle dropdown is some cases. (Istvan Tobias via ababiichuk)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/f74b2f87 Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/f74b2f87 Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/f74b2f87 Branch: refs/heads/branch-feature-AMBARI-21674 Commit: f74b2f873b147e7c5639e2a342fc0627b8335f47 Parents: ec3f1e4 Author: Istvan Tobias <[email protected]> Authored: Tue Nov 7 15:27:38 2017 +0200 Committer: ababiichuk <[email protected]> Committed: Tue Nov 7 15:27:38 2017 +0200 ---------------------------------------------------------------------- .../menu-button/menu-button.component.html | 17 +-- .../menu-button/menu-button.component.less | 22 ++- .../menu-button/menu-button.component.ts | 144 ++++++++++++++++--- 3 files changed, 154 insertions(+), 29 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/f74b2f87/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html index ca70927..5e2b15f 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.html @@ -15,14 +15,15 @@ limitations under the License. --> -<div #dropdown [ngClass]="{'dropdown': hasSubItems, 'text-center': true}"> - <a [ngClass]="iconClass + ' icon'" (mousedown)="onMouseDown($event)" (mouseup)="onMouseUp($event)" - (click)="$event.stopPropagation()"></a> - <a #dropdownToggle class="dropdown-toggle caret" data-toggle="dropdown" *ngIf="hasCaret"></a> - <br> - <a *ngIf="label" (mousedown)="onMouseDown($event)" [ngClass]="labelClass" (mouseup)="onMouseUp($event)" - (click)="$event.stopPropagation()">{{label}}</a> - <ul data-component="dropdown-list" *ngIf="hasSubItems" [items]="subItems" (selectedItemChange)="updateValue($event)" +<div #dropdown [ngClass]="{'dropdown': hasSubItems, 'text-center': true, 'open': dropdownIsOpen}"> + <a class="dropdown-toggle" [ngClass]="(labelClass || '') + (hasCaret ? ' has-caret' : '')" + (click)="onMouseClick($event)" + (mousedown)="onMouseDown($event)"> + <i *ngIf="iconClass" [ngClass]="['icon', iconClass]"></i> + <i *ngIf="hasCaret" [ngClass]="['fa ', caretClass ]"></i> + <span *ngIf="label" class="menu-button-label">{{label}}</span> + </a> + <ul data-component="dropdown-list" *ngIf="hasSubItems" [items]="subItems" (selectedItemChange)="onDropdownItemChange($event)" [isMultipleChoice]="isMultipleChoice" [additionalLabelComponentSetter]="additionalLabelComponentSetter" [ngClass]="{'dropdown-menu': true, 'dropdown-menu-right': isRightAlign}"></ul> </div> http://git-wip-us.apache.org/repos/asf/ambari/blob/f74b2f87/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less index 615db24..0207561 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.less @@ -21,14 +21,26 @@ cursor: pointer; display: inline-block; position: relative; - a:hover, a:focus { + a { + text-align: center; text-decoration: none; + i { + color: @link-color; + display: inline-block; + position: relative; + &.fa-caret-down { + padding: 0 .25em; + } + } + .menu-button-label { + display: block; + } } - - .icon { - padding: @icon-padding; + a:hover, a:focus { + i { + color: @link-hover-color; + } } - .unstyled-link { color: inherit; } http://git-wip-us.apache.org/repos/asf/ambari/blob/f74b2f87/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts ---------------------------------------------------------------------- diff --git a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts index 0aa7c7e..5932e1b 100644 --- a/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts +++ b/ambari-logsearch/ambari-logsearch-web/src/app/components/menu-button/menu-button.component.ts @@ -19,7 +19,6 @@ import {Component, Input, ViewChild, ElementRef} from '@angular/core'; import {ListItem} from '@app/classes/list-item'; import {ComponentActionsService} from '@app/services/component-actions.service'; -import * as $ from 'jquery'; @Component({ selector: 'menu-button', @@ -64,6 +63,37 @@ export class MenuButtonComponent { @Input() badge: string; + @Input() + caretClass: string = 'fa-caret-down'; + + /** + * The minimum time to handle a mousedown as a longclick. Default is 500 ms (0.5sec) + * @default 500 + * @type {number} + */ + @Input() + minLongClickDelay: number = 500; + + /** + * The maximum milliseconds to wait for longclick ends. The default is 0 which means no upper limit. + * @default 0 + * @type {number} + */ + @Input() + maxLongClickDelay: number = 0; + + /** + * This is a private property to indicate the mousedown timestamp, so that we can check it when teh click event + * has been triggered. + */ + private mouseDownTimestamp: number; + + /** + * Indicates if the dropdown list is open or not. So that we use internal state to display or hide the dropdown. + * @type {boolean} + */ + private dropdownIsOpen: boolean = false; + get hasSubItems(): boolean { return Boolean(this.subItems && this.subItems.length); } @@ -72,26 +102,108 @@ export class MenuButtonComponent { return this.hasSubItems && !this.hideCaret; } - private clickStartTime: number; - - private readonly longClickInterval = 1000; - - onMouseDown(event: MouseEvent): void { - if (this.action && event.button === 0) { - this.clickStartTime = (new Date()).getTime(); + /** + * Handling the click event on the component element. + * Two goal: + * - check if we have a 'longclick' event and open the dropdown (if any) when longclick event happened + * - trigger the action or the dropdown open depending on the target element (caret will open the dropdown otherwise + * trigger the action. + * @param {MouseEvent} event + */ + onMouseClick(event: MouseEvent): void { + let el = <HTMLElement>event.target; + let now = Date.now(); + let mdt = this.mouseDownTimestamp; // mousedown time + let isLongClick = mdt && mdt + this.minLongClickDelay <= now && ( + !this.maxLongClickDelay || mdt + this.maxLongClickDelay >= now + ); + let openDropdown = this.hasSubItems && ( + el.classList.contains(this.caretClass) || isLongClick || !this.actions[this.action] + ); + if (openDropdown && this.dropdown) { + if (this.toggleDropdown()) { + this.listenToClickOut(); + } + } else if (this.action) { + this.actions[this.action](); } + this.mouseDownTimestamp = 0; + event.preventDefault(); } - onMouseUp(event: MouseEvent): void { - if (event.button === 0) { - const clickEndTime = (new Date()).getTime(); - if (this.hasSubItems && (!this.action || clickEndTime - this.clickStartTime >= this.longClickInterval)) { - $(this.dropdown.nativeElement).toggleClass('open'); - } else if (this.action) { - this.actions[this.action](); + /** + * Listening the click event on the document so that we can hide our dropdown list if the event source is not the + * component. + */ + private listenToClickOut = (): void => { + this.dropdownIsOpen && document.addEventListener('click', this.onDocumentMouseClick); + }; + + /** + * Handling the click event on the document to hide the dropdown list if it needs. + * @param {MouseEvent} event + */ + private onDocumentMouseClick = (event: MouseEvent): void => { + let el = <HTMLElement>event.target; + if (!this.dropdown.nativeElement.contains(el)) { + this.closeDropdown(); + this.removeDocumentClickListener() + } + }; + + /** + * Handling the mousedown event, so that we can check the long clicks and open the dropdown if any. + * @param {MouseEvent} event + */ + onMouseDown = (event: MouseEvent): void => { + if (this.hasSubItems) { + let el = <HTMLElement>event.target; + if (!el.classList.contains(this.caretClass)) { + this.mouseDownTimestamp = Date.now(); } - event.stopPropagation(); } + }; + + /** + * The goal is to have one and only one place where we open the dropdown. So that later if we need to change the way + * how we do, it will be easier. + */ + private openDropdown():void { + this.dropdownIsOpen = true; + } + + /** + * The goal is to have one and only one place where we close the dropdown. So that later if we need to change the way + * how we do, it will be easier. + */ + private closeDropdown():void { + this.dropdownIsOpen = false; + } + + /** + * Just a simple helper method to make the dropdown toggle more easy. + * @returns {boolean} It will return the open state of the dropdown; + */ + private toggleDropdown(): boolean { + this[this.dropdownIsOpen ? 'closeDropdown' : 'openDropdown'](); + return this.dropdownIsOpen; + } + + /** + * The goal is to simply remove the click event listeners from the document. + */ + private removeDocumentClickListener(): void { + document.removeEventListener('click', this.onDocumentMouseClick); + } + + /** + * The main goal if this function is tho handle the item change event on the child dropdown list. + * Should update the value and close the dropdown if it is not multiple choice type. + * @param {ListItem} options The selected item(s) from the dropdown list. + */ + onDropdownItemChange(options: ListItem) { + this.updateValue(options); + !this.isMultipleChoice && this.closeDropdown(); } updateValue(options: ListItem) {
