mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-01-12 20:31:21 -08:00
1141 lines
39 KiB
JavaScript
1141 lines
39 KiB
JavaScript
/*\
|
|
title: $:/core/modules/widgets/commandpalettewidget.js
|
|
type: application/javascript
|
|
module-type: widget
|
|
|
|
Command Palette Widget
|
|
|
|
\*/
|
|
(function () {
|
|
|
|
/*jslint node: true, browser: true */
|
|
/*global $tw: false */
|
|
'use strict';
|
|
|
|
var Widget = require('$:/core/modules/widgets/widget.js').widget;
|
|
|
|
class CommandPaletteWidget extends Widget {
|
|
constructor(parseTreeNode, options) {
|
|
super(parseTreeNode, options);
|
|
this.initialise(parseTreeNode, options);
|
|
this.currentSelection = 0; //0 is nothing selected, 1 is first result,...
|
|
this.symbolProviders = {};
|
|
this.actions = [];
|
|
this.blockProviderChange = false;
|
|
this.defaultSettings = {
|
|
maxResults: 15,
|
|
maxResultHintSize: 45,
|
|
neverBasic: false,
|
|
showHistoryOnOpen: true,
|
|
escapeGoesBack: true,
|
|
alwaysPassSelection: true,
|
|
theme: '$:/plugins/souk21/commandpalette/Compact.css',
|
|
};
|
|
this.settings = {};
|
|
this.commandHistoryPath = '$:/plugins/souk21/commandpalette/CommandPaletteHistory';
|
|
this.settingsPath = '$:/plugins/souk21/commandpalette/CommandPaletteSettings';
|
|
this.searchStepsPath = '$:/plugins/souk21/commandpalette/CommandPaletteSearchSteps';
|
|
this.customCommandsTag = '$:/tags/CommandPaletteCommand';
|
|
this.themesTag = '$:/tags/CommandPaletteTheme';
|
|
this.typeField = 'command-palette-type';
|
|
this.nameField = 'command-palette-name';
|
|
this.hintField = 'cp-hint';
|
|
this.modeField = 'command-palette-mode';
|
|
this.caretField = 'command-palette-caret';
|
|
this.immediateField = 'command-palette-immediate';
|
|
}
|
|
|
|
actionStringBuilder(text) {
|
|
return (e) => this.invokeActionString(text, this, e);
|
|
}
|
|
|
|
invokeFieldMangler(tiddler, message, param, e) {
|
|
let action = `<$fieldmangler tiddler="${tiddler}">
|
|
<$action-sendmessage $message="${message}" $param="${param}"/>
|
|
</$fieldmangler>`;
|
|
this.invokeActionString(action, this, e);
|
|
}
|
|
|
|
//filter = (tiddler, terms) => [tiddlers]
|
|
tagOperation(e, hintTiddler, hintTag, filter, allowNoSelection, message) {
|
|
this.blockProviderChange = true;
|
|
if (allowNoSelection) this.allowInputFieldSelection = true;
|
|
this.currentProvider = this.historyProviderBuilder(hintTiddler);
|
|
this.currentResolver = (e) => {
|
|
if (this.currentSelection === 0) return;
|
|
let tiddler = this.currentResults[this.currentSelection - 1].result.name;
|
|
this.currentProvider = (terms) => {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = hintTag;
|
|
let searches = filter(tiddler, terms);
|
|
searches = searches.map(s => { return { name: s }; });
|
|
this.showResults(searches);
|
|
}
|
|
this.input.value = "";
|
|
this.onInput(this.input.value);
|
|
this.currentResolver = (e) => {
|
|
if (!allowNoSelection && this.currentSelection === 0) return;
|
|
let tag = this.input.value;
|
|
if (this.currentSelection !== 0) {
|
|
tag = this.currentResults[this.currentSelection - 1].result.name;
|
|
}
|
|
this.invokeFieldMangler(tiddler, message, tag, e);
|
|
if (!e.getModifierState('Shift')) {
|
|
this.closePalette();
|
|
} else {
|
|
this.onInput(this.input.value);
|
|
}
|
|
}
|
|
}
|
|
this.input.value = "";
|
|
this.onInput(this.input.value);
|
|
}
|
|
|
|
refreshThemes(e) {
|
|
this.themes = this.getTiddlersWithTag(this.themesTag);
|
|
let found = false;
|
|
for (let theme of this.themes) {
|
|
let themeName = theme.fields.title;
|
|
if (themeName === this.settings.theme) {
|
|
found = true;
|
|
this.addTagIfNecessary(themeName, '$:/tags/Stylesheet', e);
|
|
} else {
|
|
this.invokeFieldMangler(themeName, 'tm-remove-tag', '$:/tags/Stylesheet', e);
|
|
}
|
|
}
|
|
if (found) return;
|
|
this.addTagIfNecessary(this.defaultSettings.theme, '$:/tags/Stylesheet', e);
|
|
}
|
|
|
|
//Re-adding an existing tag changes modification date
|
|
addTagIfNecessary(tiddler, tag, e) {
|
|
if (this.hasTag(tiddler, tag)) return;
|
|
this.invokeFieldMangler(tiddler, 'tm-add-tag', tag, e);
|
|
}
|
|
|
|
hasTag(tiddler, tag) {
|
|
return $tw.wiki.getTiddler(tiddler).fields.tags.includes(tag);
|
|
}
|
|
|
|
refreshCommands() {
|
|
this.actions = [];
|
|
this.actions.push({ name: "Refresh Command Palette", action: (e) => { this.refreshCommandPalette(); this.promptCommand('') }, keepPalette: true });
|
|
this.actions.push({ name: "Explorer", action: (e) => this.explorer(e), keepPalette: true });
|
|
this.actions.push({ name: "See History", action: (e) => this.showHistory(e), keepPalette: true });
|
|
this.actions.push({ name: "New Command Wizard", action: (e) => this.newCommandWizard(e), keepPalette: true });
|
|
this.actions.push({
|
|
name: "Add tag to tiddler",
|
|
action: (e) => this.tagOperation(e, 'Pick tiddler to tag', 'Pick tag to add (⇧⏎ to add multiple)',
|
|
(tiddler, terms) => $tw.wiki.filterTiddlers(`[!is[system]tags[]] [is[system]tags[]] -[[${tiddler}]tags[]] +[search[${terms}]]`),
|
|
true,
|
|
'tm-add-tag'),
|
|
keepPalette: true
|
|
});
|
|
this.actions.push({
|
|
name: "Remove tag",
|
|
action: (e) => this.tagOperation(e, 'Pick tiddler to untag', 'Pick tag to remove (⇧⏎ to remove multiple)',
|
|
(tiddler, terms) => $tw.wiki.filterTiddlers(`[[${tiddler}]tags[]] +[search[${terms}]]`),
|
|
false,
|
|
'tm-remove-tag'),
|
|
keepPalette: true
|
|
});
|
|
|
|
let commandTiddlers = this.getTiddlersWithTag(this.customCommandsTag);
|
|
for (let tiddler of commandTiddlers) {
|
|
if (!tiddler.fields[this.nameField] === undefined) continue;
|
|
if (!tiddler.fields[this.typeField] === undefined) continue;
|
|
let name = tiddler.fields[this.nameField];
|
|
let type = tiddler.fields[this.typeField];
|
|
let text = tiddler.fields.text;
|
|
if (text === undefined) text = '';
|
|
let textFirstLine = text.match(/^.*/)[0];
|
|
|
|
if (type === 'prompt') {
|
|
let immediate = !!tiddler.fields[this.immediateField];
|
|
let caret = tiddler.fields[this.caretField];
|
|
let action = { name: name, action: () => this.promptCommand(textFirstLine, caret), keepPalette: !immediate, immediate: immediate };
|
|
this.actions.push(action);
|
|
continue;
|
|
}
|
|
if (type === 'prompt-basic') {
|
|
let caret = tiddler.fields[this.caretField];
|
|
let action = { name: name, action: () => this.promptCommandBasic(textFirstLine, caret, name), keepPalette: true };
|
|
this.actions.push(action);
|
|
continue;
|
|
}
|
|
if (type === 'message') {
|
|
this.actions.push({ name: name, action: (e) => this.tmMessageBuilder(textFirstLine)(e) });
|
|
continue;
|
|
}
|
|
if (type === 'actionString') {
|
|
this.actions.push({ name: name, action: (e) => this.actionStringBuilder(text)(e) });
|
|
continue;
|
|
}
|
|
if (type === 'history') {
|
|
let hint = tiddler.fields[this.hintField];
|
|
let mode = tiddler.fields[this.modeField];
|
|
this.actions.push({ name: name, action: (e) => this.commandWithHistoryPicker(textFirstLine, hint, mode).handler(e), keepPalette: true });
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
newCommandWizard() {
|
|
this.blockProviderChange = true;
|
|
this.input.value = '';
|
|
this.hint.innerText = 'Command Name';
|
|
let name = '';
|
|
let type = '';
|
|
let hint = '';
|
|
|
|
let messageStep = () => {
|
|
this.input.value = '';
|
|
this.hint.innerText = 'Enter Message';
|
|
this.currentResolver = (e) => {
|
|
this.tmMessageBuilder('tm-new-tiddler',
|
|
{
|
|
title: '$:/' + name,
|
|
tags: this.customCommandsTag,
|
|
[this.typeField]: type,
|
|
[this.nameField]: name,
|
|
[this.hintField]: hint,
|
|
text: this.input.value
|
|
})(e);
|
|
this.closePalette();
|
|
}
|
|
}
|
|
|
|
let hintStep = () => {
|
|
this.input.value = '';
|
|
this.hint.innerText = 'Enter hint';
|
|
this.currentResolver = (e) => {
|
|
hint = this.input.value;
|
|
messageStep();
|
|
}
|
|
}
|
|
|
|
|
|
let typeStep = () => {
|
|
this.input.value = '';
|
|
this.hint.innerText = 'Enter type (prompt, prompt-basic, message, actionString, history)'
|
|
this.currentResolver = (e) => {
|
|
type = this.input.value;
|
|
if (type === 'history') {
|
|
hintStep();
|
|
} else {
|
|
this.tmMessageBuilder('tm-new-tiddler',
|
|
{
|
|
title: '$:/' + name,
|
|
tags: this.customCommandsTag,
|
|
[this.typeField]: type,
|
|
[this.nameField]: name
|
|
})(e);
|
|
this.closePalette();
|
|
}
|
|
}
|
|
}
|
|
|
|
this.currentProvider = (terms) => { }
|
|
this.currentResolver = (e) => {
|
|
if (this.input.value.length === 0) return;
|
|
name = this.input.value;
|
|
typeStep();
|
|
}
|
|
this.showResults([]);
|
|
}
|
|
|
|
explorer(e) {
|
|
this.blockProviderChange = true;
|
|
this.input.value = '$:/';
|
|
this.lastExplorerInput = '$:/';
|
|
this.hint.innerText = 'Explorer (⇧⏎ to add multiple)';
|
|
this.currentProvider = (terms) => this.explorerProvider('$:/', terms);
|
|
this.currentResolver = (e) => {
|
|
if (this.currentSelection === 0) return;
|
|
this.currentResults[this.currentSelection - 1].result.action(e);
|
|
}
|
|
this.onInput();
|
|
}
|
|
|
|
explorerProvider(url, terms) {
|
|
let switchFolder = (url) => {
|
|
this.input.value = url;
|
|
this.lastExplorerInput = this.input.value;
|
|
this.currentProvider = (terms) => this.explorerProvider(url, terms);
|
|
this.onInput();
|
|
};
|
|
if (!this.input.value.startsWith(url)) {
|
|
this.input.value = this.lastExplorerInput;
|
|
}
|
|
this.lastExplorerInput = this.input.value;
|
|
this.currentSelection = 0;
|
|
let search = this.input.value.substr(url.length);
|
|
let tiddlers = $tw.wiki.filterTiddlers(`[removeprefix[${url}]splitbefore[/]sort[]search[${search}]]`);
|
|
let folders = [];
|
|
let files = [];
|
|
for (let tiddler of tiddlers) {
|
|
if (tiddler.endsWith('/')) {
|
|
folders.push({ name: tiddler, action: (e) => switchFolder(`${url}${tiddler}`) });
|
|
} else {
|
|
files.push({
|
|
name: tiddler, action: (e) => {
|
|
this.navigateTo(`${url}${tiddler}`);
|
|
if (!e.getModifierState('Shift')) {
|
|
this.closePalette();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
let topResult;
|
|
if (url !== '$:/') {
|
|
let splits = url.split('/');
|
|
splits.splice(splits.length - 2);
|
|
let parent = splits.join('/') + '/';
|
|
topResult = { name: '..', action: (e) => switchFolder(parent) };
|
|
this.showResults([topResult, ...folders, ...files]);
|
|
return;
|
|
}
|
|
this.showResults([...folders, ...files]);
|
|
}
|
|
|
|
setSetting(name, value) {
|
|
//doing the validation here too (it's also done in refreshSettings, so you can load you own settings with a json file)
|
|
if (typeof value === 'string') {
|
|
if (value === 'true') value = true;
|
|
if (value === 'false') value = false;
|
|
}
|
|
this.settings[name] = value;
|
|
$tw.wiki.setTiddlerData(this.settingsPath, this.settings);
|
|
}
|
|
|
|
//loadSettings?
|
|
refreshSettings() {
|
|
//get user or default settings
|
|
this.settings = $tw.wiki.getTiddlerData(this.settingsPath, { ...this.defaultSettings });
|
|
//Adding eventual missing properties to current user settings
|
|
for (let prop in this.defaultSettings) {
|
|
if (!this.defaultSettings.hasOwnProperty(prop)) continue;
|
|
if (this.settings[prop] === undefined) {
|
|
this.settings[prop] = this.defaultSettings[prop];
|
|
}
|
|
}
|
|
//cast all booleans
|
|
for (let prop in this.settings) {
|
|
if (!this.settings.hasOwnProperty(prop)) continue;
|
|
if (typeof this.settings[prop] !== 'string') continue;
|
|
if (this.settings[prop].toLowerCase() === 'true') this.settings[prop] = true;
|
|
if (this.settings[prop].toLowerCase() === 'false') this.settings[prop] = false;
|
|
}
|
|
}
|
|
|
|
//helper function to retrieve all tiddlers (+ their fields) with a tag
|
|
getTiddlersWithTag(tag) {
|
|
let tiddlers = $tw.wiki.getTiddlersWithTag(tag);
|
|
return tiddlers.map(t => $tw.wiki.getTiddler(t));
|
|
}
|
|
|
|
render(parent, nextSibling) {
|
|
this.parentDomNode = parent;
|
|
this.execute();
|
|
this.history = $tw.wiki.getTiddlerData(this.commandHistoryPath, { history: [] }).history;
|
|
|
|
$tw.rootWidget.addEventListener('open-command-palette', (e) => this.openPalette(e));
|
|
$tw.rootWidget.addEventListener('open-command-palette-selection', (e) => this.openPaletteSelection(e));
|
|
$tw.rootWidget.addEventListener('insert-command-palette-result', (e) => this.insertSelectedResult(e));
|
|
|
|
let inputAndMainHintWrapper = this.createElement('div', { className: 'inputhintwrapper' });
|
|
this.div = this.createElement('div', { className: 'commandpalette' }, { display: 'none' });
|
|
this.input = this.createElement('input', { type: 'text' });
|
|
this.hint = this.createElement('div', { className: 'commandpalettehint commandpalettehintmain' });
|
|
inputAndMainHintWrapper.append(this.input, this.hint);
|
|
this.scrollDiv = this.createElement('div', { className: 'cp-scroll' });
|
|
this.div.append(inputAndMainHintWrapper, this.scrollDiv);
|
|
this.input.addEventListener('keydown', (e) => this.onKeyDown(e));
|
|
this.input.addEventListener('input', () => this.onInput(this.input.value));
|
|
window.addEventListener('click', (e) => this.onClick(e));
|
|
parent.insertBefore(this.div, nextSibling);
|
|
|
|
this.refreshCommandPalette();
|
|
|
|
this.symbolProviders['>'] = { searcher: (terms) => this.actionProvider(terms), resolver: (e) => this.actionResolver(e) };
|
|
this.symbolProviders['#'] = { searcher: (terms) => this.tagListProvider(terms), resolver: (e) => this.tagListResolver(e) };
|
|
this.symbolProviders['@'] = { searcher: (terms) => this.tagProvider(terms), resolver: (e) => this.defaultResolver(e) };
|
|
this.symbolProviders['?'] = { searcher: (terms) => this.helpProvider(terms), resolver: (e) => this.helpResolver(e) };
|
|
this.symbolProviders['['] = { searcher: (terms, hint) => this.filterProvider(terms, hint), resolver: (e) => this.filterResolver(e) };
|
|
this.symbolProviders['+'] = { searcher: (terms) => this.createTiddlerProvider(terms), resolver: (e) => this.createTiddlerResolver() };
|
|
this.symbolProviders['|'] = { searcher: (terms) => this.settingsProvider(terms), resolver: (e) => this.settingsResolver() };
|
|
this.currentResults = [];
|
|
this.currentProvider = {};
|
|
}
|
|
|
|
refreshSearchSteps() {
|
|
this.searchSteps = [];
|
|
let steps = $tw.wiki.getTiddlerData(this.searchStepsPath);
|
|
steps = steps.steps;
|
|
for (let step of steps) {
|
|
this.searchSteps.push(this.searchStepBuilder(step.filter, step.caret, step.hint));
|
|
}
|
|
}
|
|
|
|
refreshCommandPalette() {
|
|
this.refreshSettings();
|
|
this.refreshThemes();
|
|
this.refreshCommands();
|
|
this.refreshSearchSteps();
|
|
}
|
|
|
|
updateCommandHistory(command) {
|
|
this.history = Array.from(new Set([command.name, ...this.history]));
|
|
$tw.wiki.setTiddlerData(this.commandHistoryPath, { history: this.history });
|
|
}
|
|
|
|
historyProviderBuilder(hint, mode) {
|
|
return (terms) => {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = hint;
|
|
let results;
|
|
if (mode !== undefined && mode === 'drafts') {
|
|
results = $tw.wiki.filterTiddlers('[has:field[draft.of]]');
|
|
} else if (mode !== undefined && mode === 'story') {
|
|
results = $tw.wiki.filterTiddlers('[list[$:/StoryList]]');
|
|
} else {
|
|
results = this.getHistory();
|
|
}
|
|
results = results.map(r => { return { name: r } });
|
|
this.showResults(results);
|
|
};
|
|
}
|
|
|
|
commandWithHistoryPicker(message, hint, mode) {
|
|
let handler = (e) => {
|
|
this.blockProviderChange = true;
|
|
this.allowInputFieldSelection = true;
|
|
this.currentProvider = provider;
|
|
this.currentResolver = resolver;
|
|
this.input.value = '';
|
|
this.onInput(this.input.value);
|
|
}
|
|
let provider = this.historyProviderBuilder(hint, mode);
|
|
let resolver = (e) => {
|
|
if (this.currentSelection === 0) return;
|
|
let title = this.currentResults[this.currentSelection - 1].result.name;
|
|
this.parentWidget.dispatchEvent({
|
|
type: message,
|
|
param: title,
|
|
tiddlerTitle: title,
|
|
});
|
|
this.closePalette();
|
|
}
|
|
return {
|
|
handler,
|
|
provider,
|
|
resolver
|
|
}
|
|
}
|
|
onInput(text) {
|
|
if (this.blockProviderChange) { //prevent provider changes
|
|
this.currentProvider(text);
|
|
this.setSelectionToFirst();
|
|
return;
|
|
}
|
|
let { resolver, provider, terms } = this.parseCommand(text);
|
|
this.currentResolver = resolver;
|
|
this.currentProvider = provider;
|
|
this.currentProvider(terms);
|
|
this.setSelectionToFirst();
|
|
}
|
|
parseCommand(text) {
|
|
let terms = "";
|
|
let prefix = text.substr(0, 1);
|
|
let resolver;
|
|
let provider;
|
|
let providerSymbol = Object.keys(this.symbolProviders).find(p => p === prefix);
|
|
if (providerSymbol === undefined) {
|
|
resolver = this.defaultResolver;
|
|
provider = this.defaultProvider;
|
|
terms = text;
|
|
}
|
|
else {
|
|
provider = this.symbolProviders[providerSymbol].searcher;
|
|
resolver = this.symbolProviders[providerSymbol].resolver;
|
|
terms = text.substring(1);
|
|
}
|
|
return { prefix: providerSymbol, resolver, provider, terms }
|
|
}
|
|
onClick(e) {
|
|
if (this.isOpened && !this.div.contains(e.target)) {
|
|
this.closePalette();
|
|
}
|
|
}
|
|
openPaletteSelection(e) {
|
|
let selection = this.getCurrentSelection();
|
|
e.param = selection;
|
|
this.openPalette(e);
|
|
}
|
|
openPalette(e) {
|
|
this.isOpened = true;
|
|
this.allowInputFieldSelection = false;
|
|
this.goBack = undefined;
|
|
this.blockProviderChange = false;
|
|
let activeElement = this.getActiveElement();
|
|
this.previouslyFocused = { element: activeElement, start: activeElement.selectionStart, end: activeElement.selectionEnd, caretPos: activeElement.selectionEnd };
|
|
this.input.value = '';
|
|
if (e.param !== undefined) {
|
|
this.input.value = e.param;
|
|
}
|
|
if (this.settings.alwaysPassSelection) {
|
|
this.input.value += this.getCurrentSelection();
|
|
}
|
|
this.currentSelection = 0;
|
|
this.onInput(this.input.value); //Trigger results on open
|
|
this.div.style.display = 'flex';
|
|
this.input.focus();
|
|
}
|
|
|
|
insertSelectedResult() {
|
|
if (!this.isOpened) return;
|
|
if (this.currentSelection === 0) return; //TODO: what to do here?
|
|
let previous = this.previouslyFocused;
|
|
let previousValue = previous.element.value;
|
|
if (previousValue === undefined) return;
|
|
let selection = this.currentResults[this.currentSelection - 1].result.name;
|
|
if (previous.start !== previous.end) {
|
|
this.previouslyFocused.element.value = previousValue.substring(0, previous.start) + selection + previousValue.substring(previous.end);
|
|
} else {
|
|
this.previouslyFocused.element.value = previousValue.substring(0, previous.start) + selection + previousValue.substring(previous.start);
|
|
}
|
|
this.previouslyFocused.caretPos = previous.start + selection.length;
|
|
this.closePalette();
|
|
}
|
|
|
|
closePalette() {
|
|
this.div.style.display = 'none';
|
|
this.isOpened = false;
|
|
this.focusAtCaretPosition(this.previouslyFocused.element, this.previouslyFocused.caretPos);
|
|
}
|
|
onKeyDown(e) {
|
|
if (e.key === 'Escape') {
|
|
// \/ There's no previous state
|
|
if (!this.settings.escapeGoesBack || this.goBack === undefined) {
|
|
this.closePalette();
|
|
} else {
|
|
this.goBack();
|
|
this.goBack = undefined;
|
|
}
|
|
}
|
|
else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
let sel = this.currentSelection - 1;
|
|
|
|
if (sel === 0) {
|
|
if (!this.allowInputFieldSelection) {
|
|
sel = this.currentResults.length;
|
|
}
|
|
} else if (sel < 0) {
|
|
sel = this.currentResults.length;
|
|
}
|
|
this.setSelection(sel);
|
|
}
|
|
else if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
let sel = (this.currentSelection + 1) % (this.currentResults.length + 1);
|
|
if (!this.allowInputFieldSelection && sel === 0 && this.currentResults.length !== 0) {
|
|
sel = 1;
|
|
}
|
|
this.setSelection(sel);
|
|
}
|
|
else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
this.validateSelection(e);
|
|
}
|
|
}
|
|
addResult(result, id) {
|
|
let resultDiv = this.createElement('div', { className: 'commandpaletteresult', innerText: result.name });
|
|
if (result.hint !== undefined) {
|
|
let hint = this.createElement('div', { className: 'commandpalettehint', innerText: result.hint });
|
|
resultDiv.append(hint);
|
|
}
|
|
resultDiv.result = result;
|
|
this.currentResults.push(resultDiv);
|
|
resultDiv.addEventListener('click', (e) => { this.setSelection(id + 1); this.validateSelection(e); });
|
|
this.scrollDiv.append(resultDiv);
|
|
}
|
|
validateSelection(e) {
|
|
this.currentResolver(e);
|
|
}
|
|
defaultResolver(e) {
|
|
if (e.getModifierState('Shift')) {
|
|
this.input.value = '+' + this.input.value; //this resolver expects that the input starts with +
|
|
this.createTiddlerResolver(e);
|
|
return;
|
|
}
|
|
if (this.currentSelection === 0) return;
|
|
let selectionTitle = this.currentResults[this.currentSelection - 1].result.name;
|
|
this.closePalette();
|
|
this.navigateTo(selectionTitle);
|
|
}
|
|
navigateTo(title) {
|
|
this.parentWidget.dispatchEvent({
|
|
type: 'tm-navigate',
|
|
param: title,
|
|
navigateTo: title
|
|
});
|
|
}
|
|
|
|
showHistory() {
|
|
this.hint.innerText = 'History';
|
|
this.currentProvider = (terms) => {
|
|
let results;
|
|
if (terms.length === 0) {
|
|
results = this.getHistory();
|
|
} else {
|
|
results = this.getHistory().filter(h => h.includes(terms));
|
|
}
|
|
results = results.map(r => { return { name: r, action: () => { this.navigateTo(r); this.closePalette(); } } });
|
|
this.showResults(results);
|
|
};
|
|
this.currentResolver = (e) => {
|
|
if (this.currentSelection === 0) return;
|
|
this.currentResults[this.currentSelection - 1].result.action(e);
|
|
};
|
|
this.input.value = '';
|
|
this.blockProviderChange = true;
|
|
this.onInput(this.input.value);
|
|
}
|
|
|
|
setSelectionToFirst() {
|
|
let sel = 1;
|
|
if (this.allowInputFieldSelection || this.currentResults.length === 0) {
|
|
sel = 0;
|
|
}
|
|
this.setSelection(sel)
|
|
}
|
|
|
|
setSelection(id) {
|
|
this.currentSelection = id;
|
|
for (let i = 0; i < this.currentResults.length; i++) {
|
|
let selected = this.currentSelection === i + 1;
|
|
this.currentResults[i].className = selected ? 'commandpaletteresult commandpaletteresultselected' : 'commandpaletteresult';
|
|
}
|
|
if (this.currentSelection === 0) {
|
|
this.scrollDiv.scrollTop = 0;
|
|
return;
|
|
}
|
|
let scrollHeight = this.scrollDiv.offsetHeight;
|
|
let scrollPos = this.scrollDiv.scrollTop;
|
|
let selectionPos = this.currentResults[this.currentSelection - 1].offsetTop;
|
|
let selectionHeight = this.currentResults[this.currentSelection - 1].offsetHeight;
|
|
|
|
if (selectionPos < scrollPos || selectionPos >= scrollPos + scrollHeight) {
|
|
//select the closest scrolling position showing the selection
|
|
let a = selectionPos;
|
|
let b = selectionPos - scrollHeight + selectionHeight;
|
|
a = Math.abs(a - scrollPos);
|
|
b = Math.abs(b - scrollPos);
|
|
if (a < b) {
|
|
this.scrollDiv.scrollTop = selectionPos;
|
|
} else {
|
|
this.scrollDiv.scrollTop = selectionPos - scrollHeight + selectionHeight;
|
|
}
|
|
}
|
|
}
|
|
|
|
getHistory() {
|
|
let history = $tw.wiki.getTiddlerData('$:/HistoryList');
|
|
if (history === undefined) {
|
|
history = [];
|
|
}
|
|
history = [...history.reverse().map(x => x.title), ...$tw.wiki.filterTiddlers('[list[$:/StoryList]]')];
|
|
return Array.from(new Set(history.filter(t => this.tiddlerOrShadowExists(t))));
|
|
}
|
|
|
|
tiddlerOrShadowExists(title) {
|
|
return $tw.wiki.tiddlerExists(title) || $tw.wiki.isShadowTiddler(title);
|
|
}
|
|
|
|
defaultProvider(terms) {
|
|
this.hint.innerText = 'Search tiddlers (⇧⏎ to create)';
|
|
let searches;
|
|
if (terms.startsWith('\\')) terms = terms.substr(1);
|
|
if (terms.length === 0) {
|
|
if (this.settings.showHistoryOnOpen) {
|
|
searches = this.getHistory().map(s => { return { name: s, hint: 'history' } });
|
|
} else {
|
|
searches = [];
|
|
}
|
|
}
|
|
else {
|
|
searches = this.searchSteps.reduce((a, c) => [...a, ...c(terms)], []);
|
|
searches = Array.from(new Set(searches));
|
|
}
|
|
this.showResults(searches);
|
|
}
|
|
|
|
searchStepBuilder(filter, caret, hint) {
|
|
return (terms) => {
|
|
let search = filter.substr(0, caret) + terms + filter.substr(caret);
|
|
let results = $tw.wiki.filterTiddlers(search).map(s => { return { name: s, hint: hint } });
|
|
return results;
|
|
}
|
|
}
|
|
|
|
tagListProvider(terms) {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Search tags';
|
|
let searches;
|
|
if (terms.length === 0) {
|
|
searches = $tw.wiki.filterTiddlers('[!is[system]tags[]][is[system]tags[]][all[shadows]tags[]]');
|
|
}
|
|
else {
|
|
searches = $tw.wiki.filterTiddlers('[all[]tags[]!is[system]search[' + terms + ']][all[]tags[]is[system]search[' + terms + ']][all[shadows]tags[]search[' + terms + ']]');
|
|
}
|
|
searches = searches.map(s => { return { name: s }; });
|
|
this.showResults(searches);
|
|
}
|
|
tagListResolver(e) {
|
|
if (this.currentSelection === 0) {
|
|
let input = this.input.value.substr(1);
|
|
let exist = $tw.wiki.filterTiddlers('[tag[' + input + ']]');
|
|
if (!exist)
|
|
return;
|
|
this.input.value = '@' + input;
|
|
return;
|
|
}
|
|
let result = this.currentResults[this.currentSelection - 1];
|
|
this.input.value = '@' + result.innerText;
|
|
this.onInput(this.input.value);
|
|
}
|
|
tagProvider(terms) {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Search tiddlers with @tag(s)';
|
|
let searches = [];
|
|
if (terms.length !== 0) {
|
|
let { tags, searchTerms, tagsFilter } = this.parseTags(this.input.value);
|
|
let taggedTiddlers = $tw.wiki.filterTiddlers(tagsFilter);
|
|
|
|
if (taggedTiddlers.length !== 0) {
|
|
if (tags.length === 1) {
|
|
let tag = tags[0];
|
|
let tagTiddlerExists = this.tiddlerOrShadowExists(tag);
|
|
if (tagTiddlerExists && searchTerms.some(s => tag.includes(s))) searches.push(tag);
|
|
}
|
|
searches = [...searches, ...taggedTiddlers];
|
|
}
|
|
}
|
|
searches = searches.map(s => { return { name: s } });
|
|
this.showResults(searches);
|
|
}
|
|
|
|
parseTags(input) {
|
|
let splits = input.split(' ').filter(s => s !== '');
|
|
let tags = [];
|
|
let searchTerms = [];
|
|
for (let i = 0; i < splits.length; i++) {
|
|
if (splits[i].startsWith('@')) {
|
|
tags.push(splits[i].substr(1));
|
|
continue;
|
|
}
|
|
searchTerms.push(splits[i]);
|
|
}
|
|
let tagsFilter = `[all[tiddlers+system+shadows]${tags.reduce((a, c) => { return a + 'tag[' + c + ']' }, '')}]`;
|
|
if (searchTerms.length !== 0) {
|
|
tagsFilter = tagsFilter.substr(0, tagsFilter.length - 1); //remove last ']'
|
|
tagsFilter += `search[${searchTerms.join(' ')}]]`;
|
|
}
|
|
return { tags, searchTerms, tagsFilter };
|
|
}
|
|
|
|
settingsProvider(terms) {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Select the setting you want to change';
|
|
let isNumerical = (terms) => terms.length !== 0 && terms.match(/\D/gm) === null;
|
|
let isBoolean = (terms) => terms.length !== 0 && terms.match(/(true\b)|(false\b)/gmi) !== null;
|
|
this.showResults([
|
|
{ name: 'Theme (currently ' + this.settings.theme.match(/[^\/]*$/) + ')', action: () => this.promptForThemeSetting() },
|
|
this.settingResultBuilder('Max results', 'maxResults', 'Choose the maximum number of results', isNumerical, 'Error: value must be a positive integer'),
|
|
this.settingResultBuilder('Show history on open', 'showHistoryOnOpen', 'Chose whether to show the history when you open the palette', isBoolean, 'Error: value must be \'true\' or \'false\''),
|
|
this.settingResultBuilder('Escape to go back', 'escapeGoesBack', 'Chose whether ESC should go back when possible', isBoolean, 'Error: value must be \'true\' or \'false\''),
|
|
this.settingResultBuilder('Use selection as search query', 'alwaysPassSelection', 'Chose your current selection is passed to the command palette', isBoolean, 'Error: value must be \'true\' or \'false\''),
|
|
this.settingResultBuilder('Never Basic', 'neverBasic', 'Chose whether to override basic prompts to show filter operation', isBoolean, 'Error: value must be \'true\' or \'false\''),
|
|
this.settingResultBuilder('Field preview max size', 'maxResultHintSize', 'Choose the maximum hint length for field preview', isNumerical, 'Error: value must be a positive integer'),
|
|
]);
|
|
}
|
|
|
|
settingResultBuilder(name, settingName, hint, validator, errorMsg) {
|
|
return { name: name + ' (currently ' + this.settings[settingName] + ')', action: () => this.promptForSetting(settingName, hint, validator, errorMsg) }
|
|
}
|
|
|
|
settingsResolver(e) {
|
|
if (this.currentSelection === 0) return;
|
|
this.goBack = () => {
|
|
this.input.value = '|';
|
|
this.blockProviderChange = false;
|
|
this.onInput(this.input.value);
|
|
}
|
|
this.currentResults[this.currentSelection - 1].result.action();
|
|
}
|
|
|
|
promptForThemeSetting() {
|
|
this.blockProviderChange = true;
|
|
this.allowInputFieldSelection = false;
|
|
this.currentProvider = (terms) => {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Choose a theme';
|
|
let defaultValue = this.defaultSettings['theme'];
|
|
let results = [{ name: 'Revert to default value: ' + defaultValue.match(/[^\/]*$/), action: () => { this.setSetting('theme', defaultValue); this.refreshThemes(); } }];
|
|
for (let theme of this.themes) {
|
|
let name = theme.fields.title;
|
|
let shortName = name.match(/[^\/]*$/);
|
|
let action = () => { this.setSetting('theme', name); this.refreshThemes(); }
|
|
results.push({ name: shortName, action: action });
|
|
}
|
|
this.showResults(results);
|
|
}
|
|
this.currentResolver = (e) => {
|
|
this.currentResults[this.currentSelection - 1].result.action(e);
|
|
}
|
|
this.input.value = '';
|
|
this.onInput(this.input.value);
|
|
}
|
|
|
|
//Validator = (terms) => bool
|
|
promptForSetting(settingName, hint, validator, errorMsg) {
|
|
this.blockProviderChange = true;
|
|
this.allowInputFieldSelection = true;
|
|
this.currentProvider = (terms) => {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = hint;
|
|
let defaultValue = this.defaultSettings[settingName];
|
|
let results = [{ name: 'Revert to default value: ' + defaultValue, action: () => this.setSetting(settingName, defaultValue) }];
|
|
if (!validator(terms)) {
|
|
results.push({ name: errorMsg });
|
|
}
|
|
this.showResults(results);
|
|
};
|
|
this.currentResolver = (e) => {
|
|
if (this.currentSelection === 0) {
|
|
let input = this.input.value;
|
|
if (validator(input)) {
|
|
this.setSetting(settingName, input);
|
|
this.goBack = undefined;
|
|
this.blockProviderChange = false;
|
|
this.allowInputFieldSelection = false;
|
|
this.promptCommand('|');
|
|
}
|
|
} else {
|
|
let action = this.currentResults[this.currentSelection - 1].result.action;
|
|
if (action) {
|
|
action();
|
|
this.goBack = undefined;
|
|
this.blockProviderChange = false;
|
|
this.allowInputFieldSelection = false;
|
|
this.promptCommand('|');
|
|
}
|
|
}
|
|
}
|
|
this.input.value = this.settings[settingName];
|
|
this.onInput(this.input.value);
|
|
}
|
|
|
|
showResults(results) {
|
|
for (let cur of this.currentResults) {
|
|
cur.remove();
|
|
}
|
|
this.currentResults = [];
|
|
let resultCount = 0;
|
|
for (let result of results) {
|
|
this.addResult(result, resultCount);
|
|
resultCount++;
|
|
if (resultCount >= this.settings.maxResults)
|
|
break;
|
|
}
|
|
}
|
|
|
|
tmMessageBuilder(message, params = {}) {
|
|
return (e) => {
|
|
let event = {
|
|
type: message,
|
|
paramObject: params,
|
|
event: e,
|
|
};
|
|
this.parentWidget.dispatchEvent(event);
|
|
};
|
|
}
|
|
actionProvider(terms) {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Search commands';
|
|
let results;
|
|
if (terms.length === 0) {
|
|
results = this.getCommandHistory();
|
|
}
|
|
else {
|
|
results = this.actions.filter(a => a.name.toLowerCase().includes(terms.toLowerCase()));
|
|
}
|
|
this.showResults(results);
|
|
}
|
|
|
|
helpProvider(terms) { //TODO: tiddlerify?
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Help';
|
|
let searches = [
|
|
{ name: '... Search', action: () => this.promptCommand('') },
|
|
{ name: '> Commands', action: () => this.promptCommand('>') },
|
|
{ name: '+ Create tiddler with title', action: () => this.promptCommand('+') },
|
|
{ name: '# Search tags', action: () => this.promptCommand('#') },
|
|
{ name: '@ List tiddlers with tag', action: () => this.promptCommand('@') },
|
|
{ name: '[ Filter operation', action: () => this.promptCommand('[') },
|
|
{ name: '| Command Palette Settings', action: () => this.promptCommand('|') },
|
|
{ name: '\\ Escape first character', action: () => this.promptCommand('\\') },
|
|
{ name: '? Help', action: () => this.promptCommand('?') },
|
|
];
|
|
this.showResults(searches);
|
|
}
|
|
|
|
filterProvider(terms, hint) {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = hint === undefined ? 'Filter operation' : hint;
|
|
terms = '[' + terms;
|
|
let fields = $tw.wiki.filterTiddlers('[fields[]]');
|
|
let results = $tw.wiki.filterTiddlers(terms).map(r => { return { name: r } });
|
|
let insertResult = (i, result) => results.splice(i + 1, 0, result);
|
|
for (let i = 0; i < results.length; i++) {
|
|
let initialResult = results[i];
|
|
let alreadyMatched = false;
|
|
let date = 'Invalid Date';
|
|
if (initialResult.name.length === 17) { //to be sure to only match tiddly dates (17 char long)
|
|
date = $tw.utils.parseDate(initialResult.name).toLocaleString();
|
|
}
|
|
if (date !== "Invalid Date") {
|
|
results[i].hint = date;
|
|
results[i].action = () => { };
|
|
alreadyMatched = true;
|
|
}
|
|
let isTag = $tw.wiki.getTiddlersWithTag(initialResult.name).length !== 0;
|
|
if (isTag) {
|
|
if (alreadyMatched) {
|
|
insertResult(i, { ...results[i] });
|
|
i += 1;
|
|
}
|
|
results[i].action = () => this.promptCommand('@' + initialResult.name);
|
|
results[i].hint = 'Tag'; //Todo more info?
|
|
alreadyMatched = true;
|
|
}
|
|
let isTiddler = this.tiddlerOrShadowExists(initialResult.name);
|
|
if (isTiddler) {
|
|
if (alreadyMatched) {
|
|
insertResult(i, { ...results[i] });
|
|
i += 1;
|
|
}
|
|
results[i].action = () => { this.navigateTo(initialResult.name); this.closePalette() }
|
|
results[i].hint = 'Tiddler';
|
|
alreadyMatched = true;
|
|
}
|
|
let isField = fields.includes(initialResult.name);
|
|
if (isField) {
|
|
if (alreadyMatched) {
|
|
insertResult(i, { ...results[i] });
|
|
i += 1;
|
|
}
|
|
let parsed;
|
|
try {
|
|
parsed = $tw.wiki.parseFilter(this.input.value)
|
|
} catch (e) { } //The error is already displayed to the user
|
|
let foundTitles = [];
|
|
for (let node of parsed || []) {
|
|
if (node.operators.length !== 2) continue;
|
|
if (node.operators[0].operator === 'title' && node.operators[1].operator === 'fields') {
|
|
foundTitles.push(node.operators[0].operand);
|
|
}
|
|
}
|
|
let hint = 'Field';
|
|
if (foundTitles.length === 1) {
|
|
hint = $tw.wiki.getTiddler(foundTitles[0]).fields[initialResult.name];
|
|
if (hint instanceof Date) {
|
|
hint = hint.toLocaleString();
|
|
}
|
|
hint = hint.toString().replace(/(\r\n|\n|\r)/gm, '');
|
|
let maxSize = this.settings.maxResultHintSize - 3;
|
|
if (hint.length > maxSize) {
|
|
hint = hint.substring(0, maxSize);
|
|
hint += '...';
|
|
}
|
|
}
|
|
results[i].hint = hint;
|
|
results[i].action = () => { };
|
|
alreadyMatched = true;
|
|
}
|
|
// let isContentType = terms.includes('content-type');
|
|
}
|
|
this.showResults(results);
|
|
}
|
|
|
|
filterResolver(e) {
|
|
if (this.currentSelection === 0) return;
|
|
this.currentResults[this.currentSelection - 1].result.action();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
helpResolver(e) {
|
|
if (this.currentSelection === 0) return;
|
|
this.currentResults[this.currentSelection - 1].result.action();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
createTiddlerProvider(terms) {
|
|
this.currentSelection = 0;
|
|
this.hint.innerText = 'Create new tiddler with title @tag(s)';
|
|
this.showResults([]);
|
|
}
|
|
|
|
createTiddlerResolver(e) {
|
|
let { tags, searchTerms } = this.parseTags(this.input.value.substr(1));
|
|
let title = searchTerms.join(' ');
|
|
tags = tags.join(' ');
|
|
this.tmMessageBuilder('tm-new-tiddler', { title: title, tags: tags })(e);
|
|
this.closePalette();
|
|
}
|
|
|
|
promptCommand(value, caret) {
|
|
this.blockProviderChange = false;
|
|
this.input.value = value;
|
|
this.input.focus();
|
|
if (caret !== undefined) {
|
|
this.input.setSelectionRange(caret, caret);
|
|
}
|
|
this.onInput(this.input.value);
|
|
}
|
|
|
|
promptCommandBasic(value, caret, hint) {
|
|
if (this.settings.neverBasic === 'true' || this.settings.neverBasic === true) { //TODO: validate settings to avoid unnecessary checks
|
|
this.promptCommand(value, caret);
|
|
return;
|
|
}
|
|
this.input.value = "";
|
|
this.blockProviderChange = true;
|
|
this.currentProvider = this.basicProviderBuilder(value, caret, hint);
|
|
this.onInput(this.input.value);
|
|
}
|
|
|
|
basicProviderBuilder(value, caret, hint) {
|
|
let start = value.substr(0, caret);
|
|
let end = value.substr(caret);
|
|
return (input) => {
|
|
let { resolver, provider, terms } = this.parseCommand(start + input + end);
|
|
let backgroundProvider = provider;
|
|
backgroundProvider(terms, hint);
|
|
this.currentResolver = resolver;
|
|
}
|
|
}
|
|
|
|
getCommandHistory() {
|
|
this.history = this.history.filter(h => this.actions.some(a => a.name === h)); //get rid of deleted command that are still in history;
|
|
let results = this.history.map(h => this.actions.find(a => a.name === h));
|
|
while (results.length <= this.settings.maxResults) {
|
|
let nextDefaultAction = this.actions.find(a => !results.includes(a));
|
|
if (nextDefaultAction === undefined)
|
|
break;
|
|
results.push(nextDefaultAction);
|
|
}
|
|
return results;
|
|
}
|
|
actionResolver(e) {
|
|
if (this.currentSelection === 0)
|
|
return;
|
|
let result = this.actions.find(a => a.name === this.currentResults[this.currentSelection - 1].innerText);
|
|
if (result.keepPalette) {
|
|
let curInput = this.input.value;
|
|
this.goBack = () => {
|
|
this.input.value = curInput;
|
|
this.blockProviderChange = false;
|
|
this.onInput(this.input.value);
|
|
};
|
|
}
|
|
this.updateCommandHistory(result);
|
|
result.action(e);
|
|
e.stopPropagation();
|
|
if (result.immediate) {
|
|
this.validateSelection(e);
|
|
return;
|
|
}
|
|
if (!result.keepPalette) {
|
|
this.closePalette();
|
|
}
|
|
}
|
|
|
|
getCurrentSelection() {
|
|
let selection = window.getSelection().toString();
|
|
if (selection !== '') return selection;
|
|
let activeElement = this.getActiveElement();
|
|
if (activeElement === undefined || activeElement.selectionStart === undefined) return '';
|
|
if (activeElement.selectionStart > activeElement.selectionEnd) {
|
|
return activeElement.value.substring(activeElement.selectionStart, activeElement.selectionEnd);
|
|
} else {
|
|
return activeElement.value.substring(activeElement.selectionEnd, activeElement.selectionStart);
|
|
}
|
|
}
|
|
getActiveElement(element = document.activeElement) {
|
|
const shadowRoot = element.shadowRoot
|
|
const contentDocument = element.contentDocument
|
|
|
|
if (shadowRoot && shadowRoot.activeElement) {
|
|
return this.getActiveElement(shadowRoot.activeElement)
|
|
}
|
|
|
|
if (contentDocument && contentDocument.activeElement) {
|
|
return this.getActiveElement(contentDocument.activeElement)
|
|
}
|
|
|
|
return element
|
|
}
|
|
focusAtCaretPosition(el, caretPos) {
|
|
if (el !== null) {
|
|
el.value = el.value;
|
|
// ^ this is used to not only get "focus", but
|
|
// to make sure we don't have it everything -selected-
|
|
// (it causes an issue in chrome, and having it doesn't hurt any other browser)
|
|
if (el.createTextRange) {
|
|
var range = el.createTextRange();
|
|
range.move('character', caretPos);
|
|
range.select();
|
|
return true;
|
|
}
|
|
else {
|
|
// (el.selectionStart === 0 added for Firefox bug)
|
|
if (el.selectionStart || el.selectionStart === 0) {
|
|
el.focus();
|
|
el.setSelectionRange(caretPos, caretPos);
|
|
return true;
|
|
}
|
|
|
|
else { // fail city, fortunately this never happens (as far as I've tested) :)
|
|
el.focus();
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
createElement(name, proprieties, styles) {
|
|
let el = this.document.createElement(name);
|
|
for (let [propriety, value] of Object.entries(proprieties || {})) {
|
|
el[propriety] = value;
|
|
}
|
|
for (let [style, value] of Object.entries(styles || {})) {
|
|
el.style[style] = value;
|
|
}
|
|
return el;
|
|
}
|
|
/*
|
|
Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering
|
|
*/
|
|
refresh() {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
exports.commandpalettewidget = CommandPaletteWidget;
|
|
|
|
})();
|