├── .editorconfig
├── .eslintignore
├── .eslintrc
├── .github
└── workflows
│ └── release.yml
├── .gitignore
├── .npmrc
├── README.md
├── README_CN.md
├── README_EN.md
├── main.js
├── manifest.json
└── styles.css
/.editorconfig:
--------------------------------------------------------------------------------
1 | # top-most EditorConfig file
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | insert_final_newline = true
8 | indent_style = tab
9 | indent_size = 4
10 | tab_width = 4
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 |
3 | main.js
4 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build and Release
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | push:
8 | tags:
9 | - "*" # Push events to matching v*, i.e. v1.0, v20.15.10
10 | branches:
11 | - main
12 | path-ignores:
13 | - README*.md
14 |
15 | jobs:
16 | build-and-test:
17 | if: startsWith(github.ref, 'refs/tags/')
18 | strategy:
19 | matrix:
20 | os: [ubuntu-latest]
21 | runs-on: ${{ matrix.os }}
22 | permissions:
23 | contents: write
24 | steps:
25 | - uses: actions/checkout@v4
26 | with:
27 | fetch-depth: 0
28 | - name: Get package version
29 | run: |
30 | node -p -e '`PLUGIN_ID=${require("./manifest.json").id}`' >> $GITHUB_ENV
31 | node -p -e '`MANIFEST_VERSION=${require("./manifest.json").version}`' >> $GITHUB_ENV
32 |
33 | - name: zip build
34 | run: zip ${{ env.PLUGIN_ID }}-${{ env.MANIFEST_VERSION }}.zip styles.css main.js manifest.json
35 |
36 | - name: Create Release
37 | uses: softprops/action-gh-release@v2
38 | with:
39 | files: |
40 | styles.css
41 | main.js
42 | manifest.json
43 | ${{ env.PLUGIN_ID }}-${{ env.MANIFEST_VERSION }}.zip
44 | token: ${{ secrets.gh_token }}
45 | name: Release ${{ env.MANIFEST_VERSION }}
46 | make_latest: true
47 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | tag-version-prefix=""
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Callout Editor for Obsidian
2 | [English](README_EN.md) | 中文
3 |
4 | Callout Editor是一款Obsidian编辑插件,它通过卡片的自由创建组织,所见即所得的编辑方式,实时色彩选择以及灵活的瀑布流布局,优化了Callout的实时编辑与呈现形式。
5 |
6 | # 1.0.2 版本更新说明
7 |
8 | 很高兴地宣布 Callout Editor 1.0.2 版本发布!这次更新主要聚焦于性能优化和用户体验提升:
9 |
10 | ## 🚀 主要更新
11 | - **显著性能提升**:重构了实时保存机制,大幅减少编辑时的卡顿现象
12 | - **实时保存**:实现了内容的实时保存,可以在输入完毕等待1.5秒后自动保存
13 | - **更流畅的编辑体验**:优化了防抖处理,提供更自然的编辑响应
14 |
15 | ## 功能特点
16 |
17 | - **所见即所得编辑**:在Obsidian中提供Callout的实时编辑功能,简化卡片的创建与修改过程。
18 | - **瀑布流布局**:支持一键创建不同列数的卡片布局,以清晰、有序的方式展示笔记内容。
19 | - **颜色选择器**:通过每个卡片右下角的颜色选择器,轻松为卡片添加色彩标记,增强信息分类的直观性。
20 | - **实时内容储存**:通过在当前卡片文本框外的任何地方点击,即可确保您的修改被立即且自动保存。
21 |
22 | ## 安装步骤
23 |
24 | 请参考: [How to install Obsidian Plugins](https://forum.obsidian.md/t/plugins-mini-faq/7737)
25 |
26 | 1. 创建文件夹 `obsidian-callout-editor`
27 | 2. 下载 `main.js, manifest.json, styles.css`
28 | 3. 将下载文件放入文件夹
29 | 4. 将文件夹放入 YourVault/.obsidian/plugins
30 | 5. 重启Obsidian
31 |
32 | ## 使用指南
33 |
34 | ### 插入瀑布流卡片簇
35 |
36 | - 使用`Ctrl/Cmd + P`呼出命令面板。
37 | - 搜索并选择`Callout Editor: insert two/three/four/five columns`,即可根据需要插入卡片簇。
38 |
39 | ### 快速调整颜色
40 |
41 | - 将鼠标悬停在卡片的右下角以显示颜色选择器。
42 | - 选择适当的颜色,为卡片或文本进行视觉分类。
43 |
44 | ### 添加新卡片
45 |
46 | - 点击卡片右侧的添加按钮,即可在当前卡片旁边新增卡片。
47 |
48 | ### 保存更改
49 |
50 | - 在当前编辑区域外点击一下,即可自动保存所有更改。
51 |
52 | ## 开源贡献
53 |
54 | 欢迎任何形式的贡献,无论是功能建议、问题报告还是代码提交。请访问[GitHub仓库](https://github.com/wnhllh/obsidian-callout-editor)。
55 |
56 | ## 更新日志
57 |
58 | ### 1.0.2 (2024-04-09)
59 | - 🚀 大幅优化实时保存机制,显著减少编辑时的卡顿现象
60 | - ⚡️ 重构代码提升编辑性能
61 | - 🐛 修复中文输入法输入时的实时保存问题
62 | - 🔧 优化防抖处理,提供更流畅的编辑体验
63 |
64 | ### 1.0.1 (2024-04-08)
65 | - 🔧 优化实时保存机制,减少卡顿现象
66 | - ⚡️ 提升编辑性能
67 | - 🐛 修复输入法输入时的保存问题
68 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 | # Callout Editor for Obsidian
2 | [English](README_EN.md) | 中文
3 |
4 | Callout Editor是一款Obsidian编辑插件,它通过卡片的自由创建组织,所见即所得的编辑方式,实时色彩选择以及灵活的瀑布流布局,优化了Callout的实时编辑与呈现形式。
5 |
6 | ## 功能特点
7 |
8 | - **所见即所得编辑**:在Obsidian中提供Callout的实时编辑功能,简化卡片的创建与修改过程。
9 | - **瀑布流布局**:支持一键创建不同列数的卡片布局,以清晰、有序的方式展示笔记内容。
10 | - **颜色选择器**:通过每个卡片右下角的颜色选择器,轻松为卡片添加色彩标记,增强信息分类的直观性。
11 | - **实时内容储存**:通过在当前卡片文本框外的任何地方点击,即可确保您的修改被立即且自动保存。
12 |
13 | ## 安装步骤
14 |
15 | 请参考: [How to install Obsidian Plugins](https://forum.obsidian.md/t/plugins-mini-faq/7737)
16 |
17 | 1. 创建文件夹 `obsidian-callout-editor`
18 | 2. 下载 `main.js, manifest.json, styles.css`
19 | 3. 将下载文件放入文件夹
20 | 4. 将文件夹放入 YourVault/.obsidian/plugins
21 | 5. 重启Obsidian
22 |
23 | ## 使用指南
24 |
25 | ### 插入瀑布流卡片簇
26 |
27 | - 使用`Ctrl/Cmd + P`呼出命令面板。
28 | - 搜索并选择`Callout Editor: insert two/three/four/five columns`,即可根据需要插入卡片簇。
29 |
30 | ### 快速调整颜色
31 |
32 | - 将鼠标悬停在卡片的右下角以显示颜色选择器。
33 | - 选择适当的颜色,为卡片或文本进行视觉分类。
34 |
35 | ### 添加新卡片
36 |
37 | - 点击卡片右侧的添加按钮,即可在当前卡片旁边新增卡片。
38 |
39 | ### 保存更改
40 |
41 | - 在当前编辑区域外点击一下,即可自动保存所有更改。
42 |
43 | ## 开源贡献
44 |
45 | 欢迎任何形式的贡献,无论是功能建议、问题报告还是代码提交。请访问[GitHub仓库](https://github.com/wnhllh/obsidian-callout-editor)。
46 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 | # Callout Editor for Obsidian
2 |
3 | English | [中文](README_CN.md)
4 |
5 | Elevate your note-taking experience in Obsidian with the Callout Editor plugin. Designed to enrich your notes with a visually organized structure, the Callout Editor seamlessly integrates advanced callout functionalities directly into Obsidian's command palette. Craft your notes with precision and ease, bringing clarity and efficiency to your workflow.
6 |
7 | ## Features
8 |
9 | - **Dynamic Callout Columns**: Instantly insert two, three, four, or five columns within your notes through Obsidian's command palette (`Ctrl/Cmd + P`), enhancing the layout and readability of your content.
10 | - **Real-time Color Customization**: Hover over the bottom-right corner of any card to summon the color selector, offering a palette of colors to categorize and personalize your notes visually.
11 | - **Seamless Content Expansion**: With a simple click on the add button located at the right side of your current card, effortlessly introduce a new card to your cascading layout, enabling fluid content addition without breaking your flow.
12 | - **Intuitive Content Saving**: Secure your modifications by clicking anywhere outside the current callout text box, ensuring your changes are saved instantly and automatically.
13 |
14 | ## Installation
15 |
16 | Please Refer: [How to install Obsidian Plugins](https://forum.obsidian.md/t/plugins-mini-faq/7737)
17 |
18 | 1. Create a folder `obsidian-callout-editor`
19 | 2. Download `main.js, manifest.json, styles.css`
20 | 3. Put the downloaded files into the folder
21 | 4. Put the folder in YourVault/.obsidian/plugins
22 | 5. Reload the plugins in Obsidian settings or restart Obsidian
23 |
24 | ## Usage
25 |
26 | ### Inserting Columns
27 |
28 | - Activate Obsidian's command palette with `Ctrl/Cmd + P`.
29 | - Type and select `Callout Editor: insert two columns` (or three, four, five as per your need) to structure your note instantly.
30 |
31 | ### Changing Colors
32 |
33 | - Hover over the bottom-right corner of a callout card to display the color selector.
34 | - Select your desired color to apply to the card, categorizing your content visually.
35 |
36 | ### Adding New Cards
37 |
38 | - Click the add button on the right side of a card to add a new card directly beside it, facilitating seamless content expansion.
39 |
40 | ### Saving Changes
41 |
42 | - Click anywhere outside the current editing callout to save your changes automatically.
43 |
44 | ## Contributing
45 |
46 | Contributions to the Callout Editor are warmly welcomed. Whether it's feature requests, bug reports, or code contributions, feel free to reach out or submit a pull request on [GitHub](https://github.com/wnhllh/obsidian-callout-editor).
47 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | /*
2 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
3 | if you want to view the source, please visit the github repository of this plugin
4 | */
5 |
6 | var __defProp = Object.defineProperty;
7 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8 | var __getOwnPropNames = Object.getOwnPropertyNames;
9 | var __hasOwnProp = Object.prototype.hasOwnProperty;
10 | var __export = (target, all) => {
11 | for (var name in all)
12 | __defProp(target, name, { get: all[name], enumerable: true });
13 | };
14 | var __copyProps = (to, from, except, desc) => {
15 | if ((from && typeof from === "object") || typeof from === "function") {
16 | for (let key of __getOwnPropNames(from))
17 | if (!__hasOwnProp.call(to, key) && key !== except)
18 | __defProp(to, key, {
19 | get: () => from[key],
20 | enumerable:
21 | !(desc = __getOwnPropDesc(from, key)) ||
22 | desc.enumerable,
23 | });
24 | }
25 | return to;
26 | };
27 | var __toCommonJS = (mod) =>
28 | __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29 |
30 | // main.ts
31 | var main_exports = {};
32 | __export(main_exports, {
33 | default: () => CalloutPlugin,
34 | });
35 | module.exports = __toCommonJS(main_exports);
36 | var import_obsidian6 = require("obsidian");
37 |
38 | // src/global.ts
39 | var numLines = 4;
40 | var vacantLines = 0;
41 | // var vacantList = []
42 | var numCols = [];
43 | var contentList = [];
44 | var onEditingClass = "on-editing";
45 | var onEditingMulti = "multi-editing";
46 |
47 | // function getCaretPosition(editableElem) {
48 | // let caretPos = 0,
49 | // sel,
50 | // range
51 | // sel = activeWindow.getSelection()
52 | // if (sel && sel.rangeCount) {
53 | // range = sel.getRangeAt(0)
54 | // caretPos = range.endOffset
55 | // // if (range.commonAncestorContainer.parentNode == editableElem) {
56 | // // caretPos = range.endOffset;
57 | // // console.log("caretPos=", caretPos);
58 | // // }
59 | // }
60 | // return caretPos
61 | // }
62 |
63 | // function setCaretPosition(editableElem, newPos) {
64 | // let caretPos = 0,
65 | // sel,
66 | // range = activeDocument.createRange()
67 | // sel = activeWindow.getSelection()
68 | // if (sel && sel.rangeCount) {
69 | // // range.setStart(editableElem.childNodes[0], newPos);
70 | // range.setStart(editableElem.childNodes[1], newPos)
71 | // range.collapse(true)
72 | // sel.removeAllRanges()
73 | // sel.addRange(range)
74 | // }
75 | // return caretPos
76 | // }
77 |
78 | function HTMLtoList0(rawContent) {
79 | let contentTemp = [];
80 | let n = rawContent.length;
81 | let i = 0;
82 | while (n > 0) {
83 | if (!rawContent[i].startsWith("
", "");
86 | }
87 | if (rawContent[i].startsWith("")) {
88 | rawContent[i] = rawContent[i].replace("
", "");
89 | }
90 | if (rawContent[i].endsWith("
")) {
91 | // * 多个空行
92 | rawContent[i] = rawContent[i].replace(/
$/, "");
93 | }
94 | if (rawContent[i].contains("
")) {
95 | let temp = rawContent[i].split("
");
96 | n = n + temp.length - 1;
97 |
98 | for (let j = 0; j < temp.length; j++) {
99 | rawContent.splice(i + j + 1, 0, temp[j]);
100 | }
101 | rawContent.splice(i, 1);
102 | }
103 |
104 | if (rawContent[i].match(//g)) {
105 | const imgMatches = rawContent[i].match(
106 | //g
107 | );
108 | // 如果找到匹配项,则对每个匹配项进行替换
109 | if (imgMatches) {
110 | imgMatches.forEach((imgTag) => {
111 | // 将
标签转换为 Markdown 格式
112 | const imgUrl = imgTag.match(/\"(.*?)\"/)[1];
113 | const markdownImg = "";
114 |
115 | // 替换原内容中的
标签
116 | rawContent[i] = rawContent[i].replace(
117 | imgTag,
118 | markdownImg
119 | );
120 | });
121 | }
122 | }
123 |
124 | // if (rawContent[i].startsWith('(.*?)\)[0].replace(/\>(.*?)\, '$1')
127 | // }
128 | // if (rawContent[i].startsWith('(.*?)\)[0].replace(/\>(.*?)\, '$1')
131 | // }
132 | // if (rawContent[i].startsWith('(.*?)\)[0].replace(/\>(.*?)\, '$1')
135 | // }
136 | // if (rawContent[i].startsWith('(.*?)\)[0].replace(/\>(.*?)\, '$1')
139 | // }
140 | // if (rawContent[i].startsWith('(.*?)\)[0].replace(/\>(.*?)\, '$1')
144 | // }
145 |
146 | // if (i !== 0) {
147 | // let k = 0
148 | // if(rawContent[i].endsWith('BRPLACEHOLDER')) {
149 | // rawContent[i] = rawContent[i].slice(0,-13);
150 | // k++;
151 | // }
152 | // contentTemp.push(rawContent[i])
153 | // while(k > 0){
154 | // contentTemp.push('')
155 | // k = 0;
156 | // }
157 | // }
158 | if (rawContent[i] !== "") {
159 | contentTemp.push(rawContent[i]);
160 | } else {
161 | if (i !== 0 && i !== rawContent.length - 1) {
162 | vacantLines++;
163 | }
164 | }
165 | }
166 | n--;
167 | i++;
168 | }
169 |
170 | return contentTemp;
171 | }
172 |
173 | function HTMLtoList2(rawContent) {
174 | function processContentOptimized(htmlSegments) {
175 | // 将所有HTML片段合并为一个字符串
176 | // const fullHtml = htmlSegments.join('')
177 | const fullHtml = htmlSegments
178 | .map((segment) =>
179 | segment.endsWith("
") ? segment : segment + "
"
180 | )
181 | .join("");
182 |
183 | let filteredHtml = fullHtml.replace(
184 | /]*>[\s\S]*?<\/div>/g,
185 | ""
186 | );
187 |
188 | // 针对
元素及其全部内容的替换逻辑
189 | let processedHtml = filteredHtml
190 | .replace(/<\/p>\s*
/g, "
")
191 | .replace(
192 | /
]*>([\s\S]*?)<\/div><\/div>/g,
193 | "\n[PLACEABLE]\n"
194 | )
195 | .replace(/

]*>/g, "")
196 | .replace(/
(.*?)<\/strong>/g, "**$1**")
197 | .replace(/(.*?)<\/em>/g, "*$1*")
198 | .replace(/(.*?)<\/del>/g, "~~$1~~")
199 | .replace(/]*>(.*?)<\/h\1>/g, (match, level, content) => {
200 | return `${"#".repeat(
201 | parseInt(level, 10)
202 | )} ${content.trim()}\n\n`;
203 | })
204 | .replace(
205 | /([\s\S]*?)<\/ul>/g,
206 | (match, listItems) => {
207 | // 未选中的任务列表项
208 | let uncheckedItems = listItems
209 | .replace(
210 | /- ]*>(.*?)<\/li>/g,
211 | "- [ ] $1\n"
212 | )
213 | .trim();
214 | // 选中的任务列表项
215 | let checkedItems = uncheckedItems
216 | .replace(
217 | /
- ]*checked=""[^>]*>(.*?)<\/li>/g,
218 | "- [x] $1\n"
219 | )
220 | .trim();
221 | return `\n${checkedItems}\n`;
222 | }
223 | )
224 |
225 | // 新增:处理无序列表
226 | .replace(/
([\s\S]*?)<\/ul>/g, (match, listItems) => {
227 | return listItems.replace(/- (.*?)<\/li>/g, "- $1\n").trim();
228 | })
229 | // 新增:处理有序列表
230 | .replace(/
([\s\S]*?)<\/ol>/g, (match, listItems) => {
231 | let counter = 1;
232 | return listItems
233 | .replace(/- (.*?)<\/li>/g, () => `${counter++}. $1\n`)
234 | .trim();
235 | })
236 |
237 | .replace(
238 | /
(.*?)<\/code>
") ||
444 | rawContent[i].startsWith("
= 0) {
450 | if (contentTemp[j] == "") {
451 | contentTemp.splice(j, 1);
452 | // vacantList[vacantList.length - 1]++
453 | // vacantLines++
454 | }
455 | j--;
456 | }
457 |
458 | newList.push(contentTemp);
459 | contentTemp = [];
460 | }
461 | }
462 | return newList;
463 | }
464 |
465 | // src/calloutEditor.ts
466 | var import_obsidian = require("obsidian");
467 |
468 | // src/editorUtils.ts
469 | var getLineStartPos = (line) => ({
470 | line,
471 | ch: 0,
472 | });
473 | var getLineEndPos = (line, editor) => ({
474 | line,
475 | ch: editor.getLine(line).length,
476 | });
477 | var deleteLines = (editor, from, to) => {
478 | // console.log('deleteLines', from, ' ', to)
479 | if (to === editor.lastLine() + 1) {
480 | return replaceRangeWithoutScroll(
481 | editor,
482 | "",
483 | getLineEndPos(from - 1, editor),
484 | getLineEndPos(to, editor)
485 | );
486 | } else {
487 | return replaceRangeWithoutScroll(
488 | editor,
489 | "",
490 | getLineStartPos(from),
491 | getLineStartPos(to)
492 | );
493 | }
494 | };
495 |
496 | var getLeadingWhitespace = (lineContent) => {
497 | const indentation = lineContent.match(/^\s+/);
498 | return indentation ? indentation[0] : "";
499 | };
500 | var insertLineBelow = (editor, line) => {
501 | // console.log('insertLineBelow', line)
502 | const endOfCurrentLine = getLineEndPos(line, editor);
503 | const indentation = getLeadingWhitespace(editor.getLine(line));
504 | return replaceRangeWithoutScroll(
505 | editor,
506 | "\n" + indentation,
507 | endOfCurrentLine
508 | );
509 | };
510 | var zf = (e, t) => {
511 | if (t.line < 0) return 0;
512 | const n = t.line + 1;
513 | if (n > e.lines) return e.length;
514 | const i = e.line(n);
515 | return isFinite(t.ch)
516 | ? t.ch < 0
517 | ? i.from + Math.max(0, i.length + t.ch)
518 | : i.from + t.ch
519 | : i.to;
520 | };
521 | var replaceRangeWithoutScroll = (editor, replacement, from, to) => {
522 | const cm = editor.cm;
523 | const state = cm.state.doc;
524 | const from2 = zf(state, from);
525 | const to2 = to ? zf(state, to) : from2;
526 | return {
527 | changes: {
528 | from: from2,
529 | to: to2,
530 | insert: replacement,
531 | },
532 | scrollIntoView: false,
533 | sequential: false,
534 | };
535 | };
536 | var setLineWithoutScroll = (editor, n, text) => {
537 | // console.log('setLineWithoutScroll', n, text)
538 | const cm = editor.cm;
539 | const state = cm.state.doc;
540 | const from = zf(state, { line: n, ch: 0 });
541 | const to = zf(state, { line: n, ch: editor.getLine(n).length });
542 | return {
543 | changes: {
544 | from,
545 | to,
546 | insert: text,
547 | },
548 | scrollIntoView: false,
549 | sequential: false,
550 | };
551 | };
552 | function withoutScrollAndFocus(editorView, callback) {
553 | // 保存当前滚动位置
554 | const scrollDom = editorView.scrollDOM;
555 | const x = scrollDom.scrollLeft;
556 | const y = scrollDom.scrollTop;
557 |
558 | // 移除原来的重置滚动事件监听器
559 | // scrollDom.addEventListener('scroll', resetScroll, true)
560 |
561 | // 不再需要强制失去焦点
562 | // editorView.contentDOM.blur()
563 |
564 | callback();
565 |
566 | // 直接设置滚动位置,而不是通过事件监听
567 | requestAnimationFrame(() => {
568 | scrollDom.scrollTo(x, y);
569 | });
570 |
571 | // 不再需要移除事件监听器
572 | // scrollDom.removeEventListener('scroll', resetScroll, true)
573 | }
574 |
575 | // src/calloutEditor.ts
576 | var CalloutEditor = class {
577 | constructor(plugin) {
578 | this.plugin = plugin;
579 | }
580 |
581 | static getLineNumber(callout, line) {
582 | return callout.fromLine + (line == 0 ? 0 : line + 1);
583 | }
584 | // async setLine(content) {
585 | // const markdownView = this.plugin.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
586 | // const editor = markdownView == null ? void 0 : markdownView.editor;
587 | // const editorView = editor == null ? void 0 : editor.cm;
588 | // if (!editor || !editorView) {
589 | // console.error("Cannot get editor");
590 | // return;
591 | // }
592 | // console.log("set content: " + content)
593 | // content = content.split("\n");
594 | // withoutScrollAndFocus(editorView, () => {
595 | // editorView.dispatch(setLineWithoutScroll(editor, editor.getCursor().line + 2 + content.length, "> "));
596 | // });
597 | // }
598 | async updateLine(id, content) {
599 | const markdownView = this.plugin.app.workspace.getActiveViewOfType(
600 | import_obsidian.MarkdownView
601 | );
602 | const editor = markdownView == null ? void 0 : markdownView.editor;
603 | const editorView = editor == null ? void 0 : editor.cm;
604 | if (!editor || !editorView) {
605 | console.error("Cannot get editor");
606 | return;
607 | }
608 | content = content.split("\n");
609 | const arr = id.split("_");
610 | withoutScrollAndFocus(editorView, () => {
611 | editorView.dispatch(
612 | insertLineBelow(editor, parseInt(arr[1]) + content.length)
613 | );
614 | editorView.dispatch(
615 | setLineWithoutScroll(
616 | editor,
617 | parseInt(arr[1]) + 1 + content.length,
618 | "> test case"
619 | )
620 | );
621 | });
622 | }
623 |
624 | async addCalloutContent(id, index) {
625 | const markdownView = this.plugin.app.workspace.getActiveViewOfType(
626 | import_obsidian.MarkdownView
627 | );
628 | const editor = markdownView == null ? void 0 : markdownView.editor;
629 | const editorView = editor == null ? void 0 : editor.cm;
630 | if (!editor || !editorView) {
631 | console.error("Cannot get editor");
632 | return;
633 | }
634 | const arr = id.split("_");
635 | let n = 0;
636 | let i = index;
637 | while (i >= 0) {
638 | n = n + contentList[i].length + 2;
639 | i--;
640 | }
641 |
642 | let m = 0;
643 | let k = index;
644 | while (k > 0) {
645 | m = m + contentList[k - 1].length + 2;
646 | k--;
647 | }
648 |
649 | const lineNo = parseInt(arr[1]) + n;
650 |
651 | withoutScrollAndFocus(editorView, () => {
652 | for (let i = 0; i < 3; i++) {
653 | editorView.dispatch(insertLineBelow(editor, lineNo + i));
654 | }
655 |
656 | editorView.dispatch(
657 | setLineWithoutScroll(
658 | editor,
659 | lineNo + 1,
660 | editor.getLine(parseInt(arr[1]) + m + 1)
661 | )
662 | );
663 |
664 | editorView.dispatch(
665 | setLineWithoutScroll(editor, lineNo + 2, ">> .")
666 | );
667 | editorView.dispatch(setLineWithoutScroll(editor, lineNo + 3, ">"));
668 | });
669 | }
670 |
671 | async updateChecklistItem(id, line, isChecked) {
672 | // console.log('updateChecklistItem', id, line, isChecked)
673 |
674 | const markdownView = this.plugin.app.workspace.getActiveViewOfType(
675 | import_obsidian.MarkdownView
676 | );
677 | const editor = markdownView == null ? void 0 : markdownView.editor;
678 | const editorView = editor == null ? void 0 : editor.cm;
679 | if (!editor || !editorView) {
680 | console.error("Cannot get editor");
681 | return;
682 | }
683 |
684 | const currentLineContent = editor.getLine(line);
685 | const newLineContent = isChecked
686 | ? currentLineContent.replace("[ ]", "[x]")
687 | : currentLineContent.replace("[x]", "[ ]");
688 |
689 | withoutScrollAndFocus(editorView, () => {
690 | editorView.dispatch(
691 | setLineWithoutScroll(editor, line, newLineContent)
692 | );
693 | });
694 | }
695 |
696 | async updateCalloutTitle(id, index, newTitle) {
697 | const markdownView = this.plugin.app.workspace.getActiveViewOfType(
698 | import_obsidian.MarkdownView
699 | );
700 | const editor = markdownView == null ? void 0 : markdownView.editor;
701 | const editorView = editor == null ? void 0 : editor.cm;
702 | if (!editor || !editorView) {
703 | console.error("Cannot get editor");
704 | return;
705 | }
706 | const arr = id.split("_");
707 | let n = 0;
708 | while (index > 0) {
709 | n = n + contentList[index - 1].length + 2;
710 | index--;
711 | }
712 | const lineNo = parseInt(arr[1]) + n + 1; // Assuming this is the line number of the callout
713 |
714 | withoutScrollAndFocus(editorView, () => {
715 | const currentTitle = editor.getLine(lineNo);
716 |
717 | editorView.dispatch(
718 | setLineWithoutScroll(
719 | editor,
720 | lineNo,
721 | currentTitle.replace(
722 | /\s+[^\s\]]+](?=[^\]]*$)/,
723 | " " + newTitle + "]"
724 | )
725 | )
726 | );
727 | });
728 | }
729 |
730 | async updateCallout(id, content, rawContent) {
731 | const markdownView = this.plugin.app.workspace.getActiveViewOfType(
732 | import_obsidian.MarkdownView
733 | );
734 | const editor = markdownView?.editor;
735 | const editorView = editor?.cm;
736 | if (!editor || !editorView) {
737 | console.error("Cannot get editor");
738 | return;
739 | }
740 |
741 | // 将内容分割成行
742 | const lines = content.split("\n");
743 | const arr = id.split("_");
744 | const startLineNum = parseInt(arr[1]) + 1;
745 |
746 | // 批量处理所有更改
747 | withoutScrollAndFocus(editorView, () => {
748 | const changes = [];
749 |
750 | // 收集所有需要的更改
751 | for (let i = 0; i < lines.length; i++) {
752 | const lineNum = startLineNum + i;
753 | const currentLine = editor.getLine(lineNum);
754 | const newLine = "> " + lines[i];
755 |
756 | if (currentLine !== newLine) {
757 | changes.push(
758 | setLineWithoutScroll(editor, lineNum, newLine)
759 | );
760 | }
761 | }
762 |
763 | // 一次性应用所有更改
764 | if (changes.length > 0) {
765 | editorView.dispatch(...changes);
766 | }
767 | });
768 | }
769 |
770 | async updateCallout2(id, content, rawContent) {
771 | const markdownView = this.plugin.app.workspace.getActiveViewOfType(
772 | import_obsidian.MarkdownView
773 | );
774 | const editor = markdownView?.editor;
775 | const editorView = editor?.cm;
776 | if (!editor || !editorView) {
777 | console.error("Cannot get editor");
778 | return;
779 | }
780 |
781 | const arr = id.split("_");
782 | const baseLine = parseInt(arr[1]);
783 |
784 | // 预处理内容,保持原有的分组逻辑
785 | const newList = HTMLtoList2(rawContent);
786 |
787 | // 批量处理所有更改
788 | withoutScrollAndFocus(editorView, () => {
789 | const changes = [];
790 | let k = 0;
791 | let m = 0;
792 | let n = 0;
793 |
794 | for (let i = 0; i < newList.length; i++) {
795 | m = m + newList[i].length + 2;
796 | n = n + contentList[i].length + 2;
797 |
798 | // 处理列内容长度变化
799 | if (newList[i].length > contentList[i].length) {
800 | const title = editor.getLine(baseLine + n + 1);
801 | for (
802 | let a = 0;
803 | a < newList[i].length - contentList[i].length;
804 | a++
805 | ) {
806 | changes.push(insertLineBelow(editor, baseLine + n - 1));
807 | }
808 | changes.push(
809 | setLineWithoutScroll(
810 | editor,
811 | baseLine +
812 | n +
813 | newList[i].length -
814 | contentList[i].length +
815 | 1,
816 | title
817 | )
818 | );
819 | changes.push(
820 | setLineWithoutScroll(
821 | editor,
822 | baseLine +
823 | n +
824 | newList[i].length -
825 | contentList[i].length,
826 | "> "
827 | )
828 | );
829 | }
830 |
831 | // 更新列内容
832 | for (let j = 0; j < newList[i].length; j++) {
833 | changes.push(
834 | setLineWithoutScroll(
835 | editor,
836 | baseLine + 2 + k,
837 | ">> " + newList[i][j]
838 | )
839 | );
840 | k = k + 1;
841 | }
842 | k = k + 2;
843 |
844 | // 处理列内容缩短的情况
845 | if (newList[i].length < contentList[i].length) {
846 | changes.push(
847 | deleteLines(
848 | editor,
849 | baseLine +
850 | n -
851 | contentList[i].length +
852 | newList[i].length,
853 | baseLine +
854 | n -
855 | contentList[i].length +
856 | contentList[i].length
857 | )
858 | );
859 | }
860 | }
861 |
862 | // 一次性应用所有更改
863 | if (changes.length > 0) {
864 | editorView.dispatch({
865 | changes: changes.map((change) => ({
866 | from: change.changes.from,
867 | to: change.changes.to,
868 | insert: change.changes.insert,
869 | })),
870 | scrollIntoView: false,
871 | });
872 | }
873 | });
874 | }
875 | };
876 |
877 | // src/ext.ts
878 | var import_view = require("@codemirror/view");
879 | var getCalloutEditorExt = (
880 | plugin,
881 | updateTitleCallback,
882 | addContentCallback,
883 | updateChecklistCallback,
884 | settings
885 | ) =>
886 | import_view.ViewPlugin.fromClass(
887 | class {
888 | constructor(view) {
889 | this.settings = settings;
890 | this.listenerMap = new Map();
891 |
892 | this.setupEventListeners(view);
893 | }
894 | destroy(view) {
895 | this.removeEventListeners();
896 | }
897 | update(update) {
898 | if (plugin.isInReadingView()) return;
899 | const dom = update.view.contentDOM;
900 |
901 | this.removeEventListeners();
902 | this.setupEventListeners(update.view);
903 |
904 | function generateCalloutSelector() {
905 | const baseKeywords = [
906 | "note",
907 | "tip",
908 | "abstract",
909 | "info",
910 | "todo",
911 | "success",
912 | "question",
913 | "warning",
914 | "failure",
915 | "danger",
916 | "bug",
917 | "example",
918 | "quote",
919 | ];
920 |
921 | // 生成每个基础关键词及其"-"后缀形式的选择器
922 | const selectors = baseKeywords.flatMap((keyword) => [
923 | `.callout[data-callout='${keyword}']`,
924 | `.callout[data-callout^='${keyword}-']`, // 使用 ^= 选择器匹配以 keyword- 开头的属性值
925 | ]);
926 |
927 | // 将所有选择器连接成一个字符串,用逗号分隔
928 | return selectors.join(", ");
929 | }
930 |
931 | const selector = generateCalloutSelector();
932 | const calloutEls = dom.querySelectorAll(selector);
933 | const columnsEls = dom.querySelectorAll(
934 | ".callout" && "div[data-callout='multi-column']"
935 | );
936 |
937 | calloutEls.forEach((calloutEl) => {
938 | // 检查当前 calloutEl 是否为 columnsEls 内的任何一个元素的后代
939 | const isInsideColumn = Array.from(columnsEls).some(
940 | (columnEl) => columnEl.contains(calloutEl)
941 | );
942 |
943 | // 如果 calloutEl 在 columnEl 内部,则不执行后续逻辑
944 | if (isInsideColumn) {
945 | return;
946 | }
947 |
948 | const calloutPos = update.view.posAtDOM(calloutEl);
949 | const calloutLine =
950 | update.state.doc.lineAt(calloutPos).number - 1;
951 | const calloutId = `callout_${calloutLine}`;
952 | calloutEl.id = calloutId;
953 |
954 | const calloutContent =
955 | calloutEl.querySelector(".callout-content");
956 | calloutContent.setAttribute("contenteditable", "true");
957 |
958 | calloutContent.addEventListener("click", (e) => {
959 | e.preventDefault();
960 | e.stopPropagation();
961 | calloutEl.addClass(onEditingClass);
962 | numLines = calloutContent.innerText.split("\n").length;
963 | });
964 | });
965 |
966 | this.setupEventListeners(update.view);
967 | }
968 |
969 | setupEventListeners(view) {
970 | const contentDOM = view.contentDOM;
971 |
972 | // if (this.columnClickListener) {
973 | // contentDOM.removeEventListener('click', this.columnClickListener);
974 | // }
975 |
976 | const columnsEls = contentDOM.querySelectorAll(
977 | ".callout" && "div[data-callout='multi-column']"
978 | );
979 |
980 | columnsEls.forEach((columnEl) => {
981 | const columnPos = view.posAtDOM(columnEl);
982 | const columnLine =
983 | view.state.doc.lineAt(columnPos).number - 1;
984 | const columnId = `column_${columnLine}`;
985 |
986 | // 设置元素ID和其他属性
987 | columnEl.id = columnId;
988 | columnEl.childNodes.forEach((child) => {
989 | child.setAttribute("contenteditable", "true");
990 | });
991 |
992 | this.setupColumnEl(columnEl, view);
993 |
994 | const columnContent =
995 | columnEl.querySelector(".callout-content");
996 |
997 | // 如果存在旧的事件处理函数,则移除它
998 | if (this.listenerMap.has(columnContent)) {
999 | const oldHandler = this.listenerMap.get(columnContent);
1000 | columnContent.removeEventListener("click", oldHandler);
1001 | }
1002 |
1003 | // 创建新的事件处理函数,它可以访问到columnId
1004 | const newHandler = ((columnId) => {
1005 | return (e) => {
1006 | const target = e.target;
1007 |
1008 | numCols = [];
1009 | var numCol = [];
1010 | e.preventDefault();
1011 | e.stopPropagation();
1012 |
1013 | if (
1014 | target.matches(
1015 | 'input[type="checkbox"].task-list-item-checkbox'
1016 | )
1017 | ) {
1018 | // 在这里调用处理复选框点击的逻辑
1019 | // console.log('Checklist item clicked in', columnContent)
1020 | const dataLine =
1021 | target.getAttribute("data-line");
1022 | let lineNumber = parseInt(dataLine, 10);
1023 | const isChecked = target.checked;
1024 | lineNumber = lineNumber + columnLine;
1025 | updateChecklistCallback(
1026 | columnId,
1027 | lineNumber,
1028 | isChecked
1029 | );
1030 | } else {
1031 | columnEl.addClass(onEditingMulti);
1032 | // console.log(getCaretPosition(columnContent));
1033 | numLines =
1034 | columnContent.innerText.split("\n").length;
1035 | // const rawContent2 = columnContent.innerHTML.split('\n')
1036 | // numLines = HTMLtoList(rawContent2).length;
1037 | const cards =
1038 | columnContent.innerText.split("\n");
1039 | for (let i = 0; i < cards.length; i++) {
1040 | if (cards[i].startsWith("Note")) {
1041 | cards.splice(i, 2);
1042 | }
1043 | }
1044 | for (let i = 0; i < cards.length; i++) {
1045 | if (cards[i] != "") {
1046 | numCol.push(cards[i]);
1047 | } else {
1048 | numCols.push(numCol);
1049 | numCol = [];
1050 | }
1051 | if (i == cards.length - 1) {
1052 | numCols.push(numCol);
1053 | }
1054 | }
1055 |
1056 | let rawContent =
1057 | columnContent.innerHTML.split("\n");
1058 | rawContent = rawContent.filter(
1059 | (line) =>
1060 | !line.trim().startsWith("
{
1083 | // 对每个元素调用removeEventListener
1084 | // 这里假设添加的监听器类型是'click'
1085 | element.removeEventListener("click", handler);
1086 | });
1087 |
1088 | // 清空listenerMap,释放引用
1089 | this.listenerMap.clear();
1090 | }
1091 |
1092 | setupColumnEl(columnEl, view) {
1093 | const columnPos = view.posAtDOM(columnEl);
1094 | const columnLine = view.state.doc.lineAt(columnPos).number - 1;
1095 | const columnId = `column_${columnLine}`;
1096 |
1097 | // 设置元素ID和其他属性
1098 | columnEl.id = columnId;
1099 | columnEl.childNodes.forEach((child) => {
1100 | child.setAttribute("contenteditable", "true");
1101 | });
1102 |
1103 | const colors = [
1104 | "transparent",
1105 | "red",
1106 | "orange",
1107 | "yellow",
1108 | "blue",
1109 | "green",
1110 | "white",
1111 | ];
1112 | const calloutContents = columnEl.querySelectorAll(
1113 | ".callout-content .callout-content"
1114 | );
1115 |
1116 | calloutContents.forEach((content, index) => {
1117 | if (this.settings.enableAddButton) {
1118 | const addButton = document.createElement("div");
1119 | addButton.innerHTML = `
`;
1120 | addButton.style.position = "absolute";
1121 | addButton.style.right = "5.5px";
1122 | addButton.style.top = "50%"; // Set top to 50% of parent's height
1123 | addButton.style.transform = "translateY(-50%)"; // Translate up by half of its height
1124 | content.style.position = "relative";
1125 | content.appendChild(addButton);
1126 |
1127 | let isClickAllowed = true; // 初始时允许点击
1128 |
1129 | addButton.addEventListener("click", (e) => {
1130 | if (!isClickAllowed) return; // 如果当前不允许点击,则直接返回
1131 |
1132 | // console.log(`Add button clicked in content ${index}`)
1133 | addContentCallback(columnId, index);
1134 | isClickAllowed = false; // 禁用进一步的点击
1135 | setTimeout(() => {
1136 | isClickAllowed = true; // 经过一定时间后重新允许点击
1137 | }, 2000);
1138 | });
1139 |
1140 | // 设置按钮的初始透明度
1141 | addButton.style.opacity = "0";
1142 | // 设置过渡效果为平滑过渡
1143 | addButton.style.transition = "opacity 0.3s";
1144 |
1145 | content.addEventListener("mousemove", (e) => {
1146 | // 取父容器的边界
1147 | const rect = content.getBoundingClientRect();
1148 | // 计算鼠标指针相对于父容器的X位置
1149 | const mouseX = e.clientX - rect.left;
1150 |
1151 | // 检查鼠标指针是否在父容器右侧10px内
1152 | if (rect.width - mouseX <= 50) {
1153 | // 如果是,则显示按钮
1154 | addButton.style.opacity = "1";
1155 | } else {
1156 | // 如果不是,则隐藏按钮
1157 | addButton.style.opacity = "0";
1158 | }
1159 | });
1160 | }
1161 | // setupAddButton(content, columnId, index);
1162 |
1163 | if (this.settings.enableSvgButton) {
1164 | const svgButton = document.createElement("div");
1165 | svgButton.innerHTML = `
`;
1166 | svgButton.style.position = "absolute";
1167 | svgButton.style.right = "4px";
1168 | svgButton.style.bottom = "0";
1169 | content.style.position = "relative";
1170 | content.appendChild(svgButton);
1171 |
1172 | // 置按钮的初始透明度
1173 | svgButton.style.opacity = "0";
1174 | // 设置过渡效果为平滑过渡
1175 | svgButton.style.transition = "opacity 0.3s";
1176 |
1177 | content.addEventListener("mousemove", (e) => {
1178 | // 获取父容器的边界
1179 | const rect = content.getBoundingClientRect();
1180 | // 计算鼠标指针相对于父容器的X位置
1181 | const mouseX = e.clientX - rect.left;
1182 |
1183 | // 检查鼠标指针是否在父容器右侧10px内
1184 | if (rect.width - mouseX <= 50) {
1185 | // 如果是,则显示按钮
1186 | svgButton.style.opacity = "1";
1187 | } else {
1188 | // 如果不是,则隐藏按钮
1189 | svgButton.style.opacity = "0";
1190 | }
1191 | });
1192 |
1193 | // 鼠标离开父容���时隐藏按钮
1194 | content.addEventListener("mouseleave", () => {
1195 | svgButton.style.opacity = "0";
1196 | });
1197 |
1198 | // Create the color menu but don't append it yet
1199 | const colorMenu = createColorMenu(
1200 | colors,
1201 | (selectedColor) => {
1202 | // console.log('Selected color:', selectedColor)
1203 | updateTitleCallback(
1204 | columnId,
1205 | index,
1206 | selectedColor
1207 | );
1208 | }
1209 | );
1210 |
1211 | // Append and show/hide the menu on SVG icon click
1212 | svgButton.addEventListener("click", (e) => {
1213 | e.stopPropagation();
1214 | if (!colorMenu.isConnected) {
1215 | // If the menu isn't already in the DOM
1216 | content.appendChild(colorMenu); // Append the menu
1217 | colorMenu.style.display = "block"; // Make sure it's visible
1218 | } else {
1219 | colorMenu.style.display =
1220 | colorMenu.style.display === "none"
1221 | ? "block"
1222 | : "none"; // Toggle display
1223 | }
1224 | });
1225 |
1226 | content.appendChild(colorMenu); // Initially add to DOM but hide
1227 | colorMenu.style.display = "none"; // Start hidden
1228 | }
1229 | });
1230 |
1231 | // function setupAddButton(content, columnId, index) {
1232 | // if (!this.settings.enableAddButton) return;
1233 |
1234 | // const addButton = document.createElement('div')
1235 | // addButton.innerHTML = `
`
1236 | // addButton.style.position = 'absolute'
1237 | // addButton.style.right = '5.5px'
1238 | // addButton.style.top = '50%' // Set top to 50% of parent's height
1239 | // addButton.style.transform = 'translateY(-50%)' // Translate up by half of its height
1240 | // content.style.position = 'relative'
1241 | // content.appendChild(addButton)
1242 |
1243 | // let isClickAllowed = true // 初始时允许点击
1244 |
1245 | // addButton.addEventListener('click', (e) => {
1246 | // if (!isClickAllowed) return // 如果当前不允许点击,则直接返回
1247 |
1248 | // // console.log(`Add button clicked in content ${index}`)
1249 | // addContentCallback(columnId, index)
1250 | // isClickAllowed = false // 禁用进一步的点击
1251 | // setTimeout(() => {
1252 | // isClickAllowed = true // 经过一定时间后重新允许点击
1253 | // }, 2000)
1254 | // })
1255 |
1256 | // }
1257 |
1258 | function createColorMenu(colors, onSelect) {
1259 | const menu = document.createElement("div");
1260 | menu.style.position = "absolute";
1261 | menu.style.right = "22.5px";
1262 | menu.style.bottom = "2.5px";
1263 | menu.style.backgroundColor = "#fff";
1264 | menu.style.border = "1px solid #ccc";
1265 | menu.style.paddingTop = "2.5px";
1266 | menu.style.paddingBottom = "2.5px";
1267 |
1268 | menu.style.zIndex = "1000";
1269 | menu.style.borderRadius = "10px";
1270 |
1271 | menu.style.transform = "scale(0.65)"; // Scale down to 80% of the original size
1272 | menu.style.transformOrigin = "bottom right";
1273 | // Explicitly setting the container width; adjust as necessary.
1274 | menu.style.width = "250px"; // Set a fixed width for the menu
1275 | menu.style.overflowX = "auto"; // Allow horizontal scrolling
1276 | menu.style.overflowY = "hidden"; // Prevent vertical scrolling
1277 | menu.style.whiteSpace = "nowrap"; // Keep items in a single line
1278 | menu.style.display = "block"; // Use block layout with nowrap to enforce horizontal layout
1279 |
1280 | colors.forEach((color) => {
1281 | const colorOption = document.createElement("div");
1282 | colorOption.textContent = "11"; // Display the color value or adjust as needed
1283 |
1284 | colorOption.style.backgroundColor = color;
1285 | colorOption.style.color = color; // Ensuring text is readable
1286 | colorOption.style.fontSize = "15px";
1287 | colorOption.style.display = "inline-block"; // Inline-block for horizontal alignment
1288 | colorOption.style.padding = "0px 10px";
1289 | colorOption.style.borderRadius = "10px";
1290 | // colorOption.style.width = '15px'; // Optional: Adjust the width as needed
1291 | // colorOption.style.height = '15px'; // Optional: Adjust the width as needed
1292 |
1293 | colorOption.style.margin = "2.5px"; // Adds space between color options
1294 | colorOption.style.height = "30px"; // Optional: Adjust the height as needed
1295 | colorOption.style.boxSizing = "border-box"; // Include padding and border in the element's total width and height
1296 | colorOption.addEventListener("click", () =>
1297 | onSelect(color)
1298 | );
1299 | menu.appendChild(colorOption);
1300 | });
1301 |
1302 | return menu;
1303 | }
1304 | }
1305 | }
1306 | );
1307 |
1308 | // src/settings.ts
1309 | var import_obsidian5 = require("obsidian");
1310 | var DEFAULT_SETTINGS = {
1311 | enableSvgButton: true, // 控制SVG按钮(选颜色)的显示
1312 | enableAddButton: true, // 控制添加内容按钮的显示
1313 | };
1314 |
1315 | class CalloutPluginSettingTab extends import_obsidian5.PluginSettingTab {
1316 | constructor(app, plugin) {
1317 | super(app, plugin);
1318 | this.plugin = plugin;
1319 | }
1320 |
1321 | display() {
1322 | const { containerEl } = this;
1323 | containerEl.empty();
1324 |
1325 | containerEl.createEl("h2", { text: "Callout Editor Settings" });
1326 |
1327 | // 为 enableSvgButton 添加设置项
1328 | new import_obsidian5.Setting(containerEl)
1329 | .setName("Enable Color Selection Button")
1330 | .setDesc("Toggle to show or hide the Color Selection button.")
1331 | .addToggle((toggle) =>
1332 | toggle
1333 | .setValue(this.plugin.settings.enableSvgButton)
1334 | .onChange(async (value) => {
1335 | this.plugin.settings.enableSvgButton = value;
1336 | await this.plugin.saveSettings();
1337 | })
1338 | );
1339 |
1340 | // 为 enableAddButton 添加设置项
1341 | new import_obsidian5.Setting(containerEl)
1342 | .setName("Enable Add Button")
1343 | .setDesc("Toggle to show or hide the Add Content button.")
1344 | .addToggle((toggle) =>
1345 | toggle
1346 | .setValue(this.plugin.settings.enableAddButton)
1347 | .onChange(async (value) => {
1348 | this.plugin.settings.enableAddButton = value;
1349 | await this.plugin.saveSettings();
1350 | })
1351 | );
1352 | }
1353 | }
1354 |
1355 | // main.ts
1356 | var CalloutPlugin = class extends import_obsidian6.Plugin {
1357 | async onload() {
1358 | await this.loadSettings();
1359 | await this.loadCSS();
1360 | this.calloutEditor = new CalloutEditor(this);
1361 | let settings = DEFAULT_SETTINGS;
1362 | this.addSettingTab(new CalloutPluginSettingTab(this.app, this));
1363 | const calloutEditorExt = getCalloutEditorExt(
1364 | this,
1365 | this.updateTitle.bind(this),
1366 | this.addContent.bind(this),
1367 | this.updateChecklistItem.bind(this),
1368 | settings
1369 | );
1370 | this.registerEditorExtension(calloutEditorExt);
1371 | this.registerEvent(
1372 | this.app.workspace.on("file-open", async () => {
1373 | // console.log('File opened!') // 用于调试,确保事件被正确触发
1374 |
1375 | this.app.workspace.onLayoutReady(() => {
1376 | const markdownView = this.app.workspace.getActiveViewOfType(
1377 | import_obsidian6.MarkdownView
1378 | );
1379 | if (!markdownView) return;
1380 | this.registerDomEvent(
1381 | markdownView.contentEl,
1382 | "click",
1383 | async (e) => {
1384 | const calloutEl = activeDocument.querySelector(
1385 | "." + onEditingClass
1386 | );
1387 | const columnEl = activeDocument.querySelector(
1388 | "." + onEditingMulti
1389 | );
1390 |
1391 | const target = e.target;
1392 | if (target.isContentEditable) {
1393 | const parentCallout =
1394 | target.closest(".callout-content");
1395 | if (!parentCallout) {
1396 | if (calloutEl) {
1397 | console.log("触发保存更改逻���1");
1398 | await this.saveChanges(calloutEl);
1399 | }
1400 | if (columnEl) {
1401 | console.log("触发保存更改逻辑2");
1402 | await this.saveChanges2(columnEl);
1403 | }
1404 | }
1405 |
1406 | if (
1407 | columnEl &&
1408 | target.closest(".callout-content") &&
1409 | !target.closest(
1410 | ".callout-content .callout-content"
1411 | )
1412 | ) {
1413 | await this.saveChanges2(columnEl);
1414 | }
1415 | }
1416 | },
1417 | true
1418 | );
1419 |
1420 | // 在 CalloutPlugin 类的 onload 方法中修改注册 input 事件的部分
1421 |
1422 | this.registerDomEvent(
1423 | markdownView.contentEl,
1424 | "input",
1425 | this.debounce(async (e) => {
1426 | const target = e.target;
1427 | // 检查是否正在进行输入法输入
1428 | if (
1429 | target.isContentEditable &&
1430 | !target.getAttribute("composing")
1431 | ) {
1432 | const calloutEl = activeDocument.querySelector(
1433 | "." + onEditingClass
1434 | );
1435 | const columnEl = activeDocument.querySelector(
1436 | "." + onEditingMulti
1437 | );
1438 |
1439 | if (
1440 | calloutEl &&
1441 | target.closest("." + onEditingClass)
1442 | ) {
1443 | await this.saveChanges(calloutEl);
1444 | }
1445 | if (
1446 | columnEl &&
1447 | target.closest("." + onEditingMulti)
1448 | ) {
1449 | await this.saveChanges2(columnEl);
1450 | }
1451 | }
1452 | }, 1500)
1453 | );
1454 |
1455 | // 添加输入法事件监听
1456 | this.registerDomEvent(
1457 | markdownView.contentEl,
1458 | "compositionstart",
1459 | (e) => {
1460 | const target = e.target;
1461 | if (target.isContentEditable) {
1462 | target.setAttribute("composing", "true");
1463 | }
1464 | }
1465 | );
1466 |
1467 | this.registerDomEvent(
1468 | markdownView.contentEl,
1469 | "compositionend",
1470 | async (e) => {
1471 | const target = e.target;
1472 | if (target.isContentEditable) {
1473 | target.removeAttribute("composing");
1474 | // 输入法输入完成后再触发一次更新
1475 | const calloutEl = activeDocument.querySelector(
1476 | "." + onEditingClass
1477 | );
1478 | const columnEl = activeDocument.querySelector(
1479 | "." + onEditingMulti
1480 | );
1481 |
1482 | if (
1483 | calloutEl &&
1484 | target.closest("." + onEditingClass)
1485 | ) {
1486 | await this.saveChanges(calloutEl);
1487 | }
1488 | if (
1489 | columnEl &&
1490 | target.closest("." + onEditingMulti)
1491 | ) {
1492 | await this.saveChanges2(columnEl);
1493 | }
1494 | }
1495 | }
1496 | );
1497 |
1498 | // 添加失去焦点事件监听器
1499 | this.registerDomEvent(
1500 | markdownView.contentEl,
1501 | "blur",
1502 | async (e) => {
1503 | const target = e.target;
1504 | if (target.isContentEditable) {
1505 | const calloutEl = activeDocument.querySelector(
1506 | "." + onEditingClass
1507 | );
1508 | const columnEl = activeDocument.querySelector(
1509 | "." + onEditingMulti
1510 | );
1511 |
1512 | if (
1513 | calloutEl &&
1514 | target.closest("." + onEditingClass)
1515 | ) {
1516 | await this.saveChanges(calloutEl);
1517 | }
1518 | if (
1519 | columnEl &&
1520 | target.closest("." + onEditingMulti)
1521 | ) {
1522 | await this.saveChanges2(columnEl);
1523 | }
1524 | }
1525 | },
1526 | true
1527 | );
1528 | });
1529 | this.registerEvent();
1530 | })
1531 | );
1532 |
1533 | // 注册"插入两列"命令
1534 | this.addCommand({
1535 | id: "insert-two-columns",
1536 | name: "Insert Two Columns Layout",
1537 | editorCallback: (editor, view) => this.insertColumns(editor, 2),
1538 | });
1539 |
1540 | // 注册"插入三列"命令
1541 | this.addCommand({
1542 | id: "insert-three-columns",
1543 | name: "Insert Three Columns Layout",
1544 | editorCallback: (editor, view) => this.insertColumns(editor, 3),
1545 | });
1546 |
1547 | // 注册"插入四列"命令
1548 | this.addCommand({
1549 | id: "insert-four-columns",
1550 | name: "Insert Four Columns Layout",
1551 | editorCallback: (editor, view) => this.insertColumns(editor, 4),
1552 | });
1553 |
1554 | // 注册"插入五列"命令
1555 | this.addCommand({
1556 | id: "insert-five-columns",
1557 | name: "Insert Five Columns Layout",
1558 | editorCallback: (editor, view) => this.insertColumns(editor, 5),
1559 | });
1560 | }
1561 |
1562 | insertColumns(editor, numberOfColumns) {
1563 | let columnsMarkup = "> [!multi-column]\n";
1564 |
1565 | // 为了确保三列布局的宽度是精确的33%,我们可以特别处理这个情况
1566 | let columnWidth = numberOfColumns === 3 ? 33 : 100 / numberOfColumns;
1567 |
1568 | for (let i = 0; i < numberOfColumns; i++) {
1569 | // 使用计算得到的columnWidth而不是直接除以numberOfColumns
1570 | columnsMarkup += `>> [!Note | ${columnWidth}% notitle left grey]\n>> .\n>\n`;
1571 | }
1572 |
1573 | // 获取当前光标位置并插入文本
1574 | const cursorPos = editor.getCursor();
1575 | editor.replaceRange(columnsMarkup, cursorPos);
1576 | }
1577 |
1578 | updateChecklistItem(id, line, isChecked) {
1579 | this.calloutEditor.updateChecklistItem(id, line, isChecked);
1580 | }
1581 |
1582 | updateTitle(id, index, newTitle) {
1583 | this.calloutEditor.updateCalloutTitle(id, index, newTitle);
1584 | }
1585 |
1586 | addContent(id, index) {
1587 | this.calloutEditor.addCalloutContent(id, index);
1588 | }
1589 |
1590 | async insertLine(calloutEl) {
1591 | const el = activeDocument.querySelector("." + onEditingClass);
1592 | if (el instanceof HTMLDivElement) {
1593 | calloutEl = el;
1594 | }
1595 | await this.calloutEditor.updateLine(
1596 | calloutEl.id,
1597 | calloutEl.innerText.trim()
1598 | );
1599 | }
1600 | async saveLine(calloutEl) {
1601 | const el = activeDocument.querySelector("." + onEditingClass);
1602 | if (el instanceof HTMLDivElement) {
1603 | calloutEl = el;
1604 | }
1605 | calloutEl.setAttribute("contenteditable", false);
1606 | calloutEl.removeClass(onEditingClass);
1607 | await this.calloutEditor.updateLine(
1608 | calloutEl.id,
1609 | calloutEl.innerText.trim()
1610 | );
1611 | }
1612 |
1613 | async saveChanges(calloutEl) {
1614 | const el = activeDocument.querySelector("." + onEditingClass);
1615 | if (el instanceof HTMLDivElement) {
1616 | calloutEl = el;
1617 | }
1618 |
1619 | const calloutContent = calloutEl.querySelector(".callout-content");
1620 | const content = calloutContent.innerText.trim();
1621 |
1622 | // 获取当前内容的哈希值或简单比较
1623 | const newContent = content;
1624 | if (calloutEl._lastContent === newContent) {
1625 | // 内容没有变化,直接返回
1626 | return;
1627 | }
1628 |
1629 | // 更新缓存的内容
1630 | calloutEl._lastContent = newContent;
1631 |
1632 | // 只在真正需要时才执行这些操作
1633 | calloutEl.setAttribute("contenteditable", false);
1634 | calloutEl.removeClass(onEditingClass);
1635 |
1636 | let rawContent = calloutContent.innerHTML.split("\n");
1637 | rawContent = rawContent.filter(
1638 | (line) => !line.trim().startsWith("
!line.trim().startsWith("
{
1733 | clearTimeout(timeout);
1734 | func(...args);
1735 | };
1736 | clearTimeout(timeout);
1737 | timeout = setTimeout(later, wait);
1738 | };
1739 | }
1740 | };
1741 |
--------------------------------------------------------------------------------
/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "obsidian-callout-editor",
3 | "name": "Callout Editor",
4 | "version": "1.0.2",
5 | "minAppVersion": "1.5.12",
6 | "description": "Enhance callout with WYSIWYG real-time editing, featuring quick color selection and creation of cascading card layouts.",
7 | "author": "LLH",
8 | "authorUrl": "https://obsidian.md",
9 | "isDesktopOnly": false
10 | }
11 |
--------------------------------------------------------------------------------
/styles.css:
--------------------------------------------------------------------------------
1 | .callout {
2 | background-color: transparent;
3 | padding: 5px;
4 | }
5 |
6 | .callout-content {
7 | /* border-radius: 5px; */
8 | padding-left: 15px;
9 | padding-right: 15px;
10 | padding-top: 0px;
11 | padding-bottom: 0px;
12 | }
13 |
14 | .callout-title {
15 | padding-top: 10px;
16 | padding-left: 10px;
17 | }
18 |
19 | body {
20 | --callout-min-width: 200px;
21 | --callout-gap: 0.5rem;
22 | --callout-margin: 5px;
23 | --list-min-width: 200px;
24 | --col-rule-color: #b3b3b3;
25 | --col-rule-width: 1px;
26 | --float-small-width: 300px;
27 | --float-medium-width: 400px;
28 | --float-large-width: 600px;
29 |
30 | --col-orange: 178, 122, 64;
31 | --col-pink: 178, 22, 164;
32 | --col-green: 99, 188, 76;
33 | --col-yellow: 255, 204, 0;
34 | --col-blue: 0, 153, 255;
35 | --col-purple: 139, 65, 166;
36 | --col-red: 255, 106, 106;
37 | --col-grey: 153, 153, 153;
38 |
39 | --red: rgb(255, 236, 236);
40 | --green: rgb(238, 243, 237);
41 | --orange: rgb(249, 237, 223);
42 | --yellow: rgb(250, 243, 222);
43 | --blue: rgb(233, 243, 248);
44 | --grey: rgb(241, 241, 239);
45 |
46 | }
47 |
48 | .theme-dark {
49 | --callout-min-width: 200px;
50 | --callout-gap: 0.5rem;
51 | --callout-margin: 5px;
52 | --list-min-width: 200px;
53 | --col-rule-color: #636363;
54 | --col-rule-width: 1px;
55 | --float-small-width: 300px;
56 | --float-medium-width: 400px;
57 | --float-large-width: 600px;
58 |
59 | --col-orange: 207, 94, 87;
60 | --col-pink: 194, 95, 148;
61 | --col-green: 101, 156, 118;
62 | --col-yellow: 195, 154, 86;
63 | --col-blue: 103, 134, 196;
64 | --col-purple: 150, 107, 205;
65 | --col-red: 255, 106, 106;
66 | --col-grey: 115, 115, 115;
67 |
68 | /* 暗色模式下的背景颜色,以更暗的色调来代替 */
69 | --red: rgb(77, 48, 43);
70 | --green: rgb(42, 61, 49);
71 | --orange: rgb(87, 61, 39);
72 | --yellow: rgb(83, 68, 44);
73 | --blue: rgb(31, 57, 76);
74 | --grey: rgb(47, 47, 47);
75 | }
76 |
77 | /* remove callout-title and make callout-content as part of main callout for box formatting */
78 | div[data-callout="multi-column"].callout > .callout-title { display: none; }
79 |
80 | /* ===隐藏标题============ */
81 | .callout.callout:is(
82 | [data-callout-metadata*='notitle'],
83 | [data-callout*='notitle']
84 | )
85 | .callout-title {
86 | display: none;
87 | }
88 |
89 | /* ===隐藏边框============ */
90 | .callout.callout:is(
91 | [data-callout-metadata*='noborder'],
92 | [data-callout*='noborder']
93 | ) {
94 | border-left: 0;
95 | background-color: transparent;
96 | box-shadow: none;
97 | }
98 | .callout.callout:is(
99 | [data-callout-metadata*='noborder'],
100 | [data-callout*='noborder']
101 | )
102 | .callout-content {
103 | border-left: 0;
104 | background-color: transparent;
105 | box-shadow: none;
106 | border: none;
107 | }
108 |
109 |
110 |
111 | .callout.callout:is([data-callout-metadata*='box'], [data-callout*='box']) {
112 | border-left: 0;
113 | padding-bottom: 10px;
114 | background-color: transparent;
115 | box-shadow: none;
116 | }
117 | .callout.callout:is([data-callout-metadata*='box'], [data-callout*='box'])
118 | .callout-content {
119 | border-left: 0;
120 | background-color: transparent;
121 | box-shadow: 0 2.5px 7.5px rgba(0, 0, 0, 0.15);
122 | border: 1px solid #ddd;
123 | border-radius: 12.5px;
124 | }
125 |
126 | .callout.callout:is([data-callout-metadata*='white'], [data-callout*='white'])
127 | .callout-content {
128 | border-left: 0;
129 | background-color: rgb(250, 250, 250);
130 | box-shadow: 0 2.5px 7.5px rgba(0, 0, 0, 0.15);
131 | /* border: 1px solid rgba(0, 0, 0, 0.15); */
132 | border-radius: 5px;
133 | }
134 |
135 | .callout.callout:is([data-callout-metadata*='white'], [data-callout*='white'])
136 | .callout-title
137 | {
138 | --callout-color: grey;
139 | background-color: rgb(250, 250, 250);
140 | }
141 |
142 |
143 | .callout.callout:is([data-callout-metadata*='green'], [data-callout*='green'])
144 | .callout-content {
145 | border-left: 0;
146 | /* background-color: rgba(163, 230, 53, 0.9); */
147 | background-color: var(--green);
148 | /* box-shadow: 0 2.5px 7.5px rgba(0, 0, 0, 0.15); */
149 | /* border: 1px solid rgba(163, 230, 53, 0.9); */
150 | }
151 |
152 | .callout.callout:is([data-callout-metadata*='green'], [data-callout*='green'])
153 | .callout-title
154 | {
155 | --callout-color: var(--col-green);
156 | background-color: var(--green);
157 | }
158 |
159 | .callout.callout:is([data-callout-metadata*='orange'], [data-callout*='orange'])
160 | .callout-content {
161 | border-left: 0;
162 | background-color: var(--orange);
163 | }
164 |
165 | .callout.callout:is([data-callout-metadata*='orange'], [data-callout*='orange'])
166 | .callout-title
167 | {
168 | --callout-color: var(--col-orange);
169 | background-color: var(--orange);
170 | }
171 |
172 | .callout.callout:is([data-callout-metadata*='yellow'], [data-callout*='yellow'])
173 | .callout-content {
174 | border-left: 0;
175 | background-color: var(--yellow);
176 | }
177 |
178 | .callout.callout:is([data-callout-metadata*='yellow'], [data-callout*='yellow'])
179 | .callout-title
180 | {
181 | --callout-color: var(--col-yellow);
182 | background-color: var(--yellow);
183 | }
184 |
185 | .callout.callout:is([data-callout-metadata*='blue'], [data-callout*='blue'])
186 | .callout-content {
187 | border-left: 0;
188 | background-color: var(--blue);
189 | }
190 |
191 | .callout.callout:is([data-callout-metadata*='blue'], [data-callout*='blue'])
192 | .callout-title
193 | {
194 | --callout-color: var(--col-blue);
195 | background-color: var(--blue);
196 | }
197 |
198 | .callout.callout:is([data-callout-metadata*='red'], [data-callout*='red'])
199 | .callout-content
200 | {
201 | border-left: 0;
202 | background-color: var(--red);
203 | }
204 |
205 | .callout.callout:is([data-callout-metadata*='red'], [data-callout*='red'])
206 | .callout-title
207 | {
208 | --callout-color: var(--col-red);
209 | background-color: var(--red);
210 | }
211 |
212 |
213 | .callout.callout:is([data-callout-metadata*='grey'], [data-callout*='grey'])
214 | .callout-content {
215 | border-left: 0;
216 | background-color: var(--grey);
217 | }
218 |
219 | .callout.callout:is([data-callout-metadata*='grey'], [data-callout*='grey'])
220 | .callout-title
221 | {
222 | --callout-color: var(--col-grey);
223 | background-color: var(--grey);
224 | }
225 |
226 | /* =============== */
227 |
228 | .callout.callout:is([data-callout-metadata*='col-green'], [data-callout*='col-green'])
229 | .callout-content {
230 | border-left: 0;
231 | background-color: transparent;
232 | border: 1px solid transparent;
233 | }
234 |
235 | .callout.callout:is([data-callout-metadata*='col-green'], [data-callout*='col-green'])
236 | .callout-title
237 | {
238 | --callout-color: var(--col-green);
239 | background-color: var(--green);
240 | padding-top: 7px;
241 | padding-bottom: 7px;
242 | }
243 |
244 | .callout.callout:is([data-callout-metadata*='col-orange'], [data-callout*='col-orange'])
245 | .callout-content {
246 | border-left: 0;
247 | background-color: transparent;
248 | border: transparent;
249 | }
250 |
251 | .callout.callout:is([data-callout-metadata*='col-orange'], [data-callout*='col-orange'])
252 | .callout-title
253 | {
254 | --callout-color: var(--col-orange);
255 | background-color: var(--orange);
256 | padding-top: 7px;
257 | padding-bottom: 7px;
258 | }
259 |
260 | .callout.callout:is([data-callout-metadata*='col-yellow'], [data-callout*='col-yellow'])
261 | .callout-content {
262 | border-left: 0;
263 | background-color: transparent;
264 | border: 1px solid transparent;
265 | }
266 |
267 | .callout.callout:is([data-callout-metadata*='col-yellow'], [data-callout*='col-yellow'])
268 | .callout-title
269 | {
270 | --callout-color: var(--col-yellow);
271 | background-color: var(--yellow);
272 | padding-top: 7px;
273 | padding-bottom: 7px;
274 | }
275 |
276 | .callout.callout:is([data-callout-metadata*='col-blue'], [data-callout*='col-blue'])
277 | .callout-content {
278 | border-left: 0;
279 | background-color: transparent;
280 | border: 1px solid transparent;
281 | }
282 |
283 | .callout.callout:is([data-callout-metadata*='col-blue'], [data-callout*='col-blue'])
284 | .callout-title
285 | {
286 | --callout-color: var(--col-blue);
287 | background-color: var(--blue);
288 | padding-top: 7px;
289 | padding-bottom: 7px;
290 | }
291 |
292 | .callout.callout:is([data-callout-metadata*='col-red'], [data-callout*='col-red'])
293 | .callout-content
294 | {
295 | border-left: 0;
296 | background-color: transparent;
297 | }
298 |
299 | .callout.callout:is([data-callout-metadata*='col-red'], [data-callout*='col-red'])
300 | .callout-title
301 | {
302 | --callout-color: var(--col-red);
303 | background-color: var(--red);
304 | padding-top: 7px;
305 | padding-bottom: 7px;
306 | }
307 |
308 |
309 | .callout.callout:is([data-callout-metadata*='col-grey'], [data-callout*='col-grey'])
310 | .callout-content {
311 | border-left: 0;
312 | background-color: transparent;
313 | }
314 |
315 | .callout.callout:is([data-callout-metadata*='col-grey'], [data-callout*='col-grey'])
316 | .callout-title
317 | {
318 | --callout-color: var(--col-grey);
319 | background-color: var(--grey);
320 | padding-top: 7px;
321 | padding-bottom: 7px;
322 | }
323 |
324 |
325 | /* ===支持 right left center 语法============ */
326 | /*支持两种写法
327 | > [!note|right]
328 | > [!note right]
329 | */
330 | .callout.callout:is([data-callout-metadata*='left'], [data-callout*='left']) {
331 | float: left !important;
332 | margin: unset;
333 | }
334 | .callout.callout:is([data-callout-metadata*='right'], [data-callout*='right']) {
335 | float: right !important;
336 | margin: unset;
337 | }
338 |
339 | .callout.callout:is(
340 | [data-callout-metadata*='center'],
341 | [data-callout*='center']
342 | ) {
343 | display: block;
344 | margin: auto;
345 | float: unset;
346 | text-align: center;
347 | }
348 |
349 |
350 | /* ===黑暗颜色============ */
351 |
352 |
353 |
354 |
355 | /* ===百分比宽度============ */
356 | /*支持两种写法
357 | > [!note|30%]
358 | > [!note 30%]
359 | */
360 | .callout.callout:is([data-callout-metadata*='100%'], [data-callout*='100%']) {
361 | width: 100%;
362 | }
363 | .callout.callout:is([data-callout-metadata*='95%'], [data-callout*='95%']) {
364 | width: 95%;
365 | }
366 | .callout.callout:is([data-callout-metadata*='90%'], [data-callout*='90%']) {
367 | width: 90%;
368 | }
369 | .callout.callout:is([data-callout-metadata*='85%'], [data-callout*='85%']) {
370 | width: 85%;
371 | }
372 | .callout.callout:is([data-callout-metadata*='80%'], [data-callout*='80%']) {
373 | width: 80%;
374 | }
375 | .callout.callout:is([data-callout-metadata*='75%'], [data-callout*='75%']) {
376 | width: 75%;
377 | }
378 | .callout.callout:is([data-callout-metadata*='70%'], [data-callout*='70%']) {
379 | width: 69%;
380 | }
381 | .callout.callout:is([data-callout-metadata*='65%'], [data-callout*='65%']) {
382 | width: 64%;
383 | }
384 | .callout.callout:is([data-callout-metadata*='60%'], [data-callout*='60%']) {
385 | width: 59%;
386 | }
387 | .callout.callout:is([data-callout-metadata*='55%'], [data-callout*='55%']) {
388 | width: 54%;
389 | }
390 | .callout.callout:is([data-callout-metadata*='50%'], [data-callout*='50%']) {
391 | width: 49%;
392 | }
393 | .callout.callout:is([data-callout-metadata*='45%'], [data-callout*='45%']) {
394 | width: 44%;
395 | }
396 | .callout.callout:is([data-callout-metadata*='40%'], [data-callout*='40%']) {
397 | width: 39%;
398 | }
399 |
400 | .callout.callout:is([data-callout-metadata*='35%'], [data-callout*='35%']) {
401 | width: 34%;
402 | }
403 | .callout.callout:is([data-callout-metadata*='33%'], [data-callout*='33%']) {
404 | width: 33%;
405 | }
406 | .callout.callout:is([data-callout-metadata*='30%'], [data-callout*='30%']) {
407 | width: 29.5%;
408 | }
409 | .callout.callout:is([data-callout-metadata*='25%'], [data-callout*='25%']) {
410 | width: 25%;
411 | }
412 | .callout.callout:is([data-callout-metadata*='20%'], [data-callout*='20%']) {
413 | width: 20%;
414 | }
415 | .callout.callout:is([data-callout-metadata*='15%'], [data-callout*='15%']) {
416 | width: 15%;
417 | }
418 | .callout.callout:is([data-callout-metadata*='10%'], [data-callout*='10%']) {
419 | width: 10%;
420 | }
421 |
422 | .is-live-preview
423 | :is(.HyperMD-quote, .cm-callout)
424 | + [class='cm-active cm-line'] {
425 | transition: 0.5s line-height;
426 | }
427 |
428 | .is-live-preview :is(.HyperMD-quote, .cm-callout) + [class='cm-line'] {
429 | line-height: 0.8;
430 | transition: 0.5s line-height, 0.5s background-color;
431 | border-radius: 0;
432 | }
433 |
434 | .is-live-preview :is(.HyperMD-quote, .cm-callout) + [class='cm-line']:hover {
435 | background-color: hsla(var(--color-accent-hsl), 0.4);
436 | }
437 |
438 | /* in reading view */
439 | .markdown-source-view .contains-task-list {
440 | padding-left: 0 !important;
441 | }
442 |
--------------------------------------------------------------------------------