Skip to content

Commit

Permalink
Implement Deconstructor Management for Dynamic Class Instances in Cus…
Browse files Browse the repository at this point in the history
…tomJS (#79)

* Add deconstructor handling for dynamic class instances

Implemented a mechanism to manage deconstructors in dynamically loaded class instances within CustomJS. This includes registering deconstructors upon class evaluation and ensuring proper cleanup before reloading or discarding classes to avoid duplicate event registration. The list of deconstructors is cleared after execution to maintain clean state.

* Added a setting to specify whether the startup scripts should also be re-executed when the scripts are reloaded.

Default is NO for compatibility reasons.

* The errors thrown by `eslint` have been fixed.

* Adjust Prettier EOL handling for better cross-platform compatibility

Otherwise you suddenly have 500 errors and only the correct line ends are uploaded to git.

* Readme extended with explanations and examples for using the `deconstructor` & re-execute start scripts features.
  • Loading branch information
PxaMMaxP authored Mar 9, 2024
1 parent 1604143 commit 0b87fdb
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 1 deletion.
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

0 comments on commit 0b87fdb

Please sign in to comment.