Skip to content

Commit c7ed951

Browse files
committed
4.1.5
- Improved icon selection experience
1 parent cc6ab7e commit c7ed951

File tree

9 files changed

+346
-20
lines changed

9 files changed

+346
-20
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ Adds a "copy content" button to every admonition block.
295295

296296
# Version History
297297

298+
## 4.1.5
299+
300+
- Improved Admonition Icon selection experience
301+
298302
## 4.1.4
299303

300304
- Trimmed whitespace from content when copying to clipboard.

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"id": "obsidian-admonition",
33
"name": "Admonition",
4-
"version": "4.1.4",
4+
"version": "4.1.5",
55
"minAppVersion": "0.11.0",
66
"description": "Admonition block-styled content for Obsidian.md",
77
"author": "Jeremy Valentine",

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "obsidian-admonition",
3-
"version": "4.1.4",
3+
"version": "4.1.5",
44
"description": "Admonition block-styled content for Obsidian.md",
55
"main": "main.js",
66
"scripts": {
@@ -11,9 +11,10 @@
1111
"author": "Jeremy Valentine",
1212
"license": "MIT",
1313
"devDependencies": {
14-
"@fortawesome/fontawesome-svg-core": "^1.2.35",
15-
"@fortawesome/free-solid-svg-icons": "^5.15.1",
14+
"@fortawesome/fontawesome-svg-core": "^1.2.32",
1615
"@fortawesome/free-regular-svg-icons": "^5.15.3",
16+
"@fortawesome/free-solid-svg-icons": "^5.15.1",
17+
"@popperjs/core": "^2.9.2",
1718
"@rollup/plugin-commonjs": "^15.1.0",
1819
"@rollup/plugin-node-resolve": "^9.0.0",
1920
"@rollup/plugin-typescript": "^6.0.0",
@@ -25,6 +26,5 @@
2526
"rollup-plugin-css-only": "^3.1.0",
2627
"tslib": "^2.0.3",
2728
"typescript": "^4.0.3"
28-
},
29-
"dependencies": {}
30-
}
29+
}
30+
}

src/icons.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { fas } from "@fortawesome/free-solid-svg-icons";
21
import { faCopy } from "@fortawesome/free-regular-svg-icons";
2+
import { fas } from "@fortawesome/free-solid-svg-icons";
33
import {
4+
IconDefinition,
5+
IconName,
46
findIconDefinition,
57
icon,
68
library
79
} from "@fortawesome/fontawesome-svg-core";
810

911
library.add(fas, faCopy);
1012

11-
export { icon, findIconDefinition };
13+
export const iconNames = Object.values(fas).map(
14+
(i: IconDefinition) => i.iconName
15+
);
16+
17+
export { icon, findIconDefinition, IconName };

src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Object.fromEntries =
5050

5151
import "./main.css";
5252
import AdmonitionSetting from "./settings";
53-
import { findIconDefinition, icon } from "@fortawesome/fontawesome-svg-core";
53+
import { findIconDefinition, icon } from "./icons";
5454

5555
const DEFAULT_APP_SETTINGS: ISettingsData = {
5656
userAdmonitions: {},

src/modals.ts

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
import {
2+
App,
3+
FuzzyMatch,
4+
FuzzySuggestModal,
5+
Scope,
6+
SuggestModal,
7+
TextComponent
8+
} from "obsidian";
9+
import { createPopper, Instance as PopperInstance } from "@popperjs/core";
10+
import { findIconDefinition, icon, IconName } from "./icons";
11+
12+
class Suggester<T> {
13+
owner: SuggestModal<T>;
14+
items: T[];
15+
suggestions: HTMLDivElement[];
16+
selectedItem: number;
17+
containerEl: HTMLElement;
18+
constructor(
19+
owner: SuggestModal<T>,
20+
containerEl: HTMLElement,
21+
scope: Scope
22+
) {
23+
this.containerEl = containerEl;
24+
this.owner = owner;
25+
containerEl.on(
26+
"click",
27+
".suggestion-item",
28+
this.onSuggestionClick.bind(this)
29+
);
30+
containerEl.on(
31+
"mousemove",
32+
".suggestion-item",
33+
this.onSuggestionMouseover.bind(this)
34+
);
35+
36+
scope.register([], "ArrowUp", () => {
37+
this.setSelectedItem(this.selectedItem - 1, true);
38+
return false;
39+
});
40+
41+
scope.register([], "ArrowDown", () => {
42+
this.setSelectedItem(this.selectedItem + 1, true);
43+
return false;
44+
});
45+
46+
scope.register([], "Enter", (evt) => {
47+
this.useSelectedItem(evt);
48+
return false;
49+
});
50+
51+
scope.register([], "Tab", (evt) => {
52+
this.chooseSuggestion(evt);
53+
return false;
54+
});
55+
}
56+
chooseSuggestion(evt: KeyboardEvent) {
57+
if (!this.items || !this.items.length) return;
58+
const currentValue = this.items[this.selectedItem];
59+
if (currentValue) {
60+
this.owner.onChooseSuggestion(currentValue, evt);
61+
}
62+
}
63+
onSuggestionClick(event: MouseEvent, el: HTMLDivElement): void {
64+
event.preventDefault();
65+
if (!this.suggestions || !this.suggestions.length) return;
66+
67+
const item = this.suggestions.indexOf(el);
68+
this.setSelectedItem(item, false);
69+
this.useSelectedItem(event);
70+
}
71+
72+
onSuggestionMouseover(event: MouseEvent, el: HTMLDivElement): void {
73+
if (!this.suggestions || !this.suggestions.length) return;
74+
const item = this.suggestions.indexOf(el);
75+
this.setSelectedItem(item, false);
76+
}
77+
empty() {
78+
this.containerEl.empty();
79+
}
80+
setSuggestions(items: T[]) {
81+
this.containerEl.empty();
82+
const els: HTMLDivElement[] = [];
83+
84+
items.forEach((item) => {
85+
const suggestionEl = this.containerEl.createDiv("suggestion-item");
86+
this.owner.renderSuggestion(item, suggestionEl);
87+
els.push(suggestionEl);
88+
});
89+
this.items = items;
90+
this.suggestions = els;
91+
this.setSelectedItem(0, false);
92+
}
93+
useSelectedItem(event: MouseEvent | KeyboardEvent) {
94+
if (!this.items || !this.items.length) return;
95+
const currentValue = this.items[this.selectedItem];
96+
if (currentValue) {
97+
this.owner.selectSuggestion(currentValue, event);
98+
}
99+
}
100+
wrap(value: number, size: number): number {
101+
return ((value % size) + size) % size;
102+
}
103+
setSelectedItem(index: number, scroll: boolean) {
104+
const nIndex = this.wrap(index, this.suggestions.length);
105+
const prev = this.suggestions[this.selectedItem];
106+
const next = this.suggestions[nIndex];
107+
108+
if (prev) prev.removeClass("is-selected");
109+
if (next) next.addClass("is-selected");
110+
111+
this.selectedItem = nIndex;
112+
113+
if (scroll) {
114+
next.scrollIntoView(false);
115+
}
116+
}
117+
}
118+
119+
export abstract class SuggestionModal<T> extends FuzzySuggestModal<T> {
120+
items: T[] = [];
121+
suggestions: HTMLDivElement[];
122+
popper: PopperInstance;
123+
scope: Scope = new Scope();
124+
suggester: Suggester<FuzzyMatch<T>>;
125+
suggestEl: HTMLDivElement;
126+
promptEl: HTMLDivElement;
127+
emptyStateText: string = "No match found";
128+
limit: number = 100;
129+
constructor(app: App, inputEl: HTMLInputElement, items: T[]) {
130+
super(app);
131+
this.inputEl = inputEl;
132+
this.items = items;
133+
134+
this.suggestEl = createDiv("suggestion-container");
135+
136+
this.contentEl = this.suggestEl.createDiv("suggestion");
137+
138+
this.suggester = new Suggester(this, this.contentEl, this.scope);
139+
140+
this.scope.register([], "Escape", this.close.bind(this));
141+
142+
this.inputEl.addEventListener("input", this.onInputChanged.bind(this));
143+
this.inputEl.addEventListener("focus", this.onInputChanged.bind(this));
144+
this.inputEl.addEventListener("blur", this.close.bind(this));
145+
this.suggestEl.on(
146+
"mousedown",
147+
".suggestion-container",
148+
(event: MouseEvent) => {
149+
event.preventDefault();
150+
}
151+
);
152+
}
153+
empty() {
154+
this.suggester.empty();
155+
}
156+
onInputChanged(): void {
157+
const inputStr = this.modifyInput(this.inputEl.value);
158+
const suggestions = this.getSuggestions(inputStr);
159+
if (suggestions.length > 0) {
160+
this.suggester.setSuggestions(suggestions.slice(0, this.limit));
161+
} else {
162+
this.onNoSuggestion();
163+
}
164+
this.open();
165+
}
166+
167+
modifyInput(input: string): string {
168+
return input;
169+
}
170+
onNoSuggestion() {
171+
this.empty();
172+
this.renderSuggestion(
173+
null,
174+
this.contentEl.createDiv("suggestion-item")
175+
);
176+
}
177+
open(): void {
178+
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
179+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
180+
(<any>this.app).keymap.pushScope(this.scope);
181+
182+
document.body.appendChild(this.suggestEl);
183+
this.popper = createPopper(this.inputEl, this.suggestEl, {
184+
placement: "bottom-start",
185+
modifiers: [
186+
{
187+
name: "offset",
188+
options: {
189+
offset: [0, 10]
190+
}
191+
},
192+
{
193+
name: "flip",
194+
options: {
195+
fallbackPlacements: ["top"]
196+
}
197+
}
198+
]
199+
});
200+
}
201+
202+
close(): void {
203+
// TODO: Figure out a better way to do this. Idea from Periodic Notes plugin
204+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
205+
(<any>this.app).keymap.popScope(this.scope);
206+
207+
this.suggester.setSuggestions([]);
208+
if (this.popper) {
209+
this.popper.destroy();
210+
}
211+
212+
this.suggestEl.detach();
213+
}
214+
createPrompt(prompts: HTMLSpanElement[]) {
215+
if (!this.promptEl)
216+
this.promptEl = this.suggestEl.createDiv("prompt-instructions");
217+
let prompt = this.promptEl.createDiv("prompt-instruction");
218+
for (let p of prompts) {
219+
prompt.appendChild(p);
220+
}
221+
}
222+
abstract onChooseItem(item: T, evt: MouseEvent | KeyboardEvent): void;
223+
abstract getItemText(arg: T): string;
224+
abstract getItems(): T[];
225+
}
226+
227+
export class IconSuggestionModal extends SuggestionModal<IconName> {
228+
icons: IconName[];
229+
icon: IconName;
230+
text: TextComponent;
231+
constructor(app: App, input: TextComponent, items: IconName[]) {
232+
super(app, input.inputEl, items);
233+
this.icons = [...items];
234+
this.text = input;
235+
236+
this.createPrompts();
237+
238+
this.inputEl.addEventListener("input", this.getItem.bind(this));
239+
}
240+
createPrompts() {}
241+
getItem() {
242+
const v = this.inputEl.value,
243+
icon = this.icons.find((iconName) => iconName === v.trim());
244+
if (icon == this.icon) return;
245+
this.icon = icon;
246+
if (this.icons) this.onInputChanged();
247+
}
248+
getItemText(item: IconName) {
249+
return item;
250+
}
251+
onChooseItem(item: IconName) {
252+
this.text.setValue(item);
253+
this.icon = item;
254+
}
255+
selectSuggestion({ item }: FuzzyMatch<IconName>) {
256+
this.text.setValue(item);
257+
this.onClose();
258+
259+
this.close();
260+
}
261+
renderSuggestion(result: FuzzyMatch<IconName>, el: HTMLElement) {
262+
let { item, match: matches } = result || {};
263+
let content = el.createDiv({
264+
cls: "suggestion-content icon"
265+
});
266+
if (!item) {
267+
content.setText(this.emptyStateText);
268+
content.parentElement.addClass("is-selected");
269+
return;
270+
}
271+
272+
const matchElements = matches.matches.map((m) => {
273+
return createSpan("suggestion-highlight");
274+
});
275+
for (let i = 0; i < item.length; i++) {
276+
let match = matches.matches.find((m) => m[0] === i);
277+
if (match) {
278+
let element = matchElements[matches.matches.indexOf(match)];
279+
content.appendChild(element);
280+
element.appendText(item.substring(match[0], match[1]));
281+
282+
i += match[1] - match[0] - 1;
283+
continue;
284+
}
285+
286+
content.appendText(item[i]);
287+
}
288+
289+
const iconDiv = createDiv({
290+
cls: "suggestion-flair"
291+
});
292+
iconDiv.appendChild(
293+
icon(
294+
findIconDefinition({
295+
iconName: item,
296+
prefix: "fas"
297+
})
298+
).node[0]
299+
);
300+
301+
content.prepend(iconDiv);
302+
}
303+
getItems() {
304+
return this.icons;
305+
}
306+
}

0 commit comments

Comments
 (0)