From c021732560162b426b66000ffc68f121a6f27d9a Mon Sep 17 00:00:00 2001 From: valentine195 <38669521+valentine195@users.noreply.github.com> Date: Fri, 16 Apr 2021 15:23:25 -0400 Subject: [PATCH] 3.0.0 - Added ability to create custom admonitions (closes #7 ) --- @types/types.ts | 29 ++++ README.md | 96 ++++++------ manifest.json | 2 +- package.json | 6 +- src/icons.ts | 10 ++ src/main.css | 197 +++++------------------- src/main.ts | 160 ++++++++++++++------ src/settings.ts | 394 ++++++++++++++++++++++++++++++++++++++++++++++++ versions.json | 3 +- 9 files changed, 644 insertions(+), 253 deletions(-) create mode 100644 @types/types.ts create mode 100644 src/icons.ts create mode 100644 src/settings.ts diff --git a/@types/types.ts b/@types/types.ts new file mode 100644 index 0000000..f36d0fe --- /dev/null +++ b/@types/types.ts @@ -0,0 +1,29 @@ +import { MarkdownPostProcessorContext, Plugin_2 } from "obsidian"; + +export interface Admonition { + type: string; + icon: string; + color: string; +} + +export declare class ObsidianAdmonitionPlugin extends Plugin_2 { + removeAdmonition: (admonition: Admonition) => Promise; + admonitions: { [admonitionType: string]: Admonition }; + userAdmonitions: { [admonitionType: string]: Admonition }; + saveSettings: () => Promise; + loadSettings: () => Promise; + addAdmonition: (admonition: Admonition) => Promise; + onload: () => Promise; + onunload: () => Promise; + postprocessor: ( + type: string, + src: string, + el: HTMLElement, + ctx: MarkdownPostProcessorContext + ) => void; + getAdmonitionElement: ( + type: string, + title: string, + collapse?: string + ) => HTMLElement; +} diff --git a/README.md b/README.md index 97ac735..be4afdc 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,6 @@ Adds admonition block-styled content to Obsidian.md, styled after [Material for ## Usage - Place a code block with the admonition type: ````markdown @@ -27,7 +26,9 @@ Becomes: ```ad- # Admonition type. See below for a list of available types. title: # Admonition title. collapse: # Create a collapsible admonition. -content: # Actual text of admonition. Only required if "title" or "collapse" is used. + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. + ``` ```` @@ -44,7 +45,7 @@ The admonition will render with the type of admonition by default. If you wish t ````markdown ```ad-note title: Title -content: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. ``` ```` @@ -55,14 +56,12 @@ Leave the title field blank to only display the admonition. ````markdown ```ad-note title: -content: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla. ``` ```` ![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/no-title.png) -**Please note that when the title is included, you _must_ specify the content as well.** - ### Collapsible Use the `collapse` parameter to create a collapsible admonition. @@ -73,7 +72,6 @@ If a blank title is provided, the collapse parameter will not do anything. ![](https://raw.githubusercontent.com/valentine195/obsidian-admonition/master/images/collapse.gif) -**Please note that when the collapse parameter is included, you _must_ specify the content as well.** ## Admonition Types @@ -96,6 +94,20 @@ The following admonition types are currently supported: See [this](https://squidfunk.github.io/mkdocs-material/reference/admonitions/) for a reference of what these admonitions look like. +The default admonitions are customizable by creating a user-defined admonition of the same name. + +## Custom Admonitions + +Custom admonitions may be created in settings. + +Creating a new admonition requires three things: the type, the icon to use, and the color of the admonition. + +Only one admonition of each type may exist at any given time; if another admonition of the same type is created, it will override the previously created one. + +If a default admonition is overridden, it can be restored by deleting the user-defined admonition. + +Please note that by default, the background color of the title is simply the color of the admonition at 10% opacity. CSS must be used to update this. + ## Customization This is all of the CSS applied to the admonitions. Override these classes to customize the look. @@ -112,59 +124,50 @@ Every admonition receives the following CSS classes: color: var(--text-normal); page-break-inside: avoid; background-color: var(--background-secondary); - border-left: 0.2rem solid; + border-left: 0.2rem solid rgb(var(--admonition-color)); border-radius: 0.1rem; box-shadow: 0 0.2rem 0.5rem var(--background-modifier-box-shadow); } -.admonition-title::before { - position: absolute; - left: 0.6rem; - width: 1.25rem; - height: 1.25rem; - content: ""; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: contain; - mask-size: contain; -} + .admonition-title { position: relative; margin: 0 -0.6rem 0 -0.8rem; padding: 0.4rem 0.6rem 0.4rem 2rem; font-weight: 700; - background-color: rgba(68, 138, 255, 0.1); border-left: 0.2rem solid; + background-color: rgba(var(--admonition-color), 0.1); } -.admonition-content { - margin-bottom: 0.6rem; -} -``` - -### Type Specific -Additionally, each admonition type will receive the `.admonition-` class: - -```css -/* Example of .admonition-note */ -.admonition-note { - border-color: #448aff; +.admonition-title-icon { + position: absolute; + left: 0.6rem; + width: 1.25rem; + height: 1.25rem; + color: rgb(var(--admonition-color)); + display: flex; + justify-content: center; + align-items: center; } -.admonition-note > .admonition-title { - background-color: rgba(68, 138, 255, 0.1); + +.admonition-title.no-title { + display: none; } -.admonition-note > .admonition-title::before { - background-color: #448aff; - -webkit-mask-image: var(--admonition-icon--note); - mask-image: var(--admonition-icon--note); + +.admonition > .admonition-title.no-title + .admonition-content { + margin: 1em 0; } ``` -#### Type Icons +***Please note, as of 3.0.0, the admonition colors are no longer set in the CSS.*** -The admonition icons are svgs defined as variables on the `:root` with the name `--admonition-icon--`. Override this variable to customize the icon. Example: +Each admonition receives the `.admonition-` class. You can use this selector to override specific admonition types, but the plugin does not add any styling using this selector by default. + +To set the color of admonition types via CSS, specific the following the `--admonition-color` variable ***as an RGB triad***: ```css ---admonition-icon--quote: url("data:image/svg+xml;charset=utf-8,"); +.admonition-note { + --admonition-color: 68, 138, 255 !important; +} ``` ### Collapsible @@ -225,12 +228,19 @@ An icon without a title will have this CSS: ## Todo - [x] Add the ability to collapse the admonition -- [ ] Custom admonitions -- [ ] Settings tab to customize icon and color of all admonitions +- [x] Custom admonitions +- [x] Settings tab to customize icon and color of all admonitions - [x] Ability to render markdown inside an admonition # Version History +## 3.0.0 + +- Added ability to create custom admonitions via Settings + - Color, icon, and admonition-type are customizable + - Default admonitions can be overridden by creating a custom admonition of the same type + - Delete the custom admonition to restore default + ## 2.0.0 - To maintain compatibility with other plugins, admonition types must now be prefixed with `ad-` (as in, `ad-note`). diff --git a/manifest.json b/manifest.json index 23d2b2c..e952fda 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "obsidian-admonition", "name": "Admonition", - "version": "2.0.1", + "version": "3.0.0", "minAppVersion": "0.11.0", "description": "Admonition block-styled content for Obsidian.md", "author": "Jeremy Valentine", diff --git a/package.json b/package.json index e5e1017..8e934f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-admonition", - "version": "2.0.1", + "version": "3.0.0", "description": "Admonition block-styled content for Obsidian.md", "main": "main.js", "scripts": { @@ -11,7 +11,6 @@ "author": "Jeremy Valentine", "license": "MIT", "devDependencies": { - "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-solid-svg-icons": "^5.15.1", "@rollup/plugin-commonjs": "^15.1.0", "@rollup/plugin-node-resolve": "^9.0.0", @@ -24,5 +23,8 @@ "rollup-plugin-css-only": "^3.1.0", "tslib": "^2.0.3", "typescript": "^4.0.3" + }, + "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.35" } } diff --git a/src/icons.ts b/src/icons.ts new file mode 100644 index 0000000..19696f3 --- /dev/null +++ b/src/icons.ts @@ -0,0 +1,10 @@ +import { fas } from "@fortawesome/free-solid-svg-icons"; +import { + findIconDefinition, + icon, + library +} from "@fortawesome/fontawesome-svg-core"; + +library.add(fas); + +export { icon, findIconDefinition }; diff --git a/src/main.css b/src/main.css index 045d06c..9078232 100644 --- a/src/main.css +++ b/src/main.css @@ -1,21 +1,8 @@ +/** Constants */ :root { - --admonition-icon--note: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--abstract: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--info: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--tip: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--success: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--question: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--warning: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--failure: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--danger: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--bug: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--example: url("data:image/svg+xml;charset=utf-8,"); - --admonition-icon--quote: url("data:image/svg+xml;charset=utf-8,"); --admonition-details-icon: url("data:image/svg+xml;charset=utf-8,"); } -/** Constants */ - .admonition { margin: 1.5625em 0; padding: 0 0.6rem; @@ -23,28 +10,31 @@ color: var(--text-normal); page-break-inside: avoid; background-color: var(--background-secondary); - border-left: 0.2rem solid; + border-left: 0.2rem solid rgb(var(--admonition-color)); border-radius: 0.1rem; box-shadow: 0 0.2rem 0.5rem var(--background-modifier-box-shadow); } -.admonition-title::before { - position: absolute; - left: 0.6rem; - width: 1.25rem; - height: 1.25rem; - content: ""; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-size: contain; - mask-size: contain; -} + .admonition-title { position: relative; margin: 0 -0.6rem 0 -0.8rem; padding: 0.4rem 0.6rem 0.4rem 2rem; font-weight: 700; border-left: 0.2rem solid; + background-color: rgba(var(--admonition-color), 0.1); +} + +.admonition-title-icon { + position: absolute; + left: 0.6rem; + width: 1.25rem; + height: 1.25rem; + color: rgb(var(--admonition-color)); + display: flex; + justify-content: center; + align-items: center; } + .admonition-title.no-title { display: none; } @@ -92,148 +82,29 @@ details.admonition[open] > summary:after { transform: rotate(90deg); } -/** Type Specific */ - -.admonition-note { - border-color: #448aff; -} -.admonition-note > .admonition-title { - background-color: rgba(68, 138, 255, 0.1); -} -.admonition-note > .admonition-title::before { - background-color: #448aff; - -webkit-mask-image: var(--admonition-icon--note); - mask-image: var(--admonition-icon--note); -} +/** Invalid Setting */ -.admonition-abstract { - border-color: #00b0ff; -} -.admonition-abstract > .admonition-title { - background-color: rgba(0, 176, 255, 0.1); -} -.admonition-abstract > .admonition-title::before { - background-color: #00b0ff; - -webkit-mask-image: var(--admonition-icon--abstract); - mask-image: var(--admonition-icon--abstract); +.unset-align-items { + align-items: unset; } -.admonition-info { - border-color: #00b8d4; -} -.admonition-info > .admonition-title { - background-color: rgba(0, 184, 212, 0.1); -} -.admonition-info > .admonition-title::before { - background-color: #00b8d4; - -webkit-mask-image: var(--admonition-icon--info); - mask-image: var(--admonition-icon--info); +.has-invalid-message { + flex-grow: unset; + flex-flow: column nowrap; } -.admonition-tip { - border-color: #00bfa5; -} -.admonition-tip > .admonition-title { - background-color: rgba(0, 191, 165, 0.1); -} -.admonition-tip > .admonition-title::before { - background-color: #00bfa5; - -webkit-mask-image: var(--admonition-icon--tip); - mask-image: var(--admonition-icon--tip); +input.is-invalid { + border-color: #dc3545 !important; + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e"); + background-repeat: no-repeat; + background-position: right calc(0.375em + 0.1875rem) center; + background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem); } -.admonition-success { - border-color: #00c853; -} -.admonition-success > .admonition-title { - background-color: rgba(0, 200, 83, 0.1); -} -.admonition-success > .admonition-title::before { - background-color: #00c853; - -webkit-mask-image: var(--admonition-icon--success); - mask-image: var(--admonition-icon--success); -} - -.admonition-question { - border-color: #64dd17; -} -.admonition-question > .admonition-title { - background-color: rgba(100, 221, 23, 0.1); -} -.admonition-question > .admonition-title::before { - background-color: #64dd17; - -webkit-mask-image: var(--admonition-icon--question); - mask-image: var(--admonition-icon--question); -} - -.admonition-warning { - border-color: #ff9100; -} -.admonition-warning > .admonition-title { - background-color: rgba(255, 145, 0, 0.1); -} -.admonition-warning > .admonition-title::before { - background-color: #ff9100; - -webkit-mask-image: var(--admonition-icon--warning); - mask-image: var(--admonition-icon--warning); -} - -.admonition-failure { - border-color: #ff5252; -} -.admonition-failure > .admonition-title { - background-color: rgba(255, 82, 82, 0.1); -} -.admonition-failure > .admonition-title::before { - background-color: #ff5252; - -webkit-mask-image: var(--admonition-icon--failure); - mask-image: var(--admonition-icon--failure); -} - -.admonition-danger { - border-color: #ff1744; -} -.admonition-danger > .admonition-title { - background-color: rgba(255, 23, 68, 0.1); -} -.admonition-danger > .admonition-title::before { - background-color: #ff1744; - -webkit-mask-image: var(--admonition-icon--danger); - mask-image: var(--admonition-icon--danger); -} - -.admonition-bug { - border-color: #f50057; -} -.admonition-bug > .admonition-title { - background-color: rgba(245, 0, 87, 0.1); -} -.admonition-bug > .admonition-title::before { - background-color: #f50057; - -webkit-mask-image: var(--admonition-icon--bug); - mask-image: var(--admonition-icon--bug); -} - -.admonition-example { - border-color: #7c4dff; -} -.admonition-example > .admonition-title { - background-color: rgba(124, 77, 255, 0.1); -} -.admonition-example > .admonition-title::before { - background-color: #7c4dff; - -webkit-mask-image: var(--admonition-icon--example); - mask-image: var(--admonition-icon--example); -} - -.admonition-quote { - border-color: #9e9e9e; -} -.admonition-quote > .admonition-title { - background-color: hsla(0, 0%, 62%, 0.1); -} -.admonition-quote > .admonition-title::before { - background-color: #9e9e9e; - -webkit-mask-image: var(--admonition-icon--quote); - mask-image: var(--admonition-icon--quote); +.invalid-feedback { + display: block; + width: 100%; + margin-top: 0.25rem; + font-size: 0.875em; + color: #dc3545; } diff --git a/src/main.ts b/src/main.ts index 85b4fe3..0ebcf5b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,8 @@ import { Notice, Plugin } from "obsidian"; +import { Admonition, ObsidianAdmonitionPlugin } from "../@types/types"; +import { findIconDefinition, icon } from "./icons"; Object.fromEntries = Object.fromEntries || @@ -36,44 +38,105 @@ Object.fromEntries = }; import "./main.css"; +import AdmonitionSetting from "./settings"; const ADMONITION_MAP: { - [admonitionType: string]: string; + [admonitionType: string]: Admonition; } = { - note: "note", - seealso: "note", - abstract: "abstract", - summary: "abstract", - info: "info", - todo: "todo", - tip: "tip", - hint: "tip", - important: "tip", - success: "success", - check: "check", - done: "done", - question: "question", - help: "question", - faq: "question", - warning: "warning", - caution: "warning", - attention: "warning", - failure: "failure", - fail: "failure", - missing: "failure", - danger: "danger", - error: "danger", - bug: "bug", - example: "example", - quote: "quote", - cite: "quote" + note: { type: "note", color: "68, 138, 255", icon: "pencil-alt" }, + seealso: { type: "note", color: "68, 138, 255", icon: "pencil-alt" }, + abstract: { type: "abstract", color: "0, 176, 255", icon: "book" }, + summary: { type: "abstract", color: "0, 176, 255", icon: "book" }, + info: { type: "info", color: "0, 184, 212", icon: "info-circle" }, + todo: { type: "info", color: "0, 184, 212", icon: "info-circle" }, + tip: { type: "tip", color: "0, 191, 165", icon: "fire" }, + hint: { type: "tip", color: "0, 191, 165", icon: "fire" }, + important: { type: "tip", color: "0, 191, 165", icon: "fire" }, + success: { type: "success", color: "0, 200, 83", icon: "check-circle" }, + check: { type: "success", color: "0, 200, 83", icon: "check-circle" }, + done: { type: "success", color: "0, 200, 83", icon: "check-circle" }, + question: { + type: "question", + color: "100, 221, 23", + icon: "question-circle" + }, + help: { type: "question", color: "100, 221, 23", icon: "question-circle" }, + faq: { type: "question", color: "100, 221, 23", icon: "question-circle" }, + warning: { + type: "warning", + color: "255, 145, 0", + icon: "exclamation-triangle" + }, + caution: { + type: "warning", + color: "255, 145, 0", + icon: "exclamation-triangle" + }, + attention: { + type: "warning", + color: "255, 145, 0", + icon: "exclamation-triangle" + }, + failure: { type: "failure", color: "255, 82, 82", icon: "times-circle" }, + fail: { type: "failure", color: "255, 82, 82", icon: "times-circle" }, + missing: { type: "failure", color: "255, 82, 82", icon: "times-circle" }, + danger: { type: "danger", color: "255, 23, 68", icon: "bolt" }, + error: { type: "danger", color: "255, 23, 68", icon: "bolt" }, + bug: { type: "bug", color: "245, 0, 87", icon: "bug" }, + example: { type: "example", color: "124, 77, 255", icon: "list-ol" }, + quote: { type: "quote", color: "158, 158, 158", icon: "quote-right" }, + cite: { type: "quote", color: "158, 158, 158", icon: "quote-right" } }; +export default class ObsidianAdmonition + extends Plugin + implements ObsidianAdmonitionPlugin { + admonitions: { [admonitionType: string]: Admonition } = {}; + userAdmonitions: { [admonitionType: string]: Admonition } = {}; + async saveSettings() { + await this.saveData(this.userAdmonitions); + } + async loadSettings() { + this.userAdmonitions = await this.loadData(); + + this.admonitions = { + ...ADMONITION_MAP, + ...this.userAdmonitions + }; + } + async addAdmonition(admonition: Admonition): Promise { + this.userAdmonitions = { + ...this.userAdmonitions, + [admonition.type]: admonition + }; + this.admonitions = { + ...ADMONITION_MAP, + ...this.userAdmonitions + }; + this.registerMarkdownCodeBlockProcessor( + `ad-${admonition.type}`, + this.postprocessor.bind(this, admonition.type) + ); + await this.saveSettings(); + } -export default class ObsidianAdmonition extends Plugin { + async removeAdmonition(admonition: Admonition) { + if (this.userAdmonitions[admonition.type]) { + delete this.userAdmonitions[admonition.type]; + } + this.admonitions = { + ...ADMONITION_MAP, + ...this.userAdmonitions + }; + await this.saveSettings(); + } async onload(): Promise { console.log("Obsidian Admonition loaded"); - Object.keys(ADMONITION_MAP).forEach((type) => + await this.loadSettings(); + + this.addSettingTab(new AdmonitionSetting(this.app, this)); + + Object.keys(this.admonitions).forEach((type) => this.registerMarkdownCodeBlockProcessor( `ad-${type}`, this.postprocessor.bind(this, type) @@ -86,6 +149,9 @@ export default class ObsidianAdmonition extends Plugin { el: HTMLElement, ctx: MarkdownPostProcessorContext ) { + if (!this.admonitions[type]) { + return; + } try { /** * Find title and collapse parameters. @@ -196,39 +262,47 @@ export default class ObsidianAdmonition extends Plugin { title: string, collapse?: string ): HTMLElement { - let admonition; + let admonition, + titleEl, + attrs: { style: string; open?: string } = { + style: `--admonition-color: ${this.admonitions[type].color};` + }; if (collapse) { - let attrs; if (collapse === "open") { - attrs = { open: "open" }; - } else { - attrs = {}; + attrs.open = "open"; } admonition = createEl("details", { cls: `admonition admonition-${type}`, attr: attrs }); - admonition.createEl("summary", { + titleEl = admonition.createEl("summary", { cls: `admonition-title ${ !title.trim().length ? "no-title" : "" - }`, - text: title + }` }); } else { admonition = createDiv({ - cls: `admonition admonition-${type}` + cls: `admonition admonition-${type}`, + attr: attrs }); - admonition.createDiv({ + titleEl = admonition.createDiv({ cls: `admonition-title ${ !title.trim().length ? "no-title" : "" - }`, - text: title + }` }); } + titleEl.createDiv({ + cls: `admonition-title-icon` + }).innerHTML = icon( + findIconDefinition({ + iconName: this.admonitions[type].icon + }) + ).html[0]; + titleEl.createSpan({ text: title }); return admonition; } - onunload() { + async onunload() { console.log("Obsidian Admonition unloaded"); } } diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..11f8756 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,394 @@ +import { + PluginSettingTab, + Setting, + App, + ButtonComponent, + Modal, + TextComponent, + Notice +} from "obsidian"; +import { Admonition, ObsidianAdmonitionPlugin } from "../@types/types"; + +import { findIconDefinition, icon } from "./icons"; + +export default class AdmonitionSetting extends PluginSettingTab { + plugin: ObsidianAdmonitionPlugin; + constructor(app: App, plugin: ObsidianAdmonitionPlugin) { + super(app, plugin); + this.plugin = plugin; + } + async display(): Promise { + let { containerEl } = this; + + containerEl.empty(); + + containerEl.createEl("h2", { text: "Admonition Settings" }); + + new Setting(containerEl) + .setName("Add New") + .setDesc("Add a new Admonition type.") + .addButton( + (button: ButtonComponent): ButtonComponent => { + let b = button + .setTooltip("Add Additional") + .setButtonText("+") + .onClick(async () => { + let modal = new SettingsModal(this.app); + + modal.onClose = async () => { + if (modal.saved) { + this.plugin.addAdmonition({ + type: modal.type, + color: modal.color, + icon: modal.icon + }); + this.display(); + } + }; + + modal.open(); + }); + + return b; + } + ); + + for (let a in this.plugin.userAdmonitions) { + const admonition = this.plugin.userAdmonitions[a]; + + let setting = new Setting(containerEl); + let admonitionElement = createDiv({ + cls: "admonition", + attr: { + style: `--admonition-color: ${admonition.color}; margin: 0 !important; width: 50% !important;` + } + }); + const titleEl = admonitionElement.createDiv({ + cls: "admonition-title" + }); + const iconEl = titleEl.createDiv({ + cls: "admonition-title-icon" + }); + + titleEl.createSpan({ + text: + admonition.type[0].toUpperCase() + + admonition.type.slice(1).toLowerCase() + }); + if (admonition.icon) { + iconEl.innerHTML = icon( + findIconDefinition({ iconName: admonition.icon }) + ).html[0]; + } + setting.infoEl.replaceWith(admonitionElement); + + setting.addButton((b) => { + b.setIcon("pencil") + .setTooltip("Edit") + .onClick(() => { + let modal = new SettingsModal(this.app, admonition); + + modal.onClose = async () => { + if (modal.saved) { + this.plugin.addAdmonition({ + type: modal.type, + color: modal.color, + icon: modal.icon + }); + this.display(); + } + }; + + modal.open(); + }); + }); + setting.addButton((b) => { + b.setIcon("trash") + .setTooltip("Delete") + .onClick(() => { + this.plugin.removeAdmonition(admonition); + this.display(); + }); + }); + } + } +} + +class SettingsModal extends Modal { + color: string = "#7d7d7d"; + icon: string = ""; + type: string = ""; + saved: boolean = false; + error: boolean = false; + constructor(app: App, admonition?: Admonition) { + super(app); + if (admonition) { + this.color = admonition.color; + this.icon = admonition.icon; + this.type = admonition.type; + } + } + + async display() { + let { contentEl } = this; + + contentEl.empty(); + + const settingDiv = contentEl.createDiv(); + const previewEl = contentEl.createDiv({ + cls: "admonition", + attr: { + style: `--admonition-color: ${this.color};` + } + }); + const titleEl = previewEl.createDiv({ + cls: "admonition-title" + }); + const iconEl = titleEl.createDiv({ + cls: "admonition-title-icon" + }); + + const titleSpan = titleEl.createSpan({ + text: this.type.length + ? this.type[0].toUpperCase() + this.type.slice(1).toLowerCase() + : "..." + }); + + if (this.icon) { + iconEl.innerHTML = icon( + findIconDefinition({ iconName: this.icon }) + ).html[0]; + } + previewEl.createDiv().createEl("p", { + cls: "admonition-content", + text: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla et euismod nulla." + }); + let typeText: TextComponent; + new Setting(settingDiv) + .setName("Admonition Type") + .setDesc("This is used to create the admonition (e.g., ad-note)") + + .addText((text) => { + typeText = text; + typeText.setValue(this.type).onChange((v) => { + if (!v.length) { + SettingsModal.setValidationError( + text, + "Admonition type cannot be empty." + ); + return; + } + + if (v.includes(" ")) { + SettingsModal.setValidationError( + text, + "Admonition type cannot include spaces." + ); + return; + } + SettingsModal.removeValidationError(text); + + this.type = v; + titleSpan.textContent = + this.type[0].toUpperCase() + + this.type.slice(1).toLowerCase(); + }); + }); + let iconText: TextComponent; + const iconSetting = new Setting(settingDiv) + .setName("Admonition Icon") + .addText((text) => { + iconText = text; + iconText.setValue(this.icon).onChange((v) => { + let ic = findIconDefinition({ + iconName: v + }); + if (!ic) { + SettingsModal.setValidationError( + text, + "Invalid icon name." + ); + return; + } + + if (v.length == 0) { + SettingsModal.setValidationError( + text, + "Icon cannot be empty." + ); + return; + } + + SettingsModal.removeValidationError(text); + + this.icon = v; + + iconEl.innerHTML = icon(ic).html[0]; + }); + }); + + const desc = iconSetting.descEl.createDiv(); + desc.createEl("a", { + text: "Font Awesome Icon", + href: "https://fontawesome.com/icons?d=gallery&p=2&s=solid&m=free" + }); + desc.createSpan({ text: " to use next to the title." }); + + const color = new Setting(settingDiv).setName("Color"); + const colorInput = color.controlEl.createEl( + "input", + { + type: "color", + value: this.color + }, + (el) => { + el.value = rgbToHex(this.color); + el.oninput = ({ target }) => { + let color = hexToRgb((target as HTMLInputElement).value); + console.log( + "🚀 ~ file: settings.ts ~ line 118 ~ SettingsModal ~ display ~ color", + color + ); + if (!color) return; + this.color = `${color.r}, ${color.g}, ${color.b}`; + previewEl.setAttribute( + "style", + `--admonition-color: ${this.color};` + ); + }; + } + ); + + let footerEl = contentEl.createDiv(); + let footerButtons = new Setting(footerEl); + footerButtons.addButton((b) => { + b.setTooltip("Save") + .setIcon("checkmark") + .onClick(async () => { + let error = false; + if (!typeText.inputEl.value.length) { + SettingsModal.setValidationError( + typeText, + "Admonition type cannot be empty." + ); + error = true; + } + + if (typeText.inputEl.value.includes(" ")) { + SettingsModal.setValidationError( + typeText, + "Admonition type cannot include spaces." + ); + error = true; + } + + if ( + !findIconDefinition({ + iconName: iconText.inputEl.value + }) + ) { + SettingsModal.setValidationError( + iconText, + "Invalid icon name." + ); + error = true; + } + + if (iconText.inputEl.value.length == 0) { + SettingsModal.setValidationError( + iconText, + "Icon cannot be empty." + ); + error = true; + } + + if (error) { + new Notice("Fix errors before saving."); + return; + } + this.saved = true; + this.close(); + }); + return b; + }); + footerButtons.addExtraButton((b) => { + b.setIcon("cross") + .setTooltip("Cancel") + .onClick(() => { + this.saved = false; + this.close(); + }); + return b; + }); + } + onOpen() { + this.display(); + } + + static setValidationError(textInput: TextComponent, message?: string) { + textInput.inputEl.addClass("is-invalid"); + if (message) { + textInput.inputEl.parentElement.addClasses([ + "has-invalid-message", + "unset-align-items" + ]); + textInput.inputEl.parentElement.parentElement.addClass( + ".unset-align-items" + ); + let mDiv = textInput.inputEl.parentElement.querySelector( + ".invalid-feedback" + ) as HTMLDivElement; + + if (!mDiv) { + mDiv = createDiv({ cls: "invalid-feedback" }); + } + mDiv.innerText = message; + mDiv.insertAfter(textInput.inputEl); + } + } + static removeValidationError(textInput: TextComponent) { + textInput.inputEl.removeClass("is-invalid"); + textInput.inputEl.parentElement.removeClasses([ + "has-invalid-message", + "unset-align-items" + ]); + textInput.inputEl.parentElement.parentElement.removeClass( + ".unset-align-items" + ); + + if (textInput.inputEl.parentElement.children[1]) { + textInput.inputEl.parentElement.removeChild( + textInput.inputEl.parentElement.children[1] + ); + } + } +} + +function hexToRgb(hex: string) { + let result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + console.log( + "🚀 ~ file: settings.ts ~ line 156 ~ hexToRgb ~ result", + result + ); + return result + ? { + r: parseInt(result[1], 16), + g: parseInt(result[2], 16), + b: parseInt(result[3], 16) + } + : null; +} +function componentToHex(c: number) { + var hex = c.toString(16); + return hex.length == 1 ? "0" + hex : hex; +} +function rgbToHex(rgb: string) { + let result = /^(\d+),\s?(\d+),\s?(\d+)/i.exec(rgb); + if (!result || result.length) { + return ""; + } + return `#${componentToHex(Number(result[1]))}${componentToHex( + Number(result[2]) + )}${componentToHex(Number(result[3]))}`; +} diff --git a/versions.json b/versions.json index 0bb9099..dbcdeb7 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,6 @@ { "0.2.3": "0.11.0", "1.0.1": "0.11.0", - "2.0.1": "0.11.0" + "2.0.1": "0.11.0", + "3.0.0": "0.11.0" }