Fixed tabindex and add support for input suggestion to Connector Wizard

This commit is contained in:
NI
2020-02-05 18:55:15 +08:00
parent 05a8300105
commit 0a930d1345
3 changed files with 599 additions and 43 deletions

View File

@@ -489,6 +489,20 @@ body {
color: #e9a;
}
.form1 > fieldset .field.highlight > input[type="text"],
.form1 > fieldset .field.highlight > input[type="file"],
.form1 > fieldset .field.highlight > input[type="email"],
.form1 > fieldset .field.highlight > input[type="number"],
.form1 > fieldset .field.highlight > input[type="search"],
.form1 > fieldset .field.highlight > input[type="tel"],
.form1 > fieldset .field.highlight > input[type="url"],
.form1 > fieldset .field.highlight > input[type="password"],
.form1 > fieldset .field.highlight > select,
.form1 > fieldset .field.highlight > textarea {
background: #666;
border-bottom: 2px solid #ccc;
}
.form1 > fieldset .field.error > input[type="text"],
.form1 > fieldset .field.error > input[type="file"],
.form1 > fieldset .field.error > input[type="email"],
@@ -552,6 +566,7 @@ body {
border-style: solid;
}
.form1 > fieldset .field > button:focus,
.form1 > fieldset .field > button:hover {
border-color: #a56;
background: #c78;
@@ -562,10 +577,6 @@ body {
border-color: #a56;
}
.form1 > fieldset .field > button:focus {
outline: 1px dotted #eee;
}
.form1 > fieldset .field > button.secondary {
float: right;
background: transparent;
@@ -573,6 +584,7 @@ body {
border-color: #eee;
}
.form1 > fieldset .field > button.secondary:focus,
.form1 > fieldset .field > button.secondary:hover {
border-color: #ddd;
color: #ddd;
@@ -582,3 +594,79 @@ body {
border-color: #666;
color: #666;
}
.form1 > fieldset .field > ul.input-suggestions {
background: #262626;
box-shadow: 0 0 3px #0006;
border: 1px solid #666;
position: relative;
margin: 3px;
}
.form1 > fieldset .field > ul.input-suggestions::before,
.form1 > fieldset .field > ul.input-suggestions::after {
top: -5px;
left: 5px;
position: absolute;
z-index: 1;
display: block;
content: " ";
width: 10px;
height: 10px;
background: #262626;
transform: rotate(45deg);
}
.form1 > fieldset .field > ul.input-suggestions::after {
top: -6px;
z-index: 0;
background: #666;
}
.form1 > fieldset .field > ul.input-suggestions > li {
position: relative;
z-index: 2;
padding: 10px;
cursor: pointer;
border-bottom: 1px solid #222;
}
.form1 > fieldset .field > ul.input-suggestions > li:hover,
.form1 > fieldset .field > ul.input-suggestions > li.current {
background: #555;
border-bottom: 1px solid #555;
}
.form1 > fieldset .field > ul.input-suggestions > li:first-child:hover::before,
.form1
> fieldset
.field
> ul.input-suggestions
> li.current:first-child::before {
top: -6px;
position: absolute;
z-index: 0;
left: 5px;
display: block;
content: " ";
width: 10px;
height: 10px;
background: #555;
transform: rotate(45deg);
}
.form1 > fieldset .field > ul.input-suggestions > li:last-child {
border-bottom: none;
}
.form1 > fieldset .field > ul.input-suggestions > li > .sugt-title {
color: #fff;
font-weight: bold;
margin: 0 5px;
}
.form1 > fieldset .field > ul.input-suggestions > li > .sugt-value {
color: #fdd;
font-size: 0.9em;
margin: 0 5px;
}

View File

@@ -61,7 +61,7 @@
v-for="(field, key) in current.fields"
:key="key"
class="field"
:class="{ error: field.error.length > 0 }"
:class="{ error: field.error.length > 0, highlight: field.highlighted }"
>
{{ field.field.name }}
@@ -74,8 +74,13 @@
:name="field.field.name"
:placeholder="field.field.example"
:autofocus="field.autofocus"
@input="verify(key, field, false)"
@change="verify(key, field, true)"
:tabindex="field.tabIndex"
:disabled="field.field.readonly"
@keydown="keydown($event, key, field)"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@input="changed(key, field, false)"
@change="changed(key, field, true)"
/>
<input
@@ -87,18 +92,28 @@
:name="field.field.name"
:placeholder="field.field.example"
:autofocus="field.autofocus"
@input="verify(key, field, false)"
@change="verify(key, field, true)"
:tabindex="field.tabIndex"
:disabled="field.field.readonly"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@input="changed(key, field, false)"
@change="changed(key, field, true)"
/>
<input
v-if="field.field.type === 'checkbox'"
v-model="field.field.value"
v-focus="field.autofocus"
type="checkbox"
autocomplete="off"
:name="field.field.name"
@input="verify(key, field, false)"
@change="verify(key, field, true)"
:autofocus="field.autofocus"
:tabindex="field.tabIndex"
:disabled="field.field.readonly"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@input="changed(key, field, false)"
@change="changed(key, field, true)"
/>
<textarea
@@ -109,42 +124,56 @@
:placeholder="field.field.example"
:name="field.field.name"
:autofocus="field.autofocus"
@input="verify(key, field, false)"
:tabindex="field.tabIndex"
:disabled="field.field.readonly"
@keydown="keydown(key, field)"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@input="changed(key, field, false)"
@keyup="expandTextarea"
@change="verify(key, field, true)"
@change="changed(key, field, true)"
></textarea>
<input
v-if="field.field.type === 'textfile'"
v-focus="field.autofocus"
type="file"
autocomplete="off"
:placeholder="field.field.example"
:name="field.field.name + '-file'"
:autofocus="field.autofocus"
:tabindex="field.tabIndex"
:disabled="field.field.readonly"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@change="importFile($event.target, field)"
/>
<input
v-if="field.field.type === 'textfile'"
v-model="field.field.value"
v-focus="field.autofocus"
type="text"
autocomplete="off"
:name="field.field.name"
:placeholder="field.field.example"
:autofocus="field.autofocus"
style="display: none"
@input="verify(key, field, false)"
@change="verify(key, field, true)"
@input="changed(key, field, false)"
@change="changed(key, field, true)"
/>
<select
v-if="field.field.type === 'select'"
v-model="field.field.value"
v-focus="field.autofocus"
autocomplete="off"
:name="field.field.name"
:autofocus="field.autofocus"
:value="field.field.value"
@input="verify(key, field, false)"
@change="verify(key, field, true)"
:tabindex="field.tabIndex"
:disabled="field.field.readonly"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@input="changed(key, field, false)"
@change="changed(key, field, true)"
>
<option
v-for="(option, oKey) in field.field.example.split(',')"
@@ -169,19 +198,46 @@
>
<input
v-model="field.field.value"
v-focus="field.autofocus && oKey === 0"
type="radio"
autocomplete="off"
:name="field.field.name"
:autofocus="field.autofocus && oKey === 0"
:value="option"
:checked="field.field.value === option"
:aria-checked="field.field.value === option"
@input="verify(key, field, false)"
@change="verify(key, field, true)"
:tabindex="field.nextSubTabIndex(oKey)"
:disabled="field.field.readonly"
@focus="focus(key, field, true)"
@blur="focus(key, field, false)"
@input="changed(key, field, false)"
@change="changed(key, field, true)"
/>
{{ option }}
</label>
</div>
<ul
v-if="field.suggestion.suggestions.length > 0"
class="input-suggestions lst-nostyle"
@mouseenter="field.holdSuggestions(true)"
@mouseleave="field.holdSuggestions(false)"
>
<li
v-for="(suggestion, sKey) in field.suggestion.suggestions"
:key="sKey"
:class="{ current: sKey === field.suggestion.selected }"
@click="clickInputSuggestion(key, field, sKey)"
>
<div class="sugt-title">
{{ suggestion.title }}
</div>
<div class="sugt-value">
{{ suggestion.value }}
</div>
</li>
</ul>
<div v-if="field.error.length > 0" class="error">{{ field.error }}</div>
<div v-else-if="field.message.length > 0" class="message">
{{ field.message }}
@@ -196,8 +252,11 @@
<div class="field">
<button
v-if="current.submittable"
v-focus="submitterTabIndex === 1"
type="submit"
:disabled="current.submitting || disabled"
:tabindex="submitterTabIndex"
:autofocus="submitterTabIndex === 1"
@click="submitAndGetNext"
>
{{ current.actionText }}
@@ -205,6 +264,7 @@
<button
v-if="current.cancellable"
:disabled="current.submitting || disabled"
:tabindex="submitterTabIndex + 1"
class="secondary"
@click="cancelAndGetNext"
>
@@ -227,24 +287,16 @@
<script>
import "./connector.css";
import * as command from "../commands/commands.js";
import * as fieldBuilder from "./connector_field_builder.js";
const preloaderIDPrefix = "connector-resource-preload-control-";
function buildField(i, field) {
return {
verified: false,
inputted: false,
error: "",
message: "",
field: field,
autofocus: i == 0
};
}
const hightlightClearTimeout = 1000;
function buildEmptyCurrent() {
return {
data: null,
alert: false,
clearHightlightTimeout: null,
title: "",
message: "",
fields: [],
@@ -279,6 +331,8 @@ export default {
currentConnectorCloseWait: null,
current: buildEmptyCurrent(),
preloaderIDName: "",
fieldValueBackup: [],
submitterTabIndex: 1,
working: false,
disabled: false,
cancelled: false
@@ -335,14 +389,21 @@ export default {
this.current.type = next.type();
this.current.data = next.data();
let fields = null,
tabIndex = 1;
switch (this.current.type) {
case command.NEXT_PROMPT:
let fields = this.current.data.inputs();
fields = this.current.data.inputs();
for (let i in fields) {
this.current.fields.push(buildField(i, fields[i]));
for (let i = 0; i < fields.length; i++) {
const f = fieldBuilder.build(tabIndex, i, fields[i]);
this.current.fields.push(f);
tabIndex = f.nextTabIndex();
}
this.submitterTabIndex = tabIndex > 0 ? tabIndex : 1;
this.current.actionText = this.current.data.actionText();
this.current.submittable = true;
this.current.alert = true;
@@ -404,7 +465,11 @@ export default {
this.getConnector().wizard.close();
await waiter;
try {
await waiter;
} catch (e) {
// Do nothing
}
},
runWizard() {
if (this.currentConnectorCloseWait !== null) {
@@ -438,7 +503,7 @@ export default {
getFieldValues() {
let mod = {};
for (let i in this.current.fields) {
for (let i = 0; i < this.current.fields.length; i++) {
mod[this.current.fields[i].field.name] = this.current.fields[
i
].field.value;
@@ -446,6 +511,45 @@ export default {
return mod;
},
createFieldValueBackup() {
let backup = [];
for (let i = 0; i < this.current.fields.length; i++) {
backup.push(this.current.fields[i].field.value);
}
this.fieldValueBackup = backup;
},
clearFieldValueBackup() {
this.fieldValueBackup = [];
},
clearFieldHighlights() {
for (let i = 0; i < this.current.fields.length; i++) {
this.current.fields[i].highlighted = false;
}
},
delayedClearFieldHighlights(timeout) {
const self = this;
if (self.clearHightlightTimeout === null) {
clearTimeout(self.clearHightlightTimeout);
self.clearHightlightTimeout = null;
}
self.clearHightlightTimeout = setTimeout(() => {
self.clearHightlightTimeout = null;
self.clearFieldHighlights();
}, timeout);
},
restoreFieldValuesFromBackup(except) {
for (let i = 0; i < this.fieldValueBackup.length; i++) {
if (except === i) {
continue;
}
this.current.fields[i].field.value = this.fieldValueBackup[i];
}
},
expandTextarea(event) {
event.target.style.overflowY = "hidden";
event.target.style.height = "";
@@ -484,7 +588,7 @@ export default {
verify(key, field, force) {
try {
field.message = "" + field.field.verify(field.field.value);
field.inputted = true;
field.modified = true;
field.verified = true;
field.error = "";
} catch (e) {
@@ -492,27 +596,27 @@ export default {
field.message = "";
field.verified = false;
if (field.inputted || force) {
if (field.modified || force) {
field.error = "" + e;
}
}
field.highlighted = false;
if (
!field.verified &&
(field.inputted || force) &&
(field.modified || force) &&
field.error.length <= 0
) {
field.error = "Invalid";
}
this.current.fields[key] = field;
return field.verified;
},
verifyAll() {
let verified = true;
for (let i in this.current.fields) {
for (let i = 0; i < this.current.fields.length; i++) {
if (this.verify(i, this.current.fields[i], true)) {
continue;
}
@@ -522,6 +626,157 @@ export default {
return verified;
},
focus(key, field, focused) {
field.highlighted = false;
if (!focused) {
// Don't reset a holding field
if (!field.inputted) {
field.resetSuggestions(false);
} else if (field.resetSuggestions(false)) {
this.clickInputSuggestion(
key,
field,
field.selectedSuggestionIndex()
);
}
return;
}
this.createFieldValueBackup();
field.reloadSuggestions();
},
applySuggestion(key, field, suggestion) {
this.restoreFieldValuesFromBackup(-1);
field.field.value = suggestion.value;
for (let i = 0; i < this.current.fields.length; i++) {
this.current.fields[i].highlighted = false;
if (
i === key ||
this.current.fields[i].inputted ||
this.current.fields[i].field.readonly
) {
continue;
}
if (
typeof suggestion.fields[this.current.fields[i].field.name] ===
"undefined"
) {
continue;
}
this.current.fields[i].field.value =
suggestion.fields[this.current.fields[i].field.name];
if (!this.verify(i, this.current.fields[i], true)) {
continue;
}
this.current.fields[i].highlighted = true;
}
},
applySuggestionAndVerify(key, field, force, suggestion) {
field.inputted = true;
this.applySuggestion(key, field, suggestion);
return this.verify(key, field, force);
},
changed(key, field, force) {
this.createFieldValueBackup();
field.highlighted = false;
field.inputted = true;
field.enableInputSuggestionsOnAllInput();
field.reloadSuggestions();
this.verify(key, field, force);
},
keydown(event, key, field) {
switch (event.key) {
case "ArrowUp":
event.preventDefault();
field.moveSuggestionsCursor(true);
this.applySuggestionAndVerify(
key,
field,
true,
field.curentSuggestion()
);
break;
case "ArrowDown":
event.preventDefault();
field.moveSuggestionsCursor(false);
this.applySuggestionAndVerify(
key,
field,
true,
field.curentSuggestion()
);
break;
case "Escape":
if (!field.suggestionsPending()) {
return;
}
event.preventDefault();
this.restoreFieldValuesFromBackup(key);
this.clearFieldValueBackup();
this.clearFieldHighlights();
this.verify(key, field, true);
field.disableSuggestionsForInput(field.field.value);
field.resetSuggestions(true);
break;
case "Enter":
if (!field.suggestionsPending()) {
return;
}
event.preventDefault();
this.clickInputSuggestion(
key,
field,
field.selectedSuggestionIndex()
);
break;
}
},
clickInputSuggestion(key, field, index) {
const self = this;
field.selectSuggestion(index);
if (
self.applySuggestionAndVerify(
key,
field,
true,
field.curentSuggestion()
)
) {
field.disableSuggestionsForInput(field.field.value);
} else {
field.enableInputSuggestionsOnAllInput();
}
field.resetSuggestions(true);
self.clearFieldValueBackup();
self.delayedClearFieldHighlights(hightlightClearTimeout);
},
async submitAndGetNext() {
if (this.current.submitting || this.disabled) {
return;

View File

@@ -0,0 +1,213 @@
// Sshwifty - A Web SSH client
//
// Copyright (C) 2019-2020 Rui NI <nirui@gmx.com>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
function getTabIndex(tabIndex, field) {
if (field.readonly) {
return 0;
}
switch (field.type) {
case "text":
case "password":
case "checkbox":
case "textarea":
case "textfile":
case "select":
case "radio":
return tabIndex;
default:
return 0;
}
}
export function build(tabIndex, i, field) {
return {
verified: false,
modified: false,
inputted: false,
highlighted: false,
error: "",
message: "",
field: field,
autofocus: tabIndex === 1 && !field.readonly,
tabIndex: getTabIndex(tabIndex, field),
blockedSuggestionValue: "",
blockingSuggestion: false,
nextTabIndex() {
if (this.field.readonly) {
return this.tabIndex;
}
switch (this.field.type) {
case "radio":
return this.tabIndex + this.field.example.split(",").length;
default:
return this.tabIndex + 1;
}
},
nextSubTabIndex(subIndex) {
if (this.field.readonly) {
return 0;
}
return this.tabIndex + subIndex;
},
suggestion: {
selected: -1,
suggestions: [],
orignalValue: "",
orignalValueStored: false,
holding: false,
needsReset: false,
reset() {
this.selected = -1;
this.suggestions = [];
this.holding = false;
this.needsReset = false;
this.clearStored();
return true;
},
softReset() {
if (this.holding) {
this.needsReset = true;
return false;
}
return this.reset();
},
hold(toHold) {
this.holding = toHold;
if (this.holding || !this.needsReset) {
return;
}
this.reset();
},
storeOrignal(val) {
if (this.orignalValueStored) {
return;
}
this.orignalValue = val;
this.orignalValueStored = true;
},
loadStored(defaultValue) {
return this.orignalValueStored ? this.orignalValue : defaultValue;
},
clearStored() {
this.orignalValue = "";
this.orignalValueStored = false;
},
select(index, fieldValue) {
if (this.selected < 0) {
this.storeOrignal(fieldValue);
}
if (index < -1 || index >= this.suggestions.length) {
return;
}
this.selected = index;
},
cursorUp(fieldValue) {
this.select(this.selected - 1, fieldValue);
},
cursorDown(fieldValue) {
this.select(this.selected + 1, fieldValue);
},
cursorMove(toUp, fieldValue) {
toUp ? this.cursorUp(fieldValue) : this.cursorDown(fieldValue);
},
reload(fieldValue, suggestions) {
this.selected = -1;
this.suggestions = [];
this.clearStored();
if (suggestions.length === 1 && suggestions[0].value === fieldValue) {
return;
}
for (let v in suggestions) {
this.suggestions.push({
title: suggestions[v].title,
value: suggestions[v].value,
fields: suggestions[v].meta
});
}
},
current(defaultValue) {
if (this.selected < 0) {
return {
title: "Input",
value: this.loadStored(defaultValue),
fields: {}
};
}
return this.suggestions[this.selected];
}
},
disableSuggestionsForInput(val) {
this.blockedSuggestionValue = val;
this.blockingSuggestion = true;
},
enableInputSuggestionsOnAllInput() {
this.blockedSuggestionValue = "";
this.blockingSuggestion = false;
},
suggestionsPending() {
return this.suggestion.suggestions.length > 0;
},
reloadSuggestions() {
if (
this.blockingSuggestion &&
this.field.value === this.blockedSuggestionValue
) {
return;
}
this.suggestion.reload(
this.field.value,
this.field.suggestions(this.field.value)
);
},
resetSuggestions(force) {
return force ? this.suggestion.reset() : this.suggestion.softReset();
},
holdSuggestions(toHold) {
this.suggestion.hold(toHold);
},
moveSuggestionsCursor(toUp) {
this.suggestion.cursorMove(toUp, this.field.value);
},
selectSuggestion(index) {
this.suggestion.select(index, this.field.value);
},
curentSuggestion() {
return this.suggestion.current(this.field.value);
},
selectedSuggestionIndex() {
return this.suggestion.selected;
}
};
}