Compare commits

..

19 Commits

Author SHA1 Message Date
zsviczian
049b5bfa85 2.14.0 2025-07-22 08:45:29 +02:00
zsviczian
743afa73b4 Merge pull request #2413 from rlan/master
Add Taiwan-idiomatic Traditional Chinese
2025-07-22 08:17:25 +02:00
zsviczian
6469eec051 added NotebookLM link 2025-07-22 06:25:12 +02:00
zsviczian
9f46821a41 Merge pull request #2412 from dmscode/zh-2025-07-20
Update zh-cn.ts to 207fea3
2025-07-21 10:07:01 +02:00
dmscode
1fb3a47bdc Update zh-cn.ts to 207fea3 2025-07-21 14:08:59 +08:00
稻米鼠
bcd0cdda65 Merge pull request #1 from PlayerMiller109/zh-2025-07-20
Supplement (2.13.0...2.13.1)
2025-07-21 13:56:57 +08:00
zsviczian
207fea3f57 ContentSearcher.ts, added to ScriptInstallPrompt.ts and Settings.ts 2025-07-20 21:07:03 +02:00
Rick Lan
e55ba3cc21 Add Taiwan-idiomatic Traditional Chinese 2025-07-20 20:54:04 +09:00
PlayerMiller109
71c87a7630 Supplement (2.13.0...2.13.1) 2025-07-20 10:31:38 +08:00
dmscode
e598a91f43 Update zh-cn.ts to 972fe1b 2025-07-20 06:09:52 +08:00
zsviczian
972fe1baea copy settings to the clipboard
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-07-19 18:43:01 +02:00
zsviczian
d7e1268afa updated info links in settings 2025-07-19 17:45:05 +02:00
zsviczian
7a3b937ea7 Merge pull request #2411 from dmscode/zh-2025-07-19
Fixed for issue 2175
2025-07-19 08:17:27 +02:00
dmscode
c850cd15ae Fixed for issue 2175 2025-07-19 10:02:10 +08:00
zsviczian
e9f70fd09e 2.13.2 2025-07-16 19:12:09 +02:00
zsviczian
2083443dfe Merge pull request #2408 from duianto/patch-1
Fix typos
2025-07-16 19:03:47 +02:00
zsviczian
69d9b8c1c9 Merge pull request #2407 from dmscode/zh-2025-07-13
Update zh-cn.ts to 6434a6e
2025-07-16 19:02:14 +02:00
duianto
65f4c9f3b3 Fix typos 2025-07-15 10:30:02 +02:00
dmscode
b89a106523 Update zh-cn.ts to 6434a6e 2025-07-13 07:59:32 +08:00
17 changed files with 1811 additions and 217 deletions

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.13.1",
"version": "2.14.0",
"minAppVersion": "1.5.7",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.13.1",
"version": "2.14.0",
"minAppVersion": "1.5.7",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -49,7 +49,7 @@ const jsxRuntimeShim = `
const mathjaxtosvg_pkg = isLib ? "" : fs.readFileSync("./MathjaxToSVG/dist/index.js", "utf8");
const LANGUAGES = ['ru', 'zh-cn']; //english is not compressed as it is always loaded by default
const LANGUAGES = ['ru', 'zh-cn', 'zh-tw']; //english is not compressed as it is always loaded by default
function trimLastSemicolon(input) {
if (input.endsWith(";")) {

View File

@@ -79,7 +79,7 @@ export const obsidianToExcalidrawMap: { [key: string]: string } = {
'ur': 'ur-PK', // Assuming Pakistan for Urdu
'vi': 'vi-VN',
'zh': 'zh-CN',
'zh-TW': 'zh-TW',
'zh-tw': 'zh-TW',
};

View File

@@ -3,8 +3,10 @@ import {
ButtonComponent,
DropdownComponent,
getIcon,
htmlToMarkdown,
Modifier,
normalizePath,
Notice,
PluginSettingTab,
Setting,
TextComponent,
@@ -44,6 +46,7 @@ import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils";
import { PDFExportSettingsComponent, PDFExportSettings } from "src/shared/Dialogs/PDFExportSettingsComponent";
import { ContentSearcher } from "src/shared/components/ContentSearcher";
export interface ExcalidrawSettings {
disableDoubleClickTextEditing: boolean;
@@ -604,6 +607,31 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
containerEl.addClass("excalidraw-settings");
this.containerEl.empty();
// ------------------------------------------------
// Search and Settings to Clipboard
// ------------------------------------------------
const notebookLMLinkContainer = createDiv("setting-item-description excalidraw-settings-links-container");
new ContentSearcher(containerEl, notebookLMLinkContainer);
notebookLMLinkContainer.createEl("a",{
href: "https://notebooklm.google.com/notebook/42d76a2f-c11d-4002-9286-1683c43d0ab0",
attr: {
"aria-label": t("NOTEBOOKLM_LINK_ARIA"),
"style": "margin: auto;"
}},
(a)=> {
//Lucide: message-circle-question-mark
a.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><path d="M12 17h.01"/></svg>${
t("NOTEBOOKLM_LINK_TEXT")
}`;
}
);
// ------------------------------------------------
// Promo links
// ------------------------------------------------
const coffeeDiv = containerEl.createDiv("coffee");
coffeeDiv.addClass("ex-coffee-div");
const coffeeLink = coffeeDiv.createEl("a", {
@@ -618,41 +646,53 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
const iconLinks = [
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"></path><path d="M9 18c-4.51 2-5-2-7-2"></path></svg>`,
icon: getIcon("bug").outerHTML,
href: "https://github.com/zsviczian/obsidian-excalidraw-plugin/issues",
aria: "Report bugs and raise feature requsts on the plugin's GitHub page",
text: "Bugs and Feature Requests",
aria: t("LINKS_BUGS_ARIA"),
text: t("LINKS_BUGS"),
},
{
icon: getIcon("globe").outerHTML,
href: "https://excalidraw-obsidian.online/",
aria: t("LINKS_WIKI_ARIA"),
text: t("LINKS_WIKI"),
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 19c-2.3 0-6.4-.2-8.1-.6-.7-.2-1.2-.7-1.4-1.4-.3-1.1-.5-3.4-.5-5s.2-3.9.5-5c.2-.7.7-1.2 1.4-1.4C5.6 5.2 9.7 5 12 5s6.4.2 8.1.6c.7.2 1.2.7 1.4 1.4.3 1.1.5 3.4.5 5s-.2 3.9-.5 5c-.2.7-.7 1.2-1.4 1.4-1.7.4-5.8.6-8.1.6 0 0 0 0 0 0z"></path><polygon points="10 15 15 12 10 9"></polygon></svg>`,
icon: getIcon("youtube").outerHTML,
href: "https://www.youtube.com/@VisualPKM",
aria: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
text: "Visual PKM on YouTube",
aria: t("LINKS_YT_ARIA"),
text: t("LINKS_YT"),
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" stroke="none" strokeWidth="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>`,
href: "https://discord.gg/DyfAXFwUHc",
aria: "Join the Visual Thinking Workshop Discord Server",
text: "Community on Discord",
aria: t("LINKS_DISCORD_ARIA"),
text: t("LINKS_DISCORD"),
},
{
icon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"></path></svg>`,
icon: getIcon("twitter").outerHTML,
href: "https://twitter.com/zsviczian",
aria: "Follow me on Twitter",
text: "Follow me on Twitter",
aria: t("LINKS_TWITTER"),
text: t("LINKS_TWITTER"),
},
{
icon: getIcon("graduation-cap").outerHTML,
href: "https://visual-thinking-workshop.com",
aria: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
text: "Join the Visual Thinking Workshop",
}
aria: t("LINKS_VTW_ARIA"),
text: t("LINKS_VTW"),
},
{
icon: getIcon("book").outerHTML,
href: "https://sketch-your-mind.com",
aria: t("LINKS_BOOK_ARIA"),
text: t("LINKS_BOOK"),
},
];
const linksEl = containerEl.createDiv("setting-item-description excalidraw-settings-links-container");
iconLinks.forEach(({ icon, href, aria, text }) => {
linksEl.createEl("a",{href, attr: { "aria-label": aria }}, (a)=> {
a.innerHTML = icon + text;
a.innerHTML = icon + text;
});
});

View File

@@ -158,10 +158,11 @@ export default {
CONVERT_FILE: "Convert to new format",
BACKUP_AVAILABLE: "We encountered an error while loading your drawing. This might have occurred if Obsidian unexpectedly closed during a save operation. For example, if you accidentally closed Obsidian on your mobile device while saving.<br><br><b>GOOD NEWS:</b> Fortunately, a local backup is available. However, please note that if you last modified this drawing on a different device (e.g., tablet) and you are now on your desktop, that other device likely has a more recent backup.<br><br>I recommend trying to open the drawing on your other device first and restore the backup from its local storage.<br><br>Would you like to load the backup?",
BACKUP_RESTORED: "Backup restored",
BACKUP_SAVE_AS_FILE: "This drawing is empty. A non-empty backup is avalable. Would you like to resture it as a new file and open it in a new tab?",
BACKUP_SAVE_AS_FILE: "This drawing is empty. A non-empty backup is available. Would you like to restore it as a new file and open it in a new tab?",
BACKUP_SAVE: "Restore",
BACKUP_DELETE: "Delete Backup",
BACKUP_CANCEL: "Cancel", CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
BACKUP_CANCEL: "Cancel",
CACHE_NOT_READY: "I apologize for the inconvenience, but an error occurred while loading your file.<br><br><mark>Having a little patience can save you a lot of time...</mark><br><br>The plugin has a backup cache, but it appears that you have just started Obsidian. Initializing the Backup Cache may take some time, usually up to a minute or more depending on your device's performance. You will receive a notification in the top right corner when the cache initialization is complete.<br><br>Please press OK to attempt loading the file again and check if the cache has finished initializing. If you see a completely empty file behind this message, I recommend waiting until the backup cache is ready before proceeding. Alternatively, you can choose Cancel to manually correct your file.<br>",
OBSIDIAN_TOOLS_PANEL: "Obsidian Tools Panel",
ERROR_SAVING_IMAGE: "Unknown error occurred while fetching the image. It could be that for some reason the image is not available or rejected the fetch request from Obsidian",
WARNING_PASTING_ELEMENT_AS_TEXT: "PASTING EXCALIDRAW ELEMENTS AS A TEXT ELEMENT IS NOT ALLOWED",
@@ -191,7 +192,32 @@ export default {
SAVE_IS_TAKING_LONG: "Saving your previous file is taking a long time. Please wait...",
SAVE_IS_TAKING_VERY_LONG: "For better performance, consider splitting large drawings into several smaller files.",
//ContentSearcher.ts
SEARCH_COPIED_TO_CLIPBOARD: "Markdown ready on clipboard",
SEARCH_COPY_TO_CLIPBOARD_ARIA: "Copy the entire dialog to the clipboard as Markdown. Ideal for use with tools like ChatGPT to search and understand.",
SEARCH_SHOWHIDE_ARIA: "Show/Hide search bar",
SEARCH_NEXT: "Next",
SEARCH_PREVIOUS: "Previous",
//settings.ts
NOTEBOOKLM_LINK_ARIA: "Ask NotebookLM for help about the plugin. This model is pre-loaded with all my video transcripts, release notes and other helpful content. Chat with NotebookLM to explore my 250+ videos and the Excalidraw documentation.",
NOTEBOOKLM_LINK_TEXT: "Learn the Plugin. Access the NotebookLM knowledgebase.",
LINKS_BUGS_ARIA: "Report bugs and raise feature requsts on the plugin's GitHub page",
LINKS_BUGS: "Report Bugs",
LINKS_YT_ARIA: "Check out my YouTube channel to learn about Visual Thinking and Excalidraw",
LINKS_YT: "Learn on YouTube",
LINKS_DISCORD_ARIA: "Join the Visual Thinking Workshop Discord Server",
LINKS_DISCORD: "Join the Community",
LINKS_TWITTER: "Follow me",
LINKS_VTW_ARIA: "Learn about Visual PKM, Excalidraw, Obsidian, ExcaliBrain and more",
LINKS_VTW: "Join a Workshop",
LINKS_BOOK_ARIA: "Read Sketch Your Mind, my book on Visual Thinking",
LINKS_BOOK: "Read the Book",
LINKS_WIKI: "Plugin Wiki",
LINKS_WIKI_ARIA: "Explore the Excalidraw Plugin Wiki",
RELEASE_NOTES_NAME: "Display Release Notes after update",
RELEASE_NOTES_DESC:
"<b><u>Toggle ON:</u></b> Display release notes each time you update Excalidraw to a newer version.<br>" +
@@ -209,7 +235,7 @@ export default {
CROP_SUFFIX_NAME: "Crop file suffix",
CROP_SUFFIX_DESC:
"The last part of the filename for new drawings created when cropping an image. " +
"Leave empty if you don't need a sufix.",
"Leave empty if you don't need a suffix.",
CROP_PREFIX_NAME: "Crop file prefix",
CROP_PREFIX_DESC:
"The first part of the filename for new drawings created when cropping an image. " +
@@ -224,7 +250,7 @@ export default {
"Leave empty if you don't need a prefix.",
ANNOTATE_PRESERVE_SIZE_NAME: "Preserve image size when annotating",
ANNOTATE_PRESERVE_SIZE_DESC:
"When annotating an image in markdown the replacment image link will include the width of the original image.",
"When annotating an image in markdown the replacement image link will include the width of the original image.",
CROP_FOLDER_NAME: "Crop file folder (CaSE senSItive!)",
CROP_FOLDER_DESC:
"Default location for new drawings created when cropping an image. If empty, drawings will be created following the Vault attachments settings.",
@@ -244,7 +270,7 @@ export default {
"Template.md, the setting would be: Excalidraw/Template.md (or just Excalidraw/Template - you may omit the .md file extension). " +
"If you are using Excalidraw in compatibility mode, then your template must be a legacy Excalidraw file as well " +
"such as Excalidraw/Template.excalidraw. <br><b>Template Folder:</b> You can also set a folder as your template. " +
"In this case you will be prompted which tempalte to use when creating a new drawing.<br>" +
"In this case you will be prompted which template to use when creating a new drawing.<br>" +
"<b>Pro Tip:</b> If you are using the Obsidian Templater plugin, you can add Templater code to your different Excalidraw " +
"templates to automate configuration of your drawings.",
SCRIPT_FOLDER_NAME: "Excalidraw Automate script folder (CASE SeNSitiVE!)",
@@ -356,7 +382,7 @@ FILENAME_HEAD: "Filename",
LEFTHANDED_MODE_NAME: "Left-handed mode",
LEFTHANDED_MODE_DESC:
"Currently only has effect in tray-mode. If turned on, the tray will be on the right side." +
"<br><b><u>Toggle ON:</u></b> Left-handed mode.<br><b><u>Toggle OFF:</u></b> Right-handed moded",
"<br><b><u>Toggle ON:</u></b> Left-handed mode.<br><b><u>Toggle OFF:</u></b> Right-handed mode.",
IFRAME_MATCH_THEME_NAME: "Markdown embeds to match Excalidraw theme",
IFRAME_MATCH_THEME_DESC:
"<b><u>Toggle ON:</u></b> Set this to true if for example you are using Obsidian in dark-mode but use excalidraw with a light background. " +
@@ -410,7 +436,7 @@ FILENAME_HEAD: "Filename",
"⚠️ You must close and reopen the Excalidraw/markdown file for changes to take effect. ⚠️",
HOTKEY_OVERRIDE_HEAD: "Hotkey overrides",
HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while using Excalidraw, thus " +
`you can add <code>${labelCTRL()}+G</code> if you want to default to Group Object in Excalidraw instead of opening Graph View.`,
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
@@ -545,7 +571,7 @@ FILENAME_HEAD: "Filename",
"Use the <code>http://iframely.server.crestify.com/iframely?url=</code> to get title of page when dropping a link into Excalidraw",
PDF_TO_IMAGE: "PDF to Image",
PDF_TO_IMAGE_SCALE_NAME: "PDF to Image conversion scale",
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper imagee. " +
PDF_TO_IMAGE_SCALE_DESC: "Sets the resolution of the image that is generated from the PDF page. Higher resolution will result in bigger images in memory and consequently a higher load on your system (slower performance), but sharper image. " +
"Additionally, if you want to copy PDF pages (as images) to Excalidraw.com, the bigger image size may result in exceeding the 2MB limit on Excalidraw.com.",
EMBED_TOEXCALIDRAW_HEAD: "Embed files into Excalidraw",
EMBED_TOEXCALIDRAW_DESC: "In the Embed Files section of Excalidraw Settings, you can configure how various files are embedded into Excalidraw. This includes options for embedding interactive markdown files, PDFs, and markdown files as images.",
@@ -705,7 +731,7 @@ FILENAME_HEAD: "Filename",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_NAME: "Linter compatibility",
DUMMY_TEXT_ELEMENT_LINT_SUPPORT_DESC: "Excalidraw is sensitive to the file structure below <code># Excalidraw Data</code>. Automatic linting of documents can create errors in Excalidraw Data. " +
"While I've made some effort to make the data loading resilient to " +
"lint changes, this solution is not foolproof.<br><mark>The best is to avoid liniting or otherwise automatically changing Excalidraw documents using different plugins.</mark><br>" +
"lint changes, this solution is not foolproof.<br><mark>The best is to avoid linting or otherwise automatically changing Excalidraw documents using different plugins.</mark><br>" +
"Use this setting if for good reasons you have decided to ignore my recommendation and configured linting of Excalidraw files.<br> " +
"The <code>## Text Elements</code> section is sensitive to empty lines. A common linting approach is to add an empty line after section headings. In case of Excalidraw this will break/change the first text element in your drawing. " +
"To overcome this, you can enable this setting. When enabled, Excalidraw will add a dummy element to the beginning of <code>## Text Elements</code> that the linter can safely modify." ,
@@ -822,10 +848,10 @@ FILENAME_HEAD: "Filename",
<strong>Note:</strong> If you're using Obsidian Sync and want to synchronize these font files across your devices, ensure that Obsidian Sync is set to synchronize "All other file types".`,
LOAD_CHINESE_FONTS_NAME: "Load Chinese fonts from file at startup",
LOAD_JAPANESE_FONTS_NAME: "Load Japanese fonts from file at startup",
LOAD_KOREAN_FONTS_NAME: "Load Korean fonts frome file at startup",
LOAD_KOREAN_FONTS_NAME: "Load Korean fonts from file at startup",
SCRIPT_SETTINGS_HEAD: "Settings for installed Scripts",
SCRIPT_SETTINGS_DESC: "Some of the Excalidraw Automate Scripts include settings. Settings are organized by script. Settings will only become visible in this list after you have executed the newly downloaded script once.",
TASKBONE_HEAD: "Taskbone Optical Character Recogntion",
TASKBONE_HEAD: "Taskbone Optical Character Recognition",
TASKBONE_DESC: "This is an experimental integration of optical character recognition into Excalidraw. Please note, that taskbone is an independent external service not provided by Excalidraw, nor the Excalidraw-Obsidian plugin project. " +
"The OCR service will grab legible text from freedraw lines and embedded pictures on your canvas and place the recognized text in the frontmatter of your drawing as well as onto clipboard. " +
"Having the text in the frontmatter will enable you to search in Obsidian for the text contents of these. " +
@@ -834,7 +860,7 @@ FILENAME_HEAD: "Filename",
TASKBONE_ENABLE_DESC: "By enabling this service your agree to the Taskbone <a href='https://www.taskbone.com/legal/terms/' target='_blank'>Terms and Conditions</a> and the " +
"<a href='https://www.taskbone.com/legal/privacy/' target='_blank'>Privacy Policy</a>.",
TASKBONE_APIKEY_NAME: "Taskbone API Key",
TASKBONE_APIKEY_DESC: "Taskbone offers a free service with a reasonable number of scans per month. If you want to use this feature more frequently, or you want to supoprt " +
TASKBONE_APIKEY_DESC: "Taskbone offers a free service with a reasonable number of scans per month. If you want to use this feature more frequently, or you want to support " +
"the developer of Taskbone (as you can imagine, there is no such thing as 'free', providing this awesome OCR service costs some money to the developer of Taskbone), you can " +
"purchase a paid API key from <a href='https://www.taskbone.com/' target='_blank'>taskbone.com</a>. In case you have purchased a key, simply overwrite this auto generated free-tier API-key with your paid key.",
@@ -879,7 +905,7 @@ FILENAME_HEAD: "Filename",
//ExcalidrawData.ts
LOAD_FROM_BACKUP: "Excalidraw file was corrupted. Loading from backup file.",
FONT_LOAD_SLOW: "Loading Fonts...\n\n This is taking longer than expected. If this delay occurs regulary then you may download the fonts locally to your Vault. \n\n" +
FONT_LOAD_SLOW: "Loading Fonts...\n\n This is taking longer than expected. If this delay occurs regularly then you may download the fonts locally to your Vault. \n\n" +
"(click=dismiss, right-click=Info)",
FONT_INFO_TITLE: "Starting v2.5.3 fonts load from the Internet",
FONT_INFO_DETAILED: `

View File

@@ -158,9 +158,11 @@ export default {
CONVERT_FILE: "转换为新格式",
BACKUP_AVAILABLE: "加载绘图文件时出错,可能是由于 Obsidian 在上次保存时意外退出了(手机上更容易发生这种意外)。<br><br><b>好消息:</b>这台设备上存在备份。您是否想要恢复本设备上的备份?<br><br>(我建议您先尝试在最近使用过的其他设备上打开该绘图,以检查是否有更新的备份。)",
BACKUP_RESTORED: "已恢复备份",
BACKUP_SAVE_AS_FILE : "此绘图为空,但有一个较大的备份可用。您是否想将其另存为新文件并在新标签页中打开?" ,
DO_YOU_WANT_TO_DELETE_THE_BACKUP : "该备份[未]作为恢复文件保存到您的存储库中。您是否想删除备份数据?" ,
CACHE_NOT_READY: "抱歉,加载绘图文件时出错。<br><br><mark>现在有耐心,将来更省心。</mark><br><br>该插件有备份机制,但您似乎刚刚打开 Obsidian需要等待一分钟或更长的时间来读取缓存。缓存读取完毕时您将会在右上角收到提示。<br><br>请点击 OK 并耐心等待缓存,或者选择点击取消后手动修复你的文件。<br>",
BACKUP_SAVE_AS_FILE : "此绘图为空。存在一个非空的备份。您是否希望将其恢复为新文件并在新标签页中打开?" ,
BACKUP_SAVE : "恢复" ,
BACKUP_DELETE : "删除备份" ,
BACKUP_CANCEL : "取消" ,
CACHE_NOT_READY : "很抱歉给您带来不便,加载文件时发生了错误。<br><br><mark>稍作等待可能会节省您大量时间……</mark><br><br>插件有一个备份缓存,但似乎您刚刚启动了 Obsidian。初始化备份缓存可能需要一些时间通常取决于设备性能可能需要一分钟或更长时间。当缓存初始化完成时您会在右上角收到通知。<br><br>请按“确定”尝试重新加载文件,并检查缓存是否已完成初始化。如果在此消息后看到一个完全空白的文件,我建议等待备份缓存准备就绪后再继续操作。或者,您也可以选择“取消”以手动修复您的文件。<br>" ,
OBSIDIAN_TOOLS_PANEL: "Obsidian 工具面板",
ERROR_SAVING_IMAGE: "获取图像时发生未知错误。可能是由于某种原因,图像不可用或拒绝了 Obsidian 的获取请求。",
WARNING_PASTING_ELEMENT_AS_TEXT: "你不能将 Excalidraw 元素粘贴为文本元素!",
@@ -190,7 +192,27 @@ export default {
SAVE_IS_TAKING_LONG: "保存您之前的文件花费的时间较长,请稍候...",
SAVE_IS_TAKING_VERY_LONG: "为了更好的性能,请考虑将大型绘图拆分成几个较小的文件。",
//ContentSearcher.ts
SEARCH_COPIED_TO_CLIPBOARD: "Markdown 已复制到剪贴板",
SEARCH_COPY_TO_CLIPBOARD_ARIA: "将整个对话框复制为 Markdown 到剪贴板。非常适合搭配 ChatGPT 等工具进行搜索和理解。",
SEARCH_NEXT: "下一个",
SEARCH_PREVIOUS: "上一个",
//settings.ts
LINKS_BUGS_ARIA: "在插件的 GitHub 页面报告错误和提交功能请求",
LINKS_BUGS: "报告错误",
LINKS_YT_ARIA: "访问我的 YouTube 频道学习视觉思维和 Excalidraw",
LINKS_YT: "在 YouTube 学习",
LINKS_DISCORD_ARIA: "加入视觉思维研讨会 Discord 服务器",
LINKS_DISCORD: "加入社区",
LINKS_TWITTER: "关注我",
LINKS_VTW_ARIA: "了解视觉知识管理、Excalidraw、Obsidian、ExcaliBrain 等内容",
LINKS_VTW: "参加研讨会",
LINKS_BOOK_ARIA: "阅读我的视觉思维著作《Sketch Your Mind》",
LINKS_BOOK: "阅读书籍",
RELEASE_NOTES_NAME: "显示更新说明",
RELEASE_NOTES_DESC:
"<b>开启:</b>每次更新本插件后,显示最新发行版本的说明。<br>" +
@@ -388,7 +410,7 @@ FILENAME_HEAD: "文件名",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"在触控笔模式下使用涂鸦功能会显示十字准星 <b><u>打开:</u></b> 显示 <b><u>关闭:</u></b> 隐藏<br>"+
"效果取决于设备。十字准星通常在绘图板、MS Surface 上可见。但在 iOS 上不可见。",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "在鼠标悬停预览时将 Excalidraw 文件渲染图片",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"...即使文件具有 `<b>excalidraw-open-md: true</b>` frontmatter 属性。<br>" +
"当此设置关闭且文件默认设置为以 md 格式打开时,悬停预览将显示文档的 Markdown 部分(背景笔记)。" +
@@ -920,8 +942,12 @@ FILENAME_HEAD: "文件名",
//IFrameActionsMenu.tsx
NARROW_TO_HEADING: "缩放至标题",
PIN_VIEW: "锁定视图",
DO_NOT_PIN_VIEW: "不锁定视图",
NARROW_TO_BLOCK: "缩放至块",
SHOW_ENTIRE_FILE: "显示全部",
SELECT_SECTION: "从文档选择章节",
SELECT_VIEW: "从 base 选择视图",
ZOOM_TO_FIT: "缩放至合适大小",
RELOAD: "重载链接",
OPEN_IN_BROWSER: "在浏览器中打开",

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
import { App, Modal } from "obsidian";
export class FloatingModal extends Modal {
private dragging = false;
private offsetX = 0;
private offsetY = 0;
private pointerDownHandler: (e: PointerEvent) => void;
private pointerMoveHandler: (e: PointerEvent) => void;
private pointerUpHandler: () => void;
constructor(app: App) {
super(app);
// Initialize event handlers with proper binding
this.pointerDownHandler = this.handlePointerDown.bind(this);
this.pointerMoveHandler = this.handlePointerMove.bind(this);
this.pointerUpHandler = this.handlePointerUp.bind(this);
}
private handlePointerDown(e: PointerEvent): void {
// Ignore if clicking on interactive elements
if (
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement ||
e.target instanceof HTMLButtonElement ||
(e.target as HTMLElement).closest(".clickable-icon")
) {
return;
}
this.dragging = true;
const { modalEl } = this;
this.offsetX = e.clientX - modalEl.getBoundingClientRect().left;
this.offsetY = e.clientY - modalEl.getBoundingClientRect().top;
// Add global event listeners for move and up events
document.addEventListener("pointermove", this.pointerMoveHandler);
document.addEventListener("pointerup", this.pointerUpHandler);
// Capture the pointer to ensure we get events even when outside the target
(e.target as HTMLElement).setPointerCapture(e.pointerId);
}
private handlePointerMove(e: PointerEvent): void {
if (!this.dragging) return;
const { modalEl } = this;
e.preventDefault();
const x = e.clientX - this.offsetX;
const y = e.clientY - this.offsetY;
// Position the modal element
modalEl.style.left = `${x}px`;
modalEl.style.top = `${y}px`;
modalEl.style.transform = "none"; // Remove centering transform
}
private handlePointerUp(): void {
this.dragging = false;
document.removeEventListener("pointermove", this.pointerMoveHandler);
document.removeEventListener("pointerup", this.pointerUpHandler);
}
open(): void {
super.open();
setTimeout(() => {
//@ts-ignore
const { containerEl, modalEl, bgEl } = this;
containerEl.style.pointerEvents = "none";
if (bgEl) bgEl.style.display = "none";
// Set initial position and make modal draggable
if (modalEl) {
modalEl.style.pointerEvents = "auto";
// Position absolute is needed for custom positioning
modalEl.style.position = "absolute";
// Center the modal initially
const rect = modalEl.getBoundingClientRect();
const centerX = window.innerWidth / 2 - rect.width / 2;
const centerY = window.innerHeight / 2 - rect.height / 2;
modalEl.style.left = `${centerX}px`;
modalEl.style.top = `${centerY}px`;
modalEl.style.transform = "none";
// Add pointer down listener to start dragging
modalEl.addEventListener("pointerdown", this.pointerDownHandler);
}
});
}
close(): void {
const { modalEl } = this;
// Clean up event listeners
if (modalEl) {
modalEl.removeEventListener("pointerdown", this.pointerDownHandler);
}
document.removeEventListener("pointermove", this.pointerMoveHandler);
document.removeEventListener("pointerup", this.pointerUpHandler);
super.close();
}
}

View File

@@ -17,6 +17,16 @@ I build this plugin in my free time, as a labor of love. Curious about the philo
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
`,
"2.14.0":`
## A Big "Small" Update
- Added search to Excalidraw Settings, plus added a link to access the public NotebookLM workbook pre-loaded with everything about the plugin
- New Taiwan-idiomatic Traditional Chinese translation by [@rlan](https://github.com/rlan) [#2413](https://github.com/zsviczian/obsidian-excalidraw-plugin/pull/2413)
`,
"2.13.2":`
## New
- Excalidraw now properly supports drag and drop of obsidian links from Bases.
- ExcalidrawAutomate exposes a new class: \`FloatingModal\`. This is a modified version of the Obsidian.Modal class that allows the modal to be dragged around the screen and that does not dim the background. You can use it to create custom dialogs that behave like Obsidian modals but with more flexibility.
`,
"2.13.1":`
## New
- Support for Obsidian bases as embeddables in Excalidraw.

View File

@@ -25,8 +25,10 @@ export class ReleaseNotes extends Modal {
async onClose() {
this.contentEl.empty();
await this.plugin.loadSettings();
this.plugin.settings.previousRelease = PLUGIN_VERSION
await this.plugin.saveSettings();
if(this.plugin.settings.previousRelease !== PLUGIN_VERSION) {
this.plugin.settings.previousRelease = PLUGIN_VERSION;
await this.plugin.saveSettings();
}
}
async createForm() {
@@ -39,7 +41,8 @@ export class ReleaseNotes extends Modal {
.slice(0, 10)
.join("\n\n---\n")
: FIRST_RUN;
await MarkdownRenderer.renderMarkdown(
await MarkdownRenderer.render(
this.app,
message,
this.contentEl,
"",

View File

@@ -1,80 +1,27 @@
import { MarkdownRenderer, Modal, Notice, request } from "obsidian";
import ExcalidrawPlugin from "../../core/main";
import { errorlog, escapeRegExp } from "../../utils/utils";
import { errorlog } from "../../utils/utils";
import { log } from "src/utils/debugHelper";
import { ContentSearcher } from "../components/ContentSearcher";
const URL =
"https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/index-new.md";
export class ScriptInstallPrompt extends Modal {
private contentDiv: HTMLDivElement;
constructor(private plugin: ExcalidrawPlugin) {
super(plugin.app);
// this.titleEl.setText(t("INSTAL_MODAL_TITLE"));
}
async onOpen(): Promise<void> {
const searchBarWrapper = document.createElement("div");
searchBarWrapper.classList.add('search-bar-wrapper');
const searchBar = document.createElement("input");
searchBar.type = "text";
searchBar.id = "search-bar";
searchBar.placeholder = "Search...";
//searchBar.style.width = "calc(100% - 120px)"; // space for the buttons and hit count
const nextButton = document.createElement("button");
nextButton.textContent = "→";
nextButton.onclick = () => this.navigateSearchResults("next");
const prevButton = document.createElement("button");
prevButton.textContent = "←";
prevButton.onclick = () => this.navigateSearchResults("previous");
const hitCount = document.createElement("span");
hitCount.id = "hit-count";
hitCount.classList.add('hit-count');
searchBarWrapper.appendChild(prevButton);
searchBarWrapper.appendChild(nextButton);
searchBarWrapper.appendChild(searchBar);
searchBarWrapper.appendChild(hitCount);
this.contentEl.prepend(searchBarWrapper);
searchBar.addEventListener("input", (e) => {
this.clearHighlights();
const searchTerm = (e.target as HTMLInputElement).value;
if (searchTerm && searchTerm.length > 0) {
this.highlightSearchTerm(searchTerm);
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
hitCount.textContent = totalHits > 0 ? `1/${totalHits}` : "";
setTimeout(()=>this.navigateSearchResults("next"));
} else {
hitCount.textContent = "";
}
});
searchBar.addEventListener("keydown", (e) => {
// If Ctrl/Cmd + F is pressed, focus on search bar
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
e.preventDefault();
searchBar.focus();
}
// If Enter is pressed, navigate to next result
else if (e.key === "Enter") {
e.preventDefault();
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
}
});
this.contentEl.classList.add("excalidraw-scriptengine-install");
this.contentDiv = document.createElement("div");
this.contentEl.appendChild(this.contentDiv);
new ContentSearcher(this.contentDiv);
this.containerEl.classList.add("excalidraw-scriptengine-install");
try {
const source = await request({ url: URL });
@@ -111,99 +58,6 @@ export class ScriptInstallPrompt extends Modal {
}
}
highlightSearchTerm(searchTerm: string): void {
// Create a walker to traverse text nodes
const walker = document.createTreeWalker(
this.contentDiv,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node: Text) => {
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
}
}
);
const nodesToReplace: Text[] = [];
while (walker.nextNode()) {
nodesToReplace.push(walker.currentNode as Text);
}
nodesToReplace.forEach(node => {
const nodeContent = node.nodeValue!;
const newNode = document.createDocumentFragment();
let lastIndex = 0;
let match;
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
// Iterate over all matches in the text node
while ((match = regex.exec(nodeContent)) !== null) {
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
const highlighted = document.createElement('mark');
highlighted.className = 'search-highlight';
highlighted.textContent = match[0];
highlighted.classList.add('search-result');
newNode.appendChild(before);
newNode.appendChild(highlighted);
lastIndex = regex.lastIndex;
}
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
node.replaceWith(newNode);
});
}
clearHighlights(): void {
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
el.outerHTML = el.innerHTML;
});
}
navigateSearchResults(direction: "next" | "previous"): void {
const highlights: HTMLElement[] = Array.from(
this.contentDiv.querySelectorAll("mark.search-highlight")
);
if (highlights.length === 0) return;
const currentActiveIndex = highlights.findIndex((highlight) =>
highlight.classList.contains("active-highlight")
);
if (currentActiveIndex !== -1) {
highlights[currentActiveIndex].classList.remove("active-highlight");
highlights[currentActiveIndex].style.border = "none";
}
let nextActiveIndex = 0;
if (direction === "next") {
nextActiveIndex =
currentActiveIndex === highlights.length - 1
? 0
: currentActiveIndex + 1;
} else if (direction === "previous") {
nextActiveIndex =
currentActiveIndex === 0
? highlights.length - 1
: currentActiveIndex - 1;
}
const nextActiveHighlight = highlights[nextActiveIndex];
nextActiveHighlight.classList.add("active-highlight");
nextActiveHighlight.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
// Update the hit count
const hitCount = document.getElementById("hit-count");
hitCount.textContent = `${nextActiveIndex + 1}/${highlights.length}`;
}
onClose(): void {
this.contentEl.empty();
}

View File

@@ -31,6 +31,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "The ExcalidrawPlugin object",
after: "",
},
{
field: "FloatingModal",
code: null,
desc: "A modified version of the Obsidian.Modal class that allows the modal to be dragged around the screen and that does not dim the background.",
after: "",
},
{
field: "elementsDict",
code: null,

View File

@@ -88,6 +88,7 @@ import { exportToPDF, getMarginValue, getPageDimensions, PageDimensions, PageOri
import { FrameRenderingOptions } from "src/types/utilTypes";
import { CaptureUpdateAction } from "src/constants/constants";
import { AutoexportConfig } from "src/types/excalidrawViewTypes";
import { FloatingModal } from "./Dialogs/FloatingModal";
extendPlugins([
HarmonyPlugin,
@@ -170,6 +171,15 @@ export class ExcalidrawAutomate {
return obsidian_module;
};
/**
* This is a modified version of the Obsidian.Modal class
* that allows the modal to be dragged around the screen
* and that does not dim the background.
*/
get FloatingModal() {
return FloatingModal;
}
/**
* Retrieves the laser pointer settings from the plugin.
* @returns {Object} The laser pointer settings.

View File

@@ -0,0 +1,306 @@
import { t } from "src/lang/helpers";
import { escapeRegExp } from "../../utils/utils";
// @ts-ignore
import { getIcon, htmlToMarkdown, Notice } from "obsidian";
export class ContentSearcher {
private contentDiv: HTMLElement;
private searchBar: HTMLInputElement;
private prevButton: HTMLButtonElement;
private nextButton: HTMLButtonElement;
private exportMarkdown: HTMLButtonElement;
private showHideButton: HTMLButtonElement;
private customElemenentContainer: HTMLDivElement;
private inputContainer: HTMLDivElement;
private customElement: HTMLElement;
private hitCount: HTMLSpanElement;
private searchBarWrapper: HTMLDivElement;
constructor(contentDiv: HTMLElement, customElement?: HTMLElement) {
this.contentDiv = contentDiv;
this.customElement = customElement;
this.createSearchElements();
this.setupEventListeners();
contentDiv.prepend(this.getSearchBarWrapper());
}
/**
* Creates search UI elements styled like Obsidian's native search
*/
private createSearchElements(): void {
this.searchBarWrapper = createDiv("document-search-container");
const documentSearch = createDiv("document-search");
this.inputContainer = createDiv("search-input-container document-search-input");
this.searchBar = createEl("input",{type: "text", placeholder: "Find..."});
this.hitCount = createDiv("document-search-count");
this.inputContainer.appendChild(this.searchBar);
this.inputContainer.appendChild(this.hitCount);
const buttonContainer = createDiv("document-search-buttons");
this.prevButton = createEl("button", {
cls: ["clickable-icon", "document-search-button"],
attr: {
"aria-label": t("SEARCH_PREVIOUS"),
"data-tooltip-position": "top",
},
type: "button",
});
this.prevButton.innerHTML = getIcon("arrow-up").outerHTML;
this.nextButton = createEl("button", {
cls: ["clickable-icon", "document-search-button"],
attr: {
"aria-label": t("SEARCH_NEXT"),
"data-tooltip-position": "top",
},
type: "button",
});
this.nextButton.innerHTML = getIcon("arrow-down").outerHTML;
this.exportMarkdown = createEl("button", {
cls: ["clickable-icon", "document-search-button"],
attr: {
"aria-label": t("SEARCH_COPY_TO_CLIPBOARD_ARIA"),
"data-tooltip-position": "top",
},
type: "button",
});
this.exportMarkdown.innerHTML = getIcon("clipboard-copy").outerHTML;
this.showHideButton = createEl("button", {
cls: ["clickable-icon", "document-search-button", "search-visible"],
attr: {
"aria-label": t("SEARCH_SHOWHIDE_ARIA"),
"data-tooltip-position": "top",
},
type: "button",
});
this.showHideButton.innerHTML = getIcon("minimize-2").outerHTML;
buttonContainer.appendChild(this.prevButton);
buttonContainer.appendChild(this.nextButton);
buttonContainer.appendChild(this.exportMarkdown);
buttonContainer.appendChild(this.showHideButton);
documentSearch.appendChild(this.inputContainer);
documentSearch.appendChild(buttonContainer);
this.searchBarWrapper.appendChild(documentSearch);
this.customElemenentContainer = createDiv();
if(this.customElement) {
this.customElemenentContainer.appendChild(this.customElement);
this.searchBarWrapper.appendChild(this.customElemenentContainer)
}
}
/**
* Attach event listeners to search elements
*/
private setupEventListeners(): void {
this.nextButton.onclick = () => this.navigateSearchResults("next");
this.prevButton.onclick = () => this.navigateSearchResults("previous");
this.exportMarkdown.onclick = () => {
// Get the full HTML content first
const fullHtml = this.contentDiv.outerHTML;
// Find the index of the first <hr> element
const startIndex = fullHtml.indexOf('<hr');
// Extract HTML from the first <hr> element onwards
const html = startIndex > -1 ? fullHtml.substring(startIndex) : fullHtml;
function replaceHeading(html:string,level:number):string {
const re = new RegExp(`<summary class="excalidraw-setting-h${level}">([^<]+)<\/summary>`,"g");
return html.replaceAll(re,`<summary class="excalidraw-setting-h${level}"><h${level}>$1</h${level}></summary>`);
}
let x = replaceHeading(html,1);
x = replaceHeading(x,2);
x = replaceHeading(x,3);
x = replaceHeading(x,4);
x = x.replaceAll(/<div class="setting-item-name">([^<]+)<\/div>/g,"<h5>$1</h5>");
const md = htmlToMarkdown(x);
window.navigator.clipboard.writeText(md);
new Notice(t("SEARCH_COPIED_TO_CLIPBOARD"));
};
this.showHideButton.onclick = () => {
const setOpacity = (value:string|null) => {
this.inputContainer.style.opacity = value;
this.prevButton.style.opacity = value;
this.nextButton.style.opacity = value;
this.exportMarkdown.style.opacity = value;
this.customElemenentContainer.style.opacity = value;
}
if(this.showHideButton.hasClass("search-visible")) {
this.showHideButton.removeClass("search-visible");
this.showHideButton.addClass("search-hidden");
this.searchBarWrapper.style.backgroundColor = "transparent";
setOpacity("0");
this.showHideButton.innerHTML = getIcon("maximize-2").outerHTML;
} else {
this.showHideButton.removeClass("search-hidden");
this.showHideButton.addClass("search-visible");
this.searchBarWrapper.style.backgroundColor = null;
setOpacity(null);
this.showHideButton.innerHTML = getIcon("minimize-2").outerHTML;
}
}
this.searchBar.addEventListener("input", (e) => {
this.clearHighlights();
const searchTerm = (e.target as HTMLInputElement).value;
if (searchTerm && searchTerm.length > 0) {
this.highlightSearchTerm(searchTerm);
const totalHits = this.contentDiv.querySelectorAll("mark.search-highlight").length;
this.hitCount.textContent = totalHits > 0 ? `1 / ${totalHits}` : "";
setTimeout(() => this.navigateSearchResults("next"));
} else {
this.hitCount.textContent = "";
}
});
this.searchBar.addEventListener("keydown", (e) => {
// If Ctrl/Cmd + F is pressed, focus on search bar
if ((e.ctrlKey || e.metaKey) && e.key === "f") {
e.preventDefault();
this.searchBar.focus();
}
// If Enter is pressed, navigate to next result
else if (e.key === "Enter") {
e.preventDefault();
this.navigateSearchResults(e.shiftKey ? "previous" : "next");
}
});
}
/**
* Get the search bar wrapper element to add to the DOM
*/
public getSearchBarWrapper(): HTMLElement {
return this.searchBarWrapper;
}
/**
* Highlight all instances of the search term in the content
*/
public highlightSearchTerm(searchTerm: string): void {
// Create a walker to traverse text nodes
const walker = document.createTreeWalker(
this.contentDiv,
NodeFilter.SHOW_TEXT,
{
acceptNode: (node: Text) => {
return node.nodeValue!.toLowerCase().includes(searchTerm.toLowerCase()) ?
NodeFilter.FILTER_ACCEPT :
NodeFilter.FILTER_REJECT;
}
}
);
const nodesToReplace: Text[] = [];
while (walker.nextNode()) {
nodesToReplace.push(walker.currentNode as Text);
}
nodesToReplace.forEach(node => {
const nodeContent = node.nodeValue!;
const newNode = document.createDocumentFragment();
let lastIndex = 0;
let match;
const regex = new RegExp(escapeRegExp(searchTerm), 'gi');
// Iterate over all matches in the text node
while ((match = regex.exec(nodeContent)) !== null) {
const before = document.createTextNode(nodeContent.slice(lastIndex, match.index));
const highlighted = document.createElement('mark');
highlighted.className = 'search-highlight';
highlighted.textContent = match[0];
highlighted.classList.add('search-result');
newNode.appendChild(before);
newNode.appendChild(highlighted);
lastIndex = regex.lastIndex;
}
newNode.appendChild(document.createTextNode(nodeContent.slice(lastIndex)));
node.replaceWith(newNode);
});
}
/**
* Remove all search highlights
*/
public clearHighlights(): void {
this.contentDiv.querySelectorAll("mark.search-highlight").forEach((el) => {
el.outerHTML = el.innerHTML;
});
}
/**
* Navigate to next or previous search result
*/
public navigateSearchResults(direction: "next" | "previous"): void {
const highlights: HTMLElement[] = Array.from(
this.contentDiv.querySelectorAll("mark.search-highlight")
);
if (highlights.length === 0) return;
const currentActiveIndex = highlights.findIndex((highlight) =>
highlight.classList.contains("active-highlight")
);
if (currentActiveIndex !== -1) {
highlights[currentActiveIndex].classList.remove("active-highlight");
highlights[currentActiveIndex].style.border = "none";
}
let nextActiveIndex = 0;
if (direction === "next") {
nextActiveIndex =
currentActiveIndex === highlights.length - 1
? 0
: currentActiveIndex + 1;
} else if (direction === "previous") {
nextActiveIndex =
currentActiveIndex === 0
? highlights.length - 1
: currentActiveIndex - 1;
}
const nextActiveHighlight = highlights[nextActiveIndex];
nextActiveHighlight.classList.add("active-highlight");
// Expand all parent details elements
this.expandParentDetails(nextActiveHighlight);
// Use setTimeout to ensure DOM has time to update after expanding details
setTimeout(() => {
nextActiveHighlight.scrollIntoView({
behavior: "smooth",
block: "nearest",
});
}, 100);
// Update the hit count
this.hitCount.textContent = `${nextActiveIndex + 1} / ${highlights.length}`;
}
/**
* Expand all parent <details> elements to make the element visible
*/
private expandParentDetails(element: HTMLElement): void {
let parent = element.parentElement;
while (parent) {
if (parent.tagName === "DETAILS") {
parent.setAttribute("open", "");
}
parent = parent.parentElement;
}
}
}

View File

@@ -134,6 +134,7 @@ export class DropManager {
// Obsidian internal drag event
//---------------------------------------------------------------------------------
switch (draggable?.type) {
case "link":
case "file":
if (!onDropHook("file", [draggable.file], null)) {
const file:TFile = draggable.file;

View File

@@ -431,25 +431,6 @@ div.excalidraw-draginfo {
margin: auto;
}
.modal-content.excalidraw-scriptengine-install .search-bar-wrapper {
position: sticky;
top: 1rem;
margin-right: 1rem;
display: flex;
align-items: center;
gap: 5px;
flex-wrap: nowrap;
z-index: 10;
background: var(--background-secondary);
padding: 0.5rem;
border-bottom: 1px solid var(--background-modifier-border);
float: right;
max-width: 28rem;
}
div.search-bar-wrapper input {
margin-right: -0.5rem;
}
.modal-content.excalidraw-scriptengine-install .hit-count {
margin-left: 0.5em;
@@ -641,20 +622,27 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
}
.excalidraw-settings-links-container {
display: flex; /* Align SVG and text horizontally */
align-items: center; /* Center SVG and text vertically */
text-decoration: none; /* Remove underline from links */
color: inherit; /* Inherit text color */
text-align: center;
align-items: center;
color: inherit;
display: flex;
gap: .3em;
text-align: center;
text-decoration: none;
flex-wrap: wrap;
justify-content: center;
flex-direction: row;
}
.excalidraw-settings-links-container a {
display: flex; /* Align children horizontally */
align-items: center; /* Center items vertically */
text-align: left;
margin-right: 4px;
margin-left: 4px;
}
.excalidraw-settings-links-container svg {
.excalidraw-settings-links-container svg,
.ex-setting-actions-container svg {
margin-right: 8px;
height: 30px;
width: 30px;
@@ -776,3 +764,96 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
.excalidraw-prompt-buttonbar-bottom > div:last-child {
margin-left: auto;
}
.document-search-container {
display: flex;
flex-direction: column;
background: var(--background-secondary);
border-radius: 8px;
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.10);
padding: 0.5em 0.8em;
margin-bottom: 2em;
min-width: 18rem;
position: sticky;
top: 1rem;
margin-right: 1rem;
margin-left: 1rem;
}
.document-search {
align-items: center;
max-width: none;
}
.search-input-container.document-search-input {
display: flex;
align-items: center;
flex: 1 1 auto;
background: var(--background-primary);
border-radius: 6px;
border: 1px solid var(--background-modifier-border);
min-width: 0;
}
.search-input-container .clickable-icon {
display: flex;
align-items: center;
color: var(--text-faint);
}
.search-input-container input[type="text"] {
background: transparent;
border: none;
outline: none;
color: var(--text-normal);
font-size: 1em;
flex: 1 1 auto;
padding: 0.1em 2em;
margin: 0;
}
.document-search-count {
margin-left: 0.5em;
color: var(--text-faint);
font-size: 0.95em;
white-space: nowrap;
min-width: 3.5em;
text-align: right;
}
.document-search-buttons {
display: flex;
align-items: center;
gap: 2px;
}
.document-search-button {
background: none;
border: none;
outline: none;
box-shadow: none;
padding: 0.1em 0.2em;
margin: 0 1px;
border-radius: 4px;
cursor: pointer;
color: var(--text-faint);
transition: background 0.15s;
height: 2em;
width: 2em;
display: flex;
align-items: center;
justify-content: center;
}
.document-search-button:hover, .document-search-button:focus {
background: var(--background-modifier-hover);
color: var(--text-accent);
}
.document-search-button svg {
width: 1.3em;
height: 1.3em;
stroke: currentColor;
fill: none;
pointer-events: none;
}