|
|
@ -5,8 +5,13 @@ import '../codicon'; |
|
|
|
// match case is disabled unless regex is true
|
|
|
|
const template = html<SearchInput>`
|
|
|
|
<template role="search"> |
|
|
|
<label htmlFor="search"> |
|
|
|
<label |
|
|
|
for="search" |
|
|
|
class="${x => (x.showHelp ? 'has-helper' : '')}" |
|
|
|
@click="${(x, c) => x.handleShowHelper(c.event)}" |
|
|
|
> |
|
|
|
<code-icon icon="search" aria-label="${x => x.label}" title="${x => x.label}"></code-icon> |
|
|
|
<code-icon class="icon-small" icon="chevron-down" aria-hidden="true"></code-icon> |
|
|
|
</label> |
|
|
|
<div class="field"> |
|
|
|
<input |
|
|
@ -21,31 +26,36 @@ const template = html` |
|
|
|
aria-describedby="${x => (!x.errorMessage ? '' : 'error')}" |
|
|
|
@input="${(x, c) => x.handleInput(c.event)}" |
|
|
|
@keydown="${(x, c) => x.handleShortcutKeys(c.event as KeyboardEvent)}" |
|
|
|
@focus="${(x, c) => x.handleFocus(c.event)}" |
|
|
|
/> |
|
|
|
<div class="message" id="error" aria-live="polite">${x => x.errorMessage}</div> |
|
|
|
</div> |
|
|
|
<div class="controls"> |
|
|
|
<button |
|
|
|
class="clear-button${x => (x.value ? '' : ' clear-button__hidden')}" |
|
|
|
class="control${x => (x.value ? '' : ' is-hidden')}" |
|
|
|
type="button" |
|
|
|
role="button" |
|
|
|
aria-label="Clear" |
|
|
|
title="Clear" |
|
|
|
@click="${(x, c) => x.handleClear(c.event)}" |
|
|
|
@focus="${(x, c) => x.handleFocus(c.event)}" |
|
|
|
> |
|
|
|
<code-icon icon="close"></code-icon> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
class="control" |
|
|
|
type="button" |
|
|
|
role="checkbox" |
|
|
|
aria-label="Match All" |
|
|
|
title="Match All" |
|
|
|
aria-checked="${x => x.matchAll}" |
|
|
|
@click="${(x, c) => x.handleMatchAll(c.event)}" |
|
|
|
@focus="${(x, c) => x.handleFocus(c.event)}" |
|
|
|
> |
|
|
|
<code-icon icon="whole-word"></code-icon> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
class="control" |
|
|
|
type="button" |
|
|
|
role="checkbox" |
|
|
|
aria-label="Match Case${x => |
|
|
@ -55,20 +65,40 @@ const template = html` |
|
|
|
?disabled="${x => !x.matchRegex}" |
|
|
|
aria-checked="${x => x.matchCaseOverride}" |
|
|
|
@click="${(x, c) => x.handleMatchCase(c.event)}" |
|
|
|
@focus="${(x, c) => x.handleFocus(c.event)}" |
|
|
|
> |
|
|
|
<code-icon icon="case-sensitive"></code-icon> |
|
|
|
</button> |
|
|
|
<button |
|
|
|
class="control" |
|
|
|
type="button" |
|
|
|
role="checkbox" |
|
|
|
aria-label="Use Regular Expression" |
|
|
|
title="Use Regular Expression" |
|
|
|
aria-checked="${x => x.matchRegex}" |
|
|
|
@click="${(x, c) => x.handleMatchRegex(c.event)}" |
|
|
|
@focus="${(x, c) => x.handleFocus(c.event)}" |
|
|
|
> |
|
|
|
<code-icon icon="regex"></code-icon> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
<div class="helper" tabindex="-1" ${ref('helper')}> |
|
|
|
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('message:')}"> |
|
|
|
Search by Message <small>message: or =:</small> |
|
|
|
</button> |
|
|
|
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('author:')}"> |
|
|
|
Search by Author <small>author: or @:</small> |
|
|
|
</button> |
|
|
|
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('sha:')}"> |
|
|
|
Search by Commit SHA <small>sha: or #:</small> |
|
|
|
</button> |
|
|
|
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('file:')}"> |
|
|
|
Search by File <small>file: or ?:</small> |
|
|
|
</button> |
|
|
|
<button class="helper-button" type="button" @click="${(x, c) => x.handleInsertToken('change:')}"> |
|
|
|
Search by Changes <small>change: or ~:</small> |
|
|
|
</button> |
|
|
|
</div> |
|
|
|
</template> |
|
|
|
`;
|
|
|
|
|
|
|
@ -81,14 +111,25 @@ const styles = css` |
|
|
|
display: inline-flex; |
|
|
|
flex-direction: row; |
|
|
|
align-items: center; |
|
|
|
gap: 0.8rem; |
|
|
|
gap: 0.4rem; |
|
|
|
position: relative; |
|
|
|
|
|
|
|
flex: auto 1 1; |
|
|
|
} |
|
|
|
|
|
|
|
label { |
|
|
|
display: flex; |
|
|
|
justify-content: center; |
|
|
|
align-items: center; |
|
|
|
gap: 0.2rem; |
|
|
|
width: 3.2rem; |
|
|
|
height: 2.4rem; |
|
|
|
color: var(--vscode-input-foreground); |
|
|
|
cursor: pointer; |
|
|
|
} |
|
|
|
|
|
|
|
.icon-small { |
|
|
|
font-size: 1rem; |
|
|
|
} |
|
|
|
|
|
|
|
.field { |
|
|
@ -158,43 +199,86 @@ const styles = css` |
|
|
|
} |
|
|
|
|
|
|
|
button { |
|
|
|
display: inline-flex; |
|
|
|
justify-content: center; |
|
|
|
align-items: center; |
|
|
|
width: 2rem; |
|
|
|
height: 2rem; |
|
|
|
padding: 0; |
|
|
|
color: var(--vscode-input-foreground); |
|
|
|
border: 1px solid transparent; |
|
|
|
background: none; |
|
|
|
text-align: center; |
|
|
|
border-radius: 0.25rem; |
|
|
|
} |
|
|
|
button[role='checkbox']:focus:not([disabled]) { |
|
|
|
button:focus:not([disabled]) { |
|
|
|
outline: 1px solid var(--vscode-focusBorder); |
|
|
|
outline-offset: -1px; |
|
|
|
} |
|
|
|
button:not([disabled]) { |
|
|
|
cursor: pointer; |
|
|
|
} |
|
|
|
button:hover:not([disabled]):not([aria-checked='true']) { |
|
|
|
|
|
|
|
.control { |
|
|
|
display: inline-flex; |
|
|
|
justify-content: center; |
|
|
|
align-items: center; |
|
|
|
width: 2rem; |
|
|
|
height: 2rem; |
|
|
|
text-align: center; |
|
|
|
border-radius: 0.25rem; |
|
|
|
} |
|
|
|
.control:hover:not([disabled]):not([aria-checked='true']) { |
|
|
|
background-color: var(--vscode-inputOption-hoverBackground); |
|
|
|
} |
|
|
|
button[disabled] { |
|
|
|
.control[disabled] { |
|
|
|
opacity: 0.5; |
|
|
|
} |
|
|
|
button[disabled][aria-checked='true'] { |
|
|
|
.control[disabled][aria-checked='true'] { |
|
|
|
opacity: 0.8; |
|
|
|
} |
|
|
|
button[aria-checked='true'] { |
|
|
|
.control[aria-checked='true'] { |
|
|
|
background-color: var(--vscode-inputOption-activeBackground); |
|
|
|
color: var(--vscode-inputOption-activeForeground); |
|
|
|
border-color: var(--vscode-inputOption-activeBorder); |
|
|
|
} |
|
|
|
|
|
|
|
.clear-button__hidden { |
|
|
|
.control.is-hidden { |
|
|
|
display: none; |
|
|
|
} |
|
|
|
|
|
|
|
.has-helper { |
|
|
|
background-color: var(--vscode-input-background); |
|
|
|
border-radius: 0.3rem 0.3rem 0 0; |
|
|
|
} |
|
|
|
|
|
|
|
.helper { |
|
|
|
display: none; |
|
|
|
position: absolute; |
|
|
|
top: 100%; |
|
|
|
left: 0; |
|
|
|
z-index: 5000; |
|
|
|
width: fit-content; |
|
|
|
background-color: var(--vscode-input-background); |
|
|
|
border-radius: 0 0.3rem 0.3rem 0.3rem; |
|
|
|
outline: none; |
|
|
|
} |
|
|
|
.has-helper ~ .helper { |
|
|
|
display: block; |
|
|
|
} |
|
|
|
|
|
|
|
.helper-button { |
|
|
|
display: block; |
|
|
|
width: 100%; |
|
|
|
padding: 0.3rem 0.6rem; |
|
|
|
text-align: left; |
|
|
|
} |
|
|
|
.helper-button:hover { |
|
|
|
background-color: var(--vscode-inputOption-hoverBackground); |
|
|
|
} |
|
|
|
.helper-button:first-child { |
|
|
|
border-top-right-radius: 0.3rem; |
|
|
|
} |
|
|
|
.helper-button:last-child { |
|
|
|
border-bottom-left-radius: 0.3rem; |
|
|
|
border-bottom-right-radius: 0.3rem; |
|
|
|
} |
|
|
|
.helper-button small { |
|
|
|
opacity: 0.5; |
|
|
|
} |
|
|
|
`;
|
|
|
|
|
|
|
|
@customElement({ |
|
|
@ -204,6 +288,9 @@ const styles = css` |
|
|
|
}) |
|
|
|
export class SearchInput extends FASTElement { |
|
|
|
@observable |
|
|
|
showHelp = false; |
|
|
|
|
|
|
|
@observable |
|
|
|
errorMessage = ''; |
|
|
|
|
|
|
|
@attr |
|
|
@ -230,11 +317,35 @@ export class SearchInput extends FASTElement { |
|
|
|
} |
|
|
|
|
|
|
|
input!: HTMLInputElement; |
|
|
|
helper!: HTMLElement; |
|
|
|
|
|
|
|
override connectedCallback() { |
|
|
|
super.connectedCallback(); |
|
|
|
document.addEventListener('click', this.handleDocumentClick.bind(this)); |
|
|
|
} |
|
|
|
|
|
|
|
override disconnectedCallback() { |
|
|
|
super.disconnectedCallback(); |
|
|
|
document.removeEventListener('click', this.handleDocumentClick.bind(this)); |
|
|
|
} |
|
|
|
|
|
|
|
override focus(options?: FocusOptions): void { |
|
|
|
this.input.focus(options); |
|
|
|
} |
|
|
|
|
|
|
|
handleDocumentClick(e: MouseEvent) { |
|
|
|
if (this.showHelp === false) return; |
|
|
|
|
|
|
|
const composedPath = e.composedPath(); |
|
|
|
if (!composedPath.includes(this)) { |
|
|
|
this.showHelp = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
handleFocus(_e: Event) { |
|
|
|
this.showHelp = false; |
|
|
|
} |
|
|
|
|
|
|
|
handleClear(_e: Event) { |
|
|
|
this.value = ''; |
|
|
|
this.emitSearch(); |
|
|
@ -273,6 +384,20 @@ export class SearchInput extends FASTElement { |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
handleShowHelper(_e: Event) { |
|
|
|
this.showHelp = !this.showHelp; |
|
|
|
if (this.showHelp) { |
|
|
|
window.requestAnimationFrame(() => { |
|
|
|
this.helper.focus(); |
|
|
|
}); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
handleInsertToken(token: string) { |
|
|
|
this.value += `${this.value.length > 0 ? ' ' : ''}${token}`; |
|
|
|
this.input.focus(); |
|
|
|
} |
|
|
|
|
|
|
|
private emitSearch() { |
|
|
|
const search: SearchQuery = { |
|
|
|
query: this.value, |
|
|
|