// settings_tab.ts import PluginsAnnotations from "main"; import { handleMarkdownFilePathChange } from "manageAnnotations"; import { App, normalizePath, Notice, Platform, PluginSettingTab, Setting, TextComponent, ToggleComponent } from "obsidian"; import { PluginAnnotationDict } from "types"; import { parseFilePath, FileSuggestion, downloadJson, showConfirmationDialog, backupSettings, sortAnnotations } from "utils"; import { DEFAULT_SETTINGS } from 'defaults'; import { AnnotationControl } from "annotation_control"; import { svg_locked, svg_unlocked } from "graphics"; declare const moment: typeof import('moment'); export class PluginsAnnotationsSettingTab extends PluginSettingTab { plugin: PluginsAnnotations; private uninstalledPluginsManager: UninstalledPluginsManager | null = null; private backupManager: BackupManager | null = null; constructor(app: App, plugin: PluginsAnnotations) { super(app, plugin); this.plugin = plugin; } async display(): Promise { const createPluginsPaneFragment = (): DocumentFragment => { return createFragment((frag) => { const em = frag.createEl('em'); const link = frag.createEl('a', { href: '#', text: 'Community plugins'}); link.onclick = () => { this.app.setting.openTabById('community-plugins'); }; em.appendChild(link); }); }; // Load annotations first await this.plugin.loadSettings(); // Clean container in the preference pane const containerEl = this.containerEl; containerEl.empty(); containerEl.classList.add('plugin-comment-settings'); /* ====== Editing ====== */ new Setting(containerEl).setName('Editing annotations').setHeading(); const instructions_frag = createFragment((frag) => { const div = document.createElement('div'); div.classList.add('plugin-comment-instructions'); const p1 = document.createElement('p'); p1.appendText('To add or edit your personal annotations for the installed plugins, go to the '); p1.appendChild(createPluginsPaneFragment()); p1.appendText(' pane and click over the annotation field of the plugin you want to edit.'); div.appendChild(p1); const p2 = document.createElement('p2'); p2.innerText = "You can enter rich text annotations using Markdown just the same way you do in Obsidian. \ Once you are finished editing, the Markdown annotation will be rendered correctly."; div.appendChild(p2); const p3 = document.createElement('p'); p3.innerText = "You can directly link notes inside your \ vault by adding Obsidian links such as "; const code = document.createElement('code'); code.appendText('[[My notes/Review of plugin XYZ|my plugin note]]'); code.classList.add('plugin-comment-selectable'); p3.appendChild(code); p3.appendText('.'); div.appendChild(p3); frag.appendChild(div); }); new Setting(containerEl).setName(instructions_frag); const editable_setting = new Setting(containerEl) .setName('Editable'); let editable_toggle: ToggleComponent; editable_setting.addToggle(toggle => { editable_toggle = toggle; toggle .setValue(this.plugin.settings.editable) .onChange((value: boolean) => { this.plugin.settings.editable = value; this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }) }); editable_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { editable_toggle.setValue(DEFAULT_SETTINGS.editable); this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }); }); editable_setting.setDesc(createFragment((frag) => { frag.appendText('If disabled, the annotations cannot be edited from the preference pane and are thus \ protected against accidental changes. In the '); frag.appendChild(createPluginsPaneFragment()); frag.appendText(' pane, you can coveniently change this setting by clicking on the displayed icon'); const div = frag.createDiv(); div.classList.add('plugin-comment-icon-container') const unlock_icon = document.createElement('div'); unlock_icon.classList.add('clickable-icon'); unlock_icon.innerHTML = svg_unlocked; unlock_icon.addEventListener('click', (event:MouseEvent) => { editable_toggle.setValue(true); }); const lock_icon = document.createElement('div'); lock_icon.classList.add('clickable-icon'); lock_icon.innerHTML = svg_locked; lock_icon.addEventListener('click', (event:MouseEvent) => { editable_toggle.setValue(false); }); div.appendText('{'); div.appendChild(lock_icon); div.appendText(','); div.appendChild(unlock_icon); div.appendText('}'); frag.appendText('which either locks (make non-editable) or unlocks (make editable) your personal annotations.') })); /* ==== Storage ==== */ new Setting(containerEl).setName('Storage').setHeading(); // Add new setting for storing annotations in a Markdown file const md_file_setting = new Setting(containerEl) .setName('Store annotations in a Markdown file') .setDesc('With this option enabled, you can select a Markdown file in your vault to \ contain your personal annotations for the installed plugins. This feature is intended \ for power users who prefer to edit annotations directly from a Markdown file. \ A second advantage of this mode is that if you use links to some of your notes in \ the vault, those links will be automatically updated if your notes are later renamed.'); let file_path_field_control: TextComponent; let md_filepath_error_div: HTMLDivElement; // Add new setting for markdown file path const md_filepath_setting = new Setting(containerEl) .setName('Markdown File Path') .setDesc(createFragment((frag) => { frag.appendText('Markdown file where the plugins\' annotations are stored (e.g, '); frag.createEl('code', {'cls': 'plugin-comment-selectable'}).appendText('00 Meta/Misc/Plugins annotations.md'); frag.appendText(').'); md_filepath_error_div = frag.createDiv({text: 'Error: the filename must end with .md extension.', cls: "mod-warning" }); md_filepath_error_div.style.display = 'none'; })); let md_filepath_text: TextComponent; md_filepath_setting.addText(text => { md_filepath_text = text; let processingChange = false; file_path_field_control = text; text.setPlaceholder('E.g.: 00 Meta/Plugins annotations.md'); text.setValue(this.plugin.settings.markdown_file_path); const inputEl = text.inputEl; const fileSuggestion = new FileSuggestion(this.app, inputEl); const updateVaultFiles = () => { if(fileSuggestion) { fileSuggestion.setSuggestions(this.app.vault.getFiles().filter((f) => f.extension === "md")); } } updateVaultFiles(); const onChangeHandler = async (event: Event) => { if(processingChange) { return; } else { processingChange = true; } let filepath = inputEl.value; if(filepath === '' || filepath!==this.plugin.settings.markdown_file_path) { // if the path has changed if(filepath.trim()==='') { md_filepath_error_div.style.display = 'none'; this.plugin.settings.markdown_file_path = ''; text.setValue(this.plugin.settings.markdown_file_path); processingChange = false; this.plugin.debouncedSaveAnnotations(); return; } filepath = normalizePath(filepath); if (parseFilePath(filepath).ext !== '.md') { md_filepath_error_div.style.display = ''; this.plugin.settings.markdown_file_path = DEFAULT_SETTINGS.markdown_file_path; // reverts to the default behavior await this.plugin.saveSettings(); processingChange = false; return; } md_filepath_error_div.style.display = 'none'; const answer = await handleMarkdownFilePathChange(this.plugin, filepath); if(answer) { this.plugin.settings.markdown_file_path = filepath; await this.plugin.saveSettings(); updateVaultFiles(); } text.setValue(this.plugin.settings.markdown_file_path); } processingChange = false; }; inputEl.addEventListener('keydown', async (event) => { if (event.key === 'Enter') { event.preventDefault(); onChangeHandler(event); } }); inputEl.addEventListener('blur', onChangeHandler); // Use change explicitly instead of onChange because onChange // reacts to events of type `input` instead of `change` inputEl.addEventListener('change', onChangeHandler); }); md_filepath_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { md_filepath_text.setValue(DEFAULT_SETTINGS.markdown_file_path); this.plugin.debouncedSaveAnnotations(); }); }); md_filepath_setting.settingEl.style.display = this.plugin.settings.markdown_file_path === '' ? 'none' : ''; let md_file_toggle: ToggleComponent; md_file_setting.addToggle(toggle => { md_file_toggle = toggle; toggle .setValue(this.plugin.settings.markdown_file_path !== '') .onChange(async (value: boolean) => { if (value) { md_filepath_setting.settingEl.style.display = ''; this.plugin.settings.markdown_file_path = file_path_field_control.getValue(); } else { md_filepath_setting.settingEl.style.display = 'none'; this.plugin.settings.markdown_file_path = ''; } this.plugin.debouncedSaveAnnotations(); }) }); md_file_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { md_file_toggle.setValue(DEFAULT_SETTINGS.markdown_file_path !== ''); this.plugin.debouncedSaveAnnotations(); }); }); // Append the settings containerEl.appendChild(md_file_setting.settingEl); containerEl.appendChild(md_filepath_setting.settingEl); /* ==== Display heading ==== */ new Setting(containerEl).setName('Display').setHeading(); let label: string; let label_version: string; let label_cb: (value: string) => void; if (Platform.isMobile) { label = this.plugin.settings.label_mobile; label_version = 'mobile'; label_cb = (value: string) => { this.plugin.settings.label_mobile = value; this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }; } else { label = this.plugin.settings.label_desktop; label_version = 'desktop'; label_cb = (value: string) => { this.plugin.settings.label_desktop = value; this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }; } const label_setting = new Setting(containerEl) .setName('Annotation label') .setDesc(createFragment((frag) => { frag.appendText(`Choose the annotation label for the ${label_version} version of Obsidian. \ Use HTML code if you want to format it. Enter an empty string if you want \ to hide the label. Use `); frag.createEl('code',{cls: 'plugin-comment-selectable'}).appendText('${plugin_name}'); frag.appendText(' as a template for the plugin name; for example, you can generate automatic links to your notes with a label of the kind '); frag.createEl('code', {'cls': 'plugin-comment-selectable'}).appendText('[[00 Meta/Installed plugins/${plugin_name}|${plugin_name}]]'); frag.appendText('.'); })); let label_text: TextComponent; label_setting.addText(text => { label_text = text; text.setPlaceholder('Annotation label'); text.setValue(label); text.onChange(label_cb)}); label_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { let label: string; if(Platform.isMobile) { label = DEFAULT_SETTINGS.label_mobile; this.plugin.settings.label_mobile = label; } else { label = DEFAULT_SETTINGS.label_desktop; this.plugin.settings.label_desktop = label; } label_text.setValue(label); this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }); }); const placeholder_setting = new Setting(containerEl) .setName('Placeholder label') .setDesc(createFragment((frag) => { frag.appendText('Choose the label appearing where no user annotation is provied yet. Use '); frag.createEl('code',{cls: 'plugin-comment-selectable'}).appendText('${plugin_name}'); frag.appendText(' as a template for the plugin name; for example, you can generate automatic \ links to your notes with a placeholder of the kind '); frag.createEl('code', {'cls': 'plugin-comment-selectable'}).appendText('[[00 Meta/Installed plugins/${plugin_name}|${plugin_name}]]'); frag.appendText('.') })); let placeholder_text: TextComponent; placeholder_setting.addText(text => { placeholder_text = text; text.setPlaceholder('Annotation label'); text.setValue(this.plugin.settings.label_placeholder); text.onChange(async (value: string) => { this.plugin.settings.label_placeholder = value; this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); })}); placeholder_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { placeholder_text.setValue(DEFAULT_SETTINGS.label_placeholder); this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }); }); const hide_empty_annotations_setting = new Setting(containerEl) .setName('Hide empty annotations') .setDesc(createFragment((frag) => { frag.appendText('If this option is enabled, only annotations set by the user \ will be shown. If you want to insert an annotation to a plugin for the first \ time, hover with the mouse over the chosen plugin in the '); frag.appendChild(createPluginsPaneFragment()); frag.appendText(' pane. The annotation field will appear automatically.'); if (Platform.isMobile) { const p = frag.createEl('p'); const warning = p.createEl('span', { text: 'On mobile devices, you can hover over plugins with your finger instead of using the mouse.', }); warning.classList.add('mod-warning'); frag.appendChild(p); } })); let hide_empty_annotations_toggle: ToggleComponent; hide_empty_annotations_setting.addToggle(toggle => { hide_empty_annotations_toggle = toggle; toggle .setValue(this.plugin.settings.hide_placeholders) .onChange(async (value: boolean) => { this.plugin.settings.hide_placeholders = value; this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }) }); hide_empty_annotations_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { hide_empty_annotations_toggle.setValue(DEFAULT_SETTINGS.hide_placeholders); this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }); }); const delete_placeholder_string_setting = new Setting(containerEl) .setName('Delete placeholder text when inserting a new annotation') .setDesc('If this option is enabled, the placeholder text will be deleted \ automatically when you start typing a new annotation. If disabled, \ the placeholder text will be selected for easier replacement. \ This is a minor customization.'); let delete_placeholder_string_toggle: ToggleComponent; delete_placeholder_string_setting.addToggle(toggle => { delete_placeholder_string_toggle = toggle; toggle .setValue(this.plugin.settings.delete_placeholder_string_on_insertion) .onChange(async (value: boolean) => { this.plugin.settings.delete_placeholder_string_on_insertion = value; this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }); }); delete_placeholder_string_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { delete_placeholder_string_toggle.setValue(DEFAULT_SETTINGS.delete_placeholder_string_on_insertion); this.plugin.debouncedSaveAnnotations(() => { this.uninstalledPluginsManager?.updateUninstalledPluginSettings(containerEl); }); }); }); const show_github_icons_setting = new Setting(containerEl) .setName('Show GitHub links') .setDesc("If this option is enabled, aclickable icon linking to the plugin's GitHub page will be displayed in the Community plugin pane."); let show_github_icons_toggle: ToggleComponent; show_github_icons_setting.addToggle(toggle => { show_github_icons_toggle = toggle; toggle .setValue(this.plugin.settings.show_github_icons) .onChange(async (value: boolean) => { this.plugin.settings.show_github_icons = value; this.plugin.debouncedSaveAnnotations(); }); }); show_github_icons_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { const value = DEFAULT_SETTINGS.show_github_icons show_github_icons_toggle.setValue(value); }); }); /* ====== Backups ====== */ this.backupManager = new BackupManager(this.plugin, containerEl); /* ====== Personal annotations of no longer installed community plugins ====== */ this.uninstalledPluginsManager = new UninstalledPluginsManager(this.plugin,containerEl); } } class BackupManager { private backupTableContainer: HTMLElement; private UNTITLED_BACKUP = 'Untitled backup'; constructor(private plugin:PluginsAnnotations, private containerEl:HTMLElement) { new Setting(this.containerEl) .setName('Backups') .setHeading(); this.addBackupButtons(); // Create a wrapper div for the table this.backupTableContainer = this.containerEl.createDiv(); this.backupTableContainer.classList.add('setting-item'); this.backupTableContainer.style.display = 'none'; this.updateListBackups(); } addBackupButtons() { const export_label = (Platform.isDesktopApp) ? ' Use the export and import buttons \ to copy the current settings and annnotations to an external file and, vicevera, \ to restore them from an external file.' : ''; const backup_settings = new Setting(this.containerEl) .setName('Create a backup copy of your current settings and annotations') .setDesc('Use the backup button to create an internal backup copy. \ You can customize the names of existing backups by clicking on their names once you have created them.' + export_label) .addButton(button => button .setButtonText('Create Backup') .setCta() .onClick(async () => { const backupName = this.UNTITLED_BACKUP; await backupSettings(backupName,this.plugin.settings,this.plugin.settings.backups); await this.plugin.saveSettings(); this.updateListBackups(); }) ); backup_settings.controlEl.classList.add('plugin-comment-export-buttons'); if (Platform.isDesktopApp) { backup_settings.addButton(button => button .setButtonText('Export') .setCta() .onClick(async () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const {backups:_,...rest} = this.plugin.settings; downloadJson(rest); }) ); backup_settings.addButton(button => button .setButtonText('Import') .setCta() .onClick(async () => { // Create an input element to upload a file const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; // Only allow JSON files input.onchange = async () => { const file = input.files?.[0]; if (file) { // Read the file as text const reader = new FileReader(); reader.onload = async (event) => { try { // Parse the JSON file const importedData = JSON.parse(event.target?.result as string); // Validate and merge the imported settings if(importedData === undefined || importedData === null || typeof importedData !== 'object') throw new Error("Something went wrong with the data in the backup."); await this.plugin.loadSettings({...importedData,backups:this.plugin.settings.backups}); new Notice('Settings successfully imported.'); this.updateListBackups(); } catch (error) { console.error('Error importing settings:', error); alert('Failed to import settings. Please ensure the file is valid.'); } }; reader.readAsText(file); } }; // Trigger the file input click input.click(); }) ); } } updateListBackups() { this.backupTableContainer.innerHTML = ''; // List Existing Backups if (this.plugin.settings.backups.length > 0) { this.backupTableContainer.style.display = ''; // Sort the backups by date (most recent first) this.plugin.settings.backups.sort((a, b) => b.date.getTime() - a.date.getTime()); const tableDiv = this.backupTableContainer.createDiv({ cls: 'plugin-comment-backup-table' }); // Create the header row const headerRow = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row header' }); headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Backup name' + (Platform.isMobileApp ? '' : ' (click to edit)') }); headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: 'Created on' }); headerRow.createDiv({ cls: 'plugin-comment-backup-table-cell', text: '' }); this.plugin.settings.backups.forEach((backup) => { const rowDiv = tableDiv.createDiv({ cls: 'plugin-comment-backup-table-row' }); // Backup name cell const nameCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-name' }); const nameDiv = nameCell.createDiv({ text: backup.name, attr: { contenteditable: 'true' } }); // Handle saving the updated name when editing is complete nameDiv.addEventListener('input', () => { const newName = nameDiv.innerText.trim(); backup.name = newName === '' ? this.UNTITLED_BACKUP : newName; this.plugin.debouncedSaveAnnotations(); }); // Handle saving the updated name when editing is complete nameDiv.addEventListener('blur', () => { const newName = nameDiv.innerText.trim(); if(newName === '') { nameDiv.innerText = backup.name; } this.plugin.debouncedSaveAnnotations(); }); // Handle the Enter key to finish editing nameDiv.addEventListener('keydown', (event) => { if (event.key === 'Enter') { event.preventDefault(); nameDiv.blur(); // Trigger the blur event to save the name } }); // Created on cell const dateCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell' }); dateCell.setText(moment(backup.date).format('YYYY-MM-DD HH:mm:ss')); // Add Restore and Delete buttons to the last cell const actionCell = rowDiv.createDiv({ cls: 'plugin-comment-backup-table-cell plugin-comment-backup-buttons' }); actionCell.createEl('button', { text: 'Restore', cls: 'mod-cta' }) .addEventListener('click', async () => { const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', createFragment((frag) => { frag.appendText('You are about to restore the settings from the backup named '); frag.createEl('strong',{text: backup.name}); frag.appendText(' created on '); frag.createEl('strong',{text: moment(backup.date).format('YYYY-MM-DD HH:mm:ss')}); frag.appendText('. If you proceed, the current settings will be overwritten with those from the backup. \ If you want to keep a copy of the current settings, make a backup before proceeding.\ Do you want to proceed restoring the seettings from the backup?'); })); if(answer) { const settingsToBeRestored = structuredClone(backup.settings); if(settingsToBeRestored === undefined || settingsToBeRestored === null || typeof settingsToBeRestored !== 'object') throw new Error("Something went wrong with the data in the backup."); await this.plugin.loadSettings({...settingsToBeRestored, backups:this.plugin.settings.backups}); new Notice(`Annotations restored from backup "${backup.name}"`); this.updateListBackups(); } }); actionCell.createEl('button', { text: 'Delete', cls: 'mod-cta' }) .addEventListener('click', async () => { const answer = await showConfirmationDialog(this.plugin.app, 'Delete backup', createFragment((frag) => { frag.appendText('You are about to delete the backup named '); frag.createEl('strong',{text: backup.name}); frag.appendText(' created on '); frag.createEl('strong',{text: moment(backup.date).format('YYYY-MM-DD HH:mm:ss')}); frag.appendText('. Do you want to continue?'); })); if(answer) { const index = this.plugin.settings.backups.indexOf(backup); if (index !== -1) { this.plugin.settings.backups.splice(index, 1); // Removes the element at the found index } this.plugin.debouncedSaveAnnotations(); rowDiv.remove(); if(this.plugin.settings.backups.length===0) { this.backupTableContainer.style.display = 'none'; } } }); }); } else { this.backupTableContainer.style.display = 'none'; } } } class UninstalledPluginsManager { private uninstalledPlugins:PluginAnnotationDict = {}; constructor(private plugin:PluginsAnnotations,private containerEl: HTMLElement) { this.uninstalledPlugins = this.plugin.getUninstalledPlugins(); new Setting(containerEl).setName('Annotations of no longer installed community plugins').setHeading(); const automatic_remove_setting = new Setting(containerEl) .setName('Automatically remove personal annotations of uninstalled plugins') .setDesc('If this option is enabled, whenever a plugin is uninstalled, the \ attached personal annotation is automatically removed. \ If this option is disabled, you can still manually remove the personal \ annotations of any plugin that is no longer installed. \ The list of the no longer installed plugins is shown below, when the list is not empty.'); let automatic_remove_toggle: ToggleComponent; automatic_remove_setting.addToggle(toggle => { automatic_remove_toggle = toggle; toggle .setValue(this.plugin.settings.automatic_remove) .onChange(async (value: boolean) => { this.plugin.settings.automatic_remove = value; this.plugin.debouncedSaveAnnotations(); }); }); automatic_remove_setting.addExtraButton((button) => { button .setIcon("reset") .setTooltip("Reset to default value") .onClick(() => { automatic_remove_toggle.setValue(DEFAULT_SETTINGS.automatic_remove); this.plugin.debouncedSaveAnnotations(); }); }); // Check if uninstalledPlugins is empty if (Object.keys(this.uninstalledPlugins).length === 0) return; const list_uninstalled_label = new Setting(containerEl) .setName('List of no longer installed plugins') .setDesc('If you plan to reinstall the plugin in the future, \ it is recommended not to remove your annotations, as you can reuse them later.'); // Iterate over uninstalled plugins and add settings to the new subcontainer sortAnnotations(this.uninstalledPlugins).forEach(pluginId => { const pluginSetting = new Setting(containerEl) .setName(`Plugin ${this.uninstalledPlugins[pluginId].name}`) .addButton(button => button .setButtonText('Delete') .setCta() .onClick(async () => { delete this.plugin.settings.annotations[pluginId]; delete this.uninstalledPlugins[pluginId]; pluginSetting.settingEl.remove(); this.plugin.debouncedSaveAnnotations(); // If no more uninstalled plugins, remove the section container if (Object.keys(this.uninstalledPlugins).length === 0) { list_uninstalled_label.settingEl.remove(); } })); pluginSetting.descEl.dataset.plugin=pluginId; // Render the annotation new AnnotationControl(this.plugin,pluginSetting.descEl,pluginId,this.uninstalledPlugins[pluginId].name); // Set the attributes by applying the correct classes pluginSetting.descEl.classList.add('plugin-comment-annotation'); pluginSetting.settingEl.classList.add('plugin-comment-uninstalled'); }); } updateUninstalledPluginSettings(containerEl: HTMLElement) { const elements = containerEl.querySelectorAll('div.setting-item-description.plugin-comment-annotation'); elements.forEach((descEl) => { if (descEl instanceof HTMLElement) { const pluginId = descEl.dataset.plugin; if(pluginId) { descEl.innerHTML = ''; new AnnotationControl(this.plugin,descEl,pluginId,this.uninstalledPlugins[pluginId].name); } } }); } }