Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Deconstructor Management for Dynamic Class Instances in CustomJS #79

Merged
merged 5 commits into from
Mar 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"singleQuote": true,
"bracketSpacing": true,
"useTabs": false,
"endOfLine": "auto",
"overrides": [
{
"files": [".prettierrc", ".eslintrc"],
Expand Down
65 changes: 65 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,71 @@ Also you can register individual commands via [settings](#registered-invocable-s

`window.customJS` object is being overridden every time any `js` file is modified in the vault. If you need some data to be preserved during such modifications, store them in `window.customJS.state`.

### `deconstructor` usage

Since the `window.customJS` object is overwritten each time the `js` files are reloaded, the option of defining a `deconstructor` has been added.

In your Javascript class, which you have CustomJS load, you can define a `deconstructor`, which is then called on every reload. This gives you the option of having cleanup work carried out.

```js
deconstructor() {
...
}
```

#### Example definition of a `deconstructor`

For example, you can deregister events that you have previously registered:

```js
deconstructor() {
this.app.workspace.off('file-menu', this.eventHandler);
}
```

### Re-execute the start scripts on reload

There is also the option of having the start scripts re-executed each time the `js` files are reloaded. This can be activated in the settings and is deactivated by default.

#### Complete example `deconstructor` & re-execute start scripts

These two functions, the `deconstructor` and the automatic re-execution of the start scripts, make it possible, for example, to implement your own context menu in Obsidian.

To do this, you must register the corresponding event in the `invoke` start function and deregister it again in the `deconstructor`.

Please be aware of any binding issues and refer to the Obsidian API documentation.

```js
class AddCustomMenuEntry {
constructor() {
// Binding the event handler to the `this` context of the class.
this.eventHandler = this.eventHandler.bind(this);
}

invoke() {
this.app.workspace.on('file-menu', this.eventHandler);
}

deconstructor() {
this.app.workspace.off('file-menu', this.eventHandler);
}

eventHandler(menu, file) {
// Look in the API documentation for this feature
// https://docs.obsidian.md/Plugins/User+interface/Context+menus
menu.addSeparator();
menu.addItem((item) => {
item
.setTitle('Custom menu entry text..')
.setIcon('file-plus-2') // Look in the API documentation for the available icons
.onClick(() => { // https://docs.obsidian.md/Plugins/User+interface/Icons
// Insert the code here that is to be executed when the context menu entry is clicked.
});
});
}
}
```

## ☕️ Support

Do you find CustomJS useful? Consider buying me a coffee to fuel updates and more useful software like this. Thank you!
Expand Down
63 changes: 62 additions & 1 deletion main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@ interface CustomJSSettings {
jsFolder: string;
startupScriptNames: string[];
registeredInvocableScriptNames: string[];
rerunStartupScriptsOnFileChange: boolean;
}

const DEFAULT_SETTINGS: CustomJSSettings = {
jsFiles: '',
jsFolder: '',
startupScriptNames: [],
registeredInvocableScriptNames: [],
rerunStartupScriptsOnFileChange: false,
};

interface Invocable {
invoke: () => Promise<void>;
}
Expand All @@ -35,6 +38,8 @@ function isInvocable(x: unknown): x is Invocable {

export default class CustomJS extends Plugin {
settings: CustomJSSettings;
deconstructorsOfLoadedFiles: { deconstructor: () => void; name: string }[] =
[];

async onload() {
// eslint-disable-next-line no-console
Expand Down Expand Up @@ -112,10 +117,37 @@ export default class CustomJS extends Plugin {
}
}

async deconstructLoadedFiles() {
// Run deconstructor if exists
for (const deconstructor of this.deconstructorsOfLoadedFiles) {
try {
await deconstructor.deconstructor();
} catch (e) {
// eslint-disable-next-line no-console
console.error(`${deconstructor.name} failed`);
// eslint-disable-next-line no-console
console.error(e);
}
}

// Clear the list
this.deconstructorsOfLoadedFiles = [];
}

async reloadIfNeeded(f: TAbstractFile) {
if (f.path.endsWith('.js')) {
// Run deconstructor if exists
await this.deconstructLoadedFiles();

await this.loadClasses();

// invoke startup scripts again if wanted
if (this.settings.rerunStartupScriptsOnFileChange) {
for (const startupScriptName of this.settings.startupScriptNames) {
await this.invokeScript(startupScriptName);
}
}

// reload dataviewjs blocks if installed & version >= 0.4.11
if (this.app.plugins.enabledPlugins.has('dataview')) {
const version = this.app.plugins.plugins?.dataview?.manifest.version;
Expand All @@ -139,12 +171,27 @@ export default class CustomJS extends Plugin {
async evalFile(f: string): Promise<void> {
try {
const file = await this.app.vault.adapter.read(f);
const def = debuggableEval(`(${file})`, f) as new () => unknown;

const def = debuggableEval(`(${file})`, f) as new () => {
deconstructor?: () => void;
};

// Store the existing instance
const cls = new def();
window.customJS[cls.constructor.name] = cls;

// Check if the class has a deconstructor
if (typeof cls.deconstructor === 'function') {
// Add the deconstructor to the list
const deconstructor = cls.deconstructor.bind(cls);

const deconstructorWrapper = {
deconstructor: deconstructor,
name: `Deconstructor of ${cls.constructor.name}`,
};
this.deconstructorsOfLoadedFiles.push(deconstructorWrapper);
}

// Provide a way to create a new instance
window.customJS[`create${def.name}Instance`] = () => new def();
} catch (e) {
Expand Down Expand Up @@ -390,6 +437,20 @@ class CustomJSSettingsTab extends PluginSettingTab {
}
}),
);

new Setting(containerEl)
.setName('Re-execute the start scripts when reloading')
.setDesc(
'Decides whether the startup scripts should be executed again after reloading the scripts',
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.rerunStartupScriptsOnFileChange)
.onChange(async (value) => {
this.plugin.settings.rerunStartupScriptsOnFileChange = value;
await this.plugin.saveSettings();
}),
);
}
}

Expand Down