diff --git a/smart-tests/README.md b/smart-tests/README.md deleted file mode 100644 index 20a60c68..00000000 --- a/smart-tests/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# Project Overview - -This repository demonstrates a standardized testing approach using a unified schema for test definitions. Tests are structured into a `test` object that can be easily discovered and run by a generic test runner. - -## Key Components - -- **Test Files**: Each file that needs testing exports a `test` object containing: - - An optional `setup()` function to run once before all tests. - - A `cases` array where each element defines a test case (`name`, `before`, `params`, and `assert`). - -- **Generic Test Runner**: A runner script (e.g., `generic_test_runner.js`) that takes a directory as an argument, discovers all `.js` test files, and executes all test cases. - -## Including Tests in the Same File as Code - -In certain scenarios, placing tests in the same file as the code they test can be beneficial. For instance, a class or function can be immediately followed by its corresponding test definitions. This approach: - -- **Improves Readability**: Having the test cases next to the code they exercise provides immediate visibility into what is being tested and why. It’s easier to see the intended usage and behavior of a class or function when the tests are co-located. - -- **Encourages Maintenance**: When code and tests live together, updates to the code can be followed immediately by updates to its tests, reducing the risk of forgetting to update test coverage. It also lowers the cognitive load of switching between separate files just to understand or update related test logic. - -- **Enhances Discoverability**: Developers exploring code often start from the source file. With the tests in the same location, they quickly gain insights into expected behaviors, edge cases, and how to properly use the functionality. This makes onboarding new team members or contributors more straightforward. - -## Example Structure - -For a `Counter` class test file (`counter_test.js`): - -```js -export const test = { - setup: async () => { - // Load global config, if any - }, - cases: [ - { - name: "counter_starts_at_zero", - before: async function () { - this.counter = new Counter(); - }, - assert: async function (assert) { - assert.strictEqual(this.counter.value, 0); - }, - }, - // ...more test cases... - ], -}; -``` - -## Running Tests - -Use the generic test runner to execute all tests in a given directory: - -```bash -node generic_test_runner.js ./tests -``` - -This will: -- Recursively scan the `./tests` directory for `.js` files exporting `test` objects. -- Execute `setup()` once if defined. -- For each test case: - - Run the `before()` hook if defined. - - Execute the `assert()` function and report the results. - -Any failures cause a non-zero exit code, making it suitable for CI/CD pipelines. - -## Benefits - -- **Consistency**: By following the described schema, all tests have a uniform structure, making them easier to maintain. -- **Discoverability**: Automated runners can easily identify and execute all tests without custom logic. -- **Flexibility**: You can adapt the schema to various test scenarios, from simple unit tests to integration tests. - -## Next Steps - -- Add more test cases following the standardized schema. -- Integrate the test runner into your CI/CD workflow. -- Enhance `setup()` and `before()` hooks to handle more complex test prerequisites as your project grows. diff --git a/smart-tests/example.js b/smart-tests/example.js deleted file mode 100644 index bc639677..00000000 --- a/smart-tests/example.js +++ /dev/null @@ -1,143 +0,0 @@ -/** - * @fileoverview - * A standardized test schema for testing modules and classes. The schema follows a pattern where each test file - * exports a `test` object that can be consumed by a common test runner. This `test` object provides an optional - * `setup()` function for global initialization and a `cases` array. Each element in `cases` describes a single - * test case including setup steps, parameters, and assert logic. - * - * # Schema Explanation - * The `test` object is designed to be easily discoverable and runnable by a generic test runner. It defines: - * - An optional `setup()` function that runs once before all test cases to perform global initializations. - * - A `cases` array where each element is an object representing a single test scenario. - * - * Each test case object can have: - * - `name`: A unique string identifier for the test case. - * - `before` (optional): An async function that runs before the test case’s `assert` function. This allows you - * to set up a clean, test-specific state, such as creating a new instance of a class being tested. - * - `params` (optional): An object containing configuration or input parameters relevant to the test. This can - * be used by `before()` or `assert()` to influence the test’s setup or assertions. - * - `assert`: An async function that receives: - * - `assert`: An assertion library instance (e.g., Node.js `assert` module) to perform assertions. - * - `resp` (optional): Represents the response or result under test, if applicable. - * - * In the example below, we test a `Counter` class. Each test case uses a `before()` hook to create a fresh - * `Counter` instance. This ensures that no state leaks between test cases, promoting deterministic results. - * - * @type {{ -* setup?: () => Promise, -* cases: Array<{ -* /** -* * Name of the test case. This is a human-readable unique identifier that -* * describes the behavior being tested. It is used by test runners to report -* * which tests have passed or failed. -* * @type {string} -* */ -* name: string, -* -* /** -* * An optional async function that runs before the `assert` function. If defined, -* * this function is used to set up the test environment or instantiate objects needed -* * during the actual test run. For example, it can create a new instance of the class -* * being tested and store it on `this` for `assert` to use. -* * -* * @async -* * @returns {Promise} -* */ -* before?: () => Promise, -* -* /** -* * An optional object containing configuration or parameters needed for the test. -* * These can be referenced by `before()` or `assert()` to influence setup logic or -* * assertions. For instance, you might supply a parameter indicating how many times -* * a method should be called before checking its value. -* * -* * @type {object} -* */ -* params?: object, -* -* /** -* * The main assertion function that performs the actual test checks. It must be async. -* * Receives `assert`, which is the Node.js strict assertion module, and optionally `resp`, -* * which represents the outcome of the logic under test if relevant. Typically, `resp` -* * might hold the return value of a function call being tested. In this particular -* * schema, we rely on `before()` setting the state (such as `this.counter`) so that -* * `assert()` can verify post-conditions. -* * -* * @async -* * @param {typeof import('node:assert/strict')} assert - The assertion library for -* * validating test outcomes. -* * @param {*} [resp] - Optional result under test, if applicable. -* * @returns {Promise} -* */ -* assert: (assert: typeof import('node:assert/strict'), resp?: any) => Promise -* }> -* }} -*/ -export const test = { - /** - * Optional setup function that runs once before all test cases. Use this to load configuration, - * initialize databases, or perform any global setup needed by all test cases. In this example, - * we do not require global setup, so it is left empty. - * - * @async - * @returns {Promise} - */ - setup: async () => { - // Could be used to load configuration, initialize data, etc. - }, - - /** - * An array of test cases that verify various behaviors of the `Counter` class. Each test case - * uses a `before()` hook to create a new `Counter` instance and ensures that no state leaks - * between tests, making the suite deterministic and reliable. - */ - cases: [ - { - name: "counter_starts_at_zero", - before: async function () { - this.counter = new Counter(); - }, - params: {}, - assert: async function (a) { - a.strictEqual(this.counter.value, 0, "Counter should start at zero"); - }, - }, - { - name: "increment_increases_value", - before: async function () { - this.counter = new Counter(); - }, - params: {}, - assert: async function (a) { - const newValue = this.counter.increment(); - a.strictEqual(newValue, 1, "Increment should return 1 on first call"); - a.strictEqual(this.counter.value, 1, "Counter value should be 1 after increment"); - }, - }, - { - name: "reset_sets_value_to_zero", - before: async function () { - this.counter = new Counter(); - this.counter.increment(); // value now 1 - }, - params: {}, - assert: async function (a) { - this.counter.reset(); - a.strictEqual(this.counter.value, 0, "Counter value should be 0 after reset"); - }, - }, - { - name: "increment_multiple_times", - before: async function () { - this.counter = new Counter(); - }, - params: {}, - assert: async function (a) { - this.counter.increment(); // value: 1 - this.counter.increment(); // value: 2 - this.counter.increment(); // value: 3 - a.strictEqual(this.counter.value, 3, "After three increments, value should be 3"); - }, - }, - ], -}; \ No newline at end of file diff --git a/smart-tests/package.json b/smart-tests/package.json deleted file mode 100644 index ac507a1b..00000000 --- a/smart-tests/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "smart-tests", - "type": "module", - "version": "0.0.1" -} diff --git a/smart-tests/run.js b/smart-tests/run.js deleted file mode 100644 index 4ace2b0c..00000000 --- a/smart-tests/run.js +++ /dev/null @@ -1,216 +0,0 @@ -/** - * @fileoverview - * A generic test runner that accepts a directory as an argument and runs - * all test suites found within that directory. Each test file should export - * a `test` object with optional `setup()` and `cases[]`. - * - * Usage: - * node generic_test_runner.js path/to/test/directory - * - * The test files are expected to: - * - Export a `test` object. - * - `test.setup` is an optional async function run before all test cases. - * - `test.cases` is an array of test objects: - * { - * name: string, - * before?: () => Promise, - * params?: object, - * assert: (assertModule: typeof import('node:assert/strict'), resp?: any) => Promise - * } - * - * This runner will attempt to run all `.js` files in the specified directory as tests. - * Any test that fails will cause the process to exit with a non-zero code. - */ - -import { readdir, stat, readFile } from 'node:fs/promises'; -import { join, resolve, dirname } from 'node:path'; -import assert from 'node:assert/strict'; -import { fileURLToPath } from 'node:url'; - -/** - * Checks if a file exports a test object without executing it. - * @param {string} file_path - Path to the JavaScript file - * @returns {Promise} True if file exports a test object - */ -async function has_test_exports(file_path) { - try { - // First read the file content to check if it's an AVA test - const file_content = await readFile(file_path, 'utf-8'); - - // Skip AVA test files - if (file_content.includes('import test from "ava"') || - file_content.includes("import test from 'ava'") || - file_content.includes('require("ava")') || - file_content.includes("require('ava')")) { - return false; - } - - const module = await import(`file://${file_path}?t=${Date.now()}`); - return module.test && Array.isArray(module.test.cases); - } catch (error) { - // Ignore JSON module experimental warnings - if (error.message && error.message.includes('--experimental-json-modules')) { - return false; - } - // Silently skip files that can't be imported - return false; - } -} - -/** - * Recursively collects test files from a given directory. - * @param {string} dir_path - The directory path to search for `.js` files. - * @returns {Promise} A promise that resolves to an array of absolute file paths. - */ -async function collect_test_files(dir_path) { - // Resolve path relative to current working directory - const absolute_path = resolve(process.cwd(), dir_path); - const entries = await readdir(absolute_path, { withFileTypes: true }); - const files = []; - - for (const entry of entries) { - const full_path = join(absolute_path, entry.name); - // Skip node_modules directory - if (entry.isDirectory() && entry.name === 'node_modules') { - continue; - } - if (entry.isDirectory()) { - files.push(...await collect_test_files(full_path)); - } else if (entry.isFile() && entry.name.endsWith('.js') && !entry.name.endsWith('.config.js')) { - // Only include files that export a test object - if (await has_test_exports(full_path)) { - files.push(full_path); - } - } - } - return files; -} - -/** - * Runs all tests for a given test file's exported `test` object. - * @param {string} file_path - The path to the test file. - * @returns {Promise<{ passed: number; failed: number; skipped: number; time: number }>} Result summary. - */ -async function run_test_file(file_path) { - const start_time = process.hrtime.bigint(); - - try { - const module = await import(`file://${file_path}`); - const { test } = module; - - if (typeof test.setup === 'function') { - try { - await test.setup(); - } catch (error) { - console.error(`❌ Setup failed for ${file_path}:`, error); - return { passed: 0, failed: 1, skipped: test.cases.length - 1, time: 0 }; - } - } - - let passed = 0, failed = 0, skipped = 0; - - for (const test_case of test.cases) { - if (test_case.skip) { - console.log(`⏭️ [${file_path}] Skipping "${test_case.name}"`); - skipped++; - continue; - } - - if (typeof test.before_all === 'function') { - await test.before_all.call(test_case); - } - - if (typeof test_case.before === 'function') { - await test_case.before.call(test_case); - } - - try { - await test_case.assert.call(test_case, assert); - console.log(`✅ [${file_path}] "${test_case.name}" passed.`); - passed++; - } catch (error) { - console.error(`❌ [${file_path}] "${test_case.name}" failed:`, error); - failed++; - - // Break early if failFast is enabled - if (test.failFast) break; - } finally { - if (typeof test_case.after === 'function') { - await test_case.after.call(test_case); - } - } - } - - // Run teardown if present - if (typeof test.teardown === 'function') { - await test.teardown(); - } - - const end_time = process.hrtime.bigint(); - const duration_ms = Number(end_time - start_time) / 1_000_000; - - return { passed, failed, skipped, time: duration_ms }; - } catch (error) { - console.error(`❌ Failed to load test file ${file_path}:`, error); - return { passed: 0, failed: 1, skipped: 0, time: 0 }; - } -} - -/** - * Main entry point: runs all tests in the specified directory. - */ -(async () => { - const dir_path = process.argv[2]; - if (!dir_path) { - console.error("Usage: node run.js path/to/test/directory"); - process.exit(1); - } - - try { - const dir_stats = await stat(dir_path); - if (!dir_stats.isDirectory()) { - console.error("Provided path is not a directory."); - process.exit(1); - } - - console.log(`🔍 Collecting test files from: ${dir_path}`); - const test_files = await collect_test_files(dir_path); - - if (test_files.length === 0) { - console.warn("⚠️ No test files found!"); - process.exit(0); - } - - console.log(`🚀 Running ${test_files.length} test files...\n`); - - const start_time = process.hrtime.bigint(); - let total_passed = 0, total_failed = 0, total_skipped = 0; - - for (const file of test_files) { - const { passed, failed, skipped, time } = await run_test_file(file); - total_passed += passed; - total_failed += failed; - total_skipped += skipped; - - if (time > 0) { - console.log(`⏱️ ${file}: ${time.toFixed(2)}ms\n`); - } - } - - const end_time = process.hrtime.bigint(); - const total_time = Number(end_time - start_time) / 1_000_000; - - console.log('\n📊 Test Summary:'); - console.log(`✅ Passed: ${total_passed}`); - console.log(`❌ Failed: ${total_failed}`); - console.log(`⏭️ Skipped: ${total_skipped}`); - console.log(`⏱️ Time: ${total_time.toFixed(2)}ms`); - - if (total_failed > 0) { - process.exit(1); - } - } catch (error) { - console.error("❌ Fatal error:", error); - process.exit(1); - } -})(); \ No newline at end of file diff --git a/smart-view-2/README.md b/smart-view-2/README.md deleted file mode 100644 index ba61420c..00000000 --- a/smart-view-2/README.md +++ /dev/null @@ -1,237 +0,0 @@ -# Smart View - -Smart View is a flexible and powerful library for rendering dynamic settings interfaces in JavaScript applications. It provides a consistent API for creating and managing settings across different environments, such as web browsers and Obsidian plugins. - -## Features - -- Render various types of setting components (text, dropdown, toggle, etc.) -- Support for custom adapters to work in different environments -- Dynamic rendering based on conditions -- Markdown rendering support -- Icon support -- Extensible architecture - -## Installation - -To install Smart View, use npm: - -```bash -npm install smart-view -``` - -## Usage - -### Basic Setup - -First, import and initialize Smart View with an appropriate adapter: - -```js -import { SmartView } from 'smart-view'; -import { SmartViewNodeAdapter } from 'smart-view/adapters/node'; - -const smartView = new SmartView({ - adapter: SmartViewNodeAdapter -}); -``` - -### Rendering Settings - -Smart View uses a three-step process for rendering settings: - -1. Build HTML for setting components -2. Render the component -3. Post-process the rendered component - -#### Step 1: Build HTML - -The `build_html` function generates the HTML string for a component: - -```js -async function build_html(scope, opts = {}) { - // Generate and return HTML string - return html; -} -``` - -#### Step 2: Render - -The `render` function builds the HTML and post-processes it: - -```js -async function render(scope, opts = {}) { - let html = await build_html.call(this, scope, opts); - const frag = this.create_doc_fragment(html); - return await post_process.call(this, scope, frag, opts); -} -``` - -#### Step 3: Post-process - -The `post_process` function adds event listeners and performs other necessary operations: - -```js -async function post_process(scope, frag, opts = {}) { - // Add event listeners, perform additional operations - return frag; -} -``` - -### Use of Document Fragments - -Smart View utilizes document fragments for efficient DOM manipulation. A document fragment is a lightweight container for holding DOM nodes before they are inserted into the main document. This approach offers several benefits: - -1. **Performance**: When you make changes to a document fragment, it doesn't cause reflow or repaint of the main document. This can significantly improve performance, especially when making multiple DOM changes. - -2. **Atomic Updates**: All the changes made to a document fragment can be applied to the main document in a single operation, reducing the number of reflows and repaints. - -3. **Memory Efficiency**: Document fragments exist in memory and not as part of the main DOM tree, making them more memory-efficient for temporary storage of DOM elements. - -In Smart View, document fragments are created using the `create_doc_fragment` method: - -```js -const frag = this.create_doc_fragment(html); -``` - -This fragment is then used in the `post_process` step, where event listeners and other modifications can be applied efficiently before the fragment is inserted into the main document. - -#### Document Fragment Gotchas - -While document fragments are powerful, there are some limitations to be aware of: - -1. **No Access to Attributes**: The document fragment itself doesn't have access to attributes that are typically available on DOM elements. - -2. **No Dataset Property**: The `dataset` property, which provides access to custom data attributes, is not available on the document fragment itself. - -It's important to note that these limitations apply only to the outer-most document fragment container, not to the individual elements within the fragment. Elements inside the fragment retain all their normal properties and attributes. For example: - -```js -const frag = this.create_doc_fragment('
Content
'); - -// This won't work (undefined): -console.log(frag.dataset.custom); - -// But this will work: -const div = frag.querySelector('div'); -console.log(div.dataset.custom); // Outputs: "value" -``` - -In Smart View, we handle these limitations by ensuring that operations that need to access attributes or dataset properties are performed on the individual elements within the fragment, rather than on the fragment itself. This allows us to take full advantage of the performance benefits of document fragments while still maintaining access to all necessary element properties. - -### Creating Settings - -You can create settings programmatically or directly in HTML: - -Programmatically: -```js -const settingConfig = { - setting: 'user.email', - type: 'text', - name: 'Email Address', - description: 'Enter your email address' -}; - -const settingHtml = smartView.render_setting_html(settingConfig); -container.innerHTML += settingHtml; -await smartView.render_setting_components(container); -``` - -In HTML: -```html -
-
-``` - -Then render: -```js -const container = document.getElementById('settings-container'); -await smartView.render_setting_components(container); -``` - -## API Reference - -### SmartView Class - -#### Constructor - -- `new SmartView(options)` - - `options.adapter`: The adapter class to use for rendering - -#### Methods - -- `render_setting_components(container, options)`: Renders all setting components within a container -- `render_setting_component(settingElement, options)`: Renders a single setting component -- `render_setting_html(settingConfig)`: Generates HTML for a setting component -- `get_by_path(obj, path)`: Gets a value from an object by path -- `set_by_path(obj, path, value)`: Sets a value in an object by path -- `delete_by_path(obj, path)`: Deletes a value from an object by path -- `escape_html(str)`: Escapes HTML special characters in a string -- `add_toggle_listeners(fragment, callback)`: Adds toggle listeners to elements with data-toggle attribute -- `validate_setting(scope, opts, settingKey, settingConfig)`: Validates the setting config and determines if it should be rendered -- `create_doc_fragment(html)`: Creates a document fragment from an HTML string - -### Adapters - -Smart View uses adapters to work in different environments. Two built-in adapters are provided: - -- `SmartViewNodeAdapter`: For use in Node.js environments -- `SmartViewObsidianAdapter`: For use in Obsidian plugins - -You can create custom adapters by extending the `SmartViewAdapter` class and implementing the required methods. - -## Setting Types - -Smart View supports various setting types: - -- `text`: Single-line text input -- `password`: Password input -- `number`: Numeric input -- `dropdown`: Dropdown select -- `toggle`: On/off toggle switch -- `textarea`: Multi-line text input -- `button`: Clickable button -- `folder`: Folder selection -- `text-file`: Text file selection - -## Custom Adapters - -To create a custom adapter for a specific environment: - -1. Create a new class that extends `SmartViewAdapter` -2. Implement the required methods (e.g., `get_icon_html`, `render_markdown`) -3. Pass your custom adapter to the SmartView constructor - -Example: - -```js -class MyCustomAdapter extends SmartViewAdapter { - get_icon_html(icon_name) { - // Custom implementation - } - - async render_markdown(markdown, scope) { - // Custom implementation - } - - // Implement other required methods -} - -const smartView = new SmartView({ - adapter: MyCustomAdapter -}); -``` - -By following this pattern, you can easily extend SmartView to work in various environments while maintaining a consistent API for rendering settings. - -## Component Rendering Pattern - -Smart View uses a consistent pattern for rendering components: - -1. `build_html`: Generates the HTML string for the component -2. `render`: Calls `build_html` and `post_process` -3. `post_process`: Adds listeners and performs final operations - -This pattern, combined with the use of document fragments, allows for flexible, extensible, and efficient component rendering across different environments. diff --git a/smart-view-2/adapters/_adapter.js b/smart-view-2/adapters/_adapter.js deleted file mode 100644 index a5ae482f..00000000 --- a/smart-view-2/adapters/_adapter.js +++ /dev/null @@ -1,372 +0,0 @@ -export class SmartViewAdapter { - constructor(main) { - this.main = main; - } - // NECESSARY OVERRIDES - /** - * Retrieves the class used for settings. - * Must be overridden by subclasses to return the appropriate setting class. - * @abstract - * @returns {Function} The setting class constructor. - * @throws Will throw an error if not implemented in the subclass. - */ - get setting_class() { throw new Error("setting_class() not implemented"); } - /** - * Generates the HTML for a specified icon. - * Must be overridden by subclasses to provide the correct icon HTML. - * @abstract - * @param {string} icon_name - The name of the icon to generate HTML for. - * @returns {string} The HTML string representing the icon. - * @throws Will throw an error if not implemented in the subclass. - */ - get_icon_html(icon_name) { throw new Error("get_icon_html() not implemented"); } - /** - * Renders Markdown content within a specific scope. - * Must be overridden by subclasses to handle Markdown rendering appropriately. - * @abstract - * @param {string} markdown - The Markdown content to render. - * @param {object|null} [scope=null] - The scope within which to render the Markdown. - * @returns {Promise} A promise that resolves when rendering is complete. - * @throws Will throw an error if not implemented in the subclass. - */ - async render_markdown(markdown, scope=null) { throw new Error("render_markdown() not implemented"); } - /** - * Opens a specified URL. - * Should be overridden by subclasses to define how URLs are opened. - * @abstract - * @param {string} url - The URL to open. - */ - open_url(url) { throw new Error("open_url() not implemented"); } - /** - * Handles the selection of a folder by invoking the folder selection dialog and updating the setting. - * @abstract - * @param {string} setting - The path of the setting being modified. - * @param {string} value - The current value of the setting. - * @param {HTMLElement} elm - The HTML element associated with the setting. - * @param {object} scope - The current scope containing settings and actions. - */ - handle_folder_select(path, value, elm, scope) { throw new Error("handle_folder_select not implemented"); } - /** - * Handles the selection of a file by invoking the file selection dialog and updating the setting. - * @abstract - * @param {string} setting - The path of the setting being modified. - * @param {string} value - The current value of the setting. - * @param {HTMLElement} elm - The HTML element associated with the setting. - * @param {object} scope - The current scope containing settings and actions. - */ - handle_file_select(path, value, elm, scope) { throw new Error("handle_file_select not implemented"); } - /** - * Performs actions before a setting is changed, such as clearing notices and updating the UI. - * @abstract - * @param {string} setting - The path of the setting being changed. - * @param {*} value - The new value for the setting. - * @param {HTMLElement} elm - The HTML element associated with the setting. - * @param {object} scope - The current scope containing settings and actions. - */ - pre_change(path, value, elm) { console.warn("pre_change() not implemented"); } - /** - * Performs actions after a setting is changed, such as updating UI elements. - * @abstract - * @param {string} setting - The path of the setting that was changed. - * @param {*} value - The new value for the setting. - * @param {HTMLElement} elm - The HTML element associated with the setting. - * @param {object} changed - Additional information about the change. - */ - post_change(path, value, elm) { console.warn("post_change() not implemented"); } - /** - * Reverts a setting to its previous value in case of validation failure or error. - * @abstract - * @param {string} setting - The path of the setting to revert. - * @param {HTMLElement} elm - The HTML element associated with the setting. - * @param {object} scope - The current scope containing settings. - */ - revert_setting(path, elm, scope) { console.warn("revert_setting() not implemented"); } - // DEFAULT IMPLEMENTATIONS (may be overridden) - get setting_renderers() { - return { - text: this.render_text_component, - string: this.render_text_component, - password: this.render_password_component, - number: this.render_number_component, - dropdown: this.render_dropdown_component, - toggle: this.render_toggle_component, - textarea: this.render_textarea_component, - button: this.render_button_component, - remove: this.render_remove_component, - folder: this.render_folder_select_component, - "text-file": this.render_file_select_component, - file: this.render_file_select_component, - html: this.render_html_component, - }; - } - - async render_setting_component(elm, opts={}) { - elm.innerHTML = ""; - const path = elm.dataset.setting; - const scope = opts.scope || this.main.main; - try { - let value = elm.dataset.value ?? this.main.get_by_path(scope.settings, path); - if (typeof value === 'undefined' && typeof elm.dataset.default !== 'undefined') { - value = elm.dataset.default; - if(typeof value === 'string') value = value.toLowerCase() === 'true' ? true : value === 'false' ? false : value; - this.main.set_by_path(scope.settings, path, value); - } - - const renderer = this.setting_renderers[elm.dataset.type]; - if (!renderer) { - console.warn(`Unsupported setting type: ${elm.dataset.type}`); - return elm; - } - - const setting = renderer.call(this, elm, path, value, scope); - - if (elm.dataset.name) setting.setName(elm.dataset.name); - if (elm.dataset.description) { - const frag = this.main.create_doc_fragment(`${elm.dataset.description}`); - setting.setDesc(frag); - } - if (elm.dataset.tooltip) setting.setTooltip(elm.dataset.tooltip); - - this.add_button_if_needed(setting, elm, path, scope); - this.handle_disabled_and_hidden(elm); - return elm; - } catch(e) { - console.error({path, elm}); - console.error(e); - } - } - - render_dropdown_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - let options; - if (elm.dataset.optionsCallback) { - console.log(`getting options callback: ${elm.dataset.optionsCallback}`); - const opts_callback = this.main.get_by_path(scope, elm.dataset.optionsCallback); - if(typeof opts_callback === "function") options = opts_callback(); - else console.warn(`optionsCallback is not a function: ${elm.dataset.optionsCallback}`, scope); - } - - if (!options || !options.length) { - options = this.get_dropdown_options(elm); - } - - smart_setting.addDropdown(dropdown => { - if (elm.dataset.required) dropdown.inputEl.setAttribute("required", true); - options.forEach(option => { - const opt = dropdown.addOption(option.value, option.name ?? option.value); - opt.selected = (option.value === value); - }); - dropdown.onChange((value) => { - this.handle_on_change(path, value, elm, scope); - }); - dropdown.setValue(value); - }); - - return smart_setting; - } - - render_text_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addText(text => { - text.setPlaceholder(elm.dataset.placeholder || ""); - if (value) text.setValue(value); - let debounceTimer; - if (elm.dataset.button) { - smart_setting.addButton(button => { - button.setButtonText(elm.dataset.button); - button.onClick(async () => this.handle_on_change(path, text.getValue(), elm, scope)); - }); - } else { - text.onChange(async (value) => { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => this.handle_on_change(path, value.trim(), elm, scope), 2000); - }); - } - }); - return smart_setting; - } - - render_password_component(elm, path, value, scope ) { - const smart_setting = new this.setting_class(elm); - smart_setting.addText(text => { - text.inputEl.type = "password"; - text.setPlaceholder(elm.dataset.placeholder || ""); - if (value) text.setValue(value); - let debounceTimer; - text.onChange(async (value) => { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => this.handle_on_change(path, value, elm, scope), 2000); - }); - }); - return smart_setting; - } - - render_number_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addText(number => { - number.inputEl.type = "number"; - number.setPlaceholder(elm.dataset.placeholder || ""); - if (typeof value !== 'undefined') number.inputEl.value = parseInt(value); - number.inputEl.min = elm.dataset.min || 0; - if (elm.dataset.max) number.inputEl.max = elm.dataset.max; - let debounceTimer; - number.onChange(async (value) => { - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => this.handle_on_change(path, parseInt(value), elm, scope), 2000); - }); - }); - return smart_setting; - } - - render_toggle_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addToggle(toggle => { - let checkbox_val = value ?? true; - if (typeof checkbox_val === 'string') { - checkbox_val = checkbox_val.toLowerCase() === 'true'; - } - toggle.setValue(checkbox_val); - toggle.onChange(async (value) => this.handle_on_change(path, value, elm, scope)); - }); - return smart_setting; - } - - render_textarea_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addTextArea(textarea => { - textarea.setPlaceholder(elm.dataset.placeholder || ""); - textarea.setValue(value || ""); - let debounceTimer; - textarea.onChange(async (value) => { - value = value.split("\n").map(v => v.trim()).filter(v => v); - clearTimeout(debounceTimer); - debounceTimer = setTimeout(() => this.handle_on_change(path, value, elm, scope), 2000); - }); - }); - return smart_setting; - } - - render_button_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addButton(button => { - button.setButtonText(elm.dataset.btnText || elm.dataset.name); - button.onClick(async () => { - if (elm.dataset.confirm && !confirm(elm.dataset.confirm)) return; - if (elm.dataset.href) this.open_url(elm.dataset.href); - if (elm.dataset.callback) { - const callback = this.main.get_by_path(scope, elm.dataset.callback); - if (callback) callback(path, value, elm, scope); - } - }); - }); - return smart_setting; - } - - render_remove_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addButton(button => { - button.setButtonText(elm.dataset.btnText || elm.dataset.name || "Remove"); - button.onClick(async () => { - this.main.delete_by_path(scope.settings, path); - if (elm.dataset.callback) { - const callback = this.main.get_by_path(scope, elm.dataset.callback); - if (callback) callback(path, value, elm, scope); - } - }); - }); - return smart_setting; - } - - render_folder_select_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addFolderSelect(folder_select => { - folder_select.setPlaceholder(elm.dataset.placeholder || ""); - if (value) folder_select.setValue(value); - folder_select.inputEl.closest('div').addEventListener("click", () => { - this.handle_folder_select(path, value, elm, scope); - }); - }); - return smart_setting; - } - - render_file_select_component(elm, path, value, scope) { - const smart_setting = new this.setting_class(elm); - smart_setting.addFileSelect(file_select => { - file_select.setPlaceholder(elm.dataset.placeholder || ""); - if (value) file_select.setValue(value); - file_select.inputEl.closest('div').addEventListener("click", () => { - this.handle_file_select(path, value, elm, scope); - }); - }); - return smart_setting; - } - - render_html_component(elm, path, value, scope) { - // render html into a div - elm.innerHTML = value; - return elm; - } - - add_button_if_needed(smart_setting, elm, path, scope) { - if (elm.dataset.btn) { - smart_setting.addButton(button => { - button.setButtonText(elm.dataset.btn); - button.inputEl.addEventListener("click", (e) => { - if (elm.dataset.btnCallback && typeof scope[elm.dataset.btnCallback] === "function") { - if(elm.dataset.btnCallbackArg) scope[elm.dataset.btnCallback](elm.dataset.btnCallbackArg); - else scope[elm.dataset.btnCallback](path, null, smart_setting, scope); - } else if (elm.dataset.btnHref) { - this.open_url(elm.dataset.btnHref); - } else if (elm.dataset.callback && typeof this.main.get_by_path(scope, elm.dataset.callback) === "function") { - this.main.get_by_path(scope, elm.dataset.callback)(path, null, smart_setting, scope); - } else if (elm.dataset.href) { - this.open_url(elm.dataset.href); - } else { - console.error("No callback or href found for button."); - } - }); - if (elm.dataset.btnDisabled || (elm.dataset.disabled && elm.dataset.btnDisabled !== "false")) { - button.inputEl.disabled = true; - } - }); - } - } - - handle_disabled_and_hidden(elm) { - if (elm.dataset.disabled && elm.dataset.disabled !== "false") { - elm.classList.add("disabled"); - elm.querySelector("input, select, textarea, button").disabled = true; - } - if (elm.dataset.hidden && elm.dataset.hidden !== "false") { - elm.style.display = "none"; - } - } - - get_dropdown_options(elm) { - return Object.entries(elm.dataset).reduce((acc, [k, v]) => { - if (!k.startsWith('option')) return acc; - const [value, name] = v.split("|"); - acc.push({ value, name: name || value }); - return acc; - }, []); - } - - handle_on_change(path, value, elm, scope) { - this.pre_change(path, value, elm, scope); - if(elm.dataset.validate){ - const valid = this[elm.dataset.validate](path, value, elm, scope); - if(!valid){ - elm.querySelector('.setting-item').style.border = "2px solid red"; - this.revert_setting(path, elm, scope); - return; - } - } - this.main.set_by_path(scope.settings, path, value); - if(elm.dataset.callback){ - const callback = this.main.get_by_path(scope, elm.dataset.callback); - if(callback) callback(path, value, elm, scope); - } - this.post_change(path, value, elm, scope); - } - -} \ No newline at end of file diff --git a/smart-view-2/adapters/node.js b/smart-view-2/adapters/node.js deleted file mode 100644 index b5af6c28..00000000 --- a/smart-view-2/adapters/node.js +++ /dev/null @@ -1,250 +0,0 @@ -import { SmartViewAdapter } from "./_adapter.js"; -import * as lucide from 'lucide-static'; - -export class SmartViewNodeAdapter extends SmartViewAdapter { - /** - * @inheritdoc - * Retrieves custom setting class. - */ - get setting_class() { return Setting; } - /** - * @inheritdoc - * Retrieves the Lucide icon for the given icon name. - */ - get_icon_html(icon_name) { return lucide[icon_name]; } - /** - * Check if the given event is a "mod" event, i.e., if a control or meta key is pressed. - * This serves as a fallback behavior for environments without Obsidian's Keymap. - * @param {Event} event - The keyboard or mouse event. - * @returns {boolean} True if the event is considered a "mod" event. - */ - is_mod_event(event) { - // On Windows/Linux, Ctrl is often the "mod" key. - // On macOS, Cmd (metaKey) is the "mod" key. - // This heuristic checks both. - return !!(event && (event.ctrlKey || event.metaKey)); - } - /** - * Renders the given markdown content. - * For a Node.js/browser environment without Obsidian's MarkdownRenderer, - * we provide a simple fallback. If you want proper markdown to HTML conversion, - * integrate a library like `marked` or `showdown`. - * - * @param {string} markdown - The markdown content. - * @param {object|null} [scope=null] - The scope in which to render the markdown. - * @returns {Promise} A promise that resolves to a DocumentFragment. - */ - async render_markdown(markdown, scope=null) { - // Basic fallback: Just escape the markdown and wrap it in a
 for display.
-    // Replace this with a proper markdown -> HTML conversion if desired.
-    const html = `
${this.main.escape_html(markdown)}
`; - return this.main.create_doc_fragment(html); - } -} - -export class Setting { - constructor(element) { - this.element = element; - this.container = this.createSettingItemContainer(); - } - add_text(configurator) { - const controlContainer = this.container.querySelector('.control'); - const textInput = document.createElement('input'); - textInput.type = 'text'; - textInput.spellcheck = false; - controlContainer.appendChild(textInput); - configurator({ - inputEl: textInput, - setPlaceholder: (placeholder) => textInput.placeholder = placeholder, - setValue: (value) => textInput.value = value, - onChange: (callback) => textInput.addEventListener('change', () => callback(textInput.value)), - getValue: () => textInput.value - }); - this.element.appendChild(this.container); - } - add_dropdown(configurator) { - const controlContainer = this.container.querySelector('.control'); - const select = document.createElement('select'); - controlContainer.appendChild(select); - configurator({ - inputEl: select, - addOption: (value, name, selected = false) => { - const option = document.createElement('option'); - option.value = value; - option.textContent = name; - select.appendChild(option); - if (selected) { - option.selected = true; - option.classList.add("selected"); - } - if (value === "") option.disabled = true; - return option; - }, - onChange: (callback) => select.addEventListener('change', () => callback(select.value)), - setValue: (value) => select.value = value - }); - this.element.appendChild(this.container); - } - add_button(configurator) { - const controlContainer = this.container.querySelector('.control'); - const button = document.createElement('button'); - controlContainer.appendChild(button); - configurator({ - inputEl: button, - setButtonText: (text) => button.textContent = text, - onClick: (callback) => button.addEventListener('click', callback) - }); - this.element.appendChild(this.container); - } - create_setting_item_container() { - const container = document.createElement('div'); - container.classList.add('setting-item'); - const infoContainer = document.createElement('div'); - infoContainer.classList.add('info'); - container.appendChild(infoContainer); - // Placeholders for name and description - const namePlaceholder = document.createElement('div'); - namePlaceholder.classList.add('name'); - infoContainer.appendChild(namePlaceholder); - // const tagPlaceholder = document.createElement('div'); - // tagPlaceholder.classList.add('tag'); - // infoContainer.appendChild(tagPlaceholder); - // const descPlaceholder = document.createElement('div'); - // descPlaceholder.classList.add('description'); - // infoContainer.appendChild(descPlaceholder); - const controlContainer = document.createElement('div'); - controlContainer.classList.add('control'); - container.appendChild(controlContainer); - return container; - } - add_toggle(configurator) { - const controlContainer = this.container.querySelector('.control'); - const checkboxContainer = document.createElement('div'); - checkboxContainer.classList.add('checkbox-container'); - controlContainer.appendChild(checkboxContainer); - const checkbox = document.createElement('input'); - checkbox.type = 'checkbox'; - checkbox.tabIndex = 0; - checkboxContainer.appendChild(checkbox); - configurator({ - setValue: (value) => { - checkbox.checked = value; - checkbox.value = value; - checkboxContainer.classList.toggle('is-enabled', value); - }, - onChange: (callback) => checkbox.addEventListener('change', () => { - callback(checkbox.checked); - checkboxContainer.classList.toggle('is-enabled', checkbox.checked); - }) - }); - this.element.appendChild(this.container); - } - add_text_area(configurator) { - const controlContainer = this.container.querySelector('.control'); - const textarea = document.createElement('textarea'); - textarea.spellcheck = false; - controlContainer.appendChild(textarea); - configurator({ - inputEl: textarea, - setPlaceholder: (placeholder) => textarea.placeholder = placeholder, - setValue: (value) => textarea.value = value, - onChange: (callback) => textarea.addEventListener('change', () => callback(textarea.value)) - }); - this.element.appendChild(this.container); - } - add_folder_select(configurator) { - const container = this.container.querySelector('.control'); - const folder_select = document.createElement('div'); - folder_select.classList.add('folder-select'); - container.appendChild(folder_select); - const currentFolder = document.createElement('span'); - // currentFolder.type = 'text'; - currentFolder.classList.add('current'); - container.appendChild(currentFolder); - const browse_btn = document.createElement('button'); - browse_btn.textContent = 'Browse'; - browse_btn.classList.add('browse-button'); - container.appendChild(browse_btn); - configurator({ - inputEl: currentFolder, - setPlaceholder: (placeholder) => currentFolder.placeholder = placeholder, - setValue: (value) => currentFolder.innerText = value, - }); - this.element.appendChild(this.container); - } - add_file_select(configurator) { - const container = this.container.querySelector('.control'); - const file_select = document.createElement('div'); - file_select.classList.add('file-select'); - container.appendChild(file_select); - const current_file = document.createElement('span'); - // current_file.type = 'text'; - current_file.classList.add('current'); - container.appendChild(current_file); - const browse_btn = document.createElement('button'); - browse_btn.textContent = 'Browse'; - browse_btn.classList.add('browse-button'); - container.appendChild(browse_btn); - configurator({ - inputEl: current_file, - setPlaceholder: (placeholder) => current_file.placeholder = placeholder, - setValue: (value) => current_file.innerText = value, - }); - this.element.appendChild(this.container); - } - set_name(name) { - const nameElement = this.container.querySelector('.name'); - if (nameElement) { - nameElement.innerHTML = name; - } else { - // Create the element if it doesn't exist - const newNameElement = document.createElement('div'); - newNameElement.classList.add('name'); - newNameElement.innerHTML = name; - // this.element.appendChild(newNameElement); - const info_container = this.container.querySelector('.info'); - info_container.prepend(newNameElement); - } - } - set_desc(description) { - let desc_element = this.container.querySelector('.description'); - if (!desc_element) { - // Create the element if it doesn't exist - desc_element = document.createElement('div'); - desc_element.classList.add('description'); - const info_container = this.container.querySelector('.info'); - info_container.appendChild(desc_element); - } - - // Clear existing content - desc_element.innerHTML = ''; - - if (description instanceof DocumentFragment) { - desc_element.appendChild(description); - } else { - desc_element.innerHTML = description; - } - } - set_tooltip(tooltip) { - const elm = this.container; - const control_element = elm.querySelector('.control'); - control_element.setAttribute('title', tooltip); - elm.setAttribute('title', tooltip); - const tooltip_container = document.createElement('div'); - tooltip_container.classList.add('tooltip'); - tooltip_container.innerHTML = tooltip; - elm.insertAdjacentElement('afterend', tooltip_container); - } - // aliases - addText(configurator) { return this.add_text(configurator); } - addDropdown(configurator) { return this.add_dropdown(configurator); } - addButton(configurator) { return this.add_button(configurator); } - createSettingItemContainer() { return this.create_setting_item_container(); } - addToggle(configurator) { return this.add_toggle(configurator); } - addTextArea(configurator) { return this.add_text_area(configurator); } - addFolderSelect(configurator) { return this.add_folder_select(configurator); } - addFileSelect(configurator) { return this.add_file_select(configurator); } - setName(name) { return this.set_name(name); } - setDesc(description) { return this.set_desc(description); } - setTooltip(tooltip) { return this.set_tooltip(tooltip); } -} diff --git a/smart-view-2/adapters/obsidian.js b/smart-view-2/adapters/obsidian.js deleted file mode 100644 index 9a157edb..00000000 --- a/smart-view-2/adapters/obsidian.js +++ /dev/null @@ -1,106 +0,0 @@ -import { SmartViewAdapter } from "./_adapter.js"; -import { - Setting, - MarkdownRenderer, - Component, - getIcon, - Keymap -} from "obsidian"; - -export class SmartViewObsidianAdapter extends SmartViewAdapter { - get setting_class() { return Setting; } - - open_url(url) { window.open(url); } - - async render_file_select_component(elm, path, value) { - return super.render_text_component(elm, path, value); - } - - async render_folder_select_component(elm, path, value) { - return super.render_text_component(elm, path, value); - } - - async render_markdown(markdown, scope) { - const component = scope.env.smart_connections_plugin.connections_view; - if(!scope) return console.warn("Scope required for rendering markdown in Obsidian adapter"); - // MarkdownRenderer attempts to get container parent and throws error if not present - // So wrap in extra div to act as parent and render into inner div - const frag = this.main.create_doc_fragment("
"); - const container = frag.querySelector(".inner"); - try{ - await MarkdownRenderer.render( - scope.env.plugin.app, - markdown, - container, - scope?.file_path || "", - component || new Component() - ); - }catch(e){ - console.warn("Error rendering markdown in Obsidian adapter", e); - } - return frag; - } - get_icon_html(name) { return getIcon(name).outerHTML; } - // Obsidian Specific - is_mod_event(event) { return Keymap.isModEvent(event); } -} - -// export class SmartViewObsidianAdapter extends SmartViewNodeAdapter { -// constructor(main) { -// super(main); -// this._cached_proxy = null; // To store the cached proxy -// } - -// get setting_class() { -// // If the proxy has been cached, return it -// if (this._cached_proxy) { -// return this._cached_proxy; -// } - -// const original_class = this.main.env.plugin.obsidian.Setting; - -// // Capture `this` context for using inside the proxy -// const adapter = this; - -// // Create and cache the proxy for the class -// this._cached_proxy = new Proxy(original_class, { -// construct(target, args) { -// // Create a new instance of the class -// const instance = new target(...args); - -// // Use the `create_snake_case_wrapper` method from `adapter` -// return adapter.create_snake_case_wrapper(instance); -// }, - -// // Delegate any other static properties or methods on the class -// get(target, prop, receiver) { -// return Reflect.get(target, prop, receiver); -// } -// }); - -// return this._cached_proxy; -// } - -// // Create a proxy wrapper to handle both camelCase and snake_case methods on instances -// create_snake_case_wrapper(instance) { -// return new Proxy(instance, { -// get(target, prop) { -// // Avoid recursion by checking if the property exists before further processing -// if (prop in target) { -// return target[prop]; -// } - -// // Convert snake_case to camelCase (if prop doesn't already exist) -// const camel_case_prop = prop.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); - -// // Return the camelCase method if it exists -// if (camel_case_prop in target && typeof target[camel_case_prop] === 'function') { -// return target[camel_case_prop].bind(target); -// } - -// // Fallback to undefined or return undefined explicitly if method not found -// return undefined; -// } -// }); -// } -// } diff --git a/smart-view-2/package.json b/smart-view-2/package.json deleted file mode 100644 index 080522e6..00000000 --- a/smart-view-2/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "smart-view", - "author": "Brian Joseph Petro (🌴 Brian)", - "license": "MIT", - "version": "1.0.0", - "type": "module", - "description": "", - "main": "smart_view.js", - "repository": { - "type": "git", - "url": "brianpetro/jsbrains" - }, - "bugs": { - "url": "https://github.com/brianpetro/jsbrains/issues" - }, - "scripts": { - "test": "npx ava --verbose" - }, - "homepage": "https://jsbrains.org", - "dependencies": { - "lucide-static": "^0.414.0" - } -} \ No newline at end of file diff --git a/smart-view-2/smart_view.js b/smart-view-2/smart_view.js deleted file mode 100644 index e5ee6b5d..00000000 --- a/smart-view-2/smart_view.js +++ /dev/null @@ -1,200 +0,0 @@ -export class SmartView { - constructor(opts={}) { - this.opts = opts; - this._adapter = null; - } - - /** - * Renders all setting components within a container. - * @param {HTMLElement} container - The container element. - * @param {Object} opts - Additional options for rendering. - * @returns {Promise} - */ - async render_setting_components(container, opts={}) { - const components = container.querySelectorAll(".setting-component"); - for (const component of components) { - await this.render_setting_component(component, opts); - } - return container; - } - - /** - * Creates a document fragment from HTML string. - * @param {string} html - The HTML string. - * @returns {DocumentFragment} - */ - create_doc_fragment(html) { - return document.createRange().createContextualFragment(html); - } - - /** - * Gets the adapter instance. - * @returns {Object} The adapter instance. - */ - get adapter() { - if(!this._adapter) { - this._adapter = new this.opts.adapter(this); - } - return this._adapter; - } - - /** - * Gets an icon (implemented in adapter). - * @param {string} icon_name - The name of the icon. - * @returns {string} The icon HTML. - */ - get_icon_html(icon_name) { return this.adapter.get_icon_html(icon_name); } - - /** - * Renders a single setting component (implemented in adapter). - * @param {HTMLElement} setting_elm - The setting element. - * @param {Object} opts - Additional options for rendering. - * @returns {Promise<*>} - */ - async render_setting_component(setting_elm, opts={}) { return await this.adapter.render_setting_component(setting_elm, opts); } - - /** - * Renders markdown content (implemented in adapter). - * @param {string} markdown - The markdown content. - * @returns {Promise<*>} - */ - async render_markdown(markdown, scope=null) { return await this.adapter.render_markdown(markdown, scope); } - - /** - * Gets a value from an object by path. - * @param {Object} obj - The object to search in. - * @param {string} path - The path to the value. - * @returns {*} - */ - get_by_path(obj, path) { return get_by_path(obj, path); } - - /** - * Sets a value in an object by path. - * @param {Object} obj - The object to modify. - * @param {string} path - The path to set the value. - * @param {*} value - The value to set. - */ - set_by_path(obj, path, value) { set_by_path(obj, path, value); } - - /** - * Deletes a value from an object by path. - * @param {Object} obj - The object to modify. - * @param {string} path - The path to delete. - */ - delete_by_path(obj, path) { delete_by_path(obj, path); } - - /** - * Escapes HTML special characters in a string. - * @param {string} str - The string to escape. - * @returns {string} The escaped string. - */ - escape_html(str) { - if(typeof str !== 'string') return str; - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - ; - } - - /** - * Renders HTML for a setting component based on its configuration. - * @param {Object} setting_config - The configuration object for the setting. - * @returns {string} The rendered HTML string. - */ - render_setting_html(setting_config) { - if(setting_config.type === 'html') return setting_config.value; - const attributes = Object.entries(setting_config) - .map(([attr, value]) => { - if (attr.includes('class')) return ''; // ignore class attribute - if (typeof value === 'number') return `data-${attr.replace(/_/g, '-')}=${value}`; - return `data-${attr.replace(/_/g, '-')}="${value}"`; - }) - .join('\n') - ; - return `
`; - } - - /** - * Validates the setting config and determines if the setting should be rendered. - * @param {Object} scope - The scope object. - * @param {Object} opts - The options object. - * @param {string} setting_key - The key of the setting. - * @param {Object} setting_config - The config of the setting. - * @returns {boolean} True if the setting should be rendered, false otherwise. - */ - validate_setting(scope, opts, setting_key, setting_config) { - /** - * if settings_keys is provided, skip setting if not in settings_keys - */ - if (opts.settings_keys && !opts.settings_keys.includes(setting_key)) return false; - /** - * Conditional rendering - * @name settings_config.conditional - * @type {function} - * @param {object} scope - The scope object. - * @returns {boolean} - True if the setting should be rendered, false otherwise. - */ - if (typeof setting_config.conditional === 'function' && !setting_config.conditional(scope)) return false; - return true; - } - - /** - * Handles the smooth transition effect when opening overlays. - * @param {HTMLElement} overlay_container - The overlay container element. - */ - on_open_overlay(overlay_container) { - overlay_container.style.transition = "background-color 0.5s ease-in-out"; - overlay_container.style.backgroundColor = "var(--bold-color)"; - setTimeout(() => { overlay_container.style.backgroundColor = ""; }, 500); - } - - /** - * Renders settings components based on the provided settings configuration. - * @param {Object} scope - The scope object. - * @param {Object} settings_config - The settings configuration object. - * @returns {Promise} The rendered settings fragment. - */ - async render_settings(settings_config, opts={}) { - const scope = opts.scope || {}; - const html = Object.entries(settings_config).map(([setting_key, setting_config]) => { - if (!setting_config.setting) setting_config.setting = setting_key; - if(this.validate_setting(scope, opts, setting_key, setting_config)) return this.render_setting_html(setting_config); - return ''; - }).join('\n'); - const frag = this.create_doc_fragment(`
${html}
`); - return await this.render_setting_components(frag, opts); - } -} - - -function get_by_path(obj, path) { - if(!path) return ''; - const keys = path.split('.'); - const finalKey = keys.pop(); - const instance = keys.reduce((acc, key) => acc && acc[key], obj); - // Check if the last key is a method and bind to the correct instance - if (instance && typeof instance[finalKey] === 'function') { - return instance[finalKey].bind(instance); - } - return instance ? instance[finalKey] : undefined; -} -function set_by_path(obj, path, value) { - const keys = path.split('.'); - const final_key = keys.pop(); - const target = keys.reduce((acc, key) => { - if (!acc[key] || typeof acc[key] !== 'object') { - acc[key] = {}; - } - return acc[key]; - }, obj); - target[final_key] = value; -} -function delete_by_path(obj, path) { - const keys = path.split('.'); - const finalKey = keys.pop(); - const instance = keys.reduce((acc, key) => acc && acc[key], obj); - delete instance[finalKey]; -} \ No newline at end of file