diff --git a/plugin.json b/plugin.json index 868169e..c8c0c28 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "name": "syplugin-misuzu-custom", "author": "Misuzu2027", "url": "https://github.com/Misuzu2027/syplugin-misuzu-custom", - "version": "0.0.5", + "version": "0.0.6", "minAppVersion": "3.0.14", "backends": [ "all" diff --git a/public/i18n/en_US.json b/public/i18n/en_US.json index 1821c81..997159e 100644 --- a/public/i18n/en_US.json +++ b/public/i18n/en_US.json @@ -16,5 +16,8 @@ "KeywordFilterNotebooksMatchSubdocuments": "Keyword Filter for Notebooks - Match Subdocuments", "MiddleClickToggleDocTree": "Middle-click to expand/collapse notebooks or documents", "Image": "Image", - "MiddleClickResizeImageWidth": "Middle-click to resize image width" + "MiddleClickResizeImageWidth": "Middle-click to resize image width", + "MiddleClickResizeImageWidthDesc":"Units are required. Example: 200px. px: fixed width; vw: viewport percentage.", + "ZoomWidthLoadedImagesCurrentDocument": "Zoom the width of loaded images in the current document", + "ShowTopBatchZoomBtn": "Show a batch image zoom button at the top" } \ No newline at end of file diff --git a/public/i18n/zh_CN.json b/public/i18n/zh_CN.json index ba6bc65..65ecd28 100644 --- a/public/i18n/zh_CN.json +++ b/public/i18n/zh_CN.json @@ -16,5 +16,8 @@ "KeywordFilterNotebooksMatchSubdocuments": "关键字过滤笔记本-匹配子文档", "MiddleClickToggleDocTree":"中键展开/折叠笔记本或文档", "Image":"图片", - "MiddleClickResizeImageWidth":"中键图片缩放宽度" + "MiddleClickResizeImageWidth":"中键图片缩放宽度", + "MiddleClickResizeImageWidthDesc":"需要加上单位,例:200px。px:固定宽度;vw:视窗的比例。", + "ZoomWidthLoadedImagesCurrentDocument":"缩放当前文档已加载图片的宽度", + "ShowTopBatchZoomBtn":"顶部显示批量缩放图片按钮" } \ No newline at end of file diff --git a/src/components/img/ImageScalingService.ts b/src/components/img/ImageScalingService.ts index 9eded31..ce09cc7 100644 --- a/src/components/img/ImageScalingService.ts +++ b/src/components/img/ImageScalingService.ts @@ -1,10 +1,11 @@ import { EnvConfig } from "@/config/EnvConfig"; import { SettingService } from "@/service/SettingService"; -import { hasClosestByTagName } from "@/utils/html-util"; +import { hasClosestByTagName, isPixelOrViewportWidth } from "@/utils/html-util"; import Instance from "@/utils/Instance"; -import { getActiveTab } from "@/utils/siyuan-util"; -import { containsAllKeywords, splitKeywordStringToArray } from "@/utils/string-util"; +import { confirmDialog, getActiveTab } from "@/utils/siyuan-util"; +import { showMessage } from "siyuan"; +import { isStrBlank } from "@/utils/string-util"; export class ImageScalingService { @@ -13,27 +14,47 @@ export class ImageScalingService { return Instance.get(ImageScalingService); } + private topBarElement: HTMLElement; + public init() { this.initEventListener(); - // todo - - // EnvConfig.ins.plugin.addTopBar({ - // icon: CUSTOM_ICON_MAP.BacklinkPanelFilter.id, - // title: "缩放当前文档所有加载图片的宽度", - // position: "right", - // callback: () => { - // let currentDocument: HTMLDivElement = getActiveTab(); - // if (!currentDocument) { - // return; - // } - // const docTitleElement = currentDocument.querySelector(".protyle-title"); - // let docTitle = currentDocument.querySelector("div.protyle-title__input").textContent; - // let docId = docTitleElement.getAttribute("data-node-id"); - // TabService.ins.openBacklinkTab(docTitle, docId, null); - // } - // }); + let showTopBar = SettingService.ins.SettingConfig.topBarShowImageZoomBtn; + if (showTopBar) { + this.topBarElement = EnvConfig.ins.plugin.addTopBar({ + icon: "iconContract", + title: EnvConfig.ins.plugin.i18n.ZoomWidthLoadedImagesCurrentDocument, + position: "right", + callback: () => { + let currentDocument: HTMLDivElement = getActiveTab(); + if (!currentDocument) { + return; + } + + const widthInput = document.createElement("input"); + widthInput.className = "b3-text-field fn__block"; + + confirmDialog(EnvConfig.ins.plugin.i18n.ZoomWidthLoadedImagesCurrentDocument + , widthInput, (): boolean => { + return batchUpdateCurDocImageWidth(widthInput.value); + }); + } + }); + } else { + if (this.topBarElement) { + let topBarElements = EnvConfig.ins.plugin.topBarIcons; + for (let i = 0; i < topBarElements.length; i++) { + if (topBarElements[i].id === this.topBarElement.id) { + this.topBarElement.remove(); + + topBarElements.splice(i, 1); // 删除当前元素 + i--; // 调整索引以避免跳过下一个元素 + break; + } + } + } + } } public destroy() { @@ -41,56 +62,75 @@ export class ImageScalingService { } public initEventListener() { - document.body.addEventListener('mousedown', function (event: MouseEvent) { - if (event.button != 1) return; - // 图片宽度,目前思源只支持 px,vw。 - let imageWidthValue = SettingService.ins.SettingConfig.imageMiddleClickResizeWidth;; - if (!imageWidthValue) { - return; - } - let clickElement = event.target as HTMLElement; - // 检查点击的元素是否是图片 - if (!clickElement.matches('.protyle-wysiwyg div[data-node-id] span.img[data-type="img"] img')) { - return; - } - let imageSpanElement = clickElement.parentElement.parentElement; - // 如果图片在表格中,也不进行设置 - if (imageSpanElement.parentElement.tagName.toLowerCase() === "td") { - // return; - } - let layoutTabContainerElement = clickElement.parentElement; + let classFlag = "misuzu2027__image_zoom_mousedown"; + if (document.body.matches(`.${classFlag}`)) { + return; + } + document.body.classList.add("misuzu2027__image_zoom_mousedown") + document.body.addEventListener('mousedown', handleImageZoomMousedown); + } - // // 循环查找当前点击的文档元素 - while (layoutTabContainerElement) { - if (layoutTabContainerElement.tagName.toLowerCase() === 'div' - && layoutTabContainerElement.classList.contains('protyle') - && layoutTabContainerElement.classList.contains('fn__flex-1')) { - break; - } - layoutTabContainerElement = layoutTabContainerElement.parentElement; - } - // 默认只读模式 - let isReadonly = true; - if (layoutTabContainerElement) { - let readonlyButton = layoutTabContainerElement.querySelector('[data-type="readonly"]'); - if (readonlyButton) { - isReadonly = readonlyButton.querySelector("use").getAttribute("xlink:href") !== "#iconUnlock"; - } - } - if (isReadonly) { - console.log("点击自动设置图片宽度失败!当前是只读模式。图片地址:" + clickElement.getAttribute("src")); - return; - } - zoomImageWith(event, imageWidthValue) - }); +} + +function handleImageZoomMousedown(event: MouseEvent) { + + if (event.button != 1) return; + let imageWidthValue = SettingService.ins.SettingConfig.imageMiddleClickResizeWidth; + if (isStrBlank(imageWidthValue)) { + // showImageFaileMessage("宽度为空"); + return; + } + let clickElement = event.target as HTMLElement; + // 检查点击的元素是否是图片 + if (!clickElement.matches('.protyle-wysiwyg div[data-node-id] span.img[data-type="img"] img')) { + return; + } + let imageSpanElement = clickElement.parentElement.parentElement; + // 如果图片在表格中,也不进行设置 + if (imageSpanElement.parentElement.tagName.toLowerCase() === "td") { + // return; + } + let layoutTabContainerElement = clickElement.parentElement; + + // 图片宽度,目前思源只支持 px,vw。 + if (!isPixelOrViewportWidth(imageWidthValue)) { + showImageFaileMessage("宽度格式不正确"); + return; + } + + // // 循环查找当前点击的文档元素 + while (layoutTabContainerElement) { + if (layoutTabContainerElement.tagName.toLowerCase() === 'div' + && layoutTabContainerElement.classList.contains('protyle') + && layoutTabContainerElement.classList.contains('fn__flex-1')) { + break; + } + layoutTabContainerElement = layoutTabContainerElement.parentElement; + } + // 默认只读模式 + let isReadonly = true; + if (layoutTabContainerElement) { + let readonlyButton = layoutTabContainerElement.querySelector('[data-type="readonly"]'); + if (readonlyButton) { + isReadonly = readonlyButton.querySelector("use").getAttribute("xlink:href") !== "#iconUnlock"; + } + } + if (isReadonly) { + console.log("点击自动设置图片宽度失败!当前是只读模式。图片地址:" + clickElement.getAttribute("src")); + event.preventDefault(); + return; + } + let flag = zoomImageWith(event.target as HTMLElement, imageWidthValue); + if (flag) { + event.preventDefault(); } } -function zoomImageWith(event: MouseEvent, width: string) { - let target = event.target as HTMLElement; + +function zoomImageWith(target: HTMLElement, width: string): boolean { // 创建一个 mousedown 事件 const mouseDownEvent = new MouseEvent('mousedown', { bubbles: true, // 事件是否冒泡 @@ -113,10 +153,9 @@ function zoomImageWith(event: MouseEvent, width: string) { return; } dragElement.dispatchEvent(mouseDownEvent); - event.preventDefault(); imgElement.style.width = width; - dragElement.dispatchEvent(mouseUpEvent); + return true; } function zoomImageWithMiddleClick() { @@ -227,4 +266,51 @@ function zoomImageWithMiddleClick() { }); +} + + +function batchUpdateCurDocImageWidth(width: string): boolean { + if (isStrBlank(width)) { + showImageFaileMessage("宽度为空"); + return; + } + if (!isPixelOrViewportWidth(width)) { + showImageFaileMessage("宽度格式不正确"); + return; + } + let currentDocument: HTMLDivElement = getActiveTab(); + if (!currentDocument) { + showImageFaileMessage("没找到打开的文档"); + return; + } + // 默认只读模式 + let isReadonly = true; + let readonlyButton = currentDocument.querySelector('[data-type="readonly"]'); + if (readonlyButton) { + isReadonly = readonlyButton.querySelector("use").getAttribute("xlink:href") !== "#iconUnlock"; + } + if (isReadonly) { + showImageFaileMessage("当前文档为只读模式"); + return; + } + + let imageElementList = currentDocument.querySelectorAll(`.protyle-wysiwyg div[data-node-id] span.img[data-type="img"] img`); + for (let element of imageElementList) { + + let hasTable = hasClosestByTagName(element as HTMLElement, "TD"); + // 如果图片在表格中,也不进行设置 + if (hasTable) { + continue; + } + zoomImageWith(element as HTMLElement, width); + } + return true; +} + +function showImageFaileMessage(reason: string) { + showMessage( + `批量设置当前文档图片宽度失败 : ${reason}!`, + 4000, + "info", + ); } \ No newline at end of file diff --git a/src/components/img/input-width-dialog.svelte b/src/components/img/input-width-dialog.svelte new file mode 100644 index 0000000..d7cc11e --- /dev/null +++ b/src/components/img/input-width-dialog.svelte @@ -0,0 +1,60 @@ + diff --git a/src/components/setting/setting-util.ts b/src/components/setting/setting-util.ts index bb6b1d1..6fbc823 100644 --- a/src/components/setting/setting-util.ts +++ b/src/components/setting/setting-util.ts @@ -8,7 +8,7 @@ import SettingPageSvelte from "@/components/setting/setting-page.svelte" export function openSettingsDialog() { let isMobile = EnvConfig.ins.isMobile; // 生成Dialog内容 - const dialogId = "backlink-panel-setting-" + Date.now(); + const dialogId = "misuzu2027-setting-" + Date.now(); // 创建dialog const settingDialog = new Dialog({ title: EnvConfig.ins.i18n.PluginSettings, diff --git a/src/index.ts b/src/index.ts index ea4b79c..bb51ee0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,15 +34,15 @@ export default class PluginSample extends Plugin { } } - this.eventBus.on('switch-protyle', (e: any) => { - EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; - }) - this.eventBus.on('loaded-protyle-static', (e: any) => { - // console.log("index loaded-protyle-static ") - if (EnvConfig.ins.isMobile && !EnvConfig.ins.lastViewedDocId) { - EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; - } - }) + // this.eventBus.on('switch-protyle', (e: any) => { + // EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; + // }) + // this.eventBus.on('loaded-protyle-static', (e: any) => { + // // console.log("index loaded-protyle-static ") + // if (EnvConfig.ins.isMobile && !EnvConfig.ins.lastViewedDocId) { + // EnvConfig.ins.lastViewedDocId = e.detail.protyle.block.rootID; + // } + // }) } diff --git a/src/models/icon-constant.ts b/src/models/icon-constant.ts index 91f3112..fe2dd40 100644 --- a/src/models/icon-constant.ts +++ b/src/models/icon-constant.ts @@ -1,13 +1,10 @@ export const CUSTOM_ICON_MAP = { - BacklinkPanelFilter: { - id: "BacklinkPanelFilter", - source: ` - - - - + MisuzuImageZoom: { + id: "MisuzuImageZoom", + source: ` + ` }, LiElementExpand: { diff --git a/src/models/setting-constant.ts b/src/models/setting-constant.ts index ec0a289..6aa18d6 100644 --- a/src/models/setting-constant.ts +++ b/src/models/setting-constant.ts @@ -17,6 +17,7 @@ export function getDefaultSettingConfig() { defaultConfig.fileTreeMiddleClickToggle = false; defaultConfig.imageMiddleClickResizeWidth = null; + defaultConfig.topBarShowImageZoomBtn = false; @@ -69,7 +70,8 @@ export function getSettingTabArray(): TabProperty[] { }), new TabProperty({ key: "image-setting", name: i18n.Image, iconKey: "iconPlugin", props: [ - new ItemProperty({ key: "imageMiddleClickResizeWidth", type: "number", name: i18n.MiddleClickResizeImageWidth, description: "需要加上单位,例:200px。px:固定宽度;vw:视窗的比例。", tips: "" }), + new ItemProperty({ key: "imageMiddleClickResizeWidth", type: "text", name: i18n.MiddleClickResizeImageWidth, description: i18n.MiddleClickResizeImageWidthDesc, tips: "" }), + new ItemProperty({ key: "topBarShowImageZoomBtn", type: "switch", name: i18n.ShowTopBatchZoomBtn, description: "", tips: "" }), // new ItemProperty({ key: "documentBottomDisplay", type: "switch", name: "文档底部显示反链面板", description: "", tips: "" }), // new ItemProperty({ key: "topBarDisplay", type: "switch", name: "桌面端顶栏创建反链页签 Icon", description: "", tips: "" }), diff --git a/src/models/setting-model.ts b/src/models/setting-model.ts index cdd62e8..8f7a78c 100644 --- a/src/models/setting-model.ts +++ b/src/models/setting-model.ts @@ -20,6 +20,8 @@ export class SettingConfig { // 图片 imageMiddleClickResizeWidth: string; + topBarShowImageZoomBtn: boolean; + } diff --git a/src/service/SettingService.ts b/src/service/SettingService.ts index 8db4ac9..d8fc512 100644 --- a/src/service/SettingService.ts +++ b/src/service/SettingService.ts @@ -7,6 +7,7 @@ import { mergeObjects } from "@/utils/object-util"; import { CssService } from "./CssService"; import { CodeBlockService } from "@/components/code-block/CodeBlockService"; import { FileTreeService } from "@/components/filetree/FileTreeService"; +import { ImageScalingService } from "@/components/img/ImageScalingService"; const SettingFileName = 'misuzu2027-setting.json'; @@ -68,13 +69,14 @@ export class SettingService { this._settingConfig = { ...settingConfigParam }; this.initFunction(); plugin.saveData(SettingFileName, paramJson); - + } - public initFunction(){ + public initFunction() { CssService.ins.init(); CodeBlockService.ins.init(); FileTreeService.ins.init(); + ImageScalingService.ins.init(); } } diff --git a/src/utils/html-util.ts b/src/utils/html-util.ts index 6940355..bd5e038 100644 --- a/src/utils/html-util.ts +++ b/src/utils/html-util.ts @@ -332,4 +332,9 @@ export function hasClosestById(element: HTMLElement | null, id: string): HTMLEle } return false; -} \ No newline at end of file +} + +export function isPixelOrViewportWidth(str: string): boolean { + const regex = /^\d+(?:\.\d+)?(px|vw)$/; + return regex.test(str); +} diff --git a/src/utils/siyuan-util.ts b/src/utils/siyuan-util.ts index 5a57381..05cd8d9 100644 --- a/src/utils/siyuan-util.ts +++ b/src/utils/siyuan-util.ts @@ -1,3 +1,5 @@ +import { Dialog, getFrontend } from "siyuan"; + export function getActiveTab(): HTMLDivElement { let tab = document.querySelector("div.layout__wnd--active ul.layout-tab-bar>li.item--focus"); let dataId: string = tab?.getAttribute("data-id"); @@ -8,4 +10,72 @@ export function getActiveTab(): HTMLDivElement { `.layout-tab-container.fn__flex-1>div.protyle[data-id="${dataId}"]` ) as HTMLDivElement; return activeTab; -} \ No newline at end of file +} + +const frontEnd = getFrontend(); +export const isMobile = () => (frontEnd === "mobile" || frontEnd === "browser-mobile"); + +export const confirmDialog = (title: string, + content: string | HTMLElement, + confirm?: (ele?: HTMLElement) => boolean, + cancel?: (ele?: HTMLElement) => void, + width?: string, + height?: string) => { + + const dialog = new Dialog({ + title, + content: ` +
+
+
+
+ +
+ +
`, + width: width || (isMobile() ? "92vw" : "520px"), + height: height + }); + // confirmDialogConfirmBtn + const target: HTMLElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword"); + if (typeof content === "string") { + target.innerHTML = content; + } else { + target.appendChild(content); + } + + + + const btnsElement = dialog.element.querySelectorAll(".b3-button"); + + target.addEventListener('keydown', (e: KeyboardEvent) => { + if (e.key === 'Enter' && !e.ctrlKey) { + let confirmBtn = btnsElement[1] as HTMLElement; + confirmBtn.click(); + e.stopPropagation(); + } + }); + let firstInput = dialog.element.querySelector("input") || dialog.element.querySelector("textarea"); + if (firstInput) { + // firstInput.select(); + firstInput.focus(); + } + + btnsElement[0].addEventListener("click", () => { + if (cancel) { + cancel(target); + } + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + if (confirm) { + let flag = confirm(target); + if (flag) { + dialog.destroy(); + } + } else { + dialog.destroy(); + } + }); +}; +