mirror of https://github.com/keeweb/keeweb
extracted select-entry-filter
parent
338f6d47d1
commit
98ea7a542d
|
@ -1,90 +0,0 @@
|
|||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
const AutoTypeFilter = function (windowInfo, appModel) {
|
||||
this.title = windowInfo.title;
|
||||
this.url = windowInfo.url;
|
||||
this.text = '';
|
||||
this.ignoreWindowInfo = false;
|
||||
this.appModel = appModel;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntries = function () {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
autoType: true
|
||||
};
|
||||
this.prepareFilter();
|
||||
let entries = this.appModel.getEntriesByFilter(filter).map((e) => [e, this.getEntryRank(e)]);
|
||||
if (!this.ignoreWindowInfo) {
|
||||
entries = entries.filter((e) => e[1]);
|
||||
}
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.hasWindowInfo = function () {
|
||||
return this.title || this.url;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.prepareFilter = function () {
|
||||
this.titleLower = this.title ? this.title.toLowerCase() : null;
|
||||
this.urlLower = this.url ? this.url.toLowerCase() : null;
|
||||
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
|
||||
};
|
||||
|
||||
AutoTypeFilter.prototype.getEntryRank = function (entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts) {
|
||||
if (entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
|
||||
if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) {
|
||||
if (domain === thisDomain) {
|
||||
rank += 20;
|
||||
} else {
|
||||
rank += 10;
|
||||
}
|
||||
if (path === thisPath) {
|
||||
rank += 10;
|
||||
} else if (path && thisPath) {
|
||||
if (path.lastIndexOf(thisPath, 0) === 0) {
|
||||
rank += 5;
|
||||
} else if (thisPath.lastIndexOf(path, 0) === 0) {
|
||||
rank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === thisScheme) {
|
||||
rank += 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
} else {
|
||||
// another domain; don't show this record at all, ignore title match
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
};
|
||||
|
||||
export { AutoTypeFilter };
|
|
@ -1,7 +1,7 @@
|
|||
import { Events } from 'framework/events';
|
||||
import { AutoTypeFilter } from 'auto-type/auto-type-filter';
|
||||
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
|
||||
import { AutoTypeParser } from 'auto-type/auto-type-parser';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
import { Features } from 'util/features';
|
||||
import { Alerts } from 'comp/ui/alerts';
|
||||
|
@ -27,7 +27,6 @@ const AutoType = {
|
|||
return;
|
||||
}
|
||||
Events.on('auto-type', (e) => this.handleEvent(e));
|
||||
Events.on('main-window-blur', (e) => this.mainWindowBlur(e));
|
||||
},
|
||||
|
||||
handleEvent(e) {
|
||||
|
@ -58,12 +57,6 @@ const AutoType = {
|
|||
}
|
||||
},
|
||||
|
||||
mainWindowBlur() {
|
||||
if (this.selectEntryView) {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
}
|
||||
},
|
||||
|
||||
runAndHandleResult(result, windowId) {
|
||||
this.run(result, windowId, (err) => {
|
||||
if (err) {
|
||||
|
@ -219,7 +212,14 @@ const AutoType = {
|
|||
|
||||
selectEntryAndRun() {
|
||||
this.getActiveWindowInfo(async (e, windowInfo) => {
|
||||
const filter = new AutoTypeFilter(windowInfo, AppModel.instance);
|
||||
const filter = new SelectEntryFilter(
|
||||
windowInfo,
|
||||
AppModel.instance,
|
||||
AppModel.instance.files,
|
||||
{
|
||||
autoType: true
|
||||
}
|
||||
);
|
||||
const evt = { filter, windowInfo };
|
||||
if (!AppModel.instance.files.hasOpenFiles()) {
|
||||
logger.debug('auto-type event delayed');
|
||||
|
@ -254,8 +254,18 @@ const AutoType = {
|
|||
return;
|
||||
}
|
||||
this.focusMainWindow();
|
||||
evt.filter.ignoreWindowInfo = true;
|
||||
this.selectEntryView = new SelectEntryView({ filter: evt.filter });
|
||||
|
||||
const humanReadableTarget = evt.filter.title || evt.filter.url;
|
||||
const topMessage = humanReadableTarget
|
||||
? Locale.autoTypeMsgMatchedByWindow.replace('{}', humanReadableTarget)
|
||||
: Locale.autoTypeMsgNoWindow;
|
||||
|
||||
this.selectEntryView = new SelectEntryView({
|
||||
isAutoType: true,
|
||||
itemOptions: true,
|
||||
filter: evt.filter,
|
||||
topMessage
|
||||
});
|
||||
this.selectEntryView.on('result', (result) => {
|
||||
logger.debug('Entry selected', result);
|
||||
this.selectEntryView.off('result');
|
||||
|
@ -277,6 +287,7 @@ const AutoType = {
|
|||
try {
|
||||
await AppModel.instance.unlockAnyFile('autoTypeUnlockMessage');
|
||||
} catch {
|
||||
this.selectEntryView.emit('result', undefined);
|
||||
return;
|
||||
}
|
||||
this.selectEntryView.show();
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
import { SearchResultCollection } from 'collections/search-result-collection';
|
||||
import { Ranking } from 'util/data/ranking';
|
||||
|
||||
const urlPartsRegex = /^(\w+:\/\/)?(?:(?:www|wwws|secure)\.)?([^\/]+)\/?(.*)/;
|
||||
|
||||
class SelectEntryFilter {
|
||||
constructor(windowInfo, appModel, files, filterOptions) {
|
||||
this.title = windowInfo.title;
|
||||
this.url = windowInfo.url;
|
||||
this.text = '';
|
||||
this.appModel = appModel;
|
||||
this.files = files;
|
||||
this.filterOptions = filterOptions;
|
||||
}
|
||||
|
||||
getEntries() {
|
||||
const filter = {
|
||||
text: this.text,
|
||||
...this.filterOptions
|
||||
};
|
||||
this._prepareFilter();
|
||||
let entries = this.appModel
|
||||
.getEntriesByFilter(filter, this.files)
|
||||
.map((e) => [e, this._getEntryRank(e)]);
|
||||
entries = entries.filter((e) => e[1]);
|
||||
entries = entries.sort((x, y) =>
|
||||
x[1] === y[1] ? x[0].title.localeCompare(y[0].title) : y[1] - x[1]
|
||||
);
|
||||
entries = entries.map((p) => p[0]);
|
||||
return new SearchResultCollection(entries, { comparator: 'none' });
|
||||
}
|
||||
|
||||
_prepareFilter() {
|
||||
this.titleLower = this.title ? this.title.toLowerCase() : null;
|
||||
this.urlLower = this.url ? this.url.toLowerCase() : null;
|
||||
this.urlParts = this.url ? urlPartsRegex.exec(this.urlLower) : null;
|
||||
}
|
||||
|
||||
_getEntryRank(entry) {
|
||||
let rank = 0;
|
||||
if (this.titleLower && entry.title) {
|
||||
rank += Ranking.getStringRank(entry.title.toLowerCase(), this.titleLower);
|
||||
}
|
||||
if (this.urlParts) {
|
||||
if (entry.url) {
|
||||
const entryUrlParts = urlPartsRegex.exec(entry.url.toLowerCase());
|
||||
if (entryUrlParts) {
|
||||
const [, scheme, domain, path] = entryUrlParts;
|
||||
const [, thisScheme, thisDomain, thisPath] = this.urlParts;
|
||||
if (domain === thisDomain || thisDomain.indexOf('.' + domain) > 0) {
|
||||
if (domain === thisDomain) {
|
||||
rank += 20;
|
||||
} else {
|
||||
rank += 10;
|
||||
}
|
||||
if (path === thisPath) {
|
||||
rank += 10;
|
||||
} else if (path && thisPath) {
|
||||
if (path.lastIndexOf(thisPath, 0) === 0) {
|
||||
rank += 5;
|
||||
} else if (thisPath.lastIndexOf(path, 0) === 0) {
|
||||
rank += 3;
|
||||
}
|
||||
}
|
||||
if (scheme === thisScheme) {
|
||||
rank += 1;
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
} else {
|
||||
// another domain; don't show this record at all, ignore title match
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (entry.searchText.indexOf(this.urlLower) >= 0) {
|
||||
// the url is in some field; include it
|
||||
rank += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rank;
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectEntryFilter };
|
|
@ -14,6 +14,8 @@ import { ExtensionSaveEntryView } from 'views/extension/extension-save-entry-vie
|
|||
import { RuntimeDataModel } from 'models/runtime-data-model';
|
||||
import { AppSettingsModel } from 'models/app-settings-model';
|
||||
import { Timeouts } from 'const/timeouts';
|
||||
import { SelectEntryView } from 'views/select/select-entry-view';
|
||||
import { SelectEntryFilter } from 'comp/app/select-entry-filter';
|
||||
|
||||
const KeeWebAssociationId = 'KeeWeb';
|
||||
const KeeWebHash = '398d9c782ec76ae9e9877c2321cbda2b31fc6d18ccf0fed5ca4bd746bab4d64a'; // sha256('KeeWeb')
|
||||
|
@ -409,10 +411,30 @@ const ProtocolHandlers = {
|
|||
throw new Error('Empty url');
|
||||
}
|
||||
|
||||
// const files = getAvailableFiles(request);
|
||||
const files = getAvailableFiles(request);
|
||||
const client = getClient(request);
|
||||
|
||||
// throw makeError(Errors.noMatches);
|
||||
const filter = new SelectEntryFilter({ url: payload.url }, appModel, files);
|
||||
const entries = filter.getEntries();
|
||||
if (!entries.length) {
|
||||
throw makeError(Errors.noMatches);
|
||||
}
|
||||
|
||||
const topMessage = Locale.extensionSelectPasswordFor.replace(
|
||||
'{}',
|
||||
getHumanReadableExtensionName(client)
|
||||
);
|
||||
const selectEntryView = new SelectEntryView({
|
||||
filter,
|
||||
topMessage
|
||||
});
|
||||
|
||||
const result = await selectEntryView.showAndGetResult();
|
||||
if (!result?.entry) {
|
||||
throw makeError(Errors.userRejected);
|
||||
}
|
||||
|
||||
const entry = result.entry;
|
||||
|
||||
client.stats.passwordsRead++;
|
||||
|
||||
|
@ -423,13 +445,13 @@ const ProtocolHandlers = {
|
|||
count: 1,
|
||||
entries: [
|
||||
{
|
||||
group: 'Group1',
|
||||
login: 'urls-user',
|
||||
name: 'URLS',
|
||||
password: 'urls-passs',
|
||||
group: entry.group.title,
|
||||
login: entry.user || '',
|
||||
name: entry.title || '',
|
||||
password: entry.password?.getText() || '',
|
||||
skipAutoSubmit: 'false',
|
||||
stringFields: [],
|
||||
uuid: '7cfc6ceb56674f26bad6ff79d73a06f5'
|
||||
uuid: kdbxweb.ByteUtils.bytesToHex(entry.entry.uuid.bytes)
|
||||
}
|
||||
],
|
||||
id: ''
|
||||
|
|
|
@ -154,6 +154,7 @@
|
|||
"keyChangeMessageExpired": "Master key for this database is expired. Please enter a new key",
|
||||
"keyChangeRepeatPassword": "Password, once again",
|
||||
"keyEnter": "Enter",
|
||||
"keyEsc": "Esc",
|
||||
|
||||
"iconFavTitle": "Download and use website favicon",
|
||||
"iconSelCustom": "Select a custom icon",
|
||||
|
@ -797,5 +798,10 @@
|
|||
"extensionSaveEntryHeader": "Save password",
|
||||
"extensionSaveEntryBody": "{} is trying to save a password. Allow this?",
|
||||
"extensionSaveEntryAuto": "Save other passwords automatically in this session",
|
||||
"extensionSaveEntryNewGroup": "new group"
|
||||
"extensionSaveEntryNewGroup": "new group",
|
||||
"extensionSelectPasswordFor": "Select a password for {}",
|
||||
|
||||
"selectEntryHeader": "Select entry",
|
||||
"selectEntryEnterHint": "use the highlighted entry",
|
||||
"selectEntryEscHint": "cancel"
|
||||
}
|
||||
|
|
|
@ -321,7 +321,7 @@ class AppModel {
|
|||
}
|
||||
|
||||
getEntries() {
|
||||
const entries = this.getEntriesByFilter(this.filter);
|
||||
const entries = this.getEntriesByFilter(this.filter, this.files);
|
||||
entries.sortEntries(this.sort, this.filter);
|
||||
if (this.filter.trash) {
|
||||
this.addTrashGroups(entries);
|
||||
|
@ -329,15 +329,15 @@ class AppModel {
|
|||
return entries;
|
||||
}
|
||||
|
||||
getEntriesByFilter(filter) {
|
||||
getEntriesByFilter(filter, files) {
|
||||
const preparedFilter = this.prepareFilter(filter);
|
||||
const entries = new SearchResultCollection();
|
||||
|
||||
const devicesToMatchOtpEntries = this.files.filter((file) => file.backend === 'otp-device');
|
||||
const devicesToMatchOtpEntries = files.filter((file) => file.backend === 'otp-device');
|
||||
|
||||
const matchedOtpEntrySet = this.settings.yubiKeyMatchEntries ? new Set() : undefined;
|
||||
|
||||
this.files
|
||||
files
|
||||
.filter((file) => file.backend !== 'otp-device')
|
||||
.forEach((file) => {
|
||||
file.forEachEntry(preparedFilter, (entry) => {
|
||||
|
|
|
@ -14,7 +14,7 @@ import itemTemplate from 'templates/select/select-entry-item.hbs';
|
|||
|
||||
class SelectEntryView extends View {
|
||||
parent = 'body';
|
||||
modal = 'auto-type';
|
||||
modal = 'select-entry';
|
||||
|
||||
template = template;
|
||||
|
||||
|
@ -32,62 +32,74 @@ class SelectEntryView extends View {
|
|||
constructor(model) {
|
||||
super(model);
|
||||
this.initScroll();
|
||||
this.listenTo(Events, 'keypress:auto-type', this.keyPressed);
|
||||
this.listenTo(Events, 'main-window-blur', this.mainWindowBlur);
|
||||
this.listenTo(Events, 'keypress:select-entry', this.keyPressed);
|
||||
this.setupKeys();
|
||||
}
|
||||
|
||||
setupKeys() {
|
||||
this.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, false, 'auto-type');
|
||||
this.onKey(Keys.DOM_VK_RETURN, this.enterPressed, false, 'auto-type');
|
||||
this.onKey(
|
||||
Keys.DOM_VK_RETURN,
|
||||
this.actionEnterPressed,
|
||||
KeyHandler.SHORTCUT_ACTION,
|
||||
'auto-type'
|
||||
);
|
||||
this.onKey(Keys.DOM_VK_RETURN, this.optEnterPressed, KeyHandler.SHORTCUT_OPT, 'auto-type');
|
||||
this.onKey(
|
||||
Keys.DOM_VK_RETURN,
|
||||
this.shiftEnterPressed,
|
||||
KeyHandler.SHORTCUT_SHIFT,
|
||||
'auto-type'
|
||||
);
|
||||
this.onKey(Keys.DOM_VK_UP, this.upPressed, false, 'auto-type');
|
||||
this.onKey(Keys.DOM_VK_DOWN, this.downPressed, false, 'auto-type');
|
||||
this.onKey(Keys.DOM_VK_BACK_SPACE, this.backSpacePressed, false, 'auto-type');
|
||||
this.onKey(Keys.DOM_VK_O, this.openKeyPressed, KeyHandler.SHORTCUT_ACTION, 'auto-type');
|
||||
this.onKey(Keys.DOM_VK_ESCAPE, this.escPressed, false, 'select-entry');
|
||||
this.onKey(Keys.DOM_VK_RETURN, this.enterPressed, false, 'select-entry');
|
||||
if (this.model.isAutoType) {
|
||||
this.onKey(
|
||||
Keys.DOM_VK_RETURN,
|
||||
this.actionEnterPressed,
|
||||
KeyHandler.SHORTCUT_ACTION,
|
||||
'select-entry'
|
||||
);
|
||||
this.onKey(
|
||||
Keys.DOM_VK_RETURN,
|
||||
this.optEnterPressed,
|
||||
KeyHandler.SHORTCUT_OPT,
|
||||
'select-entry'
|
||||
);
|
||||
this.onKey(
|
||||
Keys.DOM_VK_RETURN,
|
||||
this.shiftEnterPressed,
|
||||
KeyHandler.SHORTCUT_SHIFT,
|
||||
'select-entry'
|
||||
);
|
||||
this.onKey(
|
||||
Keys.DOM_VK_O,
|
||||
this.openKeyPressed,
|
||||
KeyHandler.SHORTCUT_ACTION,
|
||||
'select-entry'
|
||||
);
|
||||
}
|
||||
this.onKey(Keys.DOM_VK_UP, this.upPressed, false, 'select-entry');
|
||||
this.onKey(Keys.DOM_VK_DOWN, this.downPressed, false, 'select-entry');
|
||||
this.onKey(Keys.DOM_VK_BACK_SPACE, this.backSpacePressed, false, 'select-entry');
|
||||
}
|
||||
|
||||
render() {
|
||||
let topMessage;
|
||||
if (this.model.filter.title || this.model.filter.url) {
|
||||
topMessage = Locale.autoTypeMsgMatchedByWindow.replace(
|
||||
'{}',
|
||||
this.model.filter.title || this.model.filter.url
|
||||
);
|
||||
} else {
|
||||
topMessage = Locale.autoTypeMsgNoWindow;
|
||||
}
|
||||
const noColor = AppSettingsModel.colorfulIcons ? '' : 'grayscale';
|
||||
this.entries = this.model.filter.getEntries();
|
||||
this.result = this.entries[0];
|
||||
|
||||
const presenter = new EntryPresenter(null, noColor, this.result && this.result.id);
|
||||
presenter.itemOptions = this.model.itemOptions;
|
||||
|
||||
let itemsHtml = '';
|
||||
const itemTemplate = this.itemTemplate;
|
||||
this.entries.forEach((entry) => {
|
||||
presenter.present(entry);
|
||||
itemsHtml += itemTemplate(presenter, DefaultTemplateOptions);
|
||||
});
|
||||
|
||||
super.render({
|
||||
isAutoType: this.model.isAutoType,
|
||||
filterText: this.model.filter.text,
|
||||
topMessage,
|
||||
topMessage: this.model.topMessage,
|
||||
itemsHtml,
|
||||
actionSymbol: Shortcuts.actionShortcutSymbol(true),
|
||||
altSymbol: Shortcuts.altShortcutSymbol(true),
|
||||
shiftSymbol: Shortcuts.shiftShortcutSymbol(true),
|
||||
keyEnter: Locale.keyEnter
|
||||
keyEnter: Locale.keyEnter,
|
||||
keyEsc: Locale.keyEsc
|
||||
});
|
||||
|
||||
document.activeElement.blur();
|
||||
|
||||
this.createScroll({
|
||||
root: this.$el.find('.select-entry__items')[0],
|
||||
scroller: this.$el.find('.scroller')[0],
|
||||
|
@ -167,6 +179,10 @@ class SelectEntryView extends View {
|
|||
}
|
||||
}
|
||||
|
||||
mainWindowBlur() {
|
||||
this.emit('result', undefined);
|
||||
}
|
||||
|
||||
keyPressed(e) {
|
||||
if (e.which && e.which !== Keys.DOM_VK_RETURN) {
|
||||
this.model.filter.text += String.fromCharCode(e.which);
|
||||
|
@ -219,6 +235,9 @@ class SelectEntryView extends View {
|
|||
if (event) {
|
||||
event.stopImmediatePropagation();
|
||||
}
|
||||
if (!this.model.itemOptions) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = itemEl.data('id');
|
||||
const entry = this.entries.get(id);
|
||||
|
@ -307,6 +326,16 @@ class SelectEntryView extends View {
|
|||
const sequence = e.item;
|
||||
this.closeWithResult(sequence);
|
||||
}
|
||||
|
||||
showAndGetResult() {
|
||||
this.render();
|
||||
return new Promise((resolve) => {
|
||||
this.once('result', (result) => {
|
||||
this.remove();
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(SelectEntryView.prototype, Scrollable);
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
<td>{{#if title}}{{title}}{{else}}({{res 'noTitle'}}){{/if}}</td>
|
||||
<td>{{user}}</td>
|
||||
<td>{{url}}</td>
|
||||
<td class="select-entry__item-options">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</td>
|
||||
{{#if itemOptions}}
|
||||
<td class="select-entry__item-options">
|
||||
<i class="fa fa-ellipsis-h"></i>
|
||||
</td>
|
||||
{{/if}}
|
||||
</tr>
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
<div class="select-entry">
|
||||
<div class="select-entry__header">
|
||||
<h1 class="select-entry__header-text">{{res 'autoTypeHeader'}}</h1>
|
||||
<h1 class="select-entry__header-text">
|
||||
{{#if isAutoType}}
|
||||
{{res 'autoTypeHeader'}}
|
||||
{{else}}
|
||||
{{res 'selectEntryHeader'}}
|
||||
{{/if}}
|
||||
</h1>
|
||||
<div class="select-entry__hint" id="select-entry__hint">
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{keyEnter}}</span>: {{res 'autoTypeSelectionHint'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{actionSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintAction'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{altSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintOpt'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{shiftSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintShift'}}</div>
|
||||
{{#if isAutoType}}
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{keyEnter}}</span>: {{res 'autoTypeSelectionHint'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{actionSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintAction'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{altSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintOpt'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{shiftSymbol}} {{keyEnter}}</span>: {{res 'autoTypeSelectionHintShift'}}</div>
|
||||
{{else}}
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{keyEnter}}</span>: {{res 'selectEntryEnterHint'}}</div>
|
||||
<div class="select-entry__hint-text"><span class="shortcut">{{keyEsc}}</span>: {{res 'selectEntryEscHint'}}</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{#if filterText}}
|
||||
<div class="select-entry__header-filter" id="select-entry__header-filter">
|
||||
<input type="text" readonly value="{{filterText}}" class="select-entry__header-filter-input" />
|
||||
<i class="select-entry__header-filter-clear fa fa-times"></i>
|
||||
</div>
|
||||
<div class="select-entry__header-filter" id="select-entry__header-filter">
|
||||
<input type="text" readonly value="{{filterText}}" class="select-entry__header-filter-input" />
|
||||
<i class="select-entry__header-filter-clear fa fa-times"></i>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="select-entry__message">
|
||||
|
|
Loading…
Reference in New Issue