diff --git a/plugin.config.json b/plugin.config.json
index c29dd3a..5e56d5a 100644
--- a/plugin.config.json
+++ b/plugin.config.json
@@ -2,6 +2,8 @@
"extraScripts": [
"cmPlugin.ts",
"panel.ts",
+ "dialog.ts",
+ "settings.ts",
"utils/utils.ts",
"utils/joplinUtils.ts",
"utils/dialogs.ts",
diff --git a/src/dialog.ts b/src/dialog.ts
new file mode 100644
index 0000000..bb95f3e
--- /dev/null
+++ b/src/dialog.ts
@@ -0,0 +1,35 @@
+export function getDialogHTML() {
+ return `
+
+
Search and replace
+
+
+
`;
+}
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 42e7bd5..1547765 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,17 +1,24 @@
import joplin from "api";
import { ContentScriptType, MenuItemLocation } from "api/types";
+import { getDialogHTML } from "./dialog";
import { getPanelHTML } from "./panel";
+import { getSettings, registerAllSettings } from "./settings";
import { Dialog } from "./utils/dialogs";
import { getEditorState } from "./utils/joplinUtils";
import { Panel } from "./utils/panels";
import { prepareRegex, sanitizeHTML } from "./utils/utils";
let dialogAlert: Dialog;
+let dialogSAR: Dialog;
+let dialogSARLastFormData;
let panel: Panel;
let selectedText: string = "";
joplin.plugins.register({
onStart: async function () {
+ // Register this plugin's settings, so the user can access them:
+ await registerAllSettings();
+
/*
Create 'Search and Replace' panel
*/
@@ -56,11 +63,39 @@ joplin.plugins.register({
}
});
+ /*
+ Create "Search & replace" dialog
+ */
+ dialogSAR = new Dialog();
+ await dialogSAR.create();
+ await dialogSAR.addScript("./webview_dialog.css");
+ await dialogSAR.setButtons([
+ {id: "replaceNext", title: "Replace next"},
+ {id: "replaceAll", title: "Replace all"},
+ {id: "cancel"}
+ ]);
+ dialogSAR.addPositiveIds("replaceNext", "replaceAll");
+ dialogSAR.template = getDialogHTML();
+ dialogSAR.setDefaultFormData({
+ "pattern-txt": "",
+ "replacement-txt": "",
+ "wrap-chk": "off",
+ "matchcase-chk": "off",
+ "matchwholeword-chk": "off",
+ "preservecase-chk": "off",
+ "matchmethod": "literal"
+ });
+ dialogSARLastFormData = {
+ ...dialogSAR.getDefaultFormData(),
+ "matchcase-chk": "on"
+ };
+
/*
Create alert dialog
*/
dialogAlert = new Dialog();
await dialogAlert.create();
+ await dialogAlert.addScript("./webview_dialog.css");
await dialogAlert.setButtons([{ id: "ok" }]);
dialogAlert.template = `
@@ -81,18 +116,77 @@ joplin.plugins.register({
// Save 'selectedText' for later:
selectedText = await joplin.commands.execute("selectedText");
- // If panel not visible:
- let isVisible = await panel.visible();
- if (!isVisible) {
- panel.setHtml(await getPanelHTML());
+ // Get the user preference (panel or dialog)
+ let guiPreference = (await getSettings()).SARGUIPreference;
- // Open panel:
- await panel.show();
- }
+ /*
+ Open a panel:
+ */
+ if (guiPreference == "panel") {
+ // If panel not visible:
+ let isVisible = await panel.visible();
+ if (!isVisible) {
+ panel.setHtml(await getPanelHTML());
+
+ // Open panel:
+ await panel.show();
+ }
+
+ // Set the text in the panel to the selectedText:
+ if (selectedText.length > 0) {
+ panel.postMessage({ name: "SARPanel.setText", value: selectedText });
+ }
+
+ /*
+ Open a dialog:
+ */
+ } else if (guiPreference == "dialog") {
+ // "Recall" last form data:
+ dialogSAR.useTemplate({
+ "pattern": selectedText.length > 0 ? sanitizeHTML(selectedText) : sanitizeHTML(dialogSARLastFormData["pattern-txt"]),
+ "replacement": sanitizeHTML(dialogSARLastFormData["replacement-txt"]),
+ "wrap": dialogSARLastFormData["wrap-chk"] == "on" ? "checked" : "",
+ "matchcase": dialogSARLastFormData["matchcase-chk"] == "on" ? "checked" : "",
+ "matchwholeword": dialogSARLastFormData["matchwholeword-chk"] == "on" ? "checked" : "",
+ "preservecase": dialogSARLastFormData["preservecase-chk"] == "on" ? "checked" : "",
+ "matchmethod-literal": dialogSARLastFormData["matchmethod"] == "literal" ? "checked" : "",
+ "matchmethod-wildcards": dialogSARLastFormData["matchmethod"] == "wildcards" ? "checked" : "",
+ "matchmethod-regex": dialogSARLastFormData["matchmethod"] == "regex" ? "checked" : ""
+ });
- // Set the text in the panel to the selectedText:
- if (selectedText.length > 0) {
- panel.postMessage({ name: "SARPanel.setText", value: selectedText });
+ // Open the dialog:
+ await dialogSAR.open();
+ let result = dialogSAR.getPreparedDialogResult();
+
+ // "Memorize" form data:
+ dialogSARLastFormData = result.formData;
+
+ if (result.confirm) {
+ let form = {
+ searchPattern: result.formData["pattern-txt"],
+ replacement: result.formData["replacement-txt"],
+ options: {
+ wrapAround: result.formData["wrap-chk"] == "on",
+ matchCase: result.formData["matchcase-chk"] == "on",
+ matchWholeWord: result.formData["matchwholeword-chk"] == "on",
+ matchMethod: result.formData["matchmethod"],
+ preserveCase: result.formData["preservecase-chk"] == "on"
+ },
+ }
+
+ switch (result.id) {
+ case "replaceNext":
+ return await joplin.commands.execute('editor.execCommand', {
+ name: "SARPlugin.replace",
+ args: [form],
+ });
+ case "replaceAll":
+ return await joplin.commands.execute('editor.execCommand', {
+ name: "SARPlugin.replaceAll",
+ args: [form],
+ });
+ }
+ }
}
},
});
diff --git a/src/panel.ts b/src/panel.ts
index b71dbbc..c72494a 100644
--- a/src/panel.ts
+++ b/src/panel.ts
@@ -59,9 +59,9 @@ export async function getPanelHTML() {
-
-
-
+
+
+
|
diff --git a/src/settings.ts b/src/settings.ts
new file mode 100644
index 0000000..5d5404e
--- /dev/null
+++ b/src/settings.ts
@@ -0,0 +1,40 @@
+import joplin from "api";
+import { SettingItemType } from "api/types";
+
+/**
+ * Returns all settings the user can/has set.
+*/
+export async function getSettings() {
+ return {
+ "SARGUIPreference": await joplin.settings.value('SARGUIPreference')
+ }
+}
+
+/**
+ * Register this plugin"s settings to Joplin.
+ */
+export async function registerAllSettings() {
+ const section = "SAROptions";
+
+ await joplin.settings.registerSection(section, {
+ label: "Search & Replace",
+ description: "Search & Replace",
+ iconName: "fas fa-search"
+ });
+
+ await joplin.settings.registerSettings({
+ ["SARGUIPreference"]: {
+ public: true,
+ section: section,
+ type: SettingItemType.String,
+ isEnum: true,
+ value: "panel",
+ label: "GUI Preference",
+ description: "If you don't like the panel, you can switch to a popup dialog instead.",
+ options: {
+ "panel": "Open a panel (default, recommended)",
+ "dialog": "Open a dialog (like in previous versions)"
+ },
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/utils/dialogs.ts b/src/utils/dialogs.ts
index e08fed1..056be29 100644
--- a/src/utils/dialogs.ts
+++ b/src/utils/dialogs.ts
@@ -20,7 +20,16 @@ export class Dialog {
public async create() {
this.viewHandle = await joplin.views.dialogs.create(this.id);
- await joplin.views.dialogs.addScript(this.viewHandle, './webview_dialog.css');
+ // await joplin.views.dialogs.addScript(this.viewHandle, './webview_dialog.css');
+ }
+
+ /**
+ * Adds and loads new JS or CSS files into the dialog.
+ * @see {@link joplin.views.dialogs.addScript}
+ * @param script
+ */
+ public async addScript(script: string) {
+ await joplin.views.dialogs.addScript(this.viewHandle, script);
}
/**
@@ -32,6 +41,9 @@ export class Dialog {
return await joplin.views.dialogs.setHtml(this.viewHandle, html);
}
+ /**
+ * Uses a template for the HTML content and inserts variables.
+ */
public async useTemplate(obj: {} = {}) {
let html = this.template;
for (var key of Object.keys(obj)) {
@@ -51,6 +63,9 @@ export class Dialog {
return await joplin.views.dialogs.setButtons(this.viewHandle, buttons);
}
+ /**
+ * Adds IDs of dialog buttons that should be interpreted as 'true'.
+ */
public addPositiveIds(...ids: string[]) {
this.positiveIds.push(...ids);
}
@@ -64,10 +79,16 @@ export class Dialog {
return this.dialogResult;
}
+ /**
+ * Returns the last (raw) dialog result.
+ */
public getDialogResult(): DialogResult {
return this.dialogResult;
}
+ /**
+ * Returns the last dialog result that has been prepared.
+ */
public getPreparedDialogResult(defaultFormData: Object = this.defaultFormData): {id: string, confirm: boolean, formData: Object} {
return {
"id": this.getPressedButton(),
@@ -84,6 +105,11 @@ export class Dialog {
this.defaultFormData = defaultFormData;
}
+ /**
+ * By default, Joplin only returns form data that has been changed. This function returns a complete form data with defaults filled in.
+ * @see setDefaultFormData
+ * @see getDefaultFormData
+ */
public getFormData(defaultFormData: Object = this.defaultFormData): Object {
let formData = Object.assign({}, defaultFormData);
if (this.dialogResult.formData) {
@@ -92,10 +118,17 @@ export class Dialog {
return formData;
}
+ /**
+ * Returns the ID of the last pressed button.
+ */
public getPressedButton(): string {
return this.dialogResult.id;
}
+ /**
+ * Returns a boolean depending on whether the ID of the last pressed button is in `Dialog.positiveIds`.
+ * @see {addPositiveIds} to add IDs that should be interpreted as `true`
+ */
public getAnswer(): boolean {
return this.positiveIds.includes(this.getPressedButton());
}
diff --git a/src/utils/panels.ts b/src/utils/panels.ts
index c5aba44..4465e73 100644
--- a/src/utils/panels.ts
+++ b/src/utils/panels.ts
@@ -35,6 +35,7 @@ export class Panel {
/**
* Adds (and loads when created) new JS or CSS files into the panel.
+ * @see {@link joplin.views.panels.addScript}
* @param script
*/
public async addScript(script: string) {
diff --git a/src/webview_dialog.css b/src/webview_dialog.css
index 92e9417..1236855 100644
--- a/src/webview_dialog.css
+++ b/src/webview_dialog.css
@@ -1,4 +1,20 @@
#joplin-plugin-content {
min-width: 400px;
font-size: 10pt;
+ font-family: Roboto;
+}
+
+input[type="text"] {
+ width: 380px;
+ display: inline-block;
+ box-sizing: border-box;
+ color: var(--joplin-color);
+ background-color: var(--joplin-background-color);
+ border: 1px solid rgba(136, 136, 136, 0.3);
+ padding: 4px 6px;
+ border-radius: 3px;
+}
+
+td {
+ vertical-align: top;
}
\ No newline at end of file