├── .config
├── .eslintignore
├── docs
├── .config
├── .prettierrc
├── public
│ ├── favicon.ico
│ ├── new-types.png
│ ├── metadata-editor.png
│ ├── open-graph-image.png
│ ├── new-types-showcase.png
│ ├── type-array-example.png
│ ├── type-color-example.png
│ ├── type-object-example.png
│ ├── type-rating-example.png
│ ├── type-select-example.png
│ ├── type-slider-example.png
│ ├── type-time-example.png
│ ├── type-toggle-example.png
│ ├── type-markdown-example.png
│ ├── property-menu-showcase.png
│ ├── property-settings-modal.png
│ ├── type-datecustom-example.png
│ ├── type-multiselect-example.png
│ ├── type-numeric-example-after.png
│ ├── metadata-editor-more-button.png
│ ├── metadata-editor-property-menu.png
│ ├── type-numeric-example-before.png
│ ├── metadata-editor-resizable-label.gif
│ ├── metadata-editor-collapsible-properties.gif
│ ├── metadata-editor-more-button-show-hidden.gif
│ ├── metadata-editor-more-button-sort-options.png
│ ├── logo.svg
│ └── bp-logo-circle.svg
├── app
│ ├── components
│ │ ├── common
│ │ │ ├── ThemeSwitcher
│ │ │ │ ├── index.tsx
│ │ │ │ ├── switcher.tsx
│ │ │ │ └── context.tsx
│ │ │ ├── EditOnGithub
│ │ │ │ └── index.tsx
│ │ │ ├── InfoButton
│ │ │ │ └── index.tsx
│ │ │ ├── GithubIcon
│ │ │ │ └── index.tsx
│ │ │ ├── Article
│ │ │ │ └── index.tsx
│ │ │ └── TocSidebar
│ │ │ │ └── index.tsx
│ │ └── ui
│ │ │ ├── skeleton.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── input.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── tooltip.tsx
│ │ │ └── button.tsx
│ ├── routes
│ │ ├── getting-started
│ │ │ ├── usage
│ │ │ │ ├── article.mdx
│ │ │ │ └── route.tsx
│ │ │ ├── installation
│ │ │ │ ├── route.tsx
│ │ │ │ └── article.mdx
│ │ │ └── introduction
│ │ │ │ ├── route.tsx
│ │ │ │ └── article.mdx
│ │ └── features
│ │ │ ├── bpjs
│ │ │ ├── (method types)
│ │ │ │ ├── import
│ │ │ │ │ ├── dataContent.mdx
│ │ │ │ │ └── type.mdx
│ │ │ │ ├── markdown
│ │ │ │ │ ├── type.mdx
│ │ │ │ │ └── options.mdx
│ │ │ │ ├── getProperty
│ │ │ │ │ ├── type.mdx
│ │ │ │ │ └── options.mdx
│ │ │ │ ├── getMetadata
│ │ │ │ │ ├── options.mdx
│ │ │ │ │ └── type.mdx
│ │ │ │ └── renderProperty
│ │ │ │ │ ├── type.mdx
│ │ │ │ │ └── options.mdx
│ │ │ └── route.tsx
│ │ │ ├── property-types
│ │ │ ├── route.tsx
│ │ │ └── article.mdx
│ │ │ ├── metadata-editor
│ │ │ ├── route.tsx
│ │ │ └── article.mdx
│ │ │ └── roadmap
│ │ │ └── article.mdx
│ ├── types.d.ts
│ ├── hooks
│ │ └── use-mobile.ts
│ ├── routes.ts
│ ├── root.tsx
│ ├── entry.server.tsx
│ ├── lib
│ │ └── utils.ts
│ └── index.css
├── README.md
├── react-router.config.ts
├── .gitignore
├── tsconfig.node.json
├── wrangler.jsonc
├── components.json
├── tsconfig.json
├── workers
│ └── app.ts
├── tsconfig.cloudflare.json
├── LICENSE
├── vite.config.ts
└── package.json
├── .npmrc
├── src
├── main.ts
├── CustomPropertyTypes
│ ├── Color
│ │ ├── registerListeners.ts
│ │ ├── renderSettings.ts
│ │ ├── index.ts
│ │ └── renderWidget.ts
│ ├── Time
│ │ ├── registerListeners.ts
│ │ ├── renderSettings.ts
│ │ ├── index.ts
│ │ └── renderWidget.ts
│ ├── DateCustom
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ ├── renderSettings.ts
│ │ └── renderWidget.ts
│ ├── Markdown
│ │ ├── registerListeners.ts
│ │ ├── renderSettings.ts
│ │ ├── index.ts
│ │ └── renderWidget.ts
│ ├── Numeric
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ └── renderSettings.ts
│ ├── Rating
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ └── renderSettings.ts
│ ├── Slider
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ ├── renderWidget.ts
│ │ └── renderSettings.ts
│ ├── Toggle
│ │ ├── registerListeners.ts
│ │ ├── renderSettings.ts
│ │ ├── index.ts
│ │ └── renderWidget.ts
│ ├── MultiSelect
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ └── renderSettings.ts
│ ├── Title
│ │ ├── renderSettings.ts
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ └── renderWidget.ts
│ ├── Created
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ ├── renderSettings.ts
│ │ └── renderWidget.ts
│ ├── index.ts
│ ├── Array
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ └── renderSettings.ts
│ ├── Object
│ │ ├── registerListeners.ts
│ │ ├── index.ts
│ │ └── renderSettings.ts
│ ├── Relation
│ │ ├── index.ts
│ │ └── renderSettings.ts
│ ├── Select
│ │ ├── index.ts
│ │ ├── registerListeners.ts
│ │ └── renderSettings.ts
│ ├── types.ts
│ └── schema.ts
├── i18next
│ └── index.ts
├── bpjs
│ ├── index.ts
│ ├── createCodeBlockProcessor.ts
│ └── createPostProcessor.ts
├── MetadataCache
│ └── index.ts
├── classes
│ ├── InputSuggest
│ │ ├── IconSuggest
│ │ │ └── index.ts
│ │ ├── PropertyTypeSuggest
│ │ │ └── index.ts
│ │ ├── FileSuggest
│ │ │ └── index.ts
│ │ ├── FolderSuggest
│ │ │ └── index.ts
│ │ ├── TagSuggest
│ │ │ └── index.ts
│ │ ├── LinkSuggest
│ │ │ └── index.ts
│ │ ├── index.ts
│ │ └── PropertySuggest
│ │ │ └── index.ts
│ ├── ConfirmationModal
│ │ └── index.ts
│ └── ColorTextComponent
│ │ └── index.ts
├── MetadataTypeManager
│ └── patchMetadataTypeManager.ts
├── lib
│ ├── constants
│ │ └── index.ts
│ └── types
│ │ ├── index.d.ts
│ │ └── oneDotNine.d.ts
├── MetadataEditor
│ ├── index.ts
│ ├── propertyEditorMenu
│ │ ├── delete.ts
│ │ ├── icon.ts
│ │ └── rename.ts
│ ├── patchMenu.ts
│ └── patchMetadataEditor
│ │ └── patchMetadataEditorProperty.ts
└── Plugin
│ └── settings.ts
├── demo-assets
├── properties.png
└── property-menu.png
├── .github
├── ISSUE_TEMPLATE
│ ├── question---discussion.md
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── release.yml
├── versions.json
├── tsconfig.node.json
├── manifest.json
├── .gitignore
├── getChangelogEntry.mjs
├── version-bump.mjs
├── .eslintrc
├── tsconfig.json
├── tsconfig.app.json
├── LICENSE
├── package.json
├── README.md
└── vite.config.ts
/.config:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/.config:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/docs/.prettierrc:
--------------------------------------------------------------------------------
1 | { "plugins": ["prettier-plugin-tailwindcss"] }
2 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { BetterProperties } from "./Plugin/plugin";
2 |
3 | export default BetterProperties;
4 |
--------------------------------------------------------------------------------
/docs/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/favicon.ico
--------------------------------------------------------------------------------
/docs/public/new-types.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/new-types.png
--------------------------------------------------------------------------------
/demo-assets/properties.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/demo-assets/properties.png
--------------------------------------------------------------------------------
/demo-assets/property-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/demo-assets/property-menu.png
--------------------------------------------------------------------------------
/docs/public/metadata-editor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor.png
--------------------------------------------------------------------------------
/docs/public/open-graph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/open-graph-image.png
--------------------------------------------------------------------------------
/docs/public/new-types-showcase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/new-types-showcase.png
--------------------------------------------------------------------------------
/docs/public/type-array-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-array-example.png
--------------------------------------------------------------------------------
/docs/public/type-color-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-color-example.png
--------------------------------------------------------------------------------
/docs/public/type-object-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-object-example.png
--------------------------------------------------------------------------------
/docs/public/type-rating-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-rating-example.png
--------------------------------------------------------------------------------
/docs/public/type-select-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-select-example.png
--------------------------------------------------------------------------------
/docs/public/type-slider-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-slider-example.png
--------------------------------------------------------------------------------
/docs/public/type-time-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-time-example.png
--------------------------------------------------------------------------------
/docs/public/type-toggle-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-toggle-example.png
--------------------------------------------------------------------------------
/docs/public/type-markdown-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-markdown-example.png
--------------------------------------------------------------------------------
/docs/public/property-menu-showcase.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/property-menu-showcase.png
--------------------------------------------------------------------------------
/docs/public/property-settings-modal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/property-settings-modal.png
--------------------------------------------------------------------------------
/docs/public/type-datecustom-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-datecustom-example.png
--------------------------------------------------------------------------------
/docs/public/type-multiselect-example.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-multiselect-example.png
--------------------------------------------------------------------------------
/docs/public/type-numeric-example-after.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-numeric-example-after.png
--------------------------------------------------------------------------------
/docs/app/components/common/ThemeSwitcher/index.tsx:
--------------------------------------------------------------------------------
1 | export { ThemeProvider, useTheme } from "./context";
2 | export { ThemeSwitcher } from "./switcher";
3 |
--------------------------------------------------------------------------------
/docs/public/metadata-editor-more-button.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor-more-button.png
--------------------------------------------------------------------------------
/docs/public/metadata-editor-property-menu.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor-property-menu.png
--------------------------------------------------------------------------------
/docs/public/type-numeric-example-before.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/type-numeric-example-before.png
--------------------------------------------------------------------------------
/docs/public/metadata-editor-resizable-label.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor-resizable-label.gif
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Better Properties Docs
2 |
3 | This is the documentation website for my plugin [Better Properties](https://github.com/unxok/obsidian-better-properties)
4 |
--------------------------------------------------------------------------------
/docs/public/metadata-editor-collapsible-properties.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor-collapsible-properties.gif
--------------------------------------------------------------------------------
/docs/public/metadata-editor-more-button-show-hidden.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor-more-button-show-hidden.gif
--------------------------------------------------------------------------------
/docs/public/metadata-editor-more-button-sort-options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/unxok/obsidian-better-properties/HEAD/docs/public/metadata-editor-more-button-sort-options.png
--------------------------------------------------------------------------------
/docs/react-router.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "@react-router/dev/config";
2 |
3 | export default {
4 | ssr: true,
5 | future: {
6 | unstable_viteEnvironmentApi: true,
7 | },
8 | } satisfies Config;
9 |
--------------------------------------------------------------------------------
/docs/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | /node_modules/
4 | *.tsbuildinfo
5 |
6 | # React Router
7 | /.react-router/
8 | /build/
9 |
10 | # Cloudflare
11 | .mf
12 | .wrangler
13 | .dev.vars*
14 | worker-configuration.d.ts
--------------------------------------------------------------------------------
/docs/app/routes/getting-started/usage/article.mdx:
--------------------------------------------------------------------------------
1 | ## Usage
2 |
3 | Stop reading the docs and go use Obsidian!
4 |
5 | Come back here if you need more information on features, or if you just want see what features are out there.
6 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Color/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Time/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/DateCustom/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Markdown/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Numeric/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Rating/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Slider/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Toggle/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/question---discussion.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Question / Discussion
3 | about: Ask a question or start a discussion that isn't a feature request or bug
4 | title: ''
5 | labels: "\U0001F64B♀️type/discussion"
6 | assignees: unxok
7 |
8 | ---
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/MultiSelect/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | _plugin: BetterProperties
6 | ) => {};
7 |
--------------------------------------------------------------------------------
/docs/app/types.d.ts:
--------------------------------------------------------------------------------
1 | import { Element, MDXContent } from "mdx/types";
2 | import { TocItem } from "rehype-mdx-toc";
3 |
4 | declare module "*.mdx" {
5 | export const toc: undefined | TocItem[];
6 | export default function MDXContent(props: MDXProps): Element;
7 | }
8 |
9 | export {};
10 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/import/dataContent.mdx:
--------------------------------------------------------------------------------
1 | #### data
2 |
3 | - For file extensions `.csv` and `.tsv` - Will be used as custom delimiter. The default for `.csv` is a comma (`,`). The default for `.tsv` is a tab character (`\t`)
4 | - For all other file extensions - Does nothing.
5 |
--------------------------------------------------------------------------------
/docs/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": ["vite.config.ts"],
4 | "compilerOptions": {
5 | "composite": true,
6 | "strict": true,
7 | "types": ["node"],
8 | "lib": ["ES2022"],
9 | "target": "ES2022",
10 | "module": "ES2022",
11 | "moduleResolution": "bundler"
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/versions.json:
--------------------------------------------------------------------------------
1 | {
2 | "0.0.1": "1.5.0",
3 | "0.1.0": "1.9.1",
4 | "0.1.1": "1.9.1",
5 | "0.1.2": "1.9.1",
6 | "0.1.3": "1.9.1",
7 | "0.1.4": "1.9.1",
8 | "0.1.5": "1.9.1",
9 | "0.1.6": "1.9.1",
10 | "0.1.7": "1.9.1",
11 | "0.1.8": "1.9.1",
12 | "0.1.9": "1.9.1",
13 | "0.1.10": "1.9.1",
14 | "0.1.11": "1.9.1",
15 | "1.0.0": "1.9.14"
16 | }
--------------------------------------------------------------------------------
/docs/app/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "~/lib/utils";
2 |
3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
10 | );
11 | }
12 |
13 | export { Skeleton };
14 |
--------------------------------------------------------------------------------
/docs/wrangler.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "node_modules/wrangler/config-schema.json",
3 | "name": "obsidian-better-properties-docs",
4 | "compatibility_date": "2025-04-04",
5 | "main": "./workers/app.ts",
6 | // "vars": {
7 | // "GITHUB_PAT": "STORED IN dev.vars",
8 | // },
9 | "observability": {
10 | "logs": {
11 | "enabled": true,
12 | },
13 | },
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
5 | "skipLibCheck": true,
6 | "module": "ESNext",
7 | "moduleResolution": "bundler",
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "noEmit": true
11 | },
12 | "include": ["vite.config.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "better-properties",
3 | "name": "Better Properties",
4 | "version": "1.0.0",
5 | "minAppVersion": "1.9.14",
6 | "description": "Adds new property types and other property-related upgrades and features.",
7 | "author": "Unxok",
8 | "authorUrl": "https://github.com/unxok",
9 | "fundingUrl": "https://buymeacoffee.com/unxok",
10 | "isDesktopOnly": false
11 | }
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Color/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 |
5 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
6 | modal,
7 | }) => {
8 | const { tabContentEl } = modal;
9 |
10 | new Setting(tabContentEl)
11 | .setName(text("common.nothingToSeeHere"))
12 | .setDesc(text("common.typeHasNoSettings"));
13 | };
14 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Time/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 |
5 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
6 | modal,
7 | }) => {
8 | const { tabContentEl } = modal;
9 |
10 | new Setting(tabContentEl)
11 | .setName(text("common.nothingToSeeHere"))
12 | .setDesc(text("common.typeHasNoSettings"));
13 | };
14 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Title/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 |
5 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
6 | modal,
7 | }) => {
8 | const { tabContentEl } = modal;
9 |
10 | new Setting(tabContentEl)
11 | .setName(text("common.nothingToSeeHere"))
12 | .setDesc(text("common.typeHasNoSettings"));
13 | };
14 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Toggle/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 |
5 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
6 | modal,
7 | }) => {
8 | const { tabContentEl } = modal;
9 |
10 | new Setting(tabContentEl)
11 | .setName(text("common.nothingToSeeHere"))
12 | .setDesc(text("common.typeHasNoSettings"));
13 | };
14 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Markdown/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 |
5 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
6 | modal,
7 | }) => {
8 | const { tabContentEl } = modal;
9 |
10 | new Setting(tabContentEl)
11 | .setName(text("common.nothingToSeeHere"))
12 | .setDesc(text("common.typeHasNoSettings"));
13 | };
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # vscode
2 | .vscode
3 |
4 | # Intellij
5 | *.iml
6 | .idea
7 |
8 | # npm
9 | node_modules
10 |
11 | # Don't include the compiled main.js file in the repo.
12 | # They should be uploaded to GitHub releases instead.
13 | main.js
14 |
15 | # Exclude sourcemaps
16 | *.map
17 |
18 | # obsidian
19 | data.json
20 |
21 | # Exclude macOS Finder (System Explorer) View States
22 | .DS_Store
23 |
24 | # all .env files
25 | .env
26 | .env.*
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Title/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 | import { refreshPropertyEditor } from "~/MetadataEditor";
4 | import { TITLE } from "~/lib/constants";
5 |
6 | export const registerListeners: CustomPropertyType["registerListeners"] = (
7 | plugin: BetterProperties
8 | ) => {
9 | plugin.registerEvent(
10 | plugin.app.vault.on("rename", () => {
11 | refreshPropertyEditor(plugin, TITLE);
12 | })
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Created/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 | import { refreshPropertyEditor } from "~/MetadataEditor";
4 | import { TITLE } from "~/lib/constants";
5 |
6 | export const registerListeners: CustomPropertyType["registerListeners"] = (
7 | plugin: BetterProperties
8 | ) => {
9 | plugin.registerEvent(
10 | plugin.app.vault.on("rename", () => {
11 | refreshPropertyEditor(plugin, TITLE);
12 | })
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/docs/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "",
8 | "css": "app/app.css",
9 | "baseColor": "neutral",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "iconLibrary": "lucide",
14 | "aliases": {
15 | "components": "~/components",
16 | "utils": "~/lib/utils",
17 | "ui": "~/components/ui",
18 | "lib": "~/lib",
19 | "hooks": "~/hooks"
20 | },
21 | "registries": {}
22 | }
23 |
--------------------------------------------------------------------------------
/docs/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.node.json" },
5 | { "path": "./tsconfig.cloudflare.json" }
6 | ],
7 | "compilerOptions": {
8 | "checkJs": true,
9 | "verbatimModuleSyntax": true,
10 | "skipLibCheck": true,
11 | "strict": true,
12 | "noEmit": true,
13 | "baseUrl": ".",
14 | "paths": {
15 | "~/*": ["./app/*"]
16 | },
17 | "noUnusedLocals": true,
18 | "resolveJsonModule": true,
19 | "noImplicitAny": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/i18next/index.ts:
--------------------------------------------------------------------------------
1 | import { NestedPaths } from "~/lib/utils";
2 | import en from "./languages/en.json";
3 |
4 | const ns = "better-properties";
5 |
6 | // add language packs below
7 |
8 | window.i18next.addResourceBundle("en", ns, en); // English
9 |
10 | //////////////////
11 |
12 | const fixedT = window.i18next.getFixedT(null, ns);
13 |
14 | type EN = typeof en;
15 | export const text = >(
16 | key: T,
17 | variables?: Record
18 | ): string => {
19 | return fixedT(key, variables);
20 | };
21 |
--------------------------------------------------------------------------------
/src/bpjs/index.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { createInlineCodePlugin } from "./createViewPlugin";
3 | import { createCodeBlockProcessor } from "./createCodeBlockProcessor";
4 | import { createPostProcessor } from "./createPostProcessor";
5 |
6 | export const registerBpJsCodeProcessors = (plugin: BetterProperties) => {
7 | plugin.registerMarkdownPostProcessor(createPostProcessor(plugin));
8 | plugin.registerMarkdownCodeBlockProcessor(
9 | ...createCodeBlockProcessor(plugin)
10 | );
11 | plugin.registerEditorExtension([createInlineCodePlugin(plugin)]);
12 | };
13 |
--------------------------------------------------------------------------------
/docs/app/components/common/EditOnGithub/index.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalLink } from "lucide-react";
2 | import { cn } from "~/lib/utils";
3 |
4 | export const EditOnGithub = ({
5 | path,
6 | className,
7 | }: {
8 | path: string;
9 | className?: string;
10 | }) => (
11 |
18 | Edit this page on Github
19 |
20 |
21 | );
22 |
--------------------------------------------------------------------------------
/docs/workers/app.ts:
--------------------------------------------------------------------------------
1 | import { createRequestHandler } from "react-router";
2 |
3 | declare module "react-router" {
4 | export interface AppLoadContext {
5 | cloudflare: {
6 | env: Env;
7 | ctx: ExecutionContext;
8 | };
9 | }
10 | }
11 |
12 | const requestHandler = createRequestHandler(
13 | () => import("virtual:react-router/server-build"),
14 | import.meta.env.MODE
15 | );
16 |
17 | export default {
18 | async fetch(request, env, ctx) {
19 | return requestHandler(request, {
20 | cloudflare: { env, ctx },
21 | });
22 | },
23 | } satisfies ExportedHandler;
24 |
--------------------------------------------------------------------------------
/getChangelogEntry.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync } from "fs";
2 |
3 | const version = JSON.parse(readFileSync("package.json", "utf8")).version;
4 | const escaped = version.replaceAll(".", "\\.");
5 | const changelogContent = readFileSync("changelog.md", "utf8");
6 |
7 | const regex = new RegExp(
8 | "^##\\s*" + escaped + "\\b[\\r\\n]+([\\s\\S]*?)(?=^##\\s|(?![\\s\\S]))",
9 | "gms"
10 | );
11 | const entry = regex.exec(changelogContent)?.[1];
12 | if (!entry) {
13 | throw new Error(
14 | `No changelog entry found for version "${version}" in changelog.md`
15 | );
16 | }
17 | console.log("## Changelog\n\n" + entry);
18 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CustomTypeKey,
3 | CustomPropertyType,
4 | RenderCustomTypeSettings,
5 | RenderCustomTypeWidget,
6 | PropertySettings,
7 | } from "./types";
8 | import { getDefaultPropertySettings, propertySettingsSchema } from "./schema";
9 | import { getPropertySettings, getPropertyTypeSettings } from "./utils";
10 |
11 | export {
12 | propertySettingsSchema,
13 | getDefaultPropertySettings,
14 | getPropertySettings,
15 | getPropertyTypeSettings,
16 | type PropertySettings,
17 | type CustomTypeKey,
18 | type CustomPropertyType,
19 | type RenderCustomTypeSettings,
20 | type RenderCustomTypeWidget,
21 | };
22 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Array/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | plugin: BetterProperties
6 | ) => {
7 | // ensures type changes of sub-properties cause a rerender for the highest parent property
8 | plugin.registerEvent(
9 | plugin.app.metadataTypeManager.on("changed", (property) => {
10 | if (!property?.includes(".")) return;
11 | const parentKey = property.split(".")[0]?.toLowerCase();
12 | if (!parentKey) return;
13 | plugin.app.metadataTypeManager.trigger("changed", parentKey);
14 | })
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Object/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { CustomPropertyType } from "../types";
3 |
4 | export const registerListeners: CustomPropertyType["registerListeners"] = (
5 | plugin: BetterProperties
6 | ) => {
7 | // ensures type changes of sub-properties cause a rerender for the highest parent property
8 | plugin.registerEvent(
9 | plugin.app.metadataTypeManager.on("changed", (property) => {
10 | if (!property?.includes(".")) return;
11 | const parentKey = property.split(".")[0]?.toLowerCase();
12 | if (!parentKey) return;
13 | plugin.app.metadataTypeManager.trigger("changed", parentKey);
14 | })
15 | );
16 | };
17 |
--------------------------------------------------------------------------------
/version-bump.mjs:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync } from "fs";
2 |
3 | const targetVersion = process.env.npm_package_version;
4 |
5 | // read minAppVersion from manifest.json and bump version to target version
6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
7 | const { minAppVersion } = manifest;
8 | manifest.version = targetVersion;
9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
10 |
11 | // update versions.json with target version and minAppVersion from manifest.json
12 | let versions = JSON.parse(readFileSync("versions.json", "utf8"));
13 | versions[targetVersion] = minAppVersion;
14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));
15 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "env": { "node": true },
5 | "plugins": [
6 | "@typescript-eslint"
7 | ],
8 | "extends": [
9 | "eslint:recommended",
10 | "plugin:@typescript-eslint/eslint-recommended",
11 | "plugin:@typescript-eslint/recommended"
12 | ],
13 | "parserOptions": {
14 | "sourceType": "module"
15 | },
16 | "rules": {
17 | "no-unused-vars": "off",
18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
19 | "@typescript-eslint/ban-ts-comment": "off",
20 | "no-prototype-builtins": "off",
21 | "@typescript-eslint/no-empty-function": "off"
22 | }
23 | }
--------------------------------------------------------------------------------
/docs/app/hooks/use-mobile.ts:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | // const MOBILE_BREAKPOINT = 768
4 | const MOBILE_BREAKPOINT = 1024;
5 |
6 | export function useIsMobile() {
7 | const [isMobile, setIsMobile] = React.useState(
8 | undefined,
9 | );
10 |
11 | React.useEffect(() => {
12 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
13 | const onChange = () => {
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
15 | };
16 | mql.addEventListener("change", onChange);
17 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
18 | return () => mql.removeEventListener("change", onChange);
19 | }, []);
20 |
21 | return !!isMobile;
22 | }
23 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/markdown/type.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 | import Options from "./options.mdx";
3 |
4 | #### Arguments
5 |
6 | | Name | Type | Default |
7 | | ------- | -------------------------------------------------------------------- | ------- |
8 | | options | {"object"}} content={ } /> | - |
9 |
10 | #### Returns
11 |
12 | `void`
13 |
14 | #### Examples
15 |
16 | ```js
17 | api.markdown({ text: "*some* **markdown** `here`" });
18 |
19 | api.markdown({
20 | text: "*some* **markdown** `here`",
21 | el: api.el.createDiv(),
22 | });
23 | ```
24 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: "✨type/feature request"
6 | assignees: unxok
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Color/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import { CustomPropertyType, PropertyTypeSchema } from "../types";
3 | import { registerListeners } from "./registerListeners";
4 | import { renderSettings } from "./renderSettings";
5 | import { renderWidget } from "./renderWidget";
6 | import * as v from "valibot";
7 |
8 | export const colorPropertyType: CustomPropertyType = {
9 | type: "color",
10 | name: () => text("customPropertyTypes.color.name"),
11 | icon: "lucide-paintbrush",
12 | validate: (v) => typeof v === "string",
13 | registerListeners,
14 | renderSettings,
15 | renderWidget,
16 | };
17 |
18 | export const colorSettingsSchema = v.optional(
19 | v.object({})
20 | ) satisfies PropertyTypeSchema;
21 |
--------------------------------------------------------------------------------
/docs/app/routes.ts:
--------------------------------------------------------------------------------
1 | import {
2 | type RouteConfig,
3 | index,
4 | prefix,
5 | route,
6 | } from "@react-router/dev/routes";
7 |
8 | export default [
9 | index("routes/getting-started/introduction/route.tsx"),
10 | ...prefix("getting-started", [
11 | route("installation", "routes/getting-started/installation/route.tsx"),
12 | route("usage", "routes/getting-started/usage/route.tsx"),
13 | ]),
14 | ...prefix("features", [
15 | route("roadmap", "routes/features/roadmap/route.tsx"),
16 | route("bpjs", "routes/features/bpjs/route.tsx"),
17 | route("metadata-editor", "routes/features/metadata-editor/route.tsx"),
18 | route("property-types", "routes/features/property-types/route.tsx"),
19 | ]),
20 | ] satisfies RouteConfig;
21 |
--------------------------------------------------------------------------------
/docs/tsconfig.cloudflare.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "include": [
4 | ".react-router/types/**/*",
5 | "app/**/*",
6 | "app/**/.server/**/*",
7 | "app/**/.client/**/*",
8 | "workers/**/*",
9 | "worker-configuration.d.ts",
10 | "**/*.d.ts"
11 | ],
12 | "compilerOptions": {
13 | "composite": true,
14 | "strict": true,
15 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
16 | "types": ["vite/client"],
17 | "target": "ES2022",
18 | "module": "ES2022",
19 | "moduleResolution": "bundler",
20 | "jsx": "react-jsx",
21 | "baseUrl": ".",
22 | "rootDirs": [".", "./.react-router/types"],
23 | "paths": {
24 | "~/*": ["./app/*"]
25 | },
26 | "esModuleInterop": true,
27 | "resolveJsonModule": true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Markdown/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import { CustomPropertyType, PropertyTypeSchema } from "../types";
3 | import { registerListeners } from "./registerListeners";
4 | import { renderSettings } from "./renderSettings";
5 | import { renderWidget } from "./renderWidget";
6 | import * as v from "valibot";
7 |
8 | export const markdownPropertyType: CustomPropertyType = {
9 | type: "markdown",
10 | name: () => text("customPropertyTypes.markdown.name"),
11 | icon: "lucide-m-square",
12 | validate: (v) => typeof v === "string",
13 | registerListeners,
14 | renderSettings,
15 | renderWidget,
16 | };
17 |
18 | export const markdownSettingsSchema = v.optional(
19 | v.object({})
20 | ) satisfies PropertyTypeSchema;
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | {
5 | "path": "./tsconfig.app.json"
6 | },
7 | {
8 | "path": "./tsconfig.node.json"
9 | }
10 | ],
11 | "compilerOptions": {
12 | "baseUrl": ".",
13 | "noImplicitAny": true,
14 | "strict": true,
15 | "target": "ESNext",
16 | "module": "nodenext",
17 | "strictNullChecks": true,
18 | "paths": {
19 | "~/*": ["./src/*"],
20 | "obsidian-typings/implementations": [
21 | "./node_modules/obsidian-typings/dist/implementations.d.ts",
22 | "./node_modules/obsidian-typings/dist/implementations.cjs"
23 | ]
24 | },
25 | "types": ["obsidian-typings", "./src/libs/types"],
26 | "resolveJsonModule": true,
27 | "moduleResolution": "nodenext"
28 | // "esModuleInterop": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
5 | "target": "ESNext",
6 | "useDefineForClassFields": true,
7 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "noEmit": true,
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "baseUrl": "./",
25 | "paths": {
26 | "~/*": ["./src/*"]
27 | }
28 | },
29 | "include": ["src"]
30 | }
31 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Title/index.ts:
--------------------------------------------------------------------------------
1 | import { TITLE } from "~/lib/constants";
2 | import { CustomPropertyType, PropertyTypeSchema } from "../types";
3 | import { registerListeners } from "./registerListeners";
4 | import { renderSettings } from "./renderSettings";
5 | import { renderWidget } from "./renderWidget";
6 | import { text } from "~/i18next";
7 | import * as v from "valibot";
8 |
9 | export const titlePropertyType: CustomPropertyType = {
10 | type: "title",
11 | name: () => text("customPropertyTypes.title.name"),
12 | icon: "lucide-letter-text",
13 | validate: (v) => typeof v === "string",
14 | registerListeners,
15 | renderSettings,
16 | renderWidget,
17 | reservedKeys: [TITLE],
18 | };
19 |
20 | export const titleSettingsSchema = v.optional(
21 | v.object({})
22 | ) satisfies PropertyTypeSchema;
23 |
--------------------------------------------------------------------------------
/docs/app/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator";
3 |
4 | import { cn } from "~/lib/utils";
5 |
6 | function Separator({
7 | className,
8 | orientation = "horizontal",
9 | decorative = true,
10 | ...props
11 | }: React.ComponentProps) {
12 | return (
13 |
23 | );
24 | }
25 |
26 | export { Separator };
27 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Time/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "time" satisfies CustomTypeKey;
13 |
14 | export const timePropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.time.name"),
17 | icon: "lucide-clock",
18 | validate: (v) => typeof v === "string",
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const timeSettingsSchema = v.optional(
25 | v.object({})
26 | ) satisfies PropertyTypeSchema;
27 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Toggle/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "toggle" satisfies CustomTypeKey;
13 |
14 | export const togglePropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.toggle.name"),
17 | icon: "lucide-toggle-left",
18 | validate: (v) => typeof v === "boolean",
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const toggleSettingsSchema = v.optional(
25 | v.object({})
26 | ) satisfies PropertyTypeSchema;
27 |
--------------------------------------------------------------------------------
/src/MetadataCache/index.ts:
--------------------------------------------------------------------------------
1 | import { around, dedupe } from "monkey-around";
2 | import { MetadataCache } from "obsidian";
3 | import { getPropertyTypeSettings } from "~/CustomPropertyTypes";
4 | import { monkeyAroundKey } from "~/lib/constants";
5 | import BetterProperties from "~/main";
6 |
7 | export const patchMetadataCache = (plugin: BetterProperties) => {
8 | const removePatch = around(plugin.app.metadataCache, {
9 | getFrontmatterPropertyValuesForKey: (old) =>
10 | dedupe(monkeyAroundKey, old, function (property) {
11 | // @ts-expect-error
12 | const that = this as MetadataCache;
13 | const { suggestions } = getPropertyTypeSettings({
14 | plugin,
15 | property,
16 | type: "general",
17 | });
18 | return suggestions ?? old.call(that, property);
19 | }),
20 | });
21 |
22 | plugin.register(removePatch);
23 | };
24 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/IconSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { getIconIds, setIcon } from "obsidian";
2 | import { InputSuggest, Suggestion } from "..";
3 |
4 | export class IconSuggest extends InputSuggest {
5 | protected getSuggestions(query: string): string[] {
6 | const icons = getIconIds();
7 | if (!query) return icons;
8 | const lower = query.toLowerCase();
9 | return icons.filter((icon) => icon.toLowerCase().includes(lower));
10 | }
11 |
12 | protected parseSuggestion(value: string): Suggestion {
13 | return {
14 | title: value,
15 | icon: " ",
16 | };
17 | }
18 |
19 | renderSuggestion(value: string, el: HTMLElement): void {
20 | super.renderSuggestion.call(this, value, el);
21 |
22 | const iconEl = el.querySelector(".suggestion-flair");
23 | if (!(iconEl instanceof HTMLElement)) return;
24 | setIcon(iconEl, value);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Object/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import { CustomPropertyType, PropertyTypeSchema } from "../types";
3 | import { registerListeners } from "./registerListeners";
4 | import { renderSettings } from "./renderSettings";
5 | import { renderWidget } from "./renderWidget";
6 | import * as v from "valibot";
7 |
8 | export const objectPropertyType: CustomPropertyType = {
9 | type: "object",
10 | name: () => text("customPropertyTypes.object.name"),
11 | icon: "lucide-braces",
12 | validate: (v) =>
13 | v === null ||
14 | v === undefined ||
15 | (typeof v === "object" && !Array.isArray(v)),
16 | registerListeners,
17 | renderSettings,
18 | renderWidget,
19 | };
20 |
21 | export const objectSettingsSchema = v.optional(
22 | v.object({
23 | hideAddButton: v.optional(v.boolean()),
24 | })
25 | ) satisfies PropertyTypeSchema;
26 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/route.tsx:
--------------------------------------------------------------------------------
1 | import { createMeta } from "~/lib/utils";
2 | import type { Route } from "./+types/route";
3 | import MdxArticle, {
4 | // @ts-expect-error TODO named imports from *.mdx not being recognized by TS
5 | toc,
6 | // @ts-expect-error
7 | filepath,
8 | } from "./article.mdx";
9 | import { Article } from "~/components/common/Article";
10 |
11 | export function meta({}: Route.MetaArgs) {
12 | return createMeta({
13 | title: "bpjs",
14 | description: "Guide to using Better Properties JavaScript blocks.",
15 | });
16 | }
17 |
18 | export default function Route({}: Route.ComponentProps) {
19 | return (
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/docs/app/routes/getting-started/usage/route.tsx:
--------------------------------------------------------------------------------
1 | import { createMeta } from "~/lib/utils";
2 | import type { Route } from "./+types/route";
3 | import MdxArticle, {
4 | // @ts-expect-error TODO named imports from *.mdx not being recognized by TS
5 | toc,
6 | // @ts-expect-error
7 | filepath,
8 | } from "./article.mdx";
9 | import { Article } from "~/components/common/Article";
10 |
11 | export function meta({}: Route.MetaArgs) {
12 | return createMeta({
13 | title: "Usage",
14 | description: "Using Better Properties.",
15 | });
16 | }
17 |
18 | export default function Route({}: Route.ComponentProps) {
19 | return (
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: "\U0001F41Etype/bug"
6 | assignees: unxok
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 | - Better Properties version:
28 | - OS: [e.g. iOS]
29 | - Obsidian app version:
30 | - Obsidian installer version:
31 |
32 |
33 | **Additional context**
34 | Add any other context about the problem here.
35 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/getProperty/type.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 | import Options from "./options.mdx";
3 |
4 | #### Arguments
5 |
6 | | Name | Type | Default |
7 | | ------- | -------------------------------------------------------------------- | ------- |
8 | | options | {"object"}} content={ } /> | - |
9 |
10 | #### Returns
11 |
12 | `unknown`
13 |
14 | #### Examples
15 |
16 | ```js
17 | api.getProperty({ property: "myProperty" });
18 |
19 | api.getProperty({ property: "myProperty", path: "path/to/note.md" });
20 |
21 | api.getProperty({ property: "myProperty", subscribe: false });
22 |
23 | api.getProperty({
24 | property: "myProperty",
25 | path: "path/to/note.md",
26 | subscribe: false,
27 | });
28 | ```
29 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/markdown/options.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 |
3 | #### options
4 |
5 | | Key | Type | Default |
6 | | -------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- | -------- |
7 | | | `string` | - |
8 | | | [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) | `api.el` |
9 |
--------------------------------------------------------------------------------
/docs/app/routes/getting-started/installation/route.tsx:
--------------------------------------------------------------------------------
1 | import { createMeta } from "~/lib/utils";
2 | import type { Route } from "./+types/route";
3 | import MdxArticle, {
4 | // @ts-expect-error TODO named imports from *.mdx not being recognized by TS
5 | toc,
6 | // @ts-expect-error
7 | filepath,
8 | } from "./article.mdx";
9 | import { Article } from "~/components/common/Article";
10 |
11 | export function meta({}: Route.MetaArgs) {
12 | return createMeta({
13 | title: "Installation",
14 | description: "How to install Better Properties.",
15 | });
16 | }
17 |
18 | export default function Route({}: Route.ComponentProps) {
19 | return (
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/getMetadata/options.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 |
3 | #### options
4 |
5 | | Key | Type | Default |
6 | | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | --------------- |
7 | | | `string \| undefined` | `api.soucePath` |
8 | | | `boolean` | `true` |
9 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Relation/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "relation" satisfies CustomTypeKey;
13 |
14 | export const relationPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.relation.name"),
17 | icon: "lucide-arrow-up-right",
18 | validate: (v) => Array.isArray(v),
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const relationSettingsSchema = v.optional(
25 | v.object({
26 | relatedProperty: v.optional(v.string()),
27 | })
28 | ) satisfies PropertyTypeSchema;
29 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/getMetadata/type.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 | import Options from "./options.mdx";
3 |
4 | #### Arguments
5 |
6 | | Name | Type | Default |
7 | | ------- | --------------------------------------------------------------------------------- | ----------- |
8 | | options | {"object \| undefined"}} content={ } /> | `undefined` |
9 |
10 | #### Returns
11 |
12 | [CachedMetadata](https://docs.obsidian.md/Reference/TypeScript+API/CachedMetadata)
13 |
14 | #### Examples
15 |
16 | ```js
17 | api.getMetadata();
18 |
19 | api.getMetadata({ path: "path/to/note.md" });
20 |
21 | api.getMetadata({ subscribe: false });
22 |
23 | api.getMetadata({
24 | path: "path/to/note.md",
25 | subscribe: false,
26 | });
27 | ```
28 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Array/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "array" satisfies CustomTypeKey;
13 |
14 | export const arrayPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.array.name"),
17 | icon: "lucide-brackets",
18 | validate: (v) => typeof v === "object" && Array.isArray(v),
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const arraySettingsSchema = v.optional(
25 | v.object({
26 | hideAddButton: v.optional(v.boolean()),
27 | })
28 | ) satisfies PropertyTypeSchema;
29 |
--------------------------------------------------------------------------------
/docs/app/routes/features/property-types/route.tsx:
--------------------------------------------------------------------------------
1 | import MdxArticle, {
2 | // @ts-expect-error TODO named imports from *.mdx not being recognized by TS
3 | toc,
4 | // @ts-expect-error
5 | filepath,
6 | } from "./article.mdx";
7 | import { Article } from "~/components/common/Article";
8 | import type { Route } from "./+types/route";
9 | import { createMeta } from "~/lib/utils";
10 |
11 | export function meta({}: Route.MetaArgs) {
12 | return createMeta({
13 | title: "Property types",
14 | description: "Descriptions and examples of newly added property types.",
15 | });
16 | }
17 |
18 | export default function Route({ loaderData }: Route.ComponentProps) {
19 | return (
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/docs/app/routes/getting-started/introduction/route.tsx:
--------------------------------------------------------------------------------
1 | import type { Route } from "./+types/route";
2 | import MdxArticle, {
3 | // @ts-expect-error TODO named imports from *.mdx not being recognized by TS
4 | toc,
5 | // @ts-expect-error
6 | filepath,
7 | } from "./article.mdx";
8 | import { Article } from "~/components/common/Article";
9 | import { createMeta } from "~/lib/utils";
10 |
11 | export function meta({}: Route.MetaArgs) {
12 | return createMeta({
13 | title: "Introduction",
14 | description: "Getting started with Better Properties.",
15 | });
16 | }
17 |
18 | export default function Route({}: Route.ComponentProps) {
19 | return (
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Created/index.ts:
--------------------------------------------------------------------------------
1 | import { CREATED } from "~/lib/constants";
2 | import { CustomPropertyType, PropertyTypeSchema } from "../types";
3 | import { registerListeners } from "./registerListeners";
4 | import { renderSettings } from "./renderSettings";
5 | import { renderWidget } from "./renderWidget";
6 | import { moment } from "obsidian";
7 | import { text } from "~/i18next";
8 | import * as v from "valibot";
9 |
10 | export const createdPropertyType: CustomPropertyType = {
11 | type: "created",
12 | name: () => text("customPropertyTypes.created.name"),
13 | icon: "lucide-clock-10",
14 | validate: (v) => moment(v?.toString()).isValid(),
15 | registerListeners,
16 | renderSettings,
17 | renderWidget,
18 | reservedKeys: [CREATED],
19 | };
20 |
21 | export const createdSettingsSchema = v.optional(
22 | v.object({
23 | format: v.optional(v.string()),
24 | })
25 | ) satisfies PropertyTypeSchema;
26 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Rating/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "rating" satisfies CustomTypeKey;
13 |
14 | export const ratingPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.rating.name"),
17 | icon: "lucide-star",
18 | validate: (v) => typeof v === "number",
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const ratingSettingsSchema = v.optional(
25 | v.object({
26 | icon: v.optional(v.string()),
27 | count: v.optional(v.number()),
28 | })
29 | ) satisfies PropertyTypeSchema;
30 |
--------------------------------------------------------------------------------
/docs/app/routes/features/metadata-editor/route.tsx:
--------------------------------------------------------------------------------
1 | import MdxArticle, {
2 | // @ts-expect-error TODO named imports from *.mdx not being recognized by TS
3 | toc,
4 | // @ts-expect-error
5 | filepath,
6 | } from "./article.mdx";
7 | import { Article } from "~/components/common/Article";
8 | import type { Route } from "./+types/route";
9 | import { createMeta } from "~/lib/utils";
10 |
11 | export function meta({}: Route.MetaArgs) {
12 | return createMeta({
13 | title: "Metadata Editor",
14 | description: "Features relating to the Metadata Editor.",
15 | });
16 | }
17 |
18 | export default function Route({ loaderData }: Route.ComponentProps) {
19 | return (
20 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Numeric/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "numeric" satisfies CustomTypeKey;
13 |
14 | export const numericPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.numeric.name"),
17 | icon: "lucide-circle-percent",
18 | validate: (v) => typeof v === "number" || typeof v === "string",
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const numericSettingsSchema = v.optional(
25 | v.object({
26 | decimalPlaces: v.optional(v.number()),
27 | })
28 | ) satisfies PropertyTypeSchema;
29 |
--------------------------------------------------------------------------------
/src/MetadataTypeManager/patchMetadataTypeManager.ts:
--------------------------------------------------------------------------------
1 | import { around, dedupe } from "monkey-around";
2 | import { getTrueProperty } from "~/CustomPropertyTypes/utils";
3 | import { monkeyAroundKey } from "~/lib/constants";
4 | import BetterProperties from "~/main";
5 |
6 | export const patchMetadataTypeManager = (plugin: BetterProperties): void => {
7 | const removePatch = around(plugin.app.metadataTypeManager, {
8 | getAssignedWidget(old) {
9 | return dedupe(monkeyAroundKey, old, function (property) {
10 | // @ts-expect-error
11 | const that: MetadataTypeManager = this;
12 | return old.call(that, getTrueProperty(property));
13 | });
14 | },
15 | setType(old) {
16 | return dedupe(monkeyAroundKey, old, function (property, type) {
17 | // @ts-expect-error
18 | const that: MetadataTypeManager = this;
19 | return old.call(that, getTrueProperty(property), type);
20 | });
21 | },
22 | });
23 |
24 | plugin.register(removePatch);
25 | };
26 |
--------------------------------------------------------------------------------
/docs/app/routes/getting-started/introduction/article.mdx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertDescription, AlertTitle } from "~/components/ui/alert";
2 | import { TriangleAlert } from "lucide-react";
3 |
4 | ## Introduction
5 |
6 | Better Properties is an [Obsidian](https://obsidian.md) plugin which adds numerous features related to [Properties](https://help.obsidian.md/properties).
7 |
8 |
9 |
10 |
11 |
12 | Obsidian internal API usage
13 |
14 | This plugin makes heavy use of the internal Obsidian API, so there may be
15 | changes that break this plugin after a new Obsidian update is released.
16 | Please [open an
17 | issue](https://github.com/unxok/obsidian-better-properties/issues) and/or be
18 | patient in the event that this happens.
19 |
20 |
21 |
22 | ### Showcase
23 |
24 | 
25 | 
26 |
--------------------------------------------------------------------------------
/docs/app/components/common/InfoButton/index.tsx:
--------------------------------------------------------------------------------
1 | import { Info } from "lucide-react";
2 | import type { ReactNode } from "react";
3 | import { Button } from "~/components/ui/button";
4 | import {
5 | Popover,
6 | PopoverContent,
7 | PopoverTrigger,
8 | } from "~/components/ui/popover";
9 |
10 | export const InfoButton = ({
11 | label,
12 | content,
13 | }: {
14 | label: ReactNode;
15 | content: ReactNode;
16 | }) => {
17 | return (
18 |
19 | {label}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | {content}
28 |
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Slider/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "slider" satisfies CustomTypeKey;
13 |
14 | export const sliderPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.slider.name"),
17 | icon: "lucide-git-commit",
18 | validate: (v) => typeof v === "number",
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const sliderSettingsSchema = v.optional(
25 | v.object({
26 | min: v.optional(v.number()),
27 | max: v.optional(v.number()),
28 | step: v.optional(v.number()),
29 | hideLimits: v.optional(v.boolean()),
30 | })
31 | ) satisfies PropertyTypeSchema;
32 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release Obsidian plugin
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - name: Use Node.js
16 | uses: actions/setup-node@v3
17 | with:
18 | node-version: "22.x"
19 |
20 | - name: Build plugin
21 | run: |
22 | npm install
23 | npm run build
24 |
25 | - name: Generate release notes from changelog
26 | id: notes
27 | run: |
28 | node ./getChangelogEntry.mjs > RELEASE_NOTES.md
29 | echo "file=RELEASE_NOTES.md" >> "$GITHUB_OUTPUT"
30 |
31 | - name: Create release
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 | run: |
35 | tag="${GITHUB_REF#refs/tags/}"
36 |
37 | gh release create "$tag" \
38 | --title="$tag" \
39 | --notes-file "${{ steps.notes.outputs.file }}" \
40 | main.js manifest.json styles.css
41 |
--------------------------------------------------------------------------------
/docs/app/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 |
3 | import { cn } from "~/lib/utils";
4 |
5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6 | return (
7 |
18 | );
19 | }
20 |
21 | export { Input };
22 |
--------------------------------------------------------------------------------
/src/bpjs/createCodeBlockProcessor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MarkdownPostProcessorContext,
3 | MarkdownRenderChild,
4 | Component,
5 | Plugin,
6 | } from "obsidian";
7 | import BetterProperties from "~/main";
8 | import { BpJsApi } from "./api";
9 |
10 | export const createCodeBlockProcessor = (
11 | plugin: BetterProperties
12 | ): Parameters => {
13 | const cb = (
14 | source: string,
15 | el: HTMLElement,
16 | ctx: MarkdownPostProcessorContext
17 | ) => {
18 | el.classList.add("better-properties-bpjs-codeblock");
19 | const mdrc = new MarkdownRenderChild(el);
20 | ctx.addChild(mdrc);
21 | const component = new Component();
22 | mdrc.addChild(component);
23 | plugin.addChild(component);
24 | const api = new BpJsApi(plugin, el, ctx.sourcePath, component, source);
25 | api.run(source);
26 | // api.monitorSubsribedPaths();
27 | };
28 |
29 | const BPJS = "bpjs";
30 |
31 | window.CodeMirror.defineMode(BPJS, (config) =>
32 | window.CodeMirror.getMode(config, "javascript")
33 | );
34 | return [BPJS, cb];
35 | };
36 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/PropertyTypeSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from "obsidian";
2 | import { InputSuggest, Suggestion } from "..";
3 |
4 | type Value = {
5 | type: string;
6 | name: string;
7 | };
8 | export class PropertyTypeSuggest extends InputSuggest {
9 | constructor(app: App, cmp: HTMLDivElement | HTMLInputElement) {
10 | super(app, cmp);
11 | }
12 |
13 | protected getSuggestions(query: string): Value[] {
14 | const { metadataTypeManager } = this.app;
15 | const arr = Object.values(metadataTypeManager.registeredTypeWidgets).map(
16 | (w) => ({
17 | type: w.type,
18 | name: w.name(),
19 | })
20 | );
21 |
22 | if (query === "") return arr;
23 | const lower = query.toLowerCase();
24 | return arr.filter(
25 | ({ type, name }) =>
26 | (type.toLowerCase().includes(lower) ||
27 | name.toLowerCase().includes(lower)) &&
28 | this.setFilterCallback({ type, name })
29 | );
30 | }
31 |
32 | protected parseSuggestion({ name, type }: Value): Suggestion {
33 | return {
34 | title: name,
35 | note: type,
36 | };
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Array/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
4 | import { text } from "~/i18next";
5 | import { typeKey } from ".";
6 |
7 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
8 | modal,
9 | plugin,
10 | property,
11 | }) => {
12 | const { tabContentEl } = modal;
13 |
14 | const settings = getPropertyTypeSettings({
15 | plugin,
16 | property,
17 | type: typeKey,
18 | });
19 |
20 | modal.onTabChange(() => {
21 | setPropertyTypeSettings({
22 | plugin,
23 | property,
24 | type: typeKey,
25 | typeSettings: settings,
26 | });
27 | });
28 |
29 | new Setting(tabContentEl)
30 | .setName(text("customPropertyTypes.object.settings.hideAddButton.title"))
31 | .setDesc(text("customPropertyTypes.object.settings.hideAddButton.desc"))
32 | .addToggle((cmp) => {
33 | cmp.setValue(!!settings.hideAddButton).onChange((b) => {
34 | settings.hideAddButton = b;
35 | });
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/DateCustom/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "datecustom" satisfies CustomTypeKey;
13 |
14 | export const dateCustomPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.datecustom.name"),
17 | icon: "lucide-calendar-plus",
18 | validate: (_v) => true, // TODO do we need to validate here?
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const dateCustomSettingsSchema = v.optional(
25 | v.object({
26 | type: v.optional(v.union([v.literal("date"), v.literal("datetime-local")])),
27 | format: v.optional(v.string()),
28 | placeholder: v.optional(v.string()),
29 | icon: v.optional(v.string()),
30 | })
31 | ) satisfies PropertyTypeSchema;
32 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/getProperty/options.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 |
3 | #### options
4 |
5 | | Key | Type | Default |
6 | | ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | --------------- |
7 | | | `string` | - |
8 | | | `string \| undefined` | `api.soucePath` |
9 | | | `boolean` | `true` |
10 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Object/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
4 | import { typeKey } from "./renderWidget";
5 | import { text } from "~/i18next";
6 |
7 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
8 | modal,
9 | plugin,
10 | property,
11 | }) => {
12 | const { tabContentEl } = modal;
13 |
14 | const settings = getPropertyTypeSettings({
15 | plugin,
16 | property,
17 | type: typeKey,
18 | });
19 |
20 | modal.onTabChange(() => {
21 | setPropertyTypeSettings({
22 | plugin,
23 | property,
24 | type: typeKey,
25 | typeSettings: settings,
26 | });
27 | });
28 |
29 | new Setting(tabContentEl)
30 | .setName(text("customPropertyTypes.object.settings.hideAddButton.title"))
31 | .setDesc(text("customPropertyTypes.object.settings.hideAddButton.desc"))
32 | .addToggle((cmp) => {
33 | cmp.setValue(!!settings.hideAddButton).onChange((b) => {
34 | settings.hideAddButton = b;
35 | });
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/docs/app/routes/features/roadmap/article.mdx:
--------------------------------------------------------------------------------
1 | ## Roadmap, bugs, and more
2 |
3 | The following are all fetched automatically from GitHub. [Click here](https://github.com/unxok/obsidian-better-properties/issues) to view all issues.
4 |
5 | If you're interested in what has changed throughout different releases, check out the [Changelog](https://github.com/unxok/obsidian-better-properties/blob/main/changelog.md).
6 |
7 | ### Roadmap
8 |
9 | Features that are planned to be added or are currently being worked on. Upvotes and comments may influence the priority of a given feature being developed!
10 |
11 |
12 |
13 | ### Bugs
14 |
15 | Known bugs. Before opening a bug report, please check this list to see if it's already been reported. If there is an existing bug report for your problem, please leave an upvote and/or leave a comment.
16 |
17 |
18 |
19 | ### Feature requests
20 |
21 | Requested features that haven't been added to the roadmap and may or may not be developed. Please give an upvote or leave a comment on the issue if you would like to see it added!
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/lib/constants/index.ts:
--------------------------------------------------------------------------------
1 | export const customPropertyTypePrefix = "better-properties:";
2 | export const monkeyAroundKey = "better-properties_monkey-around-key";
3 |
4 | export const invalidFileNameRegex = /[*"\/\\<>:|?]/;
5 |
6 | export const TITLE = "title";
7 | export const MODIFIED = "modified";
8 | export const CREATED = "created";
9 |
10 | export const selectColors = {
11 | red: "var(--better-properties-select-red)",
12 | orange: "var(--better-properties-select-orange)",
13 | yellow: "var(--better-properties-select-yellow)",
14 | green: "var(--better-properties-select-green)",
15 | blue: "var(--better-properties-select-blue)",
16 | cyan: "var(--better-properties-select-cyan)",
17 | purple: "var(--better-properties-select-purple)",
18 | pink: "var(--better-properties-select-pink)",
19 | gray: "var(--better-properties-select-gray)",
20 | transparent: "var(--better-properties-select-transparent)",
21 | };
22 |
23 | export const selectBackgroundCssVar = "--better-properties-select-bg";
24 | export const selectEmptyAttr = "data-better-properties-combobox-empty";
25 | export const backgroundCssVar = "--better-properties-bg";
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 unxok
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 unxok
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/FileSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { App, TFile } from "obsidian";
2 | import { compareFunc } from "~/lib/utils";
3 | import { InputSuggest, Suggestion } from "..";
4 |
5 | export class FileSuggest extends InputSuggest {
6 | constructor(
7 | app: App,
8 | component: HTMLDivElement | HTMLInputElement,
9 | public respectUserIgnored: boolean = true
10 | ) {
11 | super(app, component);
12 | }
13 |
14 | protected getSuggestions(query: string): TFile[] | Promise {
15 | const { app } = this;
16 | const allFiles = app.vault
17 | .getFiles()
18 | .toSorted((a, b) => compareFunc(a.path, b.path));
19 | const notIgnored = this.respectUserIgnored
20 | ? allFiles.filter((f) => !this.app.metadataCache.isUserIgnored(f.path))
21 | : allFiles;
22 | if (!query) return notIgnored.filter(this.setFilterCallback);
23 | const lower = query.toLowerCase();
24 | return notIgnored.filter(
25 | (v) => v.path.toLowerCase().includes(lower) && this.setFilterCallback(v)
26 | );
27 | }
28 |
29 | protected parseSuggestion({ path, name }: TFile): Suggestion {
30 | return {
31 | title: name,
32 | note: path,
33 | };
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/docs/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { reactRouter } from "@react-router/dev/vite";
2 | import { cloudflare } from "@cloudflare/vite-plugin";
3 | import tailwindcss from "@tailwindcss/vite";
4 | import { defineConfig } from "vite";
5 | import tsconfigPaths from "vite-tsconfig-paths";
6 | import mdx from "@mdx-js/rollup";
7 | import path from "path";
8 | import rehypeSlug from "rehype-slug";
9 | import rehypeMdxToc from "rehype-mdx-toc";
10 | import rehypeAutoLinkHeadings from "rehype-autolink-headings";
11 | import recmaExportFilepath from "recma-export-filepath";
12 | import remarkGfm from "remark-gfm";
13 | import rehypeStarryNight from "rehype-starry-night";
14 |
15 | export default defineConfig({
16 | plugins: [
17 | mdx({
18 | remarkPlugins: [remarkGfm],
19 | rehypePlugins: [
20 | rehypeSlug,
21 | rehypeMdxToc,
22 | rehypeAutoLinkHeadings,
23 | rehypeStarryNight,
24 | ],
25 | recmaPlugins: [recmaExportFilepath],
26 | }),
27 | cloudflare({ viteEnvironment: { name: "ssr" } }),
28 | tailwindcss(),
29 | reactRouter(),
30 | tsconfigPaths(),
31 | ],
32 | resolve: {
33 | alias: {
34 | "~": path.resolve(__dirname, "./app"),
35 | },
36 | },
37 | });
38 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Numeric/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 | import { typeKey } from ".";
5 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
6 |
7 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
8 | modal,
9 | plugin,
10 | property,
11 | }) => {
12 | const { tabContentEl } = modal;
13 |
14 | const settings = getPropertyTypeSettings({
15 | plugin,
16 | property: property,
17 | type: typeKey,
18 | });
19 |
20 | modal.onTabChange(() => {
21 | setPropertyTypeSettings({
22 | plugin,
23 | property: property,
24 | type: typeKey,
25 | typeSettings: { ...settings },
26 | });
27 | });
28 |
29 | new Setting(tabContentEl)
30 | .setName(text("customPropertyTypes.numeric.settings.decimalPlaces.title"))
31 | .setDesc(text("customPropertyTypes.numeric.settings.decimalPlaces.desc"))
32 | .addText((cmp) => {
33 | cmp.inputEl.type = "number";
34 | cmp.setValue(settings.decimalPlaces?.toString() ?? "");
35 | cmp.onChange((v) => {
36 | settings.decimalPlaces = Number(v);
37 | });
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/FolderSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { App, TFile, TFolder } from "obsidian";
2 | import { InputSuggest, Suggestion } from "..";
3 | import { compareFunc } from "~/lib/utils";
4 |
5 | export class FolderSuggest extends InputSuggest {
6 | constructor(
7 | app: App,
8 | component: HTMLDivElement | HTMLInputElement,
9 | public options?: {
10 | showFileCountAux: boolean;
11 | }
12 | ) {
13 | super(app, component);
14 | }
15 |
16 | protected getSuggestions(query: string): TFolder[] | Promise {
17 | const { app } = this;
18 | const allFolders = app.vault
19 | .getAllFolders(true)
20 | .toSorted((a, b) => compareFunc(a.path, b.path));
21 | if (!query) return allFolders.filter(this.setFilterCallback);
22 | const lower = query.toLowerCase();
23 | return allFolders.filter(
24 | (v) => v.path.toLowerCase().includes(lower) && this.setFilterCallback(v)
25 | );
26 | }
27 |
28 | protected parseSuggestion({ path, name, children }: TFolder): Suggestion {
29 | return {
30 | title: name,
31 | note: path,
32 | aux: this.options?.showFileCountAux
33 | ? children.filter((t) => t instanceof TFile).length.toString()
34 | : undefined,
35 | };
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/renderProperty/type.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 | import Options from "./options.mdx";
3 |
4 | #### Arguments
5 |
6 | | Name | Type | Default |
7 | | ------- | -------------------------------------------------------------------- | ------- |
8 | | options | {"object"}} content={ } /> | - |
9 |
10 | #### Returns
11 |
12 | [`PropertyWidgetComponentBase`](https://github.com/Fevol/obsidian-typings/blob/release/obsidian-catalyst/1.9.12/src/obsidian/internals/MetadataTypeManager/PropertyWidgetComponentBase.d.ts)
13 |
14 | #### Examples
15 |
16 | ```js
17 | api.renderProperty({ property: "myProperty" });
18 |
19 | api.renderProperty({ property: "myProperty", hideKey: true });
20 |
21 | api.renderProperty({ property: "myProperty", el: api.el.createDiv() });
22 |
23 | api.renderProperty({ property: "myProperty", path: "path/to/note.md" });
24 |
25 | api.renderProperty({ property: "myProperty", subscribe: false });
26 |
27 | api.renderProperty({
28 | property: "myProperty",
29 | path: "path/to/note.md",
30 | subscribe: false,
31 | });
32 | ```
33 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-better-properties",
3 | "version": "1.0.0",
4 | "description": "Upgrades Obsidian properties with new types, features, and more!",
5 | "main": "main.js",
6 | "scripts": {
7 | "dev": "vite build --watch --mode=development",
8 | "tsc": "tsc",
9 | "build": "vite build",
10 | "version": "node version-bump.mjs && git add manifest.json versions.json",
11 | "release": "node getChangelogEntry.mjs && git tag -a %npm_config_tag% -m '%npm_config_tag%' && git push origin %npm_config_tag%"
12 | },
13 | "keywords": [],
14 | "author": "Unxok",
15 | "license": "MIT",
16 | "devDependencies": {
17 | "@codemirror/language": "6.10.3",
18 | "@types/luxon": "3.4.2",
19 | "@types/node": "22.7.4",
20 | "@typescript-eslint/eslint-plugin": "5.29.0",
21 | "@typescript-eslint/parser": "5.29.0",
22 | "builtin-modules": "3.3.0",
23 | "esbuild": "0.25.5",
24 | "luxon": "3.5.0",
25 | "obsidian": "latest",
26 | "obsidian-typings": "obsidian-public-latest",
27 | "tslib": "2.7.0",
28 | "typescript": "5.8.3",
29 | "vite": "6.3.6",
30 | "vite-bundle-analyzer": "1.2.1"
31 | },
32 | "dependencies": {
33 | "math-expression-evaluator": "2.0.7",
34 | "monkey-around": "3.0.0",
35 | "valibot": "1.1.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/bpjs/createPostProcessor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | MarkdownPostProcessor,
3 | MarkdownRenderChild,
4 | Component,
5 | } from "obsidian";
6 | import BetterProperties from "~/main";
7 | import { BpJsApi } from "./api";
8 |
9 | export const createPostProcessor = (
10 | plugin: BetterProperties
11 | ): MarkdownPostProcessor => {
12 | const processor: MarkdownPostProcessor = (el, ctx) => {
13 | if (el.tagName.toLowerCase() !== "code") {
14 | el.findAll("code").forEach((codeEl) => processor(codeEl, ctx));
15 | return;
16 | }
17 |
18 | if (!el.textContent.startsWith(plugin.codePrefix)) return;
19 |
20 | const file = plugin.app.vault.getFileByPath(ctx.sourcePath);
21 | if (!file) {
22 | throw new Error(`File not found at path "${ctx.sourcePath}"`);
23 | }
24 |
25 | const code = el.textContent.slice(plugin.codePrefix.length);
26 | const spanEl = createSpan({ cls: "better-properties-bpjs-code" });
27 | el.replaceWith(spanEl);
28 | const mdrc = new MarkdownRenderChild(spanEl);
29 | ctx.addChild(mdrc);
30 | const component = new Component();
31 | mdrc.addChild(component);
32 | plugin.addChild(component);
33 | const api = new BpJsApi(plugin, spanEl, ctx.sourcePath, component, code);
34 | api.run(code);
35 | };
36 | return processor;
37 | };
38 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Better Properties
2 |
3 | 
4 | 
5 | 
6 |  
7 |
8 |
9 |
10 | What if Properties in Obsidian were... better?
11 |
12 | This [Obsidian](https://obsidian.md) plugin adds many different properties-related features, such as new property types and per-property settings.
13 |
14 | For documentation, check out the [Better Properties Docs](https://better-properties.unxok.com) website.
15 |
16 | 
17 |
18 | 
19 |
--------------------------------------------------------------------------------
/src/MetadataEditor/index.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { View } from "obsidian";
3 | import { patchMenu } from "./patchMenu";
4 | import { onFilePropertyMenu } from "./propertyEditorMenu";
5 |
6 | export { patchMetadataEditor } from "./patchMetadataEditor";
7 |
8 | export const customizePropertyEditorMenu = (plugin: BetterProperties) => {
9 | patchMenu(plugin);
10 |
11 | plugin.registerEvent(
12 | plugin.app.workspace.on(
13 | "better-properties:file-property-menu",
14 | (menu, property) => {
15 | onFilePropertyMenu(plugin, menu, property);
16 | }
17 | )
18 | );
19 | };
20 |
21 | export const refreshPropertyEditor = (
22 | plugin: BetterProperties,
23 | property: string
24 | ) => {
25 | const lower = property.toLowerCase();
26 | const withoutDots = lower.split(".")[0];
27 | plugin.app.metadataTypeManager.trigger("changed", lower);
28 | plugin.app.workspace.iterateAllLeaves((leaf) => {
29 | if (!leaf.view.hasOwnProperty("metadataEditor")) return;
30 | const view = leaf.view as View & {
31 | metadataEditor: {
32 | onMetadataTypeChange: (propName: string) => void;
33 | };
34 | };
35 |
36 | // This is to force dropdowns to re-render with updated options
37 | // the easiest way I found was to emulate a type change
38 | view.metadataEditor.onMetadataTypeChange(withoutDots);
39 | });
40 | };
41 |
--------------------------------------------------------------------------------
/docs/app/components/common/GithubIcon/index.tsx:
--------------------------------------------------------------------------------
1 | import type { ComponentProps, ReactNode } from "react";
2 |
3 | export const GithubIcon = ({
4 | className,
5 | ...props
6 | }: ComponentProps<"svg">): ReactNode => (
7 |
15 |
22 |
23 | );
24 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/TagSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { App } from "obsidian";
2 | import { InputSuggest, Suggestion } from "..";
3 | import { getAllTags, iterateFileMetadata } from "~/lib/utils";
4 |
5 | type Value = {
6 | tag: string;
7 | count: number;
8 | };
9 | export class TagSuggest extends InputSuggest {
10 | constructor(app: App, cmp: HTMLDivElement | HTMLInputElement) {
11 | super(app, cmp);
12 | }
13 |
14 | protected getSuggestions(query: string): Value[] {
15 | const { vault, metadataCache } = this.app;
16 | const record: Record = {};
17 | iterateFileMetadata({
18 | vault,
19 | metadataCache,
20 | callback: ({ metadata }) => {
21 | if (!metadata) return;
22 | const tags = getAllTags(metadata, false);
23 | tags?.forEach((t) => {
24 | t in record ? record[t]++ : (record[t] = 1);
25 | });
26 | },
27 | });
28 | const allTags = Object.entries(record).map(
29 | ([tag, count]) => ({ tag, count }),
30 | [] as Value[]
31 | );
32 |
33 | if (!query) return allTags.filter(this.setFilterCallback);
34 | const lower = query.toLowerCase();
35 | return allTags.filter(
36 | (v) => v.tag.toLowerCase().startsWith(lower) && this.setFilterCallback(v)
37 | );
38 | }
39 |
40 | protected parseSuggestion({ tag, count }: Value): Suggestion {
41 | return {
42 | title: tag,
43 | aux: count.toString(),
44 | };
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Toggle/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { ToggleComponent } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { PropertyWidgetComponentNew } from "../utils";
4 | import { PropertyRenderContext } from "obsidian-typings";
5 | import BetterProperties from "~/main";
6 |
7 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
8 | plugin,
9 | el,
10 | ctx,
11 | value,
12 | }) => {
13 | return new ToggleTypeComponent(plugin, el, value, ctx);
14 | };
15 |
16 | class ToggleTypeComponent extends PropertyWidgetComponentNew<
17 | "toggle",
18 | boolean
19 | > {
20 | type = "toggle" as const;
21 | parseValue = (v: unknown) => !!v;
22 |
23 | toggle: ToggleComponent;
24 |
25 | constructor(
26 | plugin: BetterProperties,
27 | el: HTMLElement,
28 | value: unknown,
29 | ctx: PropertyRenderContext
30 | ) {
31 | super(plugin, el, value, ctx);
32 |
33 | const parsed = this.parseValue(value);
34 | this.toggle = new ToggleComponent(el).setValue(parsed).onChange((b) => {
35 | this.setValue(b);
36 | });
37 |
38 | this.onFocus = () => {
39 | this.toggle.toggleEl.focus();
40 | };
41 | }
42 |
43 | getValue(): boolean {
44 | return !!this.toggle?.getValue();
45 | }
46 |
47 | setValue(value: unknown): void {
48 | if (this.toggle.getValue() !== value) {
49 | this.toggle.setValue(this.parseValue(value));
50 | }
51 | super.setValue(value);
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/MetadataEditor/propertyEditorMenu/delete.ts:
--------------------------------------------------------------------------------
1 | import { ConfirmationModal } from "~/classes/ConfirmationModal";
2 | import { text } from "~/i18next";
3 | import { obsidianText } from "~/i18next/obsidian";
4 | import { deleteProperty } from "~/lib/utils";
5 | import BetterProperties from "~/main";
6 |
7 | export const openDeleteModal = ({
8 | plugin,
9 | property,
10 | }: {
11 | plugin: BetterProperties;
12 | property: string;
13 | }) => {
14 | const modal = new ConfirmationModal(plugin.app)
15 | .setTitle(text("metadataEditor.propertyMenu.delete.modalTitle"))
16 | .setContent(
17 | text("metadataEditor.propertyMenu.delete.modalTitle", { property })
18 | )
19 | .setFooterCheckbox((checkbox) => {
20 | checkbox
21 | .setValue(false)
22 | .setLabel(text("common.dontAskAgain"))
23 | .onChange(async (b) => {
24 | await plugin.updateSettings((s) => {
25 | s.confirmPropertyDelete = b;
26 | return s;
27 | });
28 | });
29 | })
30 | .addFooterButton((btn) =>
31 | btn
32 | .setWarning()
33 | .setButtonText(obsidianText("interface.delete-action-short-name"))
34 | .onClick(async () => {
35 | await deleteProperty({
36 | plugin,
37 | property,
38 | });
39 | modal.close();
40 | })
41 | )
42 | .addFooterButton((btn) =>
43 | btn.setButtonText(obsidianText("dialogue.button-cancel")).onClick(() => {
44 | modal.close();
45 | })
46 | );
47 | modal.open();
48 | return;
49 | };
50 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Color/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { CustomPropertyType } from "../types";
2 | import { PropertyWidgetComponentNew } from "../utils";
3 | import { ColorTextComponent } from "~/classes/ColorTextComponent";
4 | import BetterProperties from "~/main";
5 | import { PropertyRenderContext } from "obsidian-typings";
6 |
7 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
8 | plugin,
9 | el,
10 | ctx,
11 | value,
12 | }) => {
13 | return new ColorTypeComponent(plugin, el, value, ctx);
14 | };
15 |
16 | class ColorTypeComponent extends PropertyWidgetComponentNew<"color", string> {
17 | type = "color" as const;
18 | parseValue = (v: unknown) => v?.toString() ?? "";
19 |
20 | colorText: ColorTextComponent;
21 |
22 | constructor(
23 | plugin: BetterProperties,
24 | el: HTMLElement,
25 | value: unknown,
26 | ctx: PropertyRenderContext
27 | ) {
28 | super(plugin, el, value, ctx);
29 |
30 | const parsed = this.parseValue(value);
31 | this.colorText = new ColorTextComponent(el)
32 | .setValue(parsed)
33 | .onChange((v) => {
34 | this.setValue(v);
35 | });
36 |
37 | this.onFocus = () => {
38 | this.colorText.colorInputEl.focus();
39 | };
40 | }
41 |
42 | getValue(): string {
43 | return this.colorText.getValue() ?? "";
44 | }
45 |
46 | setValue(value: unknown): void {
47 | if (this.colorText.getValue() !== value) {
48 | this.colorText.setValue(this.parseValue(value));
49 | }
50 | super.setValue(value);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/MetadataEditor/patchMenu.ts:
--------------------------------------------------------------------------------
1 | import { around, dedupe } from "monkey-around";
2 | import { Menu } from "obsidian";
3 | import { monkeyAroundKey } from "~/lib/constants";
4 | import { BetterProperties } from "~/Plugin/plugin";
5 |
6 | export const patchMenu = (plugin: BetterProperties) => {
7 | const removePatch = around(Menu.prototype, {
8 | showAtMouseEvent(old) {
9 | return dedupe(monkeyAroundKey, old, function (e) {
10 | // @ts-ignore Doesn't look like there's a way to get this typed correctly
11 | const that = this as Menu;
12 | const exit = () => {
13 | return old.call(that, e);
14 | };
15 | const { currentTarget } = e;
16 | const isMetadataPropertyIcon =
17 | currentTarget instanceof HTMLElement &&
18 | currentTarget.tagName.toLowerCase() === "span" &&
19 | currentTarget.classList.contains("metadata-property-icon");
20 |
21 | if (!isMetadataPropertyIcon) return exit();
22 |
23 | const container = currentTarget.closest(
24 | "div.metadata-property[data-property-key]"
25 | )!;
26 | const property = container.getAttribute("data-property-key") ?? "";
27 | const toReturn = exit();
28 | // trigger after running old() because "Property type" item only exists once menu opens
29 | plugin.app.workspace.trigger(
30 | "better-properties:file-property-menu",
31 | that,
32 | property
33 | );
34 |
35 | return toReturn;
36 | });
37 | },
38 | });
39 |
40 | plugin.register(removePatch);
41 | };
42 |
--------------------------------------------------------------------------------
/docs/app/components/common/ThemeSwitcher/switcher.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react";
2 | import { Button } from "~/components/ui/button";
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuRadioGroup,
7 | DropdownMenuRadioItem,
8 | DropdownMenuTrigger,
9 | } from "~/components/ui/dropdown-menu";
10 | import { useTheme } from "./context";
11 | import type { ReactNode } from "react";
12 |
13 | export const ThemeSwitcher = (): ReactNode => {
14 | const { theme, setTheme } = useTheme();
15 |
16 | return (
17 |
18 |
19 |
20 |
21 |
22 | Toggle theme
23 |
24 |
25 |
26 | setTheme(t as typeof theme)}
29 | >
30 | Light
31 | Dark
32 | System
33 |
34 |
35 |
36 | );
37 | };
38 |
--------------------------------------------------------------------------------
/docs/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { isRouteErrorResponse, Outlet } from "react-router";
2 |
3 | import type { Route } from "./+types/root";
4 | import "./app.css";
5 |
6 | export const links: Route.LinksFunction = () => [
7 | { rel: "preconnect", href: "https://fonts.googleapis.com" },
8 | {
9 | rel: "preconnect",
10 | href: "https://fonts.gstatic.com",
11 | crossOrigin: "anonymous",
12 | },
13 | {
14 | rel: "stylesheet",
15 | href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
16 | },
17 | ];
18 |
19 | export default function App() {
20 | return ;
21 | }
22 |
23 | export { Layout } from "./layout";
24 |
25 | export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
26 | let message = "Oops!";
27 | let details = "An unexpected error occurred.";
28 | let stack: string | undefined;
29 |
30 | if (isRouteErrorResponse(error)) {
31 | message = error.status === 404 ? "404" : "Error";
32 | details =
33 | error.status === 404
34 | ? "The requested page could not be found."
35 | : error.statusText || details;
36 | } else if (import.meta.env.DEV && error && error instanceof Error) {
37 | details = error.message;
38 | stack = error.stack;
39 | }
40 |
41 | return (
42 |
43 | {message}
44 | {details}
45 | {stack && (
46 |
47 | {stack}
48 |
49 | )}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/docs/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | import type { AppLoadContext, EntryContext } from "react-router";
2 | import { ServerRouter } from "react-router";
3 | import { isbot } from "isbot";
4 | import { renderToReadableStream } from "react-dom/server";
5 |
6 | export default async function handleRequest(
7 | request: Request,
8 | responseStatusCode: number,
9 | responseHeaders: Headers,
10 | routerContext: EntryContext,
11 | _loadContext: AppLoadContext,
12 | ) {
13 | let shellRendered = false;
14 | const userAgent = request.headers.get("user-agent");
15 |
16 | const body = await renderToReadableStream(
17 | ,
18 | {
19 | onError(error: unknown) {
20 | responseStatusCode = 500;
21 | // Log streaming rendering errors from inside the shell. Don't log
22 | // errors encountered during initial shell rendering since they'll
23 | // reject and get logged in handleDocumentRequest.
24 | if (shellRendered) {
25 | console.error(error);
26 | }
27 | },
28 | },
29 | );
30 | shellRendered = true;
31 |
32 | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding
33 | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
34 | if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
35 | await body.allReady;
36 | }
37 |
38 | responseHeaders.set("Content-Type", "text/html");
39 | return new Response(body, {
40 | headers: responseHeaders,
41 | status: responseStatusCode,
42 | });
43 | }
44 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Rating/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 | import { typeKey } from ".";
5 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
6 | import { IconSuggest } from "~/classes/InputSuggest/IconSuggest";
7 |
8 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
9 | modal,
10 | plugin,
11 | property,
12 | }) => {
13 | const { tabContentEl } = modal;
14 |
15 | const settings = getPropertyTypeSettings({
16 | plugin,
17 | property: property,
18 | type: typeKey,
19 | });
20 |
21 | modal.onTabChange(() => {
22 | setPropertyTypeSettings({
23 | plugin,
24 | property: property,
25 | type: typeKey,
26 | typeSettings: { ...settings },
27 | });
28 | });
29 |
30 | new Setting(tabContentEl)
31 | .setName(text("customPropertyTypes.rating.settings.icon.title"))
32 | .setDesc(text("customPropertyTypes.rating.settings.icon.desc"))
33 | .addSearch((cmp) => {
34 | cmp.setValue(settings.icon ?? "").onChange((v) => {
35 | settings.icon = v || undefined;
36 | });
37 | new IconSuggest(plugin.app, cmp.inputEl).onSelect((v) => {
38 | cmp.setValue(v);
39 | cmp.onChanged();
40 | });
41 | });
42 |
43 | new Setting(tabContentEl)
44 | .setName(text("customPropertyTypes.rating.settings.count.title"))
45 | .setDesc(text("customPropertyTypes.rating.settings.count.desc"))
46 | .addText((cmp) => {
47 | cmp.setValue(settings.count?.toString() ?? "").onChange((v) => {
48 | const n = Number(v);
49 | settings.count = !v || Number.isNaN(n) ? undefined : n;
50 | });
51 | });
52 | };
53 |
--------------------------------------------------------------------------------
/docs/app/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx";
2 | import type { href } from "react-router";
3 | import { twMerge } from "tailwind-merge";
4 |
5 | export function cn(...inputs: ClassValue[]) {
6 | return twMerge(clsx(inputs));
7 | }
8 |
9 | export type Path = Parameters[0];
10 |
11 | type Meta =
12 | | { title: string }
13 | | { name: string; content: string }
14 | | { property: string; content: string };
15 | export const createMeta = (props: {
16 | title: string;
17 | description?: string;
18 | image?: string;
19 | }): Meta[] => {
20 | const title: Meta[] = [
21 | {
22 | title: props.title + " | Better Properties",
23 | },
24 | {
25 | property: "og:title",
26 | content: props.title + " | Better Properties",
27 | },
28 | ];
29 |
30 | const description: Meta[] = props.description
31 | ? [
32 | {
33 | name: "description",
34 | content: props.description,
35 | },
36 | {
37 | property: "og:description",
38 | content: props.description,
39 | },
40 | ]
41 | : [];
42 |
43 | return [
44 | ...title,
45 | ...description,
46 | {
47 | property: "og:image",
48 | content:
49 | props.image ??
50 | "https://better-properties.unxok.com/open-graph-image.png",
51 | },
52 | {
53 | property: "og:site_name",
54 | content: "Better Properties",
55 | },
56 | {
57 | property: "og:image:width",
58 | content: "1200",
59 | },
60 | {
61 | property: "og:image:width",
62 | content: "600",
63 | },
64 | {
65 | property: "og:type",
66 | content: "object",
67 | },
68 | ];
69 | };
70 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { UserConfig, defineConfig } from "vite";
2 | import path from "path";
3 | import builtins from "builtin-modules";
4 | // import { analyzer } from "vite-bundle-analyzer";
5 |
6 | export default defineConfig(async ({ mode }) => {
7 | const { resolve } = path;
8 | const prod = mode === "production";
9 |
10 | return {
11 | plugins: [
12 | // analyzer()
13 | ],
14 | resolve: {
15 | alias: {
16 | "~": path.resolve(__dirname, "./src"),
17 | },
18 | },
19 | build: {
20 | lib: {
21 | entry: resolve(__dirname, "src/main.ts"),
22 | cssFileName: "styles",
23 | name: "main",
24 | fileName: () => "main.js",
25 | formats: ["cjs"],
26 | },
27 | minify: prod,
28 | sourcemap: prod ? false : "inline",
29 | cssCodeSplit: false,
30 | // cssCodeSplit: true,
31 | emptyOutDir: false,
32 | outDir: "",
33 | rollupOptions: {
34 | input: {
35 | main: resolve(__dirname, "src/main.ts"),
36 | },
37 | output: {
38 | entryFileNames: "main.js",
39 | assetFileNames: "styles.css",
40 | // assetFileNames: "[name].css",/
41 | },
42 | external: [
43 | "obsidian",
44 | "electron",
45 | "@codemirror/autocomplete",
46 | "@codemirror/collab",
47 | "@codemirror/commands",
48 | "@codemirror/language",
49 | "@codemirror/lint",
50 | "@codemirror/search",
51 | "@codemirror/state",
52 | "@codemirror/view",
53 | "@lezer/common",
54 | "@lezer/highlight",
55 | "@lezer/lr",
56 | ...builtins,
57 | ],
58 | onwarn(warning, warn) {
59 | // suppress eval warnings
60 | if (warning.code === "EVAL") return;
61 | warn(warning);
62 | },
63 | },
64 | },
65 | } as UserConfig;
66 | });
67 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Time/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { obsidianText } from "~/i18next/obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { PropertyWidgetComponentNew } from "../utils";
4 | import { PropertyRenderContext } from "obsidian-typings";
5 | import BetterProperties from "~/main";
6 |
7 | //TODO add option for hours, hours + minutes, hours + minutes + seconds
8 |
9 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
10 | plugin,
11 | el,
12 | ctx,
13 | value,
14 | }) => {
15 | return new TimeTypeComponent(plugin, el, value, ctx);
16 | };
17 |
18 | class TimeTypeComponent extends PropertyWidgetComponentNew<"time", string> {
19 | type = "time" as const;
20 | parseValue = (v: unknown) => v?.toString() ?? "";
21 |
22 | inputEl: HTMLInputElement;
23 |
24 | constructor(
25 | plugin: BetterProperties,
26 | el: HTMLElement,
27 | value: unknown,
28 | ctx: PropertyRenderContext
29 | ) {
30 | super(plugin, el, value, ctx);
31 |
32 | // el.classList.add();
33 |
34 | const parsed = this.parseValue(value);
35 | this.inputEl = el.createEl("input", {
36 | type: "time",
37 | cls: ["metadata-input-text", "mod-date"],
38 | attr: {
39 | placeholder: obsidianText("interface.empty-state.empty"),
40 | },
41 | });
42 |
43 | this.inputEl.value = parsed;
44 | this.inputEl.addEventListener("change", () => {
45 | this.setValue(this.inputEl.value);
46 | });
47 |
48 | this.onFocus = () => {
49 | this.inputEl.focus();
50 | };
51 | }
52 |
53 | getValue(): string {
54 | return this.inputEl.value;
55 | }
56 |
57 | setValue(value: unknown): void {
58 | if (this.inputEl.value !== value) {
59 | this.inputEl.value = this.parseValue(value);
60 | }
61 | super.setValue(value);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/classes/ConfirmationModal/index.ts:
--------------------------------------------------------------------------------
1 | import { App, ButtonComponent, Modal } from "obsidian";
2 |
3 | export class ConfirmationModal extends Modal {
4 | buttonContainerEl: HTMLElement;
5 | checkbox: Checkbox | undefined = undefined;
6 | constructor(app: App) {
7 | super(app);
8 |
9 | this.buttonContainerEl = this.modalEl.createDiv({
10 | cls: "modal-button-container",
11 | });
12 | }
13 |
14 | setFooterCheckbox(cb: (checkbox: Checkbox) => void): this {
15 | this.checkbox = new Checkbox(this.buttonContainerEl);
16 | cb(this.checkbox);
17 | return this;
18 | }
19 |
20 | addFooterButton(cb: (btn: ButtonComponent) => void): this {
21 | const btn = new ButtonComponent(this.buttonContainerEl);
22 | cb(btn);
23 | return this;
24 | }
25 | }
26 |
27 | class Checkbox {
28 | inputEl: HTMLInputElement;
29 | labelEl: HTMLLabelElement;
30 | labelTextNode: Node;
31 | onChangeCallback: (value: boolean) => void = () => {};
32 | constructor(public containerEl: HTMLElement) {
33 | this.labelEl = containerEl.createEl("label", { cls: "mod-checkbox" });
34 | this.inputEl = this.labelEl.createEl("input", {
35 | attr: {
36 | tabindex: "-1",
37 | type: "checkbox",
38 | },
39 | });
40 | this.labelTextNode = document.createTextNode("");
41 | this.labelEl.appendChild(this.labelTextNode);
42 | this.inputEl.addEventListener("change", () =>
43 | this.onChangeCallback(this.inputEl.checked)
44 | );
45 | }
46 |
47 | setLabel(label: string): this {
48 | this.labelTextNode.textContent = label;
49 | return this;
50 | }
51 |
52 | setValue(checked: boolean): this {
53 | this.inputEl.checked = checked;
54 | return this;
55 | }
56 |
57 | onChange(cb: (value: boolean) => void): this {
58 | this.onChangeCallback = cb;
59 | return this;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/docs/app/index.css:
--------------------------------------------------------------------------------
1 | @layer base {
2 | button:not(:disabled),
3 | [role="button"]:not(:disabled) {
4 | cursor: pointer;
5 | }
6 | }
7 |
8 | .icon.icon-link {
9 | padding-inline-start: 0.25rem;
10 | visibility: hidden;
11 |
12 | h1:hover &,
13 | h2:hover &,
14 | h3:hover &,
15 | h4:hover &,
16 | h5:hover &,
17 | h6:hover & {
18 | visibility: visible;
19 | }
20 |
21 | h1:has(&),
22 | h2:has(&),
23 | h3:has(&),
24 | h4:has(&),
25 | h5:has(&),
26 | h6:has(&) {
27 | display: flex;
28 | align-items: center;
29 | flex-direction: row-reverse;
30 | justify-content: start;
31 | }
32 |
33 | html.dark &::before {
34 | content: url("data:image/svg+xml;base64,PHN2ZwoJeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgoJd2lkdGg9IjE2IgoJaGVpZ2h0PSIxNiIKCXZpZXdCb3g9IjAgMCAyNCAyNCIKCWZpbGw9Im5vbmUiCglzdHJva2U9IndoaXRlIgoJc3Ryb2tlLXdpZHRoPSIyIgoJc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgoJc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIKCWNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayIKPgoJPHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiIC8+Cgk8cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiIC8+Cjwvc3ZnPgo=");
35 | }
36 | html.light &::before {
37 | content: url("data:image/svg+xml;base64,PHN2ZwoJeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIgoJd2lkdGg9IjE2IgoJaGVpZ2h0PSIxNiIKCXZpZXdCb3g9IjAgMCAyNCAyNCIKCWZpbGw9Im5vbmUiCglzdHJva2U9ImJsYWNrIgoJc3Ryb2tlLXdpZHRoPSIyIgoJc3Ryb2tlLWxpbmVjYXA9InJvdW5kIgoJc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIKCWNsYXNzPSJsdWNpZGUgbHVjaWRlLWxpbmstaWNvbiBsdWNpZGUtbGluayIKPgoJPHBhdGggZD0iTTEwIDEzYTUgNSAwIDAgMCA3LjU0LjU0bDMtM2E1IDUgMCAwIDAtNy4wNy03LjA3bC0xLjcyIDEuNzEiIC8+Cgk8cGF0aCBkPSJNMTQgMTFhNSA1IDAgMCAwLTcuNTQtLjU0bC0zIDNhNSA1IDAgMCAwIDcuMDcgNy4wN2wxLjcxLTEuNzEiIC8+Cjwvc3ZnPgo=");
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/docs/app/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as PopoverPrimitive from "@radix-ui/react-popover";
3 |
4 | import { cn } from "~/lib/utils";
5 |
6 | function Popover({
7 | ...props
8 | }: React.ComponentProps) {
9 | return ;
10 | }
11 |
12 | function PopoverTrigger({
13 | ...props
14 | }: React.ComponentProps) {
15 | return ;
16 | }
17 |
18 | function PopoverContent({
19 | className,
20 | align = "center",
21 | sideOffset = 4,
22 | ...props
23 | }: React.ComponentProps) {
24 | return (
25 |
26 |
36 |
37 | );
38 | }
39 |
40 | function PopoverAnchor({
41 | ...props
42 | }: React.ComponentProps) {
43 | return ;
44 | }
45 |
46 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
47 |
--------------------------------------------------------------------------------
/docs/app/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 |
5 | import { cn } from "~/lib/utils";
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 | destructive:
17 | "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 | outline:
19 | "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | },
25 | },
26 | );
27 |
28 | function Badge({
29 | className,
30 | variant,
31 | asChild = false,
32 | ...props
33 | }: React.ComponentProps<"span"> &
34 | VariantProps & { asChild?: boolean }) {
35 | const Comp = asChild ? Slot : "span";
36 |
37 | return (
38 |
43 | );
44 | }
45 |
46 | export { Badge, badgeVariants };
47 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/renderProperty/options.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 |
3 | #### options
4 |
5 | | Key | Type | Default |
6 | | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------- | --------------- |
7 | | | `string` | - |
8 | | | [`HTMLElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement) | `api.el` |
9 | | | `boolean` | `false` |
10 | | | `string \| undefined` | `api.soucePath` |
11 | | | `boolean` | `true` |
12 |
--------------------------------------------------------------------------------
/docs/app/components/common/Article/index.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, type ReactNode } from "react";
2 | import { EditOnGithub } from "../EditOnGithub";
3 | import type { TocItem } from "rehype-mdx-toc";
4 | import { useToc } from "../TocSidebar";
5 | import { Link } from "react-router";
6 | import { cn, type Path } from "~/lib/utils";
7 | import { buttonVariants } from "~/components/ui/button";
8 | import { ArrowRight } from "lucide-react";
9 |
10 | export const Article = ({
11 | children,
12 | path,
13 | toc,
14 | next,
15 | }: {
16 | children: ReactNode;
17 | path: string;
18 | toc: TocItem[];
19 | next?: {
20 | path: Path;
21 | label: string;
22 | };
23 | }): ReactNode => {
24 | const { setToc } = useToc();
25 |
26 | useEffect(() => {
27 | if (!toc) return;
28 | setToc(() => [...toc]);
29 | }, [toc]);
30 | return (
31 |
32 | {/*
*/}
33 |
34 | {children}
35 |
36 |
37 |
38 | {next && (
39 |
46 | {next.label}
47 |
48 |
49 | )}
50 |
51 |
52 |
53 | );
54 | };
55 |
56 | // const DotsBackground = () => (
57 | //
58 | // );
59 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/LinkSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { App, setIcon } from "obsidian";
2 | import { InputSuggest, Suggestion } from "..";
3 | import { LinkSuggestion } from "obsidian-typings";
4 | import { Icon } from "~/lib/types/icons";
5 |
6 | export class LinkSuggest extends InputSuggest {
7 | constructor(
8 | app: App,
9 | component: HTMLDivElement | HTMLInputElement,
10 | public respectUserIgnored: boolean = true
11 | ) {
12 | super(app, component);
13 | }
14 |
15 | protected getSuggestions(
16 | query: string
17 | ): LinkSuggestion[] | Promise {
18 | const { metadataCache } = this.app;
19 | const lowerQuery = query.toLowerCase();
20 | return metadataCache.getLinkSuggestions().filter((s) => {
21 | const isValidExtension = s.file
22 | ? s.file?.extension.toLowerCase() === "md"
23 | : true;
24 | const isIgnored = metadataCache.isUserIgnored(s.path);
25 | const isMatch =
26 | !query ||
27 | s.alias?.toLowerCase()?.includes(lowerQuery) ||
28 | s.path.toLowerCase().includes(lowerQuery);
29 | return (
30 | isValidExtension && !isIgnored && isMatch && this.setFilterCallback(s)
31 | );
32 | });
33 | }
34 |
35 | protected parseSuggestion({ path, file, alias }: LinkSuggestion): Suggestion {
36 | return {
37 | title: alias ?? file?.basename ?? path,
38 | note: alias
39 | ? path
40 | : file
41 | ? path.slice(0, -1 * file.basename.length)
42 | : undefined,
43 | aux: alias ? " " : undefined,
44 | };
45 | }
46 |
47 | override renderSuggestion(value: LinkSuggestion, el: HTMLElement): void {
48 | super.renderSuggestion(value, el);
49 | if (!value.alias) return;
50 | const auxFlairEl: HTMLElement | null = el.querySelector(
51 | ".suggestion-aux > .suggestion-flair"
52 | );
53 | if (!auxFlairEl) return;
54 | setIcon(auxFlairEl, "lucide-forward" satisfies Icon);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/docs/app/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 |
4 | import { cn } from "~/lib/utils";
5 |
6 | const alertVariants = cva(
7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-card text-card-foreground",
12 | destructive:
13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | },
20 | );
21 |
22 | function Alert({
23 | className,
24 | variant,
25 | ...props
26 | }: React.ComponentProps<"div"> & VariantProps) {
27 | return (
28 |
34 | );
35 | }
36 |
37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38 | return (
39 |
47 | );
48 | }
49 |
50 | function AlertDescription({
51 | className,
52 | ...props
53 | }: React.ComponentProps<"div">) {
54 | return (
55 |
63 | );
64 | }
65 |
66 | export { Alert, AlertTitle, AlertDescription };
67 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { AbstractInputSuggest, setIcon } from "obsidian";
2 |
3 | export type Suggestion = {
4 | title: string;
5 | note?: string;
6 | aux?: string;
7 | icon?: string;
8 | };
9 |
10 | export abstract class InputSuggest extends AbstractInputSuggest {
11 | /**
12 | * Get the suggestions for the popover
13 | * @note Make sure to utilize `this.setFilterCallback`
14 | */
15 | protected abstract getSuggestions(query: string): T[] | Promise;
16 |
17 | /**
18 | * Convert a suggestion value of type `T` to the `Suggestion` type
19 | */
20 | protected abstract parseSuggestion(value: T): Suggestion;
21 |
22 | /**
23 | * An additional filter that must be true to render a given suggestion
24 | */
25 | setFilterCallback: (suggestion: T) => boolean = () => true;
26 |
27 | /**
28 | * Sets `this.setFilterCallback`
29 | */
30 | setFilter(cb: this["setFilterCallback"]): this {
31 | this.setFilterCallback = cb;
32 | return this;
33 | }
34 |
35 | /**
36 | * Renders suggestions
37 | */
38 | renderSuggestion(value: T, el: HTMLElement): void {
39 | if (!this.setFilterCallback(value)) {
40 | el.remove();
41 | return;
42 | }
43 |
44 | const { title, aux, note, icon } = this.parseSuggestion(value);
45 | el.classList.add("mod-complex");
46 | if (icon) {
47 | const iconEl = el
48 | .createDiv({ cls: "suggestion-icon" })
49 | .createSpan({ cls: "suggestion-flair" });
50 | setIcon(iconEl, icon);
51 | }
52 | const contentEl = el.createDiv({ cls: "suggestion-content" });
53 | contentEl.createDiv({
54 | cls: "suggestion-title",
55 | text: title,
56 | });
57 | if (note !== undefined) {
58 | contentEl.createDiv({ cls: "suggestion-note", text: note });
59 | }
60 | if (aux !== undefined) {
61 | el.createDiv({ cls: "suggestion-aux" }).createSpan({
62 | cls: "suggestion-flair",
63 | text: aux,
64 | });
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/MultiSelect/index.ts:
--------------------------------------------------------------------------------
1 | import { text } from "~/i18next";
2 | import {
3 | CustomPropertyType,
4 | CustomTypeKey,
5 | PropertyTypeSchema,
6 | } from "../types";
7 | import { registerListeners } from "./registerListeners";
8 | import { renderSettings } from "./renderSettings";
9 | import { renderWidget } from "./renderWidget";
10 | import * as v from "valibot";
11 |
12 | export const typeKey = "multiselect" satisfies CustomTypeKey;
13 |
14 | export const multiSelectPropertyType: CustomPropertyType = {
15 | type: typeKey,
16 | name: () => text("customPropertyTypes.multiSelect.name"),
17 | icon: "lucide-list-collapse",
18 | validate: (v) => Array.isArray(v),
19 | registerListeners,
20 | renderSettings,
21 | renderWidget,
22 | };
23 |
24 | export const multiSelectSettingsSchema = v.optional(
25 | v.object({
26 | optionsType: v.optional(
27 | v.union([v.literal("manual"), v.literal("dynamic")]),
28 | "manual"
29 | ),
30 | manualAllowCreate: v.optional(v.boolean()),
31 | manualOptions: v.optional(
32 | v.array(
33 | v.object({
34 | value: v.string(),
35 | label: v.optional(v.string()),
36 | desc: v.optional(v.string()),
37 | bgColor: v.optional(v.string()),
38 | textColor: v.optional(v.string()),
39 | })
40 | )
41 | ),
42 | dynamicOptionsType: v.optional(
43 | v.union([
44 | v.literal("filesInFolder"),
45 | v.literal("filesFromTag"),
46 | v.literal("filesFromInlineJs"),
47 | v.literal("filesFromJsFile"),
48 | ])
49 | ),
50 | folderOptionsPaths: v.optional(v.array(v.string())),
51 | folderOptionsIsSubsIncluded: v.optional(v.boolean()),
52 | folderOptionsExcludeFolderNote: v.optional(v.boolean()),
53 | tagOptionsTags: v.optional(v.array(v.string())),
54 | tagOptionsIncludeNested: v.optional(v.boolean()),
55 | inlineJsOptionsCode: v.optional(v.string()),
56 | fileJsOptionsPath: v.optional(v.string()),
57 | })
58 | ) satisfies PropertyTypeSchema;
59 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "obsidian-better-properties-docs",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "react-router build",
7 | "cf-typegen": "wrangler types",
8 | "deploy": "npm run build && wrangler deploy",
9 | "dev": "react-router dev --host",
10 | "postinstall": "patch-package && npm run cf-typegen",
11 | "preview": "npm run build && vite preview",
12 | "typecheck": "npm run cf-typegen && react-router typegen && tsc -b"
13 | },
14 | "dependencies": {
15 | "@mdx-js/rollup": "^3.1.1",
16 | "@radix-ui/react-dialog": "^1.1.15",
17 | "@radix-ui/react-dropdown-menu": "^2.1.16",
18 | "@radix-ui/react-popover": "^1.1.15",
19 | "@radix-ui/react-separator": "^1.1.7",
20 | "@radix-ui/react-slot": "^1.2.3",
21 | "@radix-ui/react-tooltip": "^1.2.8",
22 | "class-variance-authority": "^0.7.1",
23 | "clsx": "^2.1.1",
24 | "isbot": "^5.1.31",
25 | "lucide-react": "^0.544.0",
26 | "react": "^19.1.1",
27 | "react-dom": "^19.1.1",
28 | "react-router": "^7.9.2",
29 | "recma-export-filepath": "^1.2.0",
30 | "rehype-autolink-headings": "^7.1.0",
31 | "rehype-mdx-toc": "^1.1.0",
32 | "rehype-slug": "^6.0.0",
33 | "rehype-starry-night": "^2.2.0",
34 | "remark-gfm": "^4.0.1",
35 | "tailwind-merge": "^3.3.1"
36 | },
37 | "devDependencies": {
38 | "@cloudflare/vite-plugin": "^1.13.5",
39 | "@octokit/openapi-types": "^26.0.0",
40 | "@react-router/dev": "^7.9.2",
41 | "@tailwindcss/vite": "^4.1.13",
42 | "@types/node": "^22",
43 | "@types/react": "^19.1.13",
44 | "@types/react-dom": "^19.1.9",
45 | "patch-package": "^8.0.1",
46 | "prettier": "^3.6.2",
47 | "prettier-plugin-tailwindcss": "^0.6.14",
48 | "tailwindcss": "^4.1.13",
49 | "tw-animate-css": "^1.4.0",
50 | "typescript": "^5.9.2",
51 | "vite": "^7.1.7",
52 | "vite-tsconfig-paths": "^5.1.4",
53 | "wrangler": "^4.40.0"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Created/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting, moment } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 | import { obsidianText } from "~/i18next/obsidian";
5 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
6 |
7 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
8 | modal,
9 | plugin,
10 | property,
11 | }) => {
12 | const { tabContentEl } = modal;
13 |
14 | const settings = getPropertyTypeSettings({
15 | plugin,
16 | property,
17 | type: "created",
18 | });
19 |
20 | modal.onTabChange(() => {
21 | setPropertyTypeSettings({
22 | plugin,
23 | property,
24 | type: "created",
25 | typeSettings: settings,
26 | });
27 | });
28 |
29 | let updateFormatSample = (_: string): void => {
30 | throw new Error("Not implemented");
31 | };
32 |
33 | new Setting(tabContentEl)
34 | .setName(text("customPropertyTypes.created.settings.format.title"))
35 | .setDesc(
36 | createFragment((el) => {
37 | el.appendText(
38 | text("customPropertyTypes.created.settings.format.desc") + " "
39 | );
40 | el.createEl("a", {
41 | text: obsidianText("plugins.daily-notes.label-syntax-link"),
42 | href: "https://momentjs.com/docs/#/displaying/format/",
43 | });
44 |
45 | el.createEl("br");
46 |
47 | el.appendText(
48 | obsidianText("plugins.daily-notes.label-syntax-live-preview") + " "
49 | );
50 | const formatSampleEl = el.createEl("b", { cls: "u-pop" });
51 | updateFormatSample = (formatStr) => {
52 | const dateStr = formatStr === "" ? "" : moment().format(formatStr);
53 | formatSampleEl.textContent = dateStr;
54 | };
55 | })
56 | )
57 | .addText((cmp) => {
58 | cmp
59 | .setPlaceholder("YYYY-MM-DD")
60 | .setValue(settings.format ?? "")
61 | .onChange((v) => {
62 | updateFormatSample(v);
63 | settings.format = v;
64 | });
65 | });
66 | };
67 |
--------------------------------------------------------------------------------
/src/MetadataEditor/propertyEditorMenu/icon.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { ConfirmationModal } from "~/classes/ConfirmationModal";
3 | import { IconSuggest } from "~/classes/InputSuggest/IconSuggest";
4 | import { getPropertyTypeSettings } from "~/CustomPropertyTypes";
5 | import { updatePropertyTypeSettings } from "~/CustomPropertyTypes/utils";
6 | import BetterProperties from "~/main";
7 | import { refreshPropertyEditor } from "..";
8 | import { obsidianText } from "~/i18next/obsidian";
9 | import { text } from "~/i18next";
10 |
11 | export const openChangeIconModal = ({
12 | plugin,
13 | property,
14 | }: {
15 | plugin: BetterProperties;
16 | property: string;
17 | }) => {
18 | const modal = new ConfirmationModal(plugin.app);
19 | let icon: string =
20 | getPropertyTypeSettings({ plugin, property, type: "general" })?.icon ?? "";
21 |
22 | modal.setTitle(
23 | text("metadataEditor.propertyMenu.icon.modalTitle", { property })
24 | );
25 |
26 | new Setting(modal.contentEl)
27 | .setName(text("metadataEditor.propertyMenu.icon.iconSetting.title"))
28 | .setDesc(text("metadataEditor.propertyMenu.icon.iconSetting.desc"))
29 | .addSearch((cmp) => {
30 | cmp.setValue(icon).onChange((v) => {
31 | icon = v;
32 | });
33 | new IconSuggest(plugin.app, cmp.inputEl).onSelect((v) => {
34 | cmp.setValue(v);
35 | cmp.onChanged();
36 | });
37 | });
38 |
39 | modal
40 | .addFooterButton((btn) =>
41 | btn.setButtonText(obsidianText("dialogue.button-cancel")).onClick(() => {
42 | modal.close();
43 | })
44 | )
45 | .addFooterButton((btn) =>
46 | btn
47 | .setButtonText(obsidianText("dialogue.button-update"))
48 | .setCta()
49 | .onClick(() => {
50 | updatePropertyTypeSettings({
51 | plugin,
52 | property,
53 | type: "general",
54 | cb: (s) => ({ ...s, icon }),
55 | });
56 | refreshPropertyEditor(plugin, property);
57 | modal.close();
58 | })
59 | );
60 |
61 | modal.open();
62 | };
63 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Select/index.ts:
--------------------------------------------------------------------------------
1 | import { CustomPropertyType, CustomTypeKey } from "..";
2 | import { renderWidget } from "./renderWidget";
3 | import { renderSettings } from "./renderSettings";
4 | import { registerListeners } from "./registerListeners";
5 | import { text } from "~/i18next";
6 | import { PropertyTypeSchema } from "../types";
7 | import * as v from "valibot";
8 |
9 | export const typeKey = "select" satisfies CustomTypeKey;
10 |
11 | export const selectPropertyType: CustomPropertyType = {
12 | type: typeKey,
13 | icon: "lucide-circle-chevron-down",
14 | name: () => text("customPropertyTypes.select.name"),
15 | validate: (v) => typeof v?.toString() === "string",
16 | renderWidget,
17 | registerListeners,
18 | renderSettings,
19 | };
20 |
21 | export const selectSettingsSchema = v.optional(
22 | v.object({
23 | useDefaultStyle: v.optional(v.boolean(), false),
24 | optionsType: v.optional(
25 | v.union([v.literal("manual"), v.literal("dynamic")]),
26 | "manual"
27 | ),
28 | manualAllowCreate: v.optional(v.boolean()),
29 | manualOptions: v.optional(
30 | v.array(
31 | v.object({
32 | value: v.string(),
33 | label: v.optional(v.string()),
34 | desc: v.optional(v.string()),
35 | bgColor: v.optional(v.string()),
36 | textColor: v.optional(v.string()),
37 | })
38 | )
39 | ),
40 | dynamicOptionsType: v.optional(
41 | v.union([
42 | v.literal("filesInFolder"),
43 | v.literal("filesFromTag"),
44 | v.literal("filesFromInlineJs"),
45 | v.literal("filesFromJsFile"),
46 | ])
47 | ),
48 | dynamicEmptyLabel: v.optional(v.string()),
49 | folderOptionsPaths: v.optional(v.array(v.string())),
50 | folderOptionsIsSubsIncluded: v.optional(v.boolean()),
51 | folderOptionsExcludeFolderNote: v.optional(v.boolean()),
52 | tagOptionsTags: v.optional(v.array(v.string())),
53 | tagOptionsIncludeNested: v.optional(v.boolean()),
54 | inlineJsOptionsCode: v.optional(v.string()),
55 | fileJsOptionsPath: v.optional(v.string()),
56 | })
57 | ) satisfies PropertyTypeSchema;
58 |
--------------------------------------------------------------------------------
/src/lib/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Events,
3 | Workspace as BaseWorkspace,
4 | MetadataCache as BaseMetadataCache,
5 | } from "obsidian";
6 | import {
7 | MetadataTypeManager as BaseMetadataTypeManager,
8 | MetadataEditor as BaseMetadataEditor,
9 | PropertyEntryData,
10 | PropertyWidget,
11 | } from "obsidian-typings";
12 |
13 | declare module "obsidian" {
14 | interface Workspace extends BaseWorkspace {
15 | on(
16 | name: "better-properties:file-property-menu",
17 | callback: (menu: Menu, property: string) => void,
18 | ctx?: unknown
19 | ): EventRef;
20 |
21 | on(
22 | name: "better-properties:property-label-width-change",
23 | callback: (newWidth: number | undefined) => void
24 | ): EventRef;
25 |
26 | // trigger(
27 | // name: "better-properties:property-label-width-change",
28 | // width: number | undefined
29 | // ): void;
30 | }
31 |
32 | interface AbstractInputSuggest extends PopoverSuggest {
33 | showSuggestions: (suggestions: T[]) => void;
34 | }
35 |
36 | interface MarkdownPreviewRenderer {
37 | onHeadingCollapseClick(e: MouseEvent, el: HTMLElement): void;
38 | }
39 |
40 | interface PropertyValueComponent {
41 | containerEl: HTMLElement;
42 | private focus(_: unknown): void;
43 | onFocus(): void;
44 | }
45 |
46 | interface MetadataTypeManager extends BaseMetadataTypeManager {
47 | on(name: "changed", cb: (property: string) => void): EventRef;
48 | }
49 |
50 | interface MetadataCache extends BaseMetadataCache {
51 | on(
52 | name: "better-properties:relation-changed",
53 | cb: (data: {
54 | file: TFile;
55 | property: string;
56 | oldValue: string[];
57 | value: string[];
58 | relatedProperty: string;
59 | }) => void
60 | ): EventRef;
61 | trigger(
62 | name: "better-properties:relation-changed",
63 | data: {
64 | file: TFile;
65 | property: string;
66 | oldValue: string[];
67 | value: string[];
68 | relatedProperty: string;
69 | }
70 | );
71 | }
72 |
73 | interface Menu {
74 | scrollEl: HTMLDivElement;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Select/registerListeners.ts:
--------------------------------------------------------------------------------
1 | import { debounce, EventRef } from "obsidian";
2 | import BetterProperties from "~/main";
3 | import { refreshPropertyEditor } from "~/MetadataEditor";
4 | import { CustomPropertyType } from "../types";
5 | import { typeKey } from ".";
6 |
7 | export const registerListeners: CustomPropertyType["registerListeners"] = (
8 | plugin: BetterProperties
9 | ) => {
10 | const vaultEventHandler = vaultEventHandlerFactory(plugin);
11 | const metadataCacheHandler = metadataCacheEventHandlerFactory(plugin);
12 | const eventRefs: EventRef[] = [
13 | plugin.app.vault.on("create", vaultEventHandler),
14 | plugin.app.vault.on("delete", vaultEventHandler),
15 | plugin.app.vault.on("rename", vaultEventHandler),
16 | plugin.app.metadataCache.on("changed", metadataCacheHandler),
17 | ];
18 | eventRefs.forEach((ref) => plugin.registerEvent(ref));
19 | };
20 |
21 | const vaultEventHandlerFactory = (plugin: BetterProperties): (() => void) => {
22 | return debounce(
23 | () => {
24 | const { propertySettings } = plugin.settings;
25 | if (!propertySettings) return;
26 |
27 | Object.entries(propertySettings).forEach(([property, settings]) => {
28 | const { optionsType, dynamicOptionsType } = settings[typeKey] ?? {};
29 | if (optionsType !== "dynamic") return;
30 | if (dynamicOptionsType !== "filesInFolder") return;
31 | refreshPropertyEditor(plugin, property);
32 | });
33 | },
34 | 250,
35 | true
36 | );
37 | };
38 |
39 | const metadataCacheEventHandlerFactory = (
40 | plugin: BetterProperties
41 | ): (() => void) => {
42 | return debounce(
43 | () => {
44 | const { propertySettings } = plugin.settings;
45 | if (!propertySettings) return;
46 |
47 | Object.entries(propertySettings).forEach(([property, settings]) => {
48 | const { optionsType, dynamicOptionsType } = settings[typeKey] ?? {};
49 | if (optionsType !== "dynamic") return;
50 | if (dynamicOptionsType !== "filesFromTag") return;
51 | refreshPropertyEditor(plugin, property);
52 | });
53 | },
54 | 250,
55 | true
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Relation/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { CustomPropertyType } from "../types";
2 | import { typeKey } from ".";
3 | import {
4 | getPropertyTypeSettings,
5 | setPropertyTypeSettings,
6 | updatePropertyTypeSettings,
7 | } from "../utils";
8 | import { Setting } from "obsidian";
9 | import { PropertySuggest } from "~/classes/InputSuggest/PropertySuggest";
10 | import { customPropertyTypePrefix } from "~/lib/constants";
11 |
12 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
13 | modal,
14 | plugin,
15 | property,
16 | }) => {
17 | const { tabContentEl } = modal;
18 |
19 | const settings = getPropertyTypeSettings({
20 | plugin,
21 | property: property,
22 | type: typeKey,
23 | });
24 |
25 | const originalRelatedProperty = settings.relatedProperty;
26 |
27 | modal.onTabChange(() => {
28 | setPropertyTypeSettings({
29 | plugin,
30 | property: property,
31 | type: typeKey,
32 | typeSettings: { ...settings },
33 | });
34 |
35 | const { relatedProperty } = settings;
36 | if (relatedProperty === originalRelatedProperty) return;
37 | if (!relatedProperty && originalRelatedProperty) {
38 | updatePropertyTypeSettings({
39 | plugin,
40 | property: originalRelatedProperty,
41 | type: typeKey,
42 | cb: (prev) => ({ ...prev, relatedProperty: undefined }),
43 | });
44 | }
45 | if (!relatedProperty) return;
46 |
47 | plugin.app.metadataTypeManager.setType(
48 | relatedProperty,
49 | customPropertyTypePrefix + typeKey
50 | );
51 | updatePropertyTypeSettings({
52 | plugin,
53 | property: relatedProperty,
54 | type: typeKey,
55 | cb: (prev) => ({ ...prev, relatedProperty: property }),
56 | });
57 | });
58 |
59 | new Setting(tabContentEl)
60 | .setName("Related property")
61 | .setDesc("")
62 | .addSearch((cmp) => {
63 | cmp.setValue(settings.relatedProperty ?? "").onChange((v) => {
64 | settings.relatedProperty = v;
65 | });
66 |
67 | new PropertySuggest(plugin, cmp.inputEl).onSelect((v) => {
68 | cmp.setValue(v.name);
69 | cmp.onChanged();
70 | });
71 | });
72 | };
73 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/types.ts:
--------------------------------------------------------------------------------
1 | import { PropertyRenderContext } from "obsidian-typings";
2 | import * as v from "valibot";
3 | import { Prettify } from "~/lib/utils";
4 | import { propertySettingsSchema } from "./schema";
5 | import BetterProperties from "~/main";
6 | import { PropertyValueComponent } from "obsidian";
7 | import { PropertySettingsModal } from "./settings";
8 | import { Icon } from "~/lib/types/icons";
9 |
10 | export type PropertySettings = Prettify<
11 | v.InferOutput
12 | >;
13 | export type CustomTypeKey = keyof PropertySettings;
14 |
15 | export type ModifiedPropertyRenderContext = PropertyRenderContext & {
16 | index?: number;
17 | };
18 |
19 | export type CustomPropertyType = {
20 | type: CustomTypeKey;
21 | icon: Icon;
22 | name(): string;
23 | validate(value: unknown): boolean;
24 | renderWidget(args: {
25 | plugin: BetterProperties;
26 | el: HTMLElement;
27 | value: unknown;
28 | ctx: ModifiedPropertyRenderContext;
29 | }): PropertyValueComponent;
30 | reservedKeys?: string[];
31 |
32 | registerListeners: (plugin: BetterProperties) => void;
33 |
34 | renderSettings: (args: {
35 | plugin: BetterProperties;
36 | modal: PropertySettingsModal;
37 | property: string;
38 | }) => void;
39 | };
40 |
41 | // export type CustomTypeWidget = {
42 | // type: CustomTypeKey;
43 | // icon: string;
44 | // default: () => Value;
45 | // name: () => string;
46 | // validate: (value: unknown) => boolean;
47 | // render: RenderCustomTypeWidget;
48 | // };
49 |
50 | export type RenderCustomTypeSettings = (args: {
51 | plugin: BetterProperties;
52 | modal: PropertySettingsModal;
53 | type: CustomTypeKey;
54 | property: string;
55 | }) => void;
56 |
57 | export type RenderCustomTypeWidget = (args: {
58 | plugin: BetterProperties;
59 | el: HTMLElement;
60 | value: Value;
61 | ctx: PropertyRenderContext;
62 | }) => PropertyValueComponent;
63 |
64 | export type PropertyTypeSchema = v.OptionalSchema<
65 | v.ObjectSchema<
66 | Record>,
67 | undefined
68 | >,
69 | undefined
70 | >;
71 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Markdown/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { obsidianText } from "~/i18next/obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { PropertyWidgetComponentNew } from "../utils";
4 | import {
5 | TEmbeddableMarkdownEditor,
6 | createEmbeddableMarkdownEditor,
7 | } from "~/classes/EmbeddableMarkdownEditor";
8 | import { PropertyRenderContext } from "obsidian-typings";
9 | import BetterProperties from "~/main";
10 |
11 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
12 | plugin,
13 | el,
14 | ctx,
15 | value,
16 | }) => {
17 | return new MarkdownTypeComponent(plugin, el, value, ctx);
18 | };
19 |
20 | class MarkdownTypeComponent extends PropertyWidgetComponentNew<
21 | "markdown",
22 | string
23 | > {
24 | type = "markdown" as const;
25 | parseValue = (v: unknown) => v?.toString() ?? "";
26 |
27 | embeddedMarkdownEditor: TEmbeddableMarkdownEditor;
28 |
29 | constructor(
30 | plugin: BetterProperties,
31 | el: HTMLElement,
32 | value: unknown,
33 | ctx: PropertyRenderContext
34 | ) {
35 | super(plugin, el, value, ctx);
36 |
37 | const parsed = this.parseValue(value);
38 | this.embeddedMarkdownEditor = createEmbeddableMarkdownEditor({
39 | app: plugin.app,
40 | container: el,
41 | options: {
42 | value: parsed,
43 | onBlur: (editor) => {
44 | const val = editor.editor?.getValue() ?? "";
45 | this.setValue(val);
46 | },
47 | placeholder: obsidianText("properties.label-no-value"),
48 | },
49 | filePath: ctx.sourcePath,
50 | });
51 |
52 | this.onFocus = () => {
53 | this.embeddedMarkdownEditor.focus();
54 | };
55 |
56 | if (!plugin.app.vault.getConfig("showLineNumber")) {
57 | el.setAttribute("data-better-properties-showLineNumber", "false");
58 | }
59 | }
60 |
61 | getValue(): string {
62 | return this.embeddedMarkdownEditor.editor?.getValue() ?? "";
63 | }
64 |
65 | setValue(value: unknown): void {
66 | if (this.embeddedMarkdownEditor.editor?.getValue() !== value) {
67 | this.embeddedMarkdownEditor.editor?.setValue(this.parseValue(value));
68 | }
69 | super.setValue(value);
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/docs/app/components/common/ThemeSwitcher/context.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | useContext,
4 | useEffect,
5 | useLayoutEffect,
6 | useState,
7 | } from "react";
8 |
9 | type Theme = "dark" | "light" | "system";
10 |
11 | type ThemeProviderProps = {
12 | children: React.ReactNode;
13 | defaultTheme?: Theme;
14 | storageKey?: string;
15 | };
16 |
17 | type ThemeProviderState = {
18 | theme: Theme;
19 | setTheme: (theme: Theme) => void;
20 | };
21 |
22 | const initialState: ThemeProviderState = {
23 | theme: "system",
24 | setTheme: () => null,
25 | };
26 |
27 | const ThemeProviderContext = createContext(initialState);
28 |
29 | export const ThemeProvider = ({
30 | children,
31 | defaultTheme = "system",
32 | storageKey = "vite-ui-theme",
33 | ...props
34 | }: ThemeProviderProps) => {
35 | const [theme, setTheme] = useState(() => defaultTheme);
36 |
37 | useLayoutEffect(() => {
38 | const storedTheme = window.localStorage.getItem(storageKey) as Theme;
39 | if (storedTheme) {
40 | setTheme(storedTheme);
41 | }
42 | }, []);
43 |
44 | useEffect(() => {
45 | const root = window.document.documentElement;
46 |
47 | root.classList.remove("light", "dark");
48 |
49 | if (theme === "system") {
50 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
51 | .matches
52 | ? "dark"
53 | : "light";
54 |
55 | root.classList.add(systemTheme);
56 | return;
57 | }
58 |
59 | root.classList.add(theme);
60 | }, [theme]);
61 |
62 | const value = {
63 | theme,
64 | setTheme: (theme: Theme) => {
65 | localStorage.setItem(storageKey, theme);
66 | setTheme(theme);
67 | },
68 | };
69 |
70 | return (
71 |
72 | {children}
73 |
74 | );
75 | };
76 |
77 | export const useTheme = () => {
78 | const context = useContext(ThemeProviderContext);
79 |
80 | if (context === undefined)
81 | throw new Error("useTheme must be used within a ThemeProvider");
82 |
83 | return context;
84 | };
85 |
--------------------------------------------------------------------------------
/docs/app/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip";
3 |
4 | import { cn } from "~/lib/utils";
5 |
6 | function TooltipProvider({
7 | delayDuration = 0,
8 | ...props
9 | }: React.ComponentProps) {
10 | return (
11 |
16 | );
17 | }
18 |
19 | function Tooltip({
20 | ...props
21 | }: React.ComponentProps) {
22 | return (
23 |
24 |
25 |
26 | );
27 | }
28 |
29 | function TooltipTrigger({
30 | ...props
31 | }: React.ComponentProps) {
32 | return ;
33 | }
34 |
35 | function TooltipContent({
36 | className,
37 | sideOffset = 0,
38 | children,
39 | ...props
40 | }: React.ComponentProps) {
41 | return (
42 |
43 |
52 | {children}
53 |
54 |
55 |
56 | );
57 | }
58 |
59 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
60 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Select/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { typeKey } from ".";
2 | import { CustomPropertyType } from "../types";
3 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
4 | import {
5 | renderDefaultSettings,
6 | renderDynamicSettings,
7 | renderFilesFromFileJsSetings,
8 | renderFilesFromFolderSettings,
9 | renderFilesFromInlineJsSettings,
10 | renderFilesFromTagSettings,
11 | renderManualSettings,
12 | } from "./utils";
13 |
14 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
15 | plugin,
16 | modal,
17 | property,
18 | }) => {
19 | const { tabContentEl: parentEl } = modal;
20 | const settings = getPropertyTypeSettings({
21 | plugin,
22 | property,
23 | type: typeKey,
24 | });
25 |
26 | modal.onTabChange(() => {
27 | setPropertyTypeSettings({
28 | plugin,
29 | property,
30 | type: typeKey,
31 | typeSettings: settings,
32 | });
33 | });
34 |
35 | renderDefaultSettings({
36 | plugin,
37 | parentEl,
38 | settings,
39 | modal,
40 | property,
41 | });
42 |
43 | if (settings.optionsType === "manual") {
44 | renderManualSettings({
45 | plugin,
46 | parentEl,
47 | settings,
48 | });
49 | }
50 |
51 | if (settings.optionsType === "dynamic") {
52 | if (!settings.dynamicOptionsType) {
53 | settings.dynamicOptionsType = "filesInFolder";
54 | }
55 |
56 | renderDynamicSettings({
57 | plugin,
58 | parentEl,
59 | settings,
60 | modal,
61 | property,
62 | });
63 |
64 | if (settings.dynamicOptionsType === "filesInFolder") {
65 | renderFilesFromFolderSettings({
66 | plugin,
67 | parentEl,
68 | settings,
69 | });
70 | return;
71 | }
72 |
73 | if (settings.dynamicOptionsType === "filesFromTag") {
74 | renderFilesFromTagSettings({
75 | plugin,
76 | parentEl,
77 | settings,
78 | });
79 | return;
80 | }
81 |
82 | if (settings.dynamicOptionsType === "filesFromInlineJs") {
83 | renderFilesFromInlineJsSettings({ plugin, parentEl, settings });
84 | }
85 |
86 | if (settings.dynamicOptionsType === "filesFromJsFile") {
87 | renderFilesFromFileJsSetings({ plugin, parentEl, settings });
88 | }
89 | }
90 | };
91 |
--------------------------------------------------------------------------------
/docs/app/routes/features/bpjs/(method types)/import/type.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 | import DataContent from "./dataContent.mdx";
3 |
4 | #### Arguments
5 |
6 | | Name | Type | Default |
7 | | --------------------------------------------------------------------- | --------------------- | --------- |
8 | | | `string` | - |
9 | | } /> | `string \| undefined` | undefined |
10 |
11 | #### Returns
12 |
13 | `Promise` - Depends on the file extension in the provided path:
14 |
15 | - `.js` - `unknown` - File contents will be executed as JavaScript. If the file exports a default, it will be returned. Default exports can be in ES6 syntax (`export default myVar`) or CommonJS syntax (`module.exports = myVar`)
16 | - `.css` - `void` - File contents will be read as CSS and inserted into `api.styleEl`. These styles will only affect elements within `api.el`.
17 | - `.json` - `object` - File contents will be parsed to JSON and returned.
18 | - `.yaml` - `object` - File contents will be parsed as YAML to JSON and returned.
19 | - `.csv` - `string[][]` - File contents will be parsed as a CSV to a two dimensional string array.
20 | - `.tsv` - `string[][]` - File contents will be parsed as a CSV to a two dimensional string array.
21 | - `.md` - `string` - File contents will be returned.
22 | - `.txt` - `string` - File contents will be returned.
23 |
24 | #### Examples
25 |
26 | ```js
27 | const exportedVar = await api.import("path/to/file.js");
28 |
29 | await api.import("path/to/file.css");
30 |
31 | const json = await api.import("path/to/file.json");
32 | const jsonFromYaml = await api.import("path/to/file.json");
33 |
34 | const twoDimensionalArray = await api.import("path/to/file.csv");
35 |
36 | // with custom delimiter
37 | const twoDimensionalArray = await api.import("path/to/file.tsv", ";");
38 |
39 | const noteContent = await api.import("path/to/note.md");
40 |
41 | const txtContent = await api.import("path/to/file.txt");
42 | ```
43 |
--------------------------------------------------------------------------------
/src/lib/types/oneDotNine.d.ts:
--------------------------------------------------------------------------------
1 | // All types in this file are for API changes in Obsidian ver 1.9+
2 |
3 | import {
4 | PropertyWidget as BasePropertyWidget,
5 | MetadataTypeManager as BaseMetadataTypeManager,
6 | PropertyInfo as BasePropertyInfo,
7 | PropertyRenderContext as BasePropertyRenderContext,
8 | } from "obsidian-typings";
9 | import { Scope as BaseScope } from "obsidian";
10 |
11 | declare module "obsidian" {
12 | interface Scope extends BaseScope {
13 | /**
14 | * Not sure if it was actually 1.9 or earlier
15 | */
16 | setTabFocusContainerEl(el: HTMLElement): void;
17 |
18 | /**
19 | * @deprecated
20 | */
21 | setTabFocusContainer(el: HTMLElement): never;
22 | }
23 | }
24 |
25 | declare module "obsidian-typings" {
26 | // interface PropertyInfo extends BasePropertyInfo {
27 | // /**
28 | // * @deprecated
29 | // * @see occurrences
30 | // */
31 | // count: never;
32 | // /**
33 | // * The number of notes that contain this property
34 | // * @remark used to be named `count`
35 | // */
36 | // occurrences: number;
37 | // /**
38 | // * @deprecated
39 | // * @see widget
40 | // */
41 | // type: never;
42 | // /**
43 | // * The type name of the corresponding type widget
44 | // * @remark used to be named `type`
45 | // */
46 | // widget: string;
47 | // }
48 | // interface PropertyRenderContext extends BasePropertyRenderContext {
49 | // /**
50 | // * @deprecated
51 | // */
52 | // metadataEditor: never;
53 | // }
54 | // interface PropertyWidget extends BasePropertyWidget {
55 | // render(
56 | // containerEl: HTMLElement,
57 | // /**
58 | // * 1.9+ changed to `T`
59 | // */
60 | // value: unknown,
61 | // context: PropertyRenderContext
62 | // ): PropertyValueComponent;
63 | // }
64 | // interface App {
65 | // MetadataTypeManager: {
66 | // /**
67 | // * @deprecated
68 | // * @see getAssignedWidget
69 | // */
70 | // getAssignedType(property: string): string | null;
71 | // /**
72 | // * Gets the type name of the currently assigned type widget for the given property
73 | // */
74 | // getAssignedWidget(property: string): string | null;
75 | // };
76 | // }
77 | }
78 |
--------------------------------------------------------------------------------
/docs/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
22 |
41 |
55 |
56 |
63 |
66 |
70 |
74 |
75 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Slider/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { SliderComponent } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { PropertyWidgetComponentNew } from "../utils";
4 | import { clampNumber } from "~/lib/utils";
5 | import { PropertyRenderContext } from "obsidian-typings";
6 | import BetterProperties from "~/main";
7 |
8 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
9 | plugin,
10 | el,
11 | ctx,
12 | value,
13 | }) => {
14 | return new SliderTypeComponent(plugin, el, value, ctx);
15 | };
16 |
17 | class SliderTypeComponent extends PropertyWidgetComponentNew<"slider", number> {
18 | type = "slider" as const;
19 | parseValue = (v: unknown) => {
20 | const n = Number(v);
21 | return Number.isNaN(n) ? 0 : n;
22 | };
23 |
24 | slider: SliderComponent;
25 |
26 | constructor(
27 | plugin: BetterProperties,
28 | el: HTMLElement,
29 | value: unknown,
30 | ctx: PropertyRenderContext
31 | ) {
32 | super(plugin, el, value, ctx);
33 |
34 | const settings = this.getSettings();
35 | const min = settings.min ?? 0;
36 | const max = settings.max ?? 100;
37 | const step = settings.step ?? 1;
38 | const clamp = (x: unknown) => clampNumber(Number(x), min, max);
39 |
40 | const parsed = clamp(this.parseValue(value));
41 |
42 | const container = el.createDiv({
43 | cls: "better-properties-slider-container",
44 | attr: {
45 | "data-better-properties-slider-hide-limit": settings.hideLimits ?? null,
46 | },
47 | });
48 |
49 | container.createDiv({
50 | cls: "better-properties-slider-limit",
51 | text: min.toString(),
52 | });
53 |
54 | this.slider = new SliderComponent(container)
55 | .setValue(parsed)
56 | .onChange((v) => {
57 | this.setValue(v);
58 | })
59 | .setLimits(min, max, step)
60 | .setDynamicTooltip();
61 |
62 | container.createDiv({
63 | cls: "better-properties-slider-limit",
64 | text: max.toString(),
65 | });
66 |
67 | this.onFocus = () => {
68 | this.slider.sliderEl.focus();
69 | };
70 | }
71 |
72 | getValue(): number {
73 | return this.slider.getValue();
74 | }
75 |
76 | setValue(value: unknown): void {
77 | if (this.slider.getValue() !== value) {
78 | this.slider.setValue(this.parseValue(value));
79 | }
80 | super.setValue(value);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Created/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { moment } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import {
4 | getPropertyTypeSettings,
5 | // getPropertyTypeSettings,
6 | PropertyWidgetComponent,
7 | } from "../utils";
8 |
9 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
10 | plugin,
11 | el,
12 | ctx,
13 | value,
14 | }) => {
15 | const settings = getPropertyTypeSettings({
16 | plugin,
17 | property: ctx.key,
18 | type: "created",
19 | });
20 |
21 | const container = el.createDiv({
22 | cls: "better-properties-property-value-inner better-properties-mod-created",
23 | });
24 |
25 | const cmp = new PropertyWidgetComponent(
26 | "created",
27 | container,
28 | () => {},
29 | () => {}
30 | );
31 |
32 | const file = plugin.app.vault.getFileByPath(ctx.sourcePath);
33 | if (!file) {
34 | throw new Error("File not found by ctx.sourcePath of: " + ctx.sourcePath);
35 | }
36 |
37 | const ctime = moment(file.stat.ctime).format("yyyy-MM-DDTHH:mm");
38 | if (!value || ctime !== value) {
39 | // property is rendered with no value
40 | // so it's likely rendered for the first time
41 |
42 | // 0 timeout because without it, it will usually just remove all properties
43 | window.setTimeout(() => {
44 | ctx.onChange(ctime);
45 | }, 0);
46 | }
47 |
48 | if (!settings.format) {
49 | const dateInput = container.createEl("input", {
50 | type: "datetime-local",
51 | cls: "metadata-input metadata-input-text mod-datetime",
52 | attr: {
53 | // disabled: "true",
54 | "aria-disabled": "true",
55 | },
56 | });
57 | const createdTime = moment(ctime).format("yyyy-MM-DDTHH:mm");
58 |
59 | dateInput.value = createdTime;
60 | dateInput.addEventListener("input", () => {
61 | dateInput.value = createdTime;
62 | });
63 |
64 | cmp.onFocus = () => {
65 | dateInput.focus();
66 | };
67 | }
68 |
69 | if (settings.format) {
70 | const dateStr = moment(ctime).format(settings.format);
71 | const inputEl = container.createEl("input", {
72 | cls: "metadata-input metadata-input-number",
73 | value: dateStr,
74 | type: "text",
75 | });
76 | inputEl.addEventListener("input", () => {
77 | inputEl.value = dateStr;
78 | });
79 | cmp.onFocus = () => inputEl.focus();
80 | }
81 |
82 | return cmp;
83 | };
84 |
--------------------------------------------------------------------------------
/docs/app/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { Slot } from "@radix-ui/react-slot";
3 | import { cva, type VariantProps } from "class-variance-authority";
4 | import { cn } from "~/lib/utils";
5 |
6 | const buttonVariants = cva(
7 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
8 | {
9 | variants: {
10 | variant: {
11 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
12 | destructive:
13 | "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
14 | outline:
15 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
16 | secondary:
17 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
18 | ghost:
19 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-9 px-4 py-2 has-[>svg]:px-3",
24 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
25 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
26 | icon: "size-9",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | },
34 | );
35 |
36 | function Button({
37 | className,
38 | variant,
39 | size,
40 | asChild = false,
41 | ...props
42 | }: React.ComponentProps<"button"> &
43 | VariantProps & {
44 | asChild?: boolean;
45 | }) {
46 | const Comp = asChild ? Slot : "button";
47 |
48 | return (
49 |
54 | );
55 | }
56 |
57 | export { Button, buttonVariants };
58 |
--------------------------------------------------------------------------------
/src/MetadataEditor/patchMetadataEditor/patchMetadataEditorProperty.ts:
--------------------------------------------------------------------------------
1 | import BetterProperties from "~/main";
2 | import { PatchedMetadataEditor } from ".";
3 | // import { around, dedupe } from "monkey-around";
4 | import { Constructor, MarkdownView } from "obsidian";
5 | // import { monkeyAroundKey } from "~/lib/constants";
6 | // import { refreshPropertyEditor } from "..";
7 | // import { MetadataEditorProperty } from "obsidian-typings";
8 |
9 | export const patchMetadataEditorProperty = (
10 | plugin: BetterProperties,
11 | metadataEditorPrototype: PatchedMetadataEditor
12 | ) => {
13 | const { app } = plugin;
14 |
15 | const proto =
16 | metadataEditorPrototype.constructor as Constructor;
17 | class ME extends proto {
18 | constructor() {
19 | super();
20 | }
21 | }
22 | const metadataEditor = new ME();
23 |
24 | metadataEditor._children = [];
25 | metadataEditor._events = [];
26 | metadataEditor.owner = {
27 | getFile: () => {},
28 | } as MarkdownView;
29 | metadataEditor.addPropertyButtonEl;
30 | metadataEditor.propertyListEl = createDiv();
31 | metadataEditor.containerEl = createDiv();
32 | metadataEditor.app = app;
33 | metadataEditor.properties = [];
34 | metadataEditor.rendered = [];
35 | metadataEditor.headingEl = createDiv();
36 | metadataEditor.addPropertyButtonEl = createEl("button");
37 | // @ts-ignore TODO
38 | metadataEditor.errorEl = createDiv();
39 | metadataEditor.owner.getHoverSource = () => "source";
40 | metadataEditor.load();
41 | metadataEditor.synchronize({ tags: "[]" });
42 | const MetadataEditorPropertyPrototype = Object.getPrototypeOf(
43 | metadataEditor.rendered[0]
44 | ) as (typeof metadataEditor.rendered)[0];
45 |
46 | MetadataEditorPropertyPrototype; // stops no-unused-variables rule
47 |
48 | // I don't this is actually needed. And it causes issues with focusing the valueEl when pressing Enter withint the keyInputEl.
49 | // const removePatch = around(MetadataEditorPropertyPrototype, {
50 | // handleUpdateKey(old) {
51 | // return dedupe(monkeyAroundKey, old, function (newKey) {
52 | // // @ts-ignore
53 | // const that = this as MetadataEditorProperty;
54 |
55 | // const returnValue = old.call(that, newKey);
56 | // refreshPropertyEditor(plugin, newKey);
57 | // return returnValue;
58 | // });
59 | // },
60 | // });
61 |
62 | // plugin.register(removePatch);
63 | };
64 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Title/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { displayTooltip, TextComponent } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import {
4 | // getPropertyTypeSettings,
5 | PropertyWidgetComponent,
6 | } from "../utils";
7 | import { tryCatch } from "~/lib/utils";
8 |
9 | /**
10 | * TODO
11 | * Current implementation is wrong. Instead should behave like the "Frontmatter Title" plugin where it *displays* the value of this property, rather than actually renaming the file.
12 | *
13 | * Might not even include this type due to the other plugin existing
14 | */
15 |
16 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
17 | plugin,
18 | el,
19 | ctx,
20 | value,
21 | }) => {
22 | // const settings = getPropertyTypeSettings({
23 | // plugin,
24 | // property: ctx.key,
25 | // type: "toggle",
26 | // });
27 |
28 | const container = el.createDiv({
29 | cls: "better-properties-property-value-inner better-properties-mod-title",
30 | });
31 |
32 | const file = plugin.app.vault.getFileByPath(ctx.sourcePath);
33 | if (!file) {
34 | throw new Error("File not found by ctx.sourcePath of: " + ctx.sourcePath);
35 | }
36 |
37 | const title = file.basename;
38 | const text = new TextComponent(container).setValue(title);
39 |
40 | const doRename = async () => {
41 | const newPath =
42 | (file.parent?.path ?? "") + "/" + text.getValue() + "." + file.extension;
43 | const { success, error } = await tryCatch(
44 | plugin.app.fileManager.renameFile(file, newPath)
45 | );
46 | if (success) return;
47 | text.setValue(title);
48 | displayTooltip(text.inputEl, error, {
49 | placement: "bottom",
50 | classes: ["mod-error"],
51 | });
52 | };
53 |
54 | if (!value || title !== value) {
55 | // property is rendered with no value
56 | // so it's likely rendered for the first time
57 |
58 | // 0 timeout because without it, it will usually just remove all properties
59 | window.setTimeout(() => {
60 | ctx.onChange(title);
61 | }, 0);
62 | }
63 |
64 | text.inputEl.addEventListener("blur", doRename);
65 | text.inputEl.addEventListener("keydown", async (e) => {
66 | if (e.key === "Enter") {
67 | await doRename();
68 | }
69 | if (e.key === "Escape") {
70 | text.setValue(title);
71 | }
72 | });
73 |
74 | const cmp = new PropertyWidgetComponent(
75 | "title",
76 | container,
77 | (v) => {
78 | text.setValue(v?.toString() ?? "");
79 | doRename();
80 | },
81 | () => {
82 | text.inputEl.focus();
83 | }
84 | );
85 | return cmp;
86 | };
87 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/schema.ts:
--------------------------------------------------------------------------------
1 | import { NonNullishObject } from "~/lib/utils";
2 | import { PropertySettings, PropertyTypeSchema } from "./types";
3 | import * as v from "valibot";
4 | import { selectSettingsSchema } from "./Select";
5 | import { toggleSettingsSchema } from "./Toggle";
6 | import { titleSettingsSchema } from "./Title";
7 | import { markdownSettingsSchema } from "./Markdown";
8 | import { createdSettingsSchema } from "./Created";
9 | import { objectSettingsSchema } from "./Object";
10 | import { colorSettingsSchema } from "./Color";
11 | import { ratingSettingsSchema } from "./Rating";
12 | import { dateCustomSettingsSchema } from "./DateCustom";
13 | import { sliderSettingsSchema } from "./Slider";
14 | import { timeSettingsSchema } from "./Time";
15 | import { multiSelectSettingsSchema } from "./MultiSelect";
16 | import { numericSettingsSchema } from "./Numeric";
17 | import { arraySettingsSchema } from "./Array";
18 | import { relationSettingsSchema } from "./Relation";
19 |
20 | type SettingsBase = v.ObjectSchema<
21 | Record,
22 | undefined
23 | >;
24 |
25 | // NOTE type key names must be all lowercase, otherwise they will fail to register
26 | export const propertySettingsSchema = v.object({
27 | general: v.optional(
28 | v.object({
29 | icon: v.optional(v.string(), ""),
30 | hidden: v.optional(v.boolean(), false),
31 | defaultValue: v.optional(v.string()),
32 | // onloadScript: z.string(),
33 | alias: v.optional(v.string()),
34 | suggestions: v.optional(v.array(v.string())),
35 | collapsed: v.optional(v.boolean()),
36 | })
37 | ),
38 | array: arraySettingsSchema,
39 | select: selectSettingsSchema,
40 | multiselect: multiSelectSettingsSchema,
41 | toggle: toggleSettingsSchema,
42 | title: titleSettingsSchema,
43 | markdown: markdownSettingsSchema,
44 | created: createdSettingsSchema,
45 | // modified: v.optional(v.object({})),
46 | object: objectSettingsSchema,
47 | color: colorSettingsSchema,
48 | rating: ratingSettingsSchema,
49 | relation: relationSettingsSchema,
50 | datecustom: dateCustomSettingsSchema,
51 | slider: sliderSettingsSchema,
52 | time: timeSettingsSchema,
53 | numeric: numericSettingsSchema,
54 | }) satisfies SettingsBase;
55 |
56 | export const getDefaultPropertySettings =
57 | (): NonNullishObject => {
58 | return Object.entries(propertySettingsSchema.entries).reduce(
59 | (acc, [k, schema]) => {
60 | const key = k as keyof typeof acc;
61 | // @ts-ignore TODO
62 | acc[key] = v.parse(schema, {});
63 | return acc;
64 | },
65 | {} as NonNullishObject
66 | );
67 | };
68 |
--------------------------------------------------------------------------------
/docs/public/bp-logo-circle.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
22 |
41 |
55 |
56 |
63 |
67 |
74 |
77 |
81 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/docs/app/routes/getting-started/installation/article.mdx:
--------------------------------------------------------------------------------
1 | import { InfoButton } from "~/components/common/InfoButton";
2 |
3 | ## Installation
4 |
5 | **Pre installation checklist**
6 |
7 | - ✅ Use consistent casing in property names
8 | - Most features within this plugin (and in Obsidian in general) will treat two property names to be the same if they only differ in the casing of their names, i.e. `myproperty` is equivalent to `MyProperty`. However, this is not 100% consistent across the app, so try to use consistent casing between uses of a property name.
9 | - ✅ **Don't** use a dot (`.`) in property names.
10 | - Better Properties and Obsidian itself (Bases for example) treat property names with dots in them as if they are a "nested property" (see [Object property type](/features/property-types#object)). Because of this, make sure your existing property names don't have a dot in them.
11 | - ✅ **Dont** use a hastag (`#`) as a sub-property name in object properties.
12 | - Better Properties uses the syntax `.#` to store the type and settings for the sub-properties of [Array type](/features/property-types#array) properties, so using a hashtag as a sub-property name in an object may cause unexpected behaviors.
13 |
14 | Ready to get started? Use one of the options below!
15 |
16 | If you enjoy this plugin, please consider giving it a [star on github](https://github.com/unxok/obsidian-better-properties) and/or [buying me a coffee](https://buymeacoffee.com/unxok)<3
17 |
18 | ### BRAT
19 |
20 | 1. Download the [Beta Reviewers Auto-update Tester (BRAT)](https://github.com/TfTHacker/obsidian42-brat) plugin from the [Obsidian community plugins directory](obsidian://show-plugin?id=obsidian42-brat) and enable it.
21 | 2. Choose one of the following:
22 | - automatic
23 | 1. Click [this link](obsidian://brat?plugin=unxok/obsidian-better-properties) then press the `Add plugin` button.
24 | - manual
25 | 1. Go to the BRAT plugin settings and select the `Add beta plugin` button.
26 | 2. Paste the following: https://github.com/unxok/obsidian-better-properties.
27 | 3. Click the `Add plugin` button.
28 |
29 | ### Manual
30 |
31 | 1. Open your vault's community plugins folder (default at `YOUR_VAULT/.obsidian/plugins`) and create a folder named `better-properties`.
32 | 2. Go to the Better Properties [releases page](https://github.com/unxok/obsidian-better-properties/releases)
33 | 3. Download `main.js`, `manifest.json`, and `styles.css`. Ensure they are placed in the folder you made in step one.
34 |
35 | ### Community plugins directory
36 |
37 | v1.0.0 has been submitted for review as of 2025-10-5 ([link](https://github.com/obsidianmd/obsidian-releases/pull/8049)). This page will be updated once it has been approved.
38 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/Slider/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
5 | import { typeKey } from ".";
6 |
7 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
8 | modal,
9 | plugin,
10 | property,
11 | }) => {
12 | const { tabContentEl } = modal;
13 |
14 | const settings = getPropertyTypeSettings({
15 | plugin,
16 | property: property,
17 | type: typeKey,
18 | });
19 |
20 | modal.onTabChange(() => {
21 | setPropertyTypeSettings({
22 | plugin,
23 | property: property,
24 | type: typeKey,
25 | typeSettings: { ...settings },
26 | });
27 | });
28 |
29 | new Setting(tabContentEl)
30 | .setName(text("customPropertyTypes.slider.settings.min.title"))
31 | .setDesc(text("customPropertyTypes.slider.settings.min.desc"))
32 | .addText((cmp) => {
33 | cmp.inputEl.setAttribute("type", "number");
34 | cmp.inputEl.setAttribute("inputmode", "decimal");
35 | cmp
36 | .setPlaceholder("0")
37 | .setValue(settings.min?.toString() ?? "")
38 | .onChange((v) => {
39 | if (!v) {
40 | settings.min = undefined;
41 | return;
42 | }
43 | settings.min = Number(v);
44 | });
45 | });
46 |
47 | new Setting(tabContentEl)
48 | .setName(text("customPropertyTypes.slider.settings.max.title"))
49 | .setDesc(text("customPropertyTypes.slider.settings.max.desc"))
50 | .addText((cmp) => {
51 | cmp.inputEl.setAttribute("type", "number");
52 | cmp.inputEl.setAttribute("inputmode", "decimal");
53 | cmp
54 | .setPlaceholder("100")
55 | .setValue(settings.max?.toString() ?? "")
56 | .onChange((v) => {
57 | if (!v) {
58 | settings.max = undefined;
59 | return;
60 | }
61 | settings.max = Number(v);
62 | });
63 | });
64 |
65 | new Setting(tabContentEl)
66 | .setName(text("customPropertyTypes.slider.settings.step.title"))
67 | .setDesc(text("customPropertyTypes.slider.settings.step.desc"))
68 | .addText((cmp) => {
69 | cmp.inputEl.setAttribute("type", "number");
70 | cmp.inputEl.setAttribute("inputmode", "decimal");
71 | cmp
72 | .setPlaceholder("1")
73 | .setValue(settings.step?.toString() ?? "")
74 | .onChange((v) => {
75 | if (!v) {
76 | settings.step = undefined;
77 | return;
78 | }
79 | settings.step = Number(v);
80 | });
81 | });
82 |
83 | new Setting(tabContentEl)
84 | .setName(text("customPropertyTypes.slider.settings.hideLimits.title"))
85 | .setDesc(text("customPropertyTypes.slider.settings.hideLimits.desc"))
86 | .addToggle((cmp) => {
87 | cmp.setValue(!!settings.hideLimits).onChange((b) => {
88 | settings.hideLimits = b;
89 | });
90 | });
91 | };
92 |
--------------------------------------------------------------------------------
/src/classes/ColorTextComponent/index.ts:
--------------------------------------------------------------------------------
1 | import { ValueComponent, TextComponent, ColorComponent } from "obsidian";
2 | import { obsidianText } from "~/i18next/obsidian";
3 |
4 | export class ColorTextComponent extends ValueComponent {
5 | private value: string = "";
6 | componentsContainerEl: HTMLElement;
7 | colorsContainerEl: HTMLElement;
8 | textComponent: TextComponent;
9 | colorComponent: ColorComponent;
10 | colorInputEl: HTMLDivElement;
11 |
12 | onChangeCallback: (value: string) => void = () => {};
13 |
14 | constructor(containerEl: HTMLElement) {
15 | super();
16 |
17 | this.componentsContainerEl = containerEl.createDiv({
18 | cls: "better-properties-color-text-component-container",
19 | });
20 |
21 | this.colorsContainerEl = this.componentsContainerEl.createDiv({
22 | cls: "better-properties-color-text-component-colors-container",
23 | attr: {
24 | // tabindex: "-1",
25 | },
26 | });
27 |
28 | this.colorComponent = new ColorComponent(this.colorsContainerEl);
29 | this.colorComponent.colorPickerEl.setAttribute("aria-hidden", "true");
30 | this.colorComponent.colorPickerEl.setAttribute("tabindex", "-1");
31 | this.colorInputEl = this.colorsContainerEl.createDiv({
32 | cls: "better-properties-swatch better-properties-color-text-component-color-input",
33 | attr: {
34 | tabindex: "0",
35 | role: "button",
36 | },
37 | });
38 | this.colorComponent.onChange((v) => {
39 | this.setColorCssVar(v);
40 | this.textComponent.inputEl.value = v;
41 | this.value = v;
42 | this.onChanged();
43 | });
44 |
45 | this.colorInputEl.addEventListener("click", () => {
46 | this.colorComponent.colorPickerEl.showPicker();
47 | });
48 |
49 | this.textComponent = new TextComponent(
50 | this.componentsContainerEl
51 | ).setPlaceholder(obsidianText("properties.label-no-value"));
52 | this.textComponent.onChange((v) => {
53 | this.setColorCssVar(v);
54 | if (isHex(v)) {
55 | this.colorComponent.colorPickerEl.value = v;
56 | }
57 | this.value = v;
58 | this.onChanged();
59 | });
60 | }
61 |
62 | getValue(): string {
63 | return this.value;
64 | }
65 |
66 | setValue(value: string): this {
67 | this.value = value;
68 | this.setColorCssVar(value);
69 | this.textComponent.inputEl.value = value;
70 | if (isHex(value)) {
71 | this.colorComponent.colorPickerEl.value = value;
72 | }
73 | return this;
74 | }
75 |
76 | onChanged() {
77 | this.onChangeCallback(this.getValue());
78 | }
79 |
80 | onChange(cb: (value: string) => void): this {
81 | this.onChangeCallback = cb;
82 | return this;
83 | }
84 |
85 | setColorCssVar(value: string) {
86 | this.colorsContainerEl.setCssProps({ "--better-properties-bg": value });
87 | }
88 | }
89 |
90 | const isHex = (str: string) => {
91 | return str.startsWith("#") && str.length === 7;
92 | };
93 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/MultiSelect/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { Setting } from "obsidian";
2 | import { typeKey } from ".";
3 | import {
4 | renderDynamicSettings,
5 | renderFilesFromFileJsSetings,
6 | renderFilesFromFolderSettings,
7 | renderFilesFromInlineJsSettings,
8 | renderFilesFromTagSettings,
9 | renderManualSettings,
10 | SelectOptionsType,
11 | } from "./utils";
12 | import { CustomPropertyType } from "../types";
13 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
14 | import { text } from "~/i18next";
15 |
16 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
17 | modal,
18 | plugin,
19 | property,
20 | }) => {
21 | const { tabContentEl: parentEl } = modal;
22 |
23 | const settings = getPropertyTypeSettings({
24 | plugin,
25 | property: property,
26 | type: typeKey,
27 | });
28 |
29 | modal.onTabChange(() => {
30 | setPropertyTypeSettings({
31 | plugin,
32 | property: property,
33 | type: typeKey,
34 | typeSettings: { ...settings },
35 | });
36 | });
37 |
38 | new Setting(parentEl)
39 | .setName(text("customPropertyTypes.select.settings.optionsType.title"))
40 | .setDesc(text("customPropertyTypes.select.settings.optionsType.desc"))
41 | .addDropdown((dropdown) => {
42 | dropdown
43 | .addOptions({
44 | manual: text(
45 | "customPropertyTypes.select.settings.optionsType.selectLabelManual"
46 | ),
47 | dynamic: text(
48 | "customPropertyTypes.select.settings.optionsType.selectLabelDynamic"
49 | ),
50 | } satisfies Record, string>)
51 | .setValue(
52 | settings?.optionsType ?? ("manual" satisfies SelectOptionsType)
53 | )
54 | .onChange((v) => {
55 | const opt = v as SelectOptionsType;
56 | // optionsTypeSettings.showGroup(opt);
57 | settings.optionsType = opt;
58 | parentEl.empty();
59 | renderSettings({ plugin, modal, property });
60 | });
61 | });
62 |
63 | if (settings.optionsType === "manual") {
64 | renderManualSettings({
65 | plugin,
66 | parentEl,
67 | settings,
68 | });
69 | }
70 |
71 | if (settings.optionsType === "dynamic") {
72 | if (!settings.dynamicOptionsType) {
73 | settings.dynamicOptionsType = "filesInFolder";
74 | }
75 |
76 | renderDynamicSettings({
77 | plugin,
78 | parentEl,
79 | settings,
80 | modal,
81 | property,
82 | });
83 |
84 | if (settings.dynamicOptionsType === "filesInFolder") {
85 | renderFilesFromFolderSettings({
86 | plugin,
87 | parentEl,
88 | settings,
89 | });
90 | return;
91 | }
92 |
93 | if (settings.dynamicOptionsType === "filesFromTag") {
94 | renderFilesFromTagSettings({
95 | plugin,
96 | parentEl,
97 | settings,
98 | });
99 | return;
100 | }
101 |
102 | if (settings.dynamicOptionsType === "filesFromInlineJs") {
103 | renderFilesFromInlineJsSettings({ plugin, parentEl, settings });
104 | }
105 |
106 | if (settings.dynamicOptionsType === "filesFromJsFile") {
107 | renderFilesFromFileJsSetings({ plugin, parentEl, settings });
108 | }
109 | }
110 | };
111 |
--------------------------------------------------------------------------------
/docs/app/routes/features/metadata-editor/article.mdx:
--------------------------------------------------------------------------------
1 | ## Metadata Editor
2 |
3 | The Metadata Editor refers to the "container" for the rendered properties of a note. This by itself is a built-in to Obsidian.
4 |
5 | Below, a CSS snippet (among others) has been added to add an orange border around the Metadata Editor.
6 |
7 | 
8 |
9 | Better Properties augments the Metadata Editor to add the features below.
10 |
11 | ### More button
12 |
13 | Opens the menu shown below. Right now there's only these two options, but I have some ideas for more. If you think of something, let me know by [opening a feature request](https://github.com/unxok/obsidian-better-properties/issues/new?template=feature_request.md).
14 |
15 | 
16 |
17 | #### Show hidden
18 |
19 | Toggles the visibility of properties marked as "hidden" in their general property settings.
20 |
21 | 
22 |
23 | #### Sort
24 |
25 | Sorts the note's properties by the chosen sorting option.
26 |
27 | 
28 |
29 | ### Resizable label width
30 |
31 | Select and drag the border (which is default invisible) between the name and value of a property to resize the width of all property labels in the Metadata Editor. Double click it to reset it back to the default.
32 |
33 | 
34 |
35 | This customized width is saved to the plugin settings and applied to all notes.
36 |
37 | ### Collapsible properties
38 |
39 | When hovering over the name of a property, a collapse indicator will show which can be used to hide the value of that property.
40 |
41 | 
42 |
43 | ### Property Menu
44 |
45 | When selecting the icon for a given property, the following menu will be shown. This by itself is built-in to Obsidian.
46 |
47 | 
48 |
49 | Better Properties augments this menu by adding or changing the following items.
50 |
51 | #### Property type
52 |
53 | This built-in item has it's submenu changed such that the types are listed in alphabetical order and types added by the plugin have their icon in your accent color.
54 |
55 | #### Settings
56 |
57 | This new item opens the settings for the given property. This can also be opened by using the command `Bett Properties: Open property settings`.
58 |
59 | Switching to a different tab or closing the modal will save and apply the changed settings.
60 |
61 | 
62 |
63 | #### Rename
64 |
65 | This new item allows you to mass-update the property name in every note it's used in. The property settings of the previous name will be transferred to the new name.
66 |
67 | #### Icon
68 |
69 | This new item allows you to override the type icon to shown for this property in the Metadata Editor.
70 |
71 | #### Delete
72 |
73 | This new item allows you to delete the given property from all notes. A confirmation modal will show before doing the deletion, but be careful because the _deletion is irreversible_
74 |
--------------------------------------------------------------------------------
/src/classes/InputSuggest/PropertySuggest/index.ts:
--------------------------------------------------------------------------------
1 | import { FuzzyMatch, FuzzySuggestModal, setIcon } from "obsidian";
2 | import { InputSuggest, Suggestion } from "..";
3 | import { getPropertyTypeSettings } from "~/CustomPropertyTypes";
4 | import BetterProperties from "~/main";
5 | import { Icon } from "~/lib/types/icons";
6 |
7 | type Value = {
8 | name: string;
9 | icon: string;
10 | };
11 |
12 | export class PropertySuggest extends InputSuggest {
13 | constructor(
14 | public plugin: BetterProperties,
15 | textInputEl: HTMLDivElement | HTMLInputElement
16 | ) {
17 | super(plugin.app, textInputEl);
18 | }
19 | protected getSuggestions(query: string): Value[] {
20 | const properties: Value[] = Object.values(
21 | this.app.metadataTypeManager.properties
22 | )
23 | .map(({ name, widget }) => {
24 | const icon =
25 | (getPropertyTypeSettings({
26 | plugin: this.plugin,
27 | property: name,
28 | type: "general",
29 | }).icon ||
30 | this.app.metadataTypeManager.getWidget(widget)?.icon) ??
31 | ("lucide-file-question" satisfies Icon);
32 | return {
33 | name,
34 | icon,
35 | };
36 | })
37 | .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
38 | if (!query) return properties;
39 | const lower = query.toLowerCase();
40 | return properties.filter((property) =>
41 | property.name.toLowerCase().includes(lower)
42 | );
43 | }
44 |
45 | protected parseSuggestion({ name }: Value): Suggestion {
46 | return {
47 | title: name,
48 | icon: " ",
49 | };
50 | }
51 |
52 | renderSuggestion(value: Value, el: HTMLElement): void {
53 | super.renderSuggestion.call(this, value, el);
54 |
55 | const iconEl = el.querySelector(".suggestion-flair");
56 | if (!(iconEl instanceof HTMLElement)) return;
57 | setIcon(iconEl, value.icon);
58 | }
59 | }
60 |
61 | export class PropertySuggestModal extends FuzzySuggestModal {
62 | constructor(public plugin: BetterProperties) {
63 | super(plugin.app);
64 | }
65 |
66 | getItems(): Value[] {
67 | return Object.values(this.app.metadataTypeManager.properties)
68 | .map(({ name, widget }) => {
69 | const icon =
70 | (getPropertyTypeSettings({
71 | plugin: this.plugin,
72 | property: name,
73 | type: "general",
74 | }).icon ||
75 | this.app.metadataTypeManager.getWidget(widget)?.icon) ??
76 | ("lucide-file-question" satisfies Icon);
77 | return {
78 | name,
79 | icon,
80 | };
81 | })
82 | .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
83 | }
84 |
85 | getItemText(item: Value): string {
86 | return item.name;
87 | }
88 |
89 | onChooseItem(_item: Value, _evt: MouseEvent | KeyboardEvent): void {
90 | throw new Error("Method not implemented");
91 | }
92 |
93 | renderSuggestion(item: FuzzyMatch, el: HTMLElement): void {
94 | super.renderSuggestion.call(this, item, el);
95 | el.empty();
96 | el.classList.add("mod-complex");
97 | el.createDiv({ cls: "suggestion-title" }).createDiv({
98 | cls: "suggestion-title",
99 | text: item.item.name,
100 | });
101 | setIcon(
102 | el
103 | .createDiv({ cls: "suggestion-aux" })
104 | .createSpan({ cls: "suggestion-flair" }),
105 | item.item.icon
106 | );
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/MetadataEditor/propertyEditorMenu/rename.ts:
--------------------------------------------------------------------------------
1 | import { TextComponent, setIcon, ButtonComponent } from "obsidian";
2 | import { ConfirmationModal } from "~/classes/ConfirmationModal";
3 | import { text } from "~/i18next";
4 | import { obsidianText } from "~/i18next/obsidian";
5 | import { Icon } from "~/lib/types/icons";
6 | import { renameProperty, findKey } from "~/lib/utils";
7 | import BetterProperties from "~/main";
8 |
9 | export const openRenameModal = ({
10 | plugin,
11 | property,
12 | }: {
13 | plugin: BetterProperties;
14 | property: string;
15 | }) => {
16 | const modal = new ConfirmationModal(plugin.app);
17 |
18 | modal
19 | .setTitle(text("metadataEditor.propertyMenu.rename.modalTitle"))
20 | .setContent(
21 | createFragment((frag) => {
22 | frag.createEl("p", {
23 | text: text("metadataEditor.propertyMenu.rename.modalDesc", {
24 | property,
25 | }),
26 | });
27 | const textCmp = new TextComponent(frag.createDiv())
28 | .setValue(property)
29 | .setPlaceholder(
30 | text(
31 | "metadataEditor.propertyMenu.rename.newPropertyNamePlaceholder"
32 | )
33 | );
34 | // const warningEl = frag.createEl("p", {
35 | // cls: "better-properties-mod-warning",
36 | // });
37 | const warningEl = createCallout({
38 | parentEl: frag,
39 | type: "warning",
40 | icon: "lucide-alert-triangle",
41 | title: "Existing property name",
42 | desc: text("metadataEditor.propertyMenu.rename.nameExistsWarning", {
43 | property,
44 | }),
45 | });
46 | let renameBtn: ButtonComponent | null = null;
47 | modal
48 | .addFooterButton((btn) => {
49 | renameBtn = btn
50 | .setButtonText(obsidianText("interface.menu.rename"))
51 | .setWarning()
52 | .onClick(async () => {
53 | await renameProperty({
54 | plugin,
55 | property,
56 | newProperty: textCmp.getValue(),
57 | });
58 | modal.close();
59 | });
60 | })
61 | .addFooterButton((btn) =>
62 | btn
63 | .setButtonText(obsidianText("dialogue.button-cancel"))
64 | .onClick(() => {
65 | modal.close();
66 | })
67 | );
68 |
69 | textCmp.onChange((v) => {
70 | if (!renameBtn) return;
71 | if (v === property) {
72 | warningEl.classList.add("better-properties-mod-hidden");
73 | renameBtn.setDisabled(true);
74 | return;
75 | }
76 | renameBtn.setDisabled(false);
77 | const existing = findKey(
78 | plugin.app.metadataTypeManager.properties,
79 | v
80 | );
81 | if (!existing) {
82 | warningEl.classList.add("better-properties-mod-hidden");
83 | return;
84 | }
85 | warningEl.classList.remove("better-properties-mod-hidden");
86 | });
87 |
88 | textCmp.onChanged();
89 | })
90 | );
91 |
92 | modal.open();
93 | };
94 |
95 | const createCallout = ({
96 | parentEl,
97 | type,
98 | icon,
99 | title,
100 | desc,
101 | }: {
102 | parentEl: HTMLElement | DocumentFragment;
103 | type: string;
104 | icon: Icon;
105 | title: string;
106 | desc: string;
107 | }) => {
108 | const calloutEl = parentEl.createDiv({
109 | cls: "callout",
110 | attr: {
111 | "data-callout": type,
112 | },
113 | });
114 | const titleEl = calloutEl.createDiv({
115 | cls: "callout-title",
116 | attr: { dir: "auto" },
117 | });
118 | setIcon(titleEl.createDiv({ cls: "callout-icon" }), icon);
119 | titleEl.createDiv({ cls: "callout-title-inner", text: title });
120 | calloutEl
121 | .createDiv({ cls: "callout-content" })
122 | .createEl("p", { text: desc, attr: { dir: "auto" } });
123 | return calloutEl;
124 | };
125 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/DateCustom/renderSettings.ts:
--------------------------------------------------------------------------------
1 | import { moment, Setting } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { text } from "~/i18next";
4 | import { typeKey } from ".";
5 | import { getPropertyTypeSettings, setPropertyTypeSettings } from "../utils";
6 | import { IconSuggest } from "~/classes/InputSuggest/IconSuggest";
7 | import { obsidianText } from "~/i18next/obsidian";
8 |
9 | export const renderSettings: CustomPropertyType["renderSettings"] = ({
10 | modal,
11 | plugin,
12 | property,
13 | }) => {
14 | const { tabContentEl } = modal;
15 |
16 | const settings = getPropertyTypeSettings({
17 | plugin,
18 | property: property,
19 | type: typeKey,
20 | });
21 |
22 | modal.onTabChange(() => {
23 | setPropertyTypeSettings({
24 | plugin,
25 | property: property,
26 | type: typeKey,
27 | typeSettings: { ...settings },
28 | });
29 | });
30 |
31 | new Setting(tabContentEl)
32 | .setName(text("customPropertyTypes.datecustom.settings.type.title"))
33 | .setDesc(text("customPropertyTypes.datecustom.settings.type.desc"))
34 | .addDropdown((cmp) => {
35 | cmp
36 | .addOptions({
37 | "date": text("datetime.date"),
38 | "datetime-local": text("datetime.dateAndTime"),
39 | } satisfies Record, string>)
40 | .setValue(settings.type ?? "date")
41 | .onChange((v) => {
42 | settings.type = v as NonNullable<(typeof settings)["type"]>;
43 | });
44 | });
45 |
46 | let renderFormatPreview = () => {};
47 | new Setting(tabContentEl)
48 | .setName(text("customPropertyTypes.datecustom.settings.format.title"))
49 | .setDesc(
50 | createFragment((el) => {
51 | el.appendText(
52 | text("customPropertyTypes.datecustom.settings.format.desc")
53 | );
54 | el.appendText(" ");
55 | el.createEl("a", {
56 | text: obsidianText("plugins.daily-notes.label-syntax-link"),
57 | href: "https://momentjs.com/docs/#/displaying/format/",
58 | attr: {
59 | target: "_blank",
60 | rel: "noopener",
61 | },
62 | });
63 | el.createEl("br");
64 | el.appendText(
65 | obsidianText("plugins.daily-notes.label-syntax-live-preview")
66 | );
67 | const formatPreviewEl = el.createEl("b", {
68 | cls: "u-pop",
69 | });
70 | renderFormatPreview = () => {
71 | const formatted = moment().format(settings.format ?? "YYYY-MM-DD");
72 | formatPreviewEl.textContent = formatted;
73 | };
74 | renderFormatPreview();
75 | })
76 | )
77 | .addText((cmp) => {
78 | cmp
79 | .setPlaceholder("YYYY-MM-DD")
80 | .setValue(settings.format ?? "")
81 | .onChange((v) => {
82 | settings.format = v || undefined;
83 | renderFormatPreview();
84 | });
85 | });
86 |
87 | new Setting(tabContentEl)
88 | .setName(text("customPropertyTypes.datecustom.settings.placeholder.title"))
89 | .setDesc(text("customPropertyTypes.datecustom.settings.placeholder.desc"))
90 | .addText((cmp) => {
91 | cmp
92 | .setPlaceholder(obsidianText("interface.empty-state.empty"))
93 | .setValue(settings.placeholder ?? "")
94 | .onChange((v) => {
95 | settings.placeholder = v || undefined;
96 | });
97 | });
98 |
99 | new Setting(tabContentEl)
100 | .setName(text("customPropertyTypes.datecustom.settings.icon.title"))
101 | .setDesc(text("customPropertyTypes.datecustom.settings.icon.desc"))
102 | .addSearch((cmp) => {
103 | cmp.setValue(settings.icon ?? "").onChange((v) => {
104 | settings.icon = v || undefined;
105 | });
106 | new IconSuggest(plugin.app, cmp.inputEl).onSelect((v) => {
107 | cmp.setValue(v);
108 | cmp.onChanged();
109 | });
110 | });
111 | };
112 |
--------------------------------------------------------------------------------
/docs/app/components/common/TocSidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | type Dispatch,
4 | type SetStateAction,
5 | useContext,
6 | type ReactNode,
7 | useState,
8 | } from "react";
9 | import { Link } from "react-router";
10 | import type { TocItem } from "rehype-mdx-toc";
11 | import {
12 | Sidebar,
13 | SidebarHeader,
14 | SidebarContent,
15 | SidebarGroup,
16 | SidebarGroupLabel,
17 | SidebarGroupContent,
18 | SidebarMenu,
19 | SidebarRail,
20 | SidebarMenuItem,
21 | SidebarMenuButton,
22 | SidebarMenuSub,
23 | SidebarFooter,
24 | } from "~/components/ui/sidebar";
25 | import { Button } from "~/components/ui/button";
26 |
27 | const TocContext = createContext<{
28 | toc: TocItem[];
29 | setToc: Dispatch>;
30 | }>({
31 | toc: [],
32 | setToc: () => {},
33 | });
34 |
35 | export const useToc = () => {
36 | const ctx = useContext(TocContext);
37 | if (ctx === undefined) {
38 | throw new Error("useToc() must be used within a ");
39 | }
40 | return ctx;
41 | };
42 |
43 | export const TocProvider = ({ children }: { children: ReactNode }) => {
44 | const [toc, setToc] = useState([]);
45 | return (
46 |
47 | {children}
48 |
49 | );
50 | };
51 |
52 | export const RightSidebar = () => {
53 | const { toc } = useToc();
54 |
55 | return (
56 |
57 | {/* */}
58 |
59 |
60 | On this page
61 |
62 |
63 | {nestTocByDepth(toc ?? [])?.[0]?.children?.map((item) =>
64 | renderNestedMenu(item),
65 | )}
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | I have spent a lot of my free time and effort to work on this
75 | plugin, and because it will{" "}
76 | never have any paid features ,
77 | coffee is a great way to show your support :)
78 |
79 | Buy me a coffee ☕
80 |
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | const renderNestedMenu = (item: TreeNode) => (
88 |
89 |
90 |
95 | {item.value}
96 |
97 |
98 | {item.children.length ? (
99 |
100 | {item.children.map((subItem) => renderNestedMenu(subItem))}
101 |
102 | ) : undefined}
103 |
104 | );
105 |
106 | type TreeNode = TocItem & { children: TreeNode[] };
107 |
108 | const nestTocByDepth = (toc: readonly TocItem[]): TreeNode[] => {
109 | const roots: TreeNode[] = [];
110 | const stack: TreeNode[] = [];
111 |
112 | for (const item of toc) {
113 | const { depth } = item;
114 | const node: TreeNode = { ...item, children: [] };
115 |
116 | stack.length = Math.min(stack.length, depth - 1);
117 |
118 | if (stack.length === 0) {
119 | roots.push(node);
120 | } else {
121 | stack[stack.length - 1].children.push(node);
122 | }
123 |
124 | stack[depth - 1] = node;
125 | }
126 |
127 | return roots;
128 | };
129 |
--------------------------------------------------------------------------------
/src/CustomPropertyTypes/DateCustom/renderWidget.ts:
--------------------------------------------------------------------------------
1 | import { moment, setIcon } from "obsidian";
2 | import { CustomPropertyType } from "../types";
3 | import { PropertyWidgetComponentNew } from "../utils";
4 | import { obsidianText } from "~/i18next/obsidian";
5 | import { PropertyRenderContext } from "obsidian-typings";
6 | import BetterProperties from "~/main";
7 |
8 | export const renderWidget: CustomPropertyType["renderWidget"] = ({
9 | plugin,
10 | el,
11 | ctx,
12 | value,
13 | }) => {
14 | return new DateTypeComponent(plugin, el, value, ctx);
15 | };
16 |
17 | class DateTypeComponent extends PropertyWidgetComponentNew<
18 | "datecustom",
19 | string
20 | > {
21 | type = "datecustom" as const;
22 | parseValue = (v: unknown) => v?.toString() ?? "";
23 |
24 | inputEl: HTMLInputElement | undefined;
25 | formatEl: HTMLDivElement | undefined;
26 | value: string;
27 | rawFormat: string = "";
28 |
29 | constructor(
30 | plugin: BetterProperties,
31 | el: HTMLElement,
32 | value: unknown,
33 | ctx: PropertyRenderContext
34 | ) {
35 | super(plugin, el, value, ctx);
36 |
37 | this.value = this.parseValue(value);
38 | this.render();
39 |
40 | this.onFocus = () => {
41 | this.inputEl?.focus();
42 | };
43 | }
44 |
45 | render(): void {
46 | this.containerEl.empty();
47 | const settings = this.getSettings();
48 | const isEmptyAttr = "data-better-properties-is-empty";
49 |
50 | const format = settings.format ?? "YYYY-MM-DD";
51 | const placeholder =
52 | settings.placeholder ?? obsidianText("interface.empty-state.empty");
53 | const icon = settings.icon ?? "lucide-calendar";
54 | const inputType = settings.type ?? "date";
55 | this.rawFormat = inputType === "date" ? "YYYY-MM-DD" : "YYYY-MM-DDTHH:mm";
56 | const max = inputType === "date" ? "9999-12-31" : "9999-12-31T23:59";
57 |
58 | const parsed = !this.value
59 | ? undefined
60 | : moment(this.parseValue(this.value));
61 |
62 | const buttonContainer = this.containerEl.createDiv({
63 | cls: "better-properties-datecustom-button-container",
64 | });
65 |
66 | this.inputEl = buttonContainer.createEl("input", {
67 | type: inputType,
68 | attr: {
69 | "max": max,
70 | "placeholder": obsidianText("interface.empty-state.empty"),
71 | "aria-hidden": "true",
72 | },
73 | });
74 |
75 | const buttonEl = buttonContainer.createDiv({
76 | cls: "better-properties-datecustom-button clickable-icon",
77 | attr: {
78 | role: "button",
79 | tabindex: "0",
80 | },
81 | });
82 | setIcon(buttonEl, icon);
83 | buttonEl.addEventListener("click", () => {
84 | this.inputEl?.showPicker();
85 | });
86 | buttonEl.addEventListener("keydown", (e) => {
87 | if (e.key !== " ") return;
88 | this.inputEl?.showPicker();
89 | });
90 |
91 | this.formatEl = this.containerEl.createDiv({
92 | cls: "better-properties-datecustom-format metadata-input-longtext",
93 | text: parsed?.format(format) ?? placeholder,
94 | attr: {
95 | [isEmptyAttr]: !this.value || null,
96 | },
97 | });
98 |
99 | this.inputEl.addEventListener("change", (e) => {
100 | const v = (e.target as HTMLInputElement).value;
101 | if (!v && this.formatEl) {
102 | this.formatEl.textContent = placeholder;
103 | this.formatEl.setAttribute(isEmptyAttr, "true");
104 | this.setValue(null);
105 | return;
106 | }
107 | const date = moment(v?.toString());
108 | if (date.isValid() && this.formatEl) {
109 | this.formatEl.removeAttribute(isEmptyAttr);
110 | this.formatEl.textContent = date.format(format);
111 | }
112 | this.setValue(date.format(this.rawFormat));
113 | });
114 | }
115 |
116 | getValue(): string {
117 | return this.inputEl?.value ?? "";
118 | }
119 |
120 | setValue(value: unknown): void {
121 | super.setValue(value);
122 |
123 | const dateStr = moment(this.parseValue(value)).format(this.rawFormat);
124 |
125 | if (this.inputEl && this.inputEl?.value !== dateStr) {
126 | this.render();
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/Plugin/settings.ts:
--------------------------------------------------------------------------------
1 | import { Prettify } from "~/lib/utils";
2 | import * as v from "valibot";
3 | import { propertySettingsSchema } from "~/CustomPropertyTypes";
4 | import { PluginSettingTab, Setting } from "obsidian";
5 | import { BetterProperties } from "./plugin";
6 | import { Icon } from "~/lib/types/icons";
7 | import { obsidianText } from "~/i18next/obsidian";
8 | import { MultiselectComponent } from "~/classes/MultiSelect";
9 | import { PropertyTypeSuggest } from "~/classes/InputSuggest/PropertyTypeSuggest";
10 | import { sortAndFilterRegisteredTypeWidgets } from "~/CustomPropertyTypes/register";
11 |
12 | export const betterPropertiesSettingsSchema = v.object({
13 | propertySettings: v.optional(
14 | v.record(v.string(), propertySettingsSchema),
15 | {}
16 | ),
17 | confirmPropertySettingsReset: v.optional(v.boolean(), true),
18 | confirmPropertyDelete: v.optional(v.boolean(), true),
19 | propertyLabelWidth: v.optional(v.number(), undefined),
20 | defaultLabelWidth: v.optional(v.string(), "9em"), // not UI facing
21 | hiddenPropertyTypes: v.optional(v.array(v.string()), [] satisfies string[]),
22 | });
23 |
24 | export type BetterPropertiesSettings = Prettify<
25 | v.InferOutput
26 | >;
27 |
28 | export const getDefaultSettings = (): BetterPropertiesSettings =>
29 | v.getDefaults(betterPropertiesSettingsSchema);
30 |
31 | export class BetterPropertiesSettingsTab extends PluginSettingTab {
32 | constructor(public plugin: BetterProperties) {
33 | super(plugin.app, plugin);
34 | }
35 |
36 | display(): void {
37 | const { plugin, containerEl } = this;
38 | containerEl.empty();
39 | const { settings } = plugin;
40 |
41 | new Setting(containerEl)
42 | .setName("Property label width")
43 | .setDesc("The width of the label for each frontmatter property.")
44 | .addExtraButton((cmp) => {
45 | cmp
46 | .setIcon("lucide-rotate-ccw" satisfies Icon)
47 | .setTooltip(
48 | obsidianText("interface.tooltip-restore-default-settings")
49 | )
50 | .onClick(() => {
51 | settings.propertyLabelWidth = undefined;
52 | plugin.app.workspace.trigger(
53 | "better-properties:property-label-width-change",
54 | undefined
55 | );
56 | this.display();
57 | });
58 | })
59 | .addSlider((cmp) => {
60 | cmp
61 | .setDynamicTooltip()
62 | .setLimits(0, 500, 1)
63 | .setValue(settings.propertyLabelWidth ?? 0)
64 | .onChange((n) => {
65 | settings.propertyLabelWidth = n;
66 | plugin.app.workspace.trigger(
67 | "better-properties:property-label-width-change",
68 | n
69 | );
70 | });
71 | });
72 |
73 | new Setting(containerEl)
74 | .setName("Hidden property types")
75 | .setDesc("The following types will be disabled.")
76 | .then((s) => {
77 | const cmp = new MultiselectComponent(s);
78 | cmp
79 | .setValues(settings.hiddenPropertyTypes ?? [])
80 | .onChange((arr) => {
81 | settings.hiddenPropertyTypes = [...arr];
82 | })
83 | .addSuggest((inputEl) => {
84 | return new PropertyTypeSuggest(plugin.app, inputEl)
85 | .setFilter((item) => !cmp.values.contains(item.type))
86 | .onSelect((t, e) => {
87 | e.preventDefault();
88 | e.stopImmediatePropagation();
89 | inputEl.textContent = t.type;
90 | cmp.inputEl.blur();
91 | cmp.inputEl.focus();
92 | });
93 | })
94 | .renderValues();
95 | });
96 |
97 | new Setting(containerEl).setHeading().setName("Warnings");
98 |
99 | new Setting(containerEl)
100 | .setName("Confirm reset property settings")
101 | .setDesc("Prompt before resetting a property's settings.")
102 | .addToggle((cmp) => {
103 | cmp
104 | .setValue(settings.confirmPropertySettingsReset ?? true)
105 | .onChange((b) => {
106 | settings.confirmPropertySettingsReset = b;
107 | });
108 | });
109 |
110 | new Setting(containerEl)
111 | .setName("Confirm delete property")
112 | .setDesc("Prompt before deleting a property from all notes.")
113 | .addToggle((cmp) => {
114 | cmp.setValue(settings.confirmPropertyDelete ?? true).onChange((b) => {
115 | settings.confirmPropertyDelete = b;
116 | });
117 | });
118 | }
119 |
120 | hide(): void {
121 | super.hide.call(this);
122 | // TODO settings won't save it app is closed while the tab is still displayed. Might be better to just do a debounce to save after every change
123 | this.plugin.saveSettings();
124 |
125 | const { plugin } = this;
126 | sortAndFilterRegisteredTypeWidgets(plugin);
127 | plugin.refreshPropertyEditors();
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/docs/app/routes/features/property-types/article.mdx:
--------------------------------------------------------------------------------
1 | import { Alert, AlertTitle, AlertDescription } from "~/components/ui/alert";
2 | import { CircleAlert } from "lucide-react";
3 |
4 | ## Property types
5 |
6 | Better Properties add numerous additional property types. If you have an idea for a new type, let me know by [opening a feature request](https://github.com/unxok/obsidian-better-properties/issues/new?template=feature_request.md).
7 |
8 | _Note:_ as of now, specific built-in property types are left untouched. Generally, I would prefer to keep it that way. But, this could change if there's enough of a reason to justify doing it.
9 |
10 | 
11 |
12 | ### Array
13 |
14 | A list of values where each "sub-property" share the same type and settings. Each sub-property is rendered as if it's a property named `.#` (i.e. `favorite quotes.#`) but without the property name being shown.
15 |
16 | 
17 |
18 | ```yaml
19 | ---
20 | favorite quotes:
21 | - Goodbye everyone, I'll remember you all in therapy.
22 | - No Patrick, mayonaise is not an instrument.
23 | ---
24 | ```
25 |
26 | ### Color
27 |
28 | A color input combined with a text input. Using the color picker will choose a color as a hex value. Using the text input will allow any valid [CSS color](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value).
29 |
30 | 
31 |
32 | ```yaml
33 | ---
34 | eye color: "#0b4660"
35 | ---
36 | ```
37 |
38 | ### Date custom
39 |
40 | A date input where the displayed date can be set to a custom format.
41 |
42 | The actual value written to the note's fronmatter will be `YYYY-MM-DD` or `YYYY-MM-DD[T]HH:MM:SS`.
43 |
44 | 
45 |
46 | ```yaml
47 | ---
48 | start date: 2025-10-15
49 | ---
50 | ```
51 |
52 | ### Markdown
53 |
54 | A live-preview markdown editor. It is the same editor that is used in the body of notes.
55 |
56 | 
57 |
58 | ```yaml
59 | ---
60 | pangram: A *quick* brown fox **jumps** over the ==lazy== dog
61 | ---
62 | ```
63 |
64 | ### Multi-Select
65 |
66 | Similar to the built-in [list type](https://help.obsidian.md/properties#List), but it does not allow duplicates, has customizable dynamic or manually defined options, and more.
67 |
68 | 
69 |
70 | ```yaml
71 | ---
72 | fruits:
73 | - bananas
74 | - grapes
75 | ---
76 | ```
77 |
78 | ### Numeric
79 |
80 | An input which is evaulated as a mathematical expression and can be rounded to a configurable number of decimal places. The variable `x` can be used to represent the current value.
81 |
82 | 
83 | 
84 |
85 | ```yaml
86 | ---
87 | savings: "25807.82"
88 | ---
89 | ```
90 |
91 | ### Object
92 |
93 | Renders an object of keys and values as sub-properties nested within the parent property. Each sub-property is rendered as if it's a property named `.`, i.e. `project.status`.
94 |
95 | 
96 |
97 | ```yaml
98 | ---
99 | project:
100 | status: in progress
101 | stakeholders:
102 | - Sandy
103 | - Patrick
104 | - Mr. Krabs
105 | ---
106 | ```
107 |
108 | ### Rating
109 |
110 | A group of icons which act as an input where you select a value along a scale. The icon and count can be customized.
111 |
112 | 
113 |
114 | ```yaml
115 | ---
116 | quality: 3
117 | ---
118 | ```
119 |
120 | ### Select
121 |
122 | A select input where dynamic or manually defined options are available to select for the value. Colors and labels for options can be customized.
123 |
124 | 
125 |
126 | ```yaml
127 | ---
128 | status: in progress
129 | ---
130 | ```
131 |
132 | ### Slider
133 |
134 | A slider input. The lower and upper limits can be customized.
135 |
136 | 
137 |
138 | ```yaml
139 | ---
140 | current HP: 18
141 | ---
142 | ```
143 |
144 | ### Time
145 |
146 | A time input.
147 |
148 | 
149 |
150 | ```yaml
151 | ---
152 | bedtime: 01:30
153 | ---
154 | ```
155 |
156 | ### Toggle
157 |
158 | A toggle input. It has only two states (`true` or `false`), unlike the built-in [Checkbox type](https://help.obsidian.md/properties#Checkbox) which technically has three.
159 |
160 | 
161 |
162 | ```yaml
163 | ---
164 | reviewed: true
165 | ---
166 | ```
167 |
--------------------------------------------------------------------------------