Skip to content

Commit 505171a

Browse files
committed
fix: refactor new note modal, add validation (#23)
1 parent d8dcbc2 commit 505171a

File tree

10 files changed

+196
-60
lines changed

10 files changed

+196
-60
lines changed

main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export default class SlurpPlugin extends Plugin {
112112
displayError = (err: Error) => new Notice(`Slurp Error! ${err.message}. If this is a bug, please report it from plugin settings.`, 0);
113113

114114
async slurp(url: string): Promise<void> {
115+
this.logger.debug("slurping", {url});
115116
try {
116117
const doc = new DOMParser().parseFromString(await fetchHtml(url), 'text/html');
117118

@@ -138,6 +139,7 @@ export default class SlurpPlugin extends Plugin {
138139
link: url
139140
});
140141
} catch (err) {
142+
this.logger.error("Unable to Slurp page", {url, err: (err as Error).message});
141143
this.displayError(err as Error);
142144
}
143145
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ProgressBarComponent } from "obsidian";
2+
3+
export class BouncingProgressBarComponent extends ProgressBarComponent {
4+
private timerId: number;
5+
6+
constructor(contentEl: HTMLElement) {
7+
super(contentEl);
8+
this.timerId = -1;
9+
this.setDisabled(true);
10+
this.setValue(0);
11+
}
12+
13+
private update() {
14+
const cur = this.getValue();
15+
this.setValue(cur + (cur == 100 ? 1 : -1 ));
16+
};
17+
18+
start() {
19+
this.setDisabled(false);
20+
this.timerId = window.setInterval(this.update, 10);
21+
};
22+
23+
stop() {
24+
if (this.timerId > 0) window.clearInterval(this.timerId);
25+
}
26+
}
27+

src/components/frontmatter-prop-settings.svelte

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -389,22 +389,11 @@
389389
opacity: 50%;
390390
}
391391
392-
.mod-prop .validation-error,
393-
.mod-prop .validation-error {
394-
color: var(--text-error);
395-
}
396-
397392
.mod-prop div.validation-error {
398-
font-size: small;
399393
text-align: center;
400394
margin: 0.75em 0;
401395
}
402396
403-
.mod-prop div.validation-error.hidden {
404-
color: transparent;
405-
background-color: transparent;
406-
}
407-
408397
/* new property button */
409398
#new-property {
410399
display: flex;

src/components/validated-text.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { TextComponent } from "obsidian";
2+
3+
type TValidator = (input: string) => string | null;
4+
type TOnValidate = (input: string, err: string[]) => void;
5+
6+
export class ValidatedTextComponent extends TextComponent {
7+
private readonly _validators = new Set<TValidator>();
8+
private readonly _errList: HTMLUListElement;
9+
private _onValidateCb: TOnValidate;
10+
private _earlyReturn: boolean;
11+
private _minLen: number;
12+
13+
14+
constructor(containerEl: HTMLElement) {
15+
super(containerEl);
16+
this._errList = containerEl.createEl("ul");
17+
this._errList.addClasses(["validation-error"]);
18+
this._earlyReturn = true;
19+
this._minLen = 3;
20+
this._onValidateCb = () => { };
21+
this.onChange(() => this.validate());
22+
containerEl.appendChild(this._errList);
23+
}
24+
25+
setValidationErrorClass(className: string) {
26+
this._errList.addClass(className);
27+
return this;
28+
}
29+
30+
setStopOnFirstError(val: boolean = true) {
31+
this._earlyReturn = val;
32+
return this;
33+
}
34+
35+
setMinimumLength(val: number = 3) {
36+
this._minLen = val;
37+
return this;
38+
}
39+
40+
addValidator(fn: TValidator) {
41+
this._validators.add(fn);
42+
return this;
43+
}
44+
45+
onValidate(fn: TOnValidate) {
46+
this._onValidateCb = fn;
47+
return this;
48+
}
49+
50+
validate() {
51+
const input = this.getValue() || "";
52+
const errs: string[] = [];
53+
54+
if (input.length >= this._minLen) {
55+
for (const fn of this._validators) {
56+
const err = fn(input);
57+
if (err !== null)
58+
errs.push(err);
59+
if (errs.length > 0 && this._earlyReturn)
60+
break;
61+
}
62+
}
63+
64+
this._errList.replaceChildren(...errs);
65+
this._onValidateCb(input, errs);
66+
}
67+
}

src/const.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import { createFrontMatterPropSettings, createFrontMatterProps } from "src/frontmatter";
22
import type { IFrontMatterPropDefault, ISettings, TFrontMatterPropDefaults } from "./types";
33

4+
export const KNOWN_BROKEN_DOMAINS = new Map<string, string|null>([
5+
["fastcompany.com", "Fast Company prevents programs like Slurp from accessing their articles."],
6+
["sparksoftcorp.com", null],
7+
]);
8+
49
export const FRONT_MATTER_ITEM_DEFAULTS: TFrontMatterPropDefaults = new Map<string, IFrontMatterPropDefault>([
510
{
611
id: "link", defaultIdx: 0, defaultKey: "link",

src/lib/logger.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ export class Logger {
5454
this.vault = plugin.app.vault;
5555
if (plugin.settings && Object.keys(plugin.settings).contains("logs")) {
5656
this.settings = plugin.settings.logs;
57-
} else
57+
} else {
5858
this.settings = { debug: true, logPath: DEFAULT_SETTINGS.logs.logPath };
59-
59+
}
60+
6061
if (this.settings.debug)
6162
plugin.registerInterval(window.setInterval(
6263
() => this.flush(), 500));
@@ -89,11 +90,11 @@ export class Logger {
8990
const msg = this.buffer[idx];
9091
const optJson = [];
9192
for (const i of msg.optionalParams || []) {
92-
try{
93-
optJson.push(JSON.stringify(serialize(i), undefined, 2));
94-
} catch (err) {
95-
optJson.push(`Unable to stringify: ${i}`);
96-
}
93+
try {
94+
optJson.push(JSON.stringify(serialize(i), undefined, 2));
95+
} catch (err) {
96+
optJson.push(`Unable to stringify: ${i}`);
97+
}
9798
}
9899
content += `##### ${msg.timestamp} | ${msg.level.padStart(5).toUpperCase()} | ${msg.msg}\n` +
99100
`- Caller: \`${msg.caller}\`\n\n`;

src/lib/util.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export const serialize = (val: unknown) => {
5858
};
5959

6060
// murmurhash3 is simple, fast, and doesn't have external dependencies.
61-
export function murmurhash3_32(key: string, seed: number = 0) {
61+
export const murmurhash3_32 = (key: string, seed: number = 0) => {
6262
var remainder, bytes, h1, h1b, c1, c2, k1, i;
6363

6464
// Initialize the variables
@@ -121,4 +121,15 @@ export function murmurhash3_32(key: string, seed: number = 0) {
121121

122122
// Return the resulting hash as an unsigned 32-bit integer
123123
return h1 >>> 0;
124+
}
125+
126+
export const extractDomain = (url: string) => {
127+
const match = url.match(/(?::\/\/)?(.[^/:]+)/i);
128+
if (match != null && match.length > 1 && typeof match[1] === 'string' && match[1].length > 0) {
129+
const parts = match[1].split('.');
130+
if (parts.length > 1 && parts[parts.length - 1].length > 1) {
131+
return parts[parts.length - 2] + '.' + parts[parts.length - 1];
132+
}
133+
}
134+
return null;
124135
}

src/modals/new-note.ts

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,70 @@
11
import type SlurpPlugin from "main";
2-
import { Modal, App, TextComponent, ProgressBarComponent, Setting } from "obsidian";
2+
import { App, ButtonComponent, Modal, Setting } from "obsidian";
3+
import { BouncingProgressBarComponent } from "../components/bouncing-progress-bar";
4+
import { ValidatedTextComponent } from "../components/validated-text";
5+
import { KNOWN_BROKEN_DOMAINS } from "../const";
6+
import { extractDomain } from "../lib/util";
37

48
export class SlurpNewNoteModal extends Modal {
5-
plugin: SlurpPlugin;
6-
url: string;
9+
private readonly plugin: SlurpPlugin;
10+
private readonly WARNING_CLS = "validation";
11+
private readonly URL_FORMAT_ERR = "Invalid URL format.";
712

813
constructor(app: App, plugin: SlurpPlugin) {
914
super(app);
1015
this.plugin = plugin;
11-
this.url = "";
1216
}
1317

14-
onOpen() {
15-
const { contentEl } = this;
18+
private validateKnownBrokenDomains(url: string) {
19+
const domain = extractDomain(url) || "";
20+
const defaultReason = "This site is known to be incompatible with Slurp.";
1621

17-
contentEl.createEl("h3", { text: "What would you like to slurp today?" })
22+
return KNOWN_BROKEN_DOMAINS.has(domain)
23+
? KNOWN_BROKEN_DOMAINS.get(domain) || defaultReason
24+
: null;
25+
}
1826

19-
const urlField = new TextComponent(contentEl)
20-
.setPlaceholder("URL")
21-
.onChange((val) => this.url = val);
22-
urlField.inputEl.setCssProps({ "width": "100%" });
27+
private validateUrlFormat(url: string) {
28+
return extractDomain(url) === null ? this.URL_FORMAT_ERR : null;
29+
}
2330

24-
const progressBar = new ProgressBarComponent(contentEl)
25-
progressBar.disabled = true;
26-
progressBar.setValue(0);
31+
onOpen() {
32+
const { contentEl } = this;
33+
let slurpBtn: ButtonComponent;
2734

28-
const doSlurp = async () => {
29-
urlField.setDisabled(true);
30-
progressBar.setDisabled(false);
31-
let progressIncrement = 1;
35+
new Setting(contentEl)
36+
.setName("What would you like to slurp today?")
37+
.setHeading();
3238

33-
const t = setInterval(() => {
34-
const cur = progressBar.getValue();
35-
if (cur == 100) progressIncrement *= -1;
36-
progressBar.setValue(cur + progressIncrement);
37-
}, 10)
39+
const urlField = new ValidatedTextComponent(contentEl)
40+
.setPlaceholder("https://www.somesite.com/...")
41+
.setMinimumLength(5)
42+
.addValidator((url: string) => this.validateUrlFormat(url))
43+
.addValidator((url: string) => this.validateKnownBrokenDomains(url))
44+
.onValidate((url: string, errs: string[]) => {
45+
slurpBtn.setDisabled(errs.length > 0 || urlField.getValue().length < 5);
46+
});
47+
48+
urlField.inputEl.setCssProps({ "width": "100%" });
3849

39-
try {
40-
this.plugin.slurp(this.url);
41-
} catch (err) { this.plugin.displayError(err as Error); }
50+
const progressBar = new BouncingProgressBarComponent(contentEl);
4251

43-
clearInterval(t);
52+
const doSlurp = () => {
53+
progressBar.start();
54+
this.plugin.slurp(urlField.getValue());
55+
progressBar.stop()
4456
this.close();
45-
};
57+
}
4658

4759
new Setting(contentEl)
48-
.addButton((btn) => btn
49-
.setButtonText("Slurp")
50-
.setCta()
51-
.onClick(doSlurp))
60+
.addButton((btn) => {
61+
btn.setButtonText("Slurp")
62+
.setCta()
63+
.setDisabled(true)
64+
.onClick(doSlurp);
65+
slurpBtn = btn;
66+
return slurpBtn;
67+
});
5268

5369
contentEl.addEventListener("keypress", (k) => (k.key === "Enter") && doSlurp());
5470
}

styles.css

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,19 @@ available in the app when your plugin is enabled.
66
If your plugin does not need CSS, delete this file.
77
88
*/
9+
10+
.validation-error {
11+
color: var(--text-error);
12+
font-size: small;
13+
}
14+
15+
.validation-error.hidden {
16+
color: transparent;
17+
background-color: transparent;
18+
}
19+
20+
ul.validation-error {
21+
list-style: none;
22+
padding: 0;
23+
margin: 0.5em 0.1em;
24+
}

test-resources/vault/.obsidian/workspace.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@
44
"type": "split",
55
"children": [
66
{
7-
"id": "8370dcfe4505b70d",
7+
"id": "632f42904151a60b",
88
"type": "tabs",
99
"children": [
1010
{
11-
"id": "50580fd4fc0dea57",
11+
"id": "718d1872bf7ff10a",
1212
"type": "leaf",
1313
"state": {
1414
"type": "markdown",
1515
"state": {
16-
"file": "Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
16+
"file": "_slurplogs/slurp-2024-05-11.md",
1717
"mode": "source",
1818
"backlinks": false,
1919
"source": false
@@ -86,7 +86,7 @@
8686
"state": {
8787
"type": "backlink",
8888
"state": {
89-
"file": "Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
89+
"file": "_slurplogs/slurp-2024-05-11.md",
9090
"collapseAll": false,
9191
"extraContext": false,
9292
"sortOrder": "alphabetical",
@@ -103,7 +103,7 @@
103103
"state": {
104104
"type": "outgoing-link",
105105
"state": {
106-
"file": "Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
106+
"file": "_slurplogs/slurp-2024-05-11.md",
107107
"linksCollapsed": false,
108108
"unlinkedCollapsed": true
109109
}
@@ -126,7 +126,7 @@
126126
"state": {
127127
"type": "outline",
128128
"state": {
129-
"file": "Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md"
129+
"file": "_slurplogs/slurp-2024-05-11.md"
130130
}
131131
}
132132
}
@@ -147,20 +147,22 @@
147147
"command-palette:Open command palette": false
148148
}
149149
},
150-
"active": "5bc75bc3168bc2c6",
150+
"active": "718d1872bf7ff10a",
151151
"lastOpenFiles": [
152+
"Slurped Pages/InvertOrNot - Smart Dark-Mode Image Inversion.md",
153+
"_slurplogs/slurp-2024-05-11.md",
152154
"Slurped Pages/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
155+
"Slurped Pages/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (1).md",
156+
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
153157
"Inbox/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
154158
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (7).md",
155-
"_slurplogs/slurp-2024-05-11.md",
156159
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (6).md",
157160
"_slurplogs/slurp-2024-05-09.md",
158161
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (5).md",
159162
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (4).md",
160163
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (3).md",
161164
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (2).md",
162165
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix (1).md",
163-
"Inbox/Slurped/Apple releases iOS 11.2 with Apple Pay Cash, fast wireless charging, and iPhone crash fix.md",
164166
"_slurplogs/slurp-2024-05-06.md",
165167
"Inbox/Slurped/obsidian-file-suggestion-component.md",
166168
"_slurplogs",

0 commit comments

Comments
 (0)