diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bd6c4e..49834e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,12 +17,12 @@ jobs: - name: Install Node.js uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 20 registry-url: "https://registry.npmjs.org" # Install pnpm - name: Install pnpm - uses: pnpm/action-setup@v2 + uses: pnpm/action-setup@v4 id: pnpm-install with: version: 8 @@ -59,4 +59,4 @@ jobs: artifactErrorsFailBuild: true artifacts: "package.zip" token: ${{ secrets.GITHUB_TOKEN }} - prerelease: true + prerelease: false diff --git a/CHANGELOG.md b/CHANGELOG.md index ce36ec8..af04807 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## 0.3.5 2024-03 +## v0.3.5 2024-04-30 + +* [Add `direction` to plugin method `Setting.addItem`](https://github.com/siyuan-note/siyuan/issues/11183) + ## 0.3.4 2024-02-20 diff --git a/README.md b/README.md index 09c8cdd..648dcb7 100644 --- a/README.md +++ b/README.md @@ -11,22 +11,38 @@ 2. Use symbolic linking instead of putting the project into the plugins directory program development 3. Built-in support for the svelte framework - > If don't want svelte, turn to this template: [frostime/plugin-sample-vite](https://github.com/frostime/plugin-sample-vite) + > **If don't want svelte, turn to this template**: [frostime/plugin-sample-vite](https://github.com/frostime/plugin-sample-vite) + > + > **We also provide with a vite+solidjs template**: [frostime/plugin-sample-vite-solidjs](https://github.com/frostime/plugin-sample-vite-solidjs) 4. Provides a github action template to automatically generate package.zip and upload to new release +> [!TIP] +> You can also use our maintained [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli) command-line tool to directly build plugins in your local terminal. +> +> Additionally, for the `make-link` related commands mentioned in this plugin, all future updates will be made in [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli). +> +> The built-in `make-link` scripts may also be removed in a future version, in favor of using the `siyuan-plugin-cli` tool, aiming to simplify the workload of maintaining multiple plugin templates. + + ## Get started -1. Make a copy of this repo as a template with the `Use this template` button, please note that the repo name must be the same as the plugin name, the default branch must be `main` +1. Use the Use this template button to make a copy of this repo as a template. Note that the repository name should match the plugin name, and the default branch must be `main`. +2. Clone your repository to the local development folder. + * Note: Unlike `plugin-sample`, this example does not recommend directly downloading the code to `{workspace}/data/plugins/`. +3. Install [NodeJS](https://nodejs.org/en/download) and [pnpm](https://pnpm.io/installation), then run `pnpm i` in the development folder to install the required dependencies. +4. Run the `pnpm run make-link` command to create a symbolic link (Windows developers, please refer to the "make-link on Windows" section below). +5. Execute `pnpm run dev` for real-time compilation. +6. Open the marketplace in SiYuan and enable the plugin in the download tab. -2. Clone your repo to a local development folder at any place - - Notice: we **don't recommand** you to place the folder under your `{workspace}/data/plugins/` folder. +### Setting the Target Directory for the make-link Command -3. Install NodeJS and pnpm, then run pnpm i in the command line under your repo folder -4. **Auto create development symbolic links** - - Make sure that SiYuan is running - - Run `pnpm run make-link`, the script will detect all the siyuan workspace, please select the targe workspace and the script will automatically create the symbolic link under the `{workspace}/data/plugins/` folder +The `make-link` command creates a symbolic link that binds your `dev` directory to the SiYuan plugin directory. You can configure the target SiYuan workspace and create the symbolic link in three ways: + +1. **Select Workspace** + - Open SiYuan, ensure the SiYuan kernel is running. + - Run `pnpm run make-link`, the script will automatically detect all SiYuan workspaces, please manually enter the number to select the workspace. ```bash >>> pnpm run make-link > plugin-sample-vite-svelte@0.0.3 make-link H:\SrcCode\开源项目\plugin-sample-vite-svelte @@ -40,22 +56,24 @@ Got target directory: H:\Media\SiYuan/data/plugins Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte ``` -4. **Manually create development symbolic links** - - Open `./scripts/make_dev_link.js` file, set `targetDir` to your SiYuan plugin directory `/data/plugins` - - Run `pnpm run make-link`, succeed if following message is shown: - ```bash - >>> pnpm run make-link - > plugin-sample-vite-svelte@0.0.1 make-link H:\SrcCode\plugin-sample-vite-svelte - > node ./scripts/make_dev_link.js +2. **Manually Configure Target Directory** + - Open the `./scripts/make_dev_link.js` file, change `targetDir` to the SiYuan plugin directory `/data/plugins`. + - Run the `pnpm run make-link` command. If you see a message similar to the one below, it indicates successful creation: - Done! Created symlink H:/SiYuanDevSpace/data/plugins/plugin-sample-vite-svelte - ``` -5. **Create development symbolic links by using environment variable** - - You can set environment variable `SIYUAN_PLUGIN_DIR` as `/data/plugins` -6. Execute pnpm run dev for real-time compilation -7. Open SiYuan marketplace and enable plugin in downloaded tab +3. **Set Environment Variable to Create Symbolic Link** + - Set the system environment variable `SIYUAN_PLUGIN_DIR` to the path `workspace/data/plugins`. -> Notice: as the `make-link` script rely on the `fetch` function, please **ensure that at least version v18 of nodejs is installed** if you want to use make-link script. +### make-link on Windows + +Due to SiYuan upgrading to Go 1.23, the old version of junction links cannot be recognized normally on Windows, so it has been changed to create `dir` symbolic links. + +> https://github.com/siyuan-note/siyuan/issues/12399 + +However, creating directory symbolic links on Windows using NodeJs may require administrator privileges. You have the following options: + +1. Run `pnpm run make-link` in a command line with administrator privileges. +2. Configure Windows settings, enable developer mode in [System Settings - Update & Security - Developer Mode] then run `pnpm run make-link`. +3. Run `pnpm run make-link-win`, this command will use a PowerShell script to request administrator privileges, requiring the system to enable PowerShell script execution permissions. ## I18n diff --git a/README_zh_CN.md b/README_zh_CN.md index ca35f7c..0ac9fca 100644 --- a/README_zh_CN.md +++ b/README_zh_CN.md @@ -10,17 +10,34 @@ 2. 使用符号链接、而不是把项目放到插件目录下的模式进行开发 3. 内置对 svelte 框架的支持 - > 如果不想要 svelte,请移步 [frostime/plugin-sample-vite](https://github.com/frostime/plugin-sample-vite) + > **如果不想要 svelte,请移步这个模板:** [frostime/plugin-sample-vite](https://github.com/frostime/plugin-sample-vite) + > + > **这里还提供了一个 vite+solidjs 的模板**: [frostime/plugin-sample-vite-solidjs](https://github.com/frostime/plugin-sample-vite-solidjs) 4. 提供一个github action 模板,能自动生成package.zip并上传到新版本中 ## 开始 -1. 通过 Use this template 按钮将该库文件复制到你自己的库中,请注意库名必须和插件名称一致,默认分支必须为 `main` +1. 通过 Use this template 按钮将该库文件复制到你自己的库中,请注意库名和插件名称一致,默认分支必须为 `main` 2. 将你的库克隆到本地开发文件夹中 * 注意: 同 `plugin-sample` 不同, 本样例并不推荐直接把代码下载到 `{workspace}/data/plugins/` 3. 安装 [NodeJS](https://nodejs.org/en/download) 和 [pnpm](https://pnpm.io/installation),然后在开发文件夹下执行 `pnpm i` 安装所需要的依赖 -3. **自动创建符号链接** +4. 运行 `pnpm run make-link` 命令创建符号链接 (Windows 下的开发者请参阅下方「Windows 下的 make-link」小节) +5. 执行 `pnpm run dev` 进行实时编译 +6. 在思源中打开集市并在下载选项卡中启用插件 + +> [!TIP] +> 你也可以使用我们维护的 [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli) 命令行工具,在本地终端中直接构建插件。 +> +> 此外,对于本插件以下提及到的 `make-link` 相关的命令,后续所有更新将在 [siyuan-plugin-cli](https://www.npmjs.com/package/siyuan-plugin-cli) 中进行。 +> +> 模板内置的 `make-link` 脚本也可能会在未来某个版本中移除,转而使用 `siyuan-plugin-cli` 工具,意在简化同时维护多个插件模板的工作量。 + +### 设置 make-link 命令的目标目录 + +make-link 命令会创建符号链接将你的 `dev` 目录绑定到思源的插件目录下。你可以有三种方式来配置目标的思源工作空间并创建符号链接: + +1. **选择工作空间** - 打开思源笔记, 确保思源内核正在运行 - 运行 `pnpm run make-link`, 脚本会自动检测所有思源的工作空间, 请在命令行中手动输入序号以选择工作空间 ```bash @@ -36,23 +53,26 @@ Got target directory: H:\Media\SiYuan/data/plugins Done! Created symlink H:\Media\SiYuan/data/plugins/plugin-sample-vite-svelte ``` -4. **手动创建符号链接** +2. **手动配置目标目录** - 打开 `./scripts/make_dev_link.js` 文件,更改 `targetDir` 为思源的插件目录 `/data/plugins` - 运行 `pnpm run make-link` 命令, 如果看到类似以下的消息,说明创建成功: - ```bash - ❯❯❯ pnpm run make-link - > plugin-sample-vite-svelte@0.0.1 make-link H:\SrcCode\plugin-sample-vite-svelte - > node ./scripts/make_dev_link.js - Done! Created symlink H:/SiYuanDevSpace/data/plugins/plugin-sample-vite-svelte - ``` -5. **设置环境变量创建符号链接** - - 你也可以设置系统的环境变量 `SIYUAN_PLUGIN_DIR` 为 `/data/plugins` 的路径 -6. 执行 `pnpm run dev` 进行实时编译 -7. 在思源中打开集市并在下载选项卡中启用插件 +3. **设置环境变量创建符号链接** + - 设置系统的环境变量 `SIYUAN_PLUGIN_DIR` 为 `工作空间/data/plugins` 的路径 -> 注意由于使用的 make-link 脚本依赖于 `fetch`,所以如果想要使用 make-link **请保证至少安装 v18 版本的 nodejs** +### Windows 下的 make-link + +由于思源升级了 Go 1.23,旧版创建的 junction link 在 windows 下无法被正常识别,故而改为创建 `dir` 符号链接。 + +> https://github.com/siyuan-note/siyuan/issues/12399 + + +不过 Windows 下使用 NodeJs 创建目录符号链接可能需要管理员权限,你可以有如下几种选择: + +1. 在具有管理员权限的命令行中运行 `pnpm run make-link` +2. 配置 Windows 设置,在 [系统设置-更新与安全-开发者模式] 中启用开发者模式,然后再运行 `pnpm run make-link` +3. 运行 `pnpm run make-link-win`,该命令会使用一个 powershell 脚本来寻求管理员权限,需要在系统中开启 PowerShell 脚本执行权限 ## 国际化 diff --git a/package.json b/package.json index 44cdd78..2b7cdec 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "plugin-sample-vite-svelte", - "version": "0.3.5", + "version": "0.3.6", "type": "module", "description": "This is a sample plugin based on vite and svelte for Siyuan (https://b3log.org/siyuan)", "repository": "", @@ -8,27 +8,30 @@ "author": "frostime", "license": "MIT", "scripts": { - "make-link": "node --no-warnings ./scripts/make_dev_link.js", - "dev": "vite build --watch", - "build": "vite build", + "dev": "cross-env NODE_ENV=development VITE_SOURCEMAP=inline vite build --watch", + "build": "cross-env NODE_ENV=production vite build", + "make-link": "node --no-warnings ./scripts/make_dev_link.js", + "make-link-win": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File ./scripts/elevate.ps1 -scriptPath ./scripts/make_dev_link.js", + "update-version": "node --no-warnings ./scripts/update_version.js", "make-install": "vite build && node --no-warnings ./scripts/make_install.js" }, "devDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^3.1.0", "@tsconfig/svelte": "^4.0.1", "@types/node": "^20.3.0", + "cross-env": "^7.0.3", "fast-glob": "^3.2.12", - "glob": "^7.2.3", + "glob": "^10.0.0", "js-yaml": "^4.1.0", "minimist": "^1.2.8", "rollup-plugin-livereload": "^2.0.5", "sass": "^1.63.3", - "siyuan": "0.9.8", - "svelte": "^4.2.0", + "siyuan": "1.0.4", + "svelte": "^4.2.19", "ts-node": "^10.9.1", "typescript": "^5.1.3", - "vite": "^5.0.0", + "vite": "^5.2.9", "vite-plugin-static-copy": "^1.0.2", "vite-plugin-zip-pack": "^1.0.5" } -} +} \ No newline at end of file diff --git a/plugin.json b/plugin.json index 071b2a3..fd63a3c 100644 --- a/plugin.json +++ b/plugin.json @@ -2,14 +2,16 @@ "name": "plugin-sample-vite-svelte", "author": "frostime", "url": "https://github.com/siyuan-note/plugin-sample-vite-svelte", - "version": "0.3.5", + "version": "0.3.6", "minAppVersion": "3.0.12", "backends": [ "windows", "linux", "darwin", "ios", - "android" + "android", + "harmony", + "docker" ], "frontends": [ "desktop", @@ -32,10 +34,12 @@ }, "funding": { "custom": [ - "https://afdian.net/a/frostime" + "" ] }, "keywords": [ - "plugin", "sample", "插件样例" + "plugin", + "sample", + "插件样例" ] } diff --git a/public/i18n/README.md b/public/i18n/README.md new file mode 100644 index 0000000..af8de98 --- /dev/null +++ b/public/i18n/README.md @@ -0,0 +1,12 @@ +思源支持的 i18n 文件范围,可以在控制台 `siyuan.config.langs` 中查看。以下是目前(2024-10-24)支持的语言方案: + +The range of i18n files supported by SiYuan can be viewed in the console under `siyuan.config.langs`. Below are the language schemes currently supported as of now (October 24, 2024) : + +```js +>>> siyuan.config.langs.map( lang => lang.name) +['de_DE', 'en_US', 'es_ES', 'fr_FR', 'he_IL', 'it_IT', 'ja_JP', 'pl_PL', 'ru_RU', 'zh_CHT', 'zh_CN'] +``` + +在插件开发中,默认使用 JSON 格式作为国际化(i18n)的载体文件。如果您更喜欢使用 YAML 语法,可以将 JSON 文件替换为 YAML 文件(例如 `en_US.yaml`),并在其中编写 i18n 文本。本模板提供了相关的 Vite 插件,可以在编译时自动将 YAML 文件转换为 JSON 文件(请参见 `/yaml-plugin.js`)。本 MD 文件 和 YAML 文件会在 `npm run build` 时自动从 `dist` 目录下删除,仅保留必要的 JSON 文件共插件系统使用。 + +In plugin development, JSON format is used by default as the carrier file for internationalization (i18n). If you prefer to use YAML syntax, you can replace the JSON file with a YAML file (e.g., `en_US.yaml`) and write the i18n text within it. This template provides a related Vite plugin that can automatically convert YAML files to JSON files during the compilation process (see `/yaml-plugin.js`). This markdown file and YAML files will be automatically removed from the `dist` directory during `npm run build`, leaving only the necessary JSON files for plugin system to use. diff --git a/public/i18n/zh_CN.json b/public/i18n/zh_CN.json new file mode 100644 index 0000000..6600f6a --- /dev/null +++ b/public/i18n/zh_CN.json @@ -0,0 +1,20 @@ +{ + "addTopBarIcon": "使用插件添加一个顶栏按钮", + "cancel": "取消", + "save": "保存", + "byeMenu": "再见,菜单!", + "helloPlugin": "你好,插件!", + "byePlugin": "再见,插件!", + "showDialog": "弹出一个对话框", + "removedData": "数据已删除", + "confirmRemove": "确认删除 ${name} 中的数据?", + "insertEmoji": "插入表情", + "removeSpace": "移除空格", + "getTab": "在日志中打印出已打开的所有自定义页签", + "name": "思源", + "hello": { + "makesure": "使用这个模板之前,请阅读官方教程, 确保自己已经理解了插件的基本开发流程。" + }, + "hintTitle": "关于", + "hintDesc": "🔗 plugin-sample-vite-svelte
💻 @frostime
💻 @88250
💻 @zxkmm" +} \ No newline at end of file diff --git a/public/i18n/zh_CN.yaml b/public/i18n/zh_CN.yaml deleted file mode 100644 index 16099a3..0000000 --- a/public/i18n/zh_CN.yaml +++ /dev/null @@ -1,21 +0,0 @@ ---- -addTopBarIcon: 使用插件添加一个顶栏按钮 -cancel: 取消 -save: 保存 -byeMenu: 再见,菜单! -helloPlugin: 你好,插件! -byePlugin: 再见,插件! -showDialog: 弹出一个对话框 -removedData: 数据已删除 -confirmRemove: 确认删除 ${name} 中的数据? -insertEmoji: 插入表情 -removeSpace: 移除空格 -getTab: 在日志中打印出已打开的所有自定义页签 -name: 思源 -hello: - makesure: 使用这个模板之前,请阅读官方教程, - 确保自己已经理解了插件的基本开发流程。 -hintTitle: 关于 -hintDesc: "\U0001F517 - plugin-sample-vite-svelte
\U0001F4BB @frostime
\U0001F4BB @88250
\U0001F4BB - @zxkmm" diff --git a/scripts/elevate.ps1 b/scripts/elevate.ps1 new file mode 100644 index 0000000..151b8ba --- /dev/null +++ b/scripts/elevate.ps1 @@ -0,0 +1,24 @@ +# Copyright (c) 2024 by frostime. All Rights Reserved. +# @Author : frostime +# @Date : 2024-09-06 19:15:53 +# @FilePath : /scripts/elevate.ps1 +# @LastEditTime : 2024-09-06 19:39:13 +# @Description : Force to elevate the script to admin privilege. + +param ( + [string]$scriptPath +) + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$projectDir = Split-Path -Parent $scriptDir + +if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + $args = "-NoProfile -ExecutionPolicy Bypass -File `"" + $MyInvocation.MyCommand.Path + "`" -scriptPath `"" + $scriptPath + "`"" + Start-Process powershell.exe -Verb RunAs -ArgumentList $args -WorkingDirectory $projectDir + exit +} + +Set-Location -Path $projectDir +& node $scriptPath + +pause diff --git a/scripts/make_dev_link.js b/scripts/make_dev_link.js index 845d298..2be3d2b 100644 --- a/scripts/make_dev_link.js +++ b/scripts/make_dev_link.js @@ -1,106 +1,29 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2023-07-15 15:31:31 + * @FilePath : /scripts/make_dev_link.js + * @LastEditTime : 2024-09-06 18:13:53 + * @Description : + */ +// make_dev_link.js import fs from 'fs'; -import http from 'node:http'; -import readline from 'node:readline'; +import { log, error, getSiYuanDir, chooseTarget, getThisPluginName, makeSymbolicLink } from './utils.js'; - -//************************************ Write you dir here ************************************ - -//Please write the "workspace/data/plugins" directory here -//请在这里填写你的 "workspace/data/plugins" 目录 let targetDir = ''; -//Like this -// let targetDir = `H:\\SiYuanDevSpace\\data\\plugins`; -//******************************************************************************************** - -const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); -const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); - -let POST_HEADER = { - // "Authorization": `Token ${token}`, - "Content-Type": "application/json", -} - -async function myfetch(url, options) { - //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 - return new Promise((resolve, reject) => { - let req = http.request(url, options, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - resolve({ - ok: true, - status: res.statusCode, - json: () => JSON.parse(data) - }); - }); - }); - req.on('error', (e) => { - reject(e); - }); - req.end(); - }); -} - -async function getSiYuanDir() { - let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; - let conf = {}; - try { - let response = await myfetch(url, { - method: 'POST', - headers: POST_HEADER - }); - if (response.ok) { - conf = await response.json(); - } else { - error(`\tHTTP-Error: ${response.status}`); - return null; - } - } catch (e) { - error(`\tError: ${e}`); - error("\tPlease make sure SiYuan is running!!!"); - return null; - } - return conf.data; -} - -async function chooseTarget(workspaces) { - let count = workspaces.length; - log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) - for (let i = 0; i < workspaces.length; i++) { - log(`\t[${i}] ${workspaces[i].path}`); - } - - if (count == 1) { - return `${workspaces[0].path}/data/plugins`; - } else { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - let index = await new Promise((resolve, reject) => { - rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => { - resolve(answer); - }); - }); - rl.close(); - return `${workspaces[index].path}/data/plugins`; - } -} - -log('>>> Try to visit constant "targetDir" in make_dev_link.js...') +/** + * 1. Get the parent directory to install the plugin + */ +log('>>> Try to visit constant "targetDir" in make_dev_link.js...'); if (targetDir === '') { - log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....') + log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); let res = await getSiYuanDir(); - - if (res === null || res === undefined || res.length === 0) { - log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); - // console.log(process.env) + if (!res || res.length === 0) { + log('>>> Can not get SiYuan directory automatically, try to visit environment variable "SIYUAN_PLUGIN_DIR"....'); let env = process.env?.SIYUAN_PLUGIN_DIR; - if (env !== undefined && env !== null && env !== '') { + if (env) { targetDir = env; log(`\tGot target directory from environment variable "SIYUAN_PLUGIN_DIR": ${targetDir}`); } else { @@ -111,76 +34,33 @@ if (targetDir === '') { targetDir = await chooseTarget(res); } - log(`>>> Successfully got target directory: ${targetDir}`); } - -//Check if (!fs.existsSync(targetDir)) { - error(`Failed! plugin directory not exists: "${targetDir}"`); - error(`Please set the plugin directory in scripts/make_dev_link.js`); + error(`Failed! Plugin directory not exists: "${targetDir}"`); + error('Please set the plugin directory in scripts/make_dev_link.js'); process.exit(1); } - -//check if plugin.json exists -if (!fs.existsSync('./plugin.json')) { - //change dir to parent - process.chdir('../'); - if (!fs.existsSync('./plugin.json')) { - error('Failed! plugin.json not found'); - process.exit(1); - } -} - -//load plugin.json -const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); -const name = plugin?.name; -if (!name || name === '') { - error('Failed! Please set plugin name in plugin.json'); - process.exit(1); -} - -//dev directory +/** + * 2. The dev directory, which contains the compiled plugin code + */ const devDir = `${process.cwd()}/dev`; -//mkdir if not exists if (!fs.existsSync(devDir)) { fs.mkdirSync(devDir); } -function cmpPath(path1, path2) { - path1 = path1.replace(/\\/g, '/'); - path2 = path2.replace(/\\/g, '/'); - // sepertor at tail - if (path1[path1.length - 1] !== '/') { - path1 += '/'; - } - if (path2[path2.length - 1] !== '/') { - path2 += '/'; - } - return path1 === path2; -} +/** + * 3. The target directory to make symbolic link to dev directory + */ +const name = getThisPluginName(); +if (name === null) { + process.exit(1); +} const targetPath = `${targetDir}/${name}`; -//如果已经存在,就退出 -if (fs.existsSync(targetPath)) { - let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); - - if (isSymbol) { - let srcPath = fs.readlinkSync(targetPath); - - if (cmpPath(srcPath, devDir)) { - log(`Good! ${targetPath} is already linked to ${devDir}`); - } else { - error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${srcPath}`); - } - } else { - error(`Failed! ${targetPath} already exists and is not a symbolic link`); - } - -} else { - //创建软链接 - fs.symlinkSync(devDir, targetPath, 'junction'); - log(`Done! Created symlink ${targetPath}`); -} +/** + * 4. Make symbolic link + */ +makeSymbolicLink(devDir, targetPath); diff --git a/scripts/make_install.js b/scripts/make_install.js index 9c2cd9b..cb2a4ac 100644 --- a/scripts/make_install.js +++ b/scripts/make_install.js @@ -1,191 +1,57 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-03-28 20:03:59 + * @FilePath : /scripts/make_install.js + * @LastEditTime : 2024-09-06 18:08:19 + * @Description : + */ +// make_install.js import fs from 'fs'; -import path from 'path'; -import http from 'node:http'; -import readline from 'node:readline'; +import { log, error, getSiYuanDir, chooseTarget, copyDirectory, getThisPluginName } from './utils.js'; +let targetDir = ''; -//************************************ Write you dir here ************************************ - -let targetDir = ''; // the target directory of the plugin, '*/data/plugin' -//******************************************************************************************** - -const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); -const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); - -let POST_HEADER = { - // "Authorization": `Token ${token}`, - "Content-Type": "application/json", -} - -async function myfetch(url, options) { - //使用 http 模块,从而兼容那些不支持 fetch 的 nodejs 版本 - return new Promise((resolve, reject) => { - let req = http.request(url, options, (res) => { - let data = ''; - res.on('data', (chunk) => { - data += chunk; - }); - res.on('end', () => { - resolve({ - ok: true, - status: res.statusCode, - json: () => JSON.parse(data) - }); - }); - }); - req.on('error', (e) => { - reject(e); - }); - req.end(); - }); -} - -async function getSiYuanDir() { - let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; - let conf = {}; - try { - let response = await myfetch(url, { - method: 'POST', - headers: POST_HEADER - }); - if (response.ok) { - conf = await response.json(); - } else { - error(`\tHTTP-Error: ${response.status}`); - return null; - } - } catch (e) { - error(`\tError: ${e}`); - error("\tPlease make sure SiYuan is running!!!"); - return null; - } - return conf.data; -} - -async function chooseTarget(workspaces) { - let count = workspaces.length; - log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`) - for (let i = 0; i < workspaces.length; i++) { - log(`\t[${i}] ${workspaces[i].path}`); - } - - if (count == 1) { - return `${workspaces[0].path}/data/plugins`; - } else { - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - let index = await new Promise((resolve, reject) => { - rl.question(`\tPlease select a workspace[0-${count-1}]: `, (answer) => { - resolve(answer); - }); - }); - rl.close(); - return `${workspaces[index].path}/data/plugins`; - } -} - -log('>>> Try to visit constant "targetDir" in make_install.js...') - +/** + * 1. Get the parent directory to install the plugin + */ +log('>>> Try to visit constant "targetDir" in make_install.js...'); if (targetDir === '') { - log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....') + log('>>> Constant "targetDir" is empty, try to get SiYuan directory automatically....'); let res = await getSiYuanDir(); - + if (res === null || res === undefined || res.length === 0) { error('>>> Can not get SiYuan directory automatically'); - process.exit(1); } else { targetDir = await chooseTarget(res); } - log(`>>> Successfully got target directory: ${targetDir}`); } - -//Check if (!fs.existsSync(targetDir)) { - error(`Failed! plugin directory not exists: "${targetDir}"`); - error(`Please set the plugin directory in scripts/make_install.js`); - process.exit(1); -} - - -//check if plugin.json exists -if (!fs.existsSync('./plugin.json')) { - //change dir to parent - process.chdir('../'); - if (!fs.existsSync('./plugin.json')) { - error('Failed! plugin.json not found'); - process.exit(1); - } -} - -//load plugin.json -const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); -const name = plugin?.name; -if (!name || name === '') { - error('Failed! Please set plugin name in plugin.json'); + error(`Failed! Plugin directory not exists: "${targetDir}"`); + error('Please set the plugin directory in scripts/make_install.js'); process.exit(1); } +/** + * 2. The dist directory, which contains the compiled plugin code + */ const distDir = `${process.cwd()}/dist`; -//mkdir if not exists if (!fs.existsSync(distDir)) { fs.mkdirSync(distDir); } -function cmpPath(path1, path2) { - path1 = path1.replace(/\\/g, '/'); - path2 = path2.replace(/\\/g, '/'); - // sepertor at tail - if (path1[path1.length - 1] !== '/') { - path1 += '/'; - } - if (path2[path2.length - 1] !== '/') { - path2 += '/'; - } - return path1 === path2; +/** + * 3. The target directory to install the plugin + */ +const name = getThisPluginName(); +if (name === null) { + process.exit(1); } - const targetPath = `${targetDir}/${name}`; - -function copyDirectory(srcDir, dstDir) { - if (!fs.existsSync(dstDir)) { - fs.mkdirSync(dstDir); - log(`Created directory ${dstDir}`); - } - //将 distDir 下的所有文件复制到 targetPath - fs.readdir(srcDir, { withFileTypes: true }, (err, files) => { - if (err) { - error('Error reading source directory:', err); - return; - } - - // 遍历源目录中的所有文件和子目录 - files.forEach((file) => { - const src = path.join(srcDir, file.name); - const dst = path.join(dstDir, file.name); - - // 判断当前项是文件还是目录 - if (file.isDirectory()) { - // 如果是目录,则递归调用复制函数复制子目录 - copyDirectory(src, dst); - } else { - // 如果是文件,则复制文件到目标目录 - fs.copyFile(src, dst, (err) => { - if (err) { - error('Error copying file:' + err); - } else { - log(`Copied file: ${src} --> ${dst}`); - } - }); - } - }); - log(`Copied ${distDir} to ${targetPath}`); - }); -} +/** + * 4. Copy the compiled plugin code to the target directory + */ copyDirectory(distDir, targetPath); - - diff --git a/scripts/update_version.js b/scripts/update_version.js new file mode 100644 index 0000000..775c98a --- /dev/null +++ b/scripts/update_version.js @@ -0,0 +1,141 @@ +// const fs = require('fs'); +// const path = require('path'); +// const readline = require('readline'); +import fs from 'node:fs'; +import path from 'node:path'; +import readline from 'node:readline'; + +// Utility to read JSON file +function readJsonFile(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, 'utf8', (err, data) => { + if (err) return reject(err); + try { + const jsonData = JSON.parse(data); + resolve(jsonData); + } catch (e) { + reject(e); + } + }); + }); +} + +// Utility to write JSON file +function writeJsonFile(filePath, jsonData) { + return new Promise((resolve, reject) => { + fs.writeFile(filePath, JSON.stringify(jsonData, null, 2), 'utf8', (err) => { + if (err) return reject(err); + resolve(); + }); + }); +} + +// Utility to prompt the user for input +function promptUser(query) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + return new Promise((resolve) => rl.question(query, (answer) => { + rl.close(); + resolve(answer); + })); +} + +// Function to parse the version string +function parseVersion(version) { + const [major, minor, patch] = version.split('.').map(Number); + return { major, minor, patch }; +} + +// Function to auto-increment version parts +function incrementVersion(version, type) { + let { major, minor, patch } = parseVersion(version); + + switch (type) { + case 'major': + major++; + minor = 0; + patch = 0; + break; + case 'minor': + minor++; + patch = 0; + break; + case 'patch': + patch++; + break; + default: + break; + } + + return `${major}.${minor}.${patch}`; +} + +// Main script +(async function () { + try { + const pluginJsonPath = path.join(process.cwd(), 'plugin.json'); + const packageJsonPath = path.join(process.cwd(), 'package.json'); + + // Read both JSON files + const pluginData = await readJsonFile(pluginJsonPath); + const packageData = await readJsonFile(packageJsonPath); + + // Get the current version from both files (assuming both have the same version) + const currentVersion = pluginData.version || packageData.version; + console.log(`\n🌟 Current version: \x1b[36m${currentVersion}\x1b[0m\n`); + + // Calculate potential new versions for auto-update + const newPatchVersion = incrementVersion(currentVersion, 'patch'); + const newMinorVersion = incrementVersion(currentVersion, 'minor'); + const newMajorVersion = incrementVersion(currentVersion, 'major'); + + // Prompt the user with formatted options + console.log('🔄 How would you like to update the version?\n'); + console.log(` 1️⃣ Auto update \x1b[33mpatch\x1b[0m version (new version: \x1b[32m${newPatchVersion}\x1b[0m)`); + console.log(` 2️⃣ Auto update \x1b[33mminor\x1b[0m version (new version: \x1b[32m${newMinorVersion}\x1b[0m)`); + console.log(` 3️⃣ Auto update \x1b[33mmajor\x1b[0m version (new version: \x1b[32m${newMajorVersion}\x1b[0m)`); + console.log(` 4️⃣ Input version \x1b[33mmanually\x1b[0m`); + // Press 0 to skip version update + console.log(' 0️⃣ Quit without updating\n'); + + const updateChoice = await promptUser('👉 Please choose (1/2/3/4): '); + + let newVersion; + + switch (updateChoice.trim()) { + case '1': + newVersion = newPatchVersion; + break; + case '2': + newVersion = newMinorVersion; + break; + case '3': + newVersion = newMajorVersion; + break; + case '4': + newVersion = await promptUser('✍️ Please enter the new version (in a.b.c format): '); + break; + case '0': + console.log('\n🛑 Skipping version update.'); + return; + default: + console.log('\n❌ Invalid option, no version update.'); + return; + } + + // Update the version in both plugin.json and package.json + pluginData.version = newVersion; + packageData.version = newVersion; + + // Write the updated JSON back to files + await writeJsonFile(pluginJsonPath, pluginData); + await writeJsonFile(packageJsonPath, packageData); + + console.log(`\n✅ Version successfully updated to: \x1b[32m${newVersion}\x1b[0m\n`); + + } catch (error) { + console.error('❌ Error:', error); + } +})(); diff --git a/scripts/utils.js b/scripts/utils.js new file mode 100644 index 0000000..210b6b1 --- /dev/null +++ b/scripts/utils.js @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2024 by frostime. All Rights Reserved. + * @Author : frostime + * @Date : 2024-09-06 17:42:57 + * @FilePath : /scripts/utils.js + * @LastEditTime : 2024-09-06 19:23:12 + * @Description : + */ +// common.js +import fs from 'fs'; +import path from 'node:path'; +import http from 'node:http'; +import readline from 'node:readline'; + +// Logging functions +export const log = (info) => console.log(`\x1B[36m%s\x1B[0m`, info); +export const error = (info) => console.log(`\x1B[31m%s\x1B[0m`, info); + +// HTTP POST headers +export const POST_HEADER = { + "Content-Type": "application/json", +}; + +// Fetch function compatible with older Node.js versions +export async function myfetch(url, options) { + return new Promise((resolve, reject) => { + let req = http.request(url, options, (res) => { + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { + resolve({ + ok: true, + status: res.statusCode, + json: () => JSON.parse(data) + }); + }); + }); + req.on('error', (e) => { + reject(e); + }); + req.end(); + }); +} + +/** + * Fetch SiYuan workspaces from port 6806 + * @returns {Promise} + */ +export async function getSiYuanDir() { + let url = 'http://127.0.0.1:6806/api/system/getWorkspaces'; + let conf = {}; + try { + let response = await myfetch(url, { + method: 'POST', + headers: POST_HEADER + }); + if (response.ok) { + conf = await response.json(); + } else { + error(`\tHTTP-Error: ${response.status}`); + return null; + } + } catch (e) { + error(`\tError: ${e}`); + error("\tPlease make sure SiYuan is running!!!"); + return null; + } + return conf?.data; // 保持原始返回值 +} + +/** + * Choose target workspace + * @param {{path: string}[]} workspaces + * @returns {string} The path of the selected workspace + */ +export async function chooseTarget(workspaces) { + let count = workspaces.length; + log(`>>> Got ${count} SiYuan ${count > 1 ? 'workspaces' : 'workspace'}`); + workspaces.forEach((workspace, i) => { + log(`\t[${i}] ${workspace.path}`); + }); + + if (count === 1) { + return `${workspaces[0].path}/data/plugins`; + } else { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + let index = await new Promise((resolve) => { + rl.question(`\tPlease select a workspace[0-${count - 1}]: `, (answer) => { + resolve(answer); + }); + }); + rl.close(); + return `${workspaces[index].path}/data/plugins`; + } +} + +/** + * Check if two paths are the same + * @param {string} path1 + * @param {string} path2 + * @returns {boolean} + */ +export function cmpPath(path1, path2) { + path1 = path1.replace(/\\/g, '/'); + path2 = path2.replace(/\\/g, '/'); + if (path1[path1.length - 1] !== '/') { + path1 += '/'; + } + if (path2[path2.length - 1] !== '/') { + path2 += '/'; + } + return path1 === path2; +} + +export function getThisPluginName() { + if (!fs.existsSync('./plugin.json')) { + process.chdir('../'); + if (!fs.existsSync('./plugin.json')) { + error('Failed! plugin.json not found'); + return null; + } + } + + const plugin = JSON.parse(fs.readFileSync('./plugin.json', 'utf8')); + const name = plugin?.name; + if (!name) { + error('Failed! Please set plugin name in plugin.json'); + return null; + } + + return name; +} + +export function copyDirectory(srcDir, dstDir) { + if (!fs.existsSync(dstDir)) { + fs.mkdirSync(dstDir); + log(`Created directory ${dstDir}`); + } + + fs.readdirSync(srcDir, { withFileTypes: true }).forEach((file) => { + const src = path.join(srcDir, file.name); + const dst = path.join(dstDir, file.name); + + if (file.isDirectory()) { + copyDirectory(src, dst); + } else { + fs.copyFileSync(src, dst); + log(`Copied file: ${src} --> ${dst}`); + } + }); + log(`All files copied!`); +} + + +export function makeSymbolicLink(srcPath, targetPath) { + if (!fs.existsSync(targetPath)) { + // fs.symlinkSync(srcPath, targetPath, 'junction'); + //Go 1.23 no longer supports junctions as symlinks + //Please refer to https://github.com/siyuan-note/siyuan/issues/12399 + fs.symlinkSync(srcPath, targetPath, 'dir'); + log(`Done! Created symlink ${targetPath}`); + return; + } + + //Check the existed target path + let isSymbol = fs.lstatSync(targetPath).isSymbolicLink(); + if (!isSymbol) { + error(`Failed! ${targetPath} already exists and is not a symbolic link`); + return; + } + let existedPath = fs.readlinkSync(targetPath); + if (cmpPath(existedPath, srcPath)) { + log(`Good! ${targetPath} is already linked to ${srcPath}`); + } else { + error(`Error! Already exists symbolic link ${targetPath}\nBut it links to ${existedPath}`); + } +} diff --git a/src/api.ts b/src/api.ts index c227518..0595371 100644 --- a/src/api.ts +++ b/src/api.ts @@ -6,7 +6,7 @@ * API 文档见 [API_zh_CN.md](https://github.com/siyuan-note/siyuan/blob/master/API_zh_CN.md) */ -import { fetchSyncPost, IWebSocketData } from "siyuan"; +import { fetchPost, fetchSyncPost, IWebSocketData } from "siyuan"; export async function request(url: string, data: any) { @@ -328,14 +328,35 @@ export async function getFile(path: string): Promise { path: path } let url = '/api/file/getFile'; - try { - let file = await fetchSyncPost(url, data); - return file; - } catch (error_msg) { + return new Promise((resolve, _) => { + fetchPost(url, data, (content: any) => { + resolve(content) + }); + }); +} + + +/** + * fetchPost will secretly convert data into json, this func merely return Blob + * @param endpoint + * @returns + */ +export const getFileBlob = async (path: string): Promise => { + const endpoint = '/api/file/getFile' + let response = await fetch(endpoint, { + method: 'POST', + body: JSON.stringify({ + path: path + }) + }); + if (!response.ok) { return null; } + let data = await response.blob(); + return data; } + export async function putFile(path: string, isDir: boolean, file: any) { let form = new FormData(); form.append('path', path); diff --git a/src/hello.svelte b/src/hello.svelte index cc9f0dd..967c7f6 100644 --- a/src/hello.svelte +++ b/src/hello.svelte @@ -1,3 +1,11 @@ + + +{#if type === "checkbox"} + + +{:else if type === "textinput"} + + +{:else if type === "textarea"} + + +
+
+ +
`, + width: args.width ?? "520px", + height: args.height + }); + const target: HTMLTextAreaElement = dialog.element.querySelector(".b3-dialog__content>div.ft__breakword>textarea"); + const btnsElement = dialog.element.querySelectorAll(".b3-button"); + btnsElement[0].addEventListener("click", () => { + if (args?.cancel) { + args.cancel(); + } + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + if (args?.confirm) { + args.confirm(target.value); + } + dialog.destroy(); + }); +}; + +export const inputDialogSync = async (args: { + title: string, placeholder?: string, defaultText?: string, + width?: string, height?: string +}) => { + return new Promise((resolve) => { + let newargs = { + ...args, confirm: (text) => { + resolve(text); + }, cancel: () => { + resolve(null); + } + }; + inputDialog(newargs); + }); +} + + +interface IConfirmDialogArgs { + title: string; + content: string | HTMLElement; + confirm?: (ele?: HTMLElement) => void; + cancel?: (ele?: HTMLElement) => void; + width?: string; + height?: string; +} + +export const confirmDialog = (args: IConfirmDialogArgs) => { + const { title, content, confirm, cancel, width, height } = args; + + const dialog = new Dialog({ + title, + content: `
+
+
+
+
+
+ +
`, + width: width, + height: height + }); + + 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"); + btnsElement[0].addEventListener("click", () => { + if (cancel) { + cancel(target); + } + dialog.destroy(); + }); + btnsElement[1].addEventListener("click", () => { + if (confirm) { + confirm(target); + } + dialog.destroy(); + }); +}; + + +export const confirmDialogSync = async (args: IConfirmDialogArgs) => { + return new Promise((resolve) => { + let newargs = { + ...args, confirm: (ele: HTMLElement) => { + resolve(ele); + }, cancel: (ele: HTMLElement) => { + resolve(ele); + } + }; + confirmDialog(newargs); + }); +}; + + +export const simpleDialog = (args: { + title: string, ele: HTMLElement | DocumentFragment, + width?: string, height?: string, + callback?: () => void; +}) => { + const dialog = new Dialog({ + title: args.title, + content: `
`, + width: args.width, + height: args.height, + destroyCallback: args.callback + }); + dialog.element.querySelector(".dialog-content").appendChild(args.ele); + return { + dialog, + close: dialog.destroy.bind(dialog) + }; +} + + +export const svelteDialog = (args: { + title: string, constructor: (container: HTMLElement) => SvelteComponent, + width?: string, height?: string, + callback?: () => void; +}) => { + let container = document.createElement('div') + container.style.display = 'contents'; + let component = args.constructor(container); + const { dialog, close } = simpleDialog({ + ...args, ele: container, callback: () => { + component.$destroy(); + if (args.callback) args.callback(); + } + }); + return { + component, + dialog, + close + } +} diff --git a/src/libs/index.d.ts b/src/libs/index.d.ts index ab2f6c3..27a27ed 100644 --- a/src/libs/index.d.ts +++ b/src/libs/index.d.ts @@ -28,12 +28,12 @@ interface ISettingItemCore { interface ISettingItem extends ISettingItemCore { title: string; description: string; + direction?: "row" | "column"; } //Interface for setting-utils interface ISettingUtilsItem extends ISettingItem { - direction?: "row" | "column"; action?: { callback: () => void; } diff --git a/src/libs/promise-pool.ts b/src/libs/promise-pool.ts new file mode 100644 index 0000000..e20b0b8 --- /dev/null +++ b/src/libs/promise-pool.ts @@ -0,0 +1,48 @@ +export default class PromiseLimitPool { + private maxConcurrent: number; + private currentRunning = 0; + private queue: (() => void)[] = []; + private promises: Promise[] = []; + + constructor(maxConcurrent: number) { + this.maxConcurrent = maxConcurrent; + } + + add(fn: () => Promise): void { + const promise = new Promise((resolve, reject) => { + const run = async () => { + try { + this.currentRunning++; + const result = await fn(); + resolve(result); + } catch (error) { + reject(error); + } finally { + this.currentRunning--; + this.next(); + } + }; + + if (this.currentRunning < this.maxConcurrent) { + run(); + } else { + this.queue.push(run); + } + }); + this.promises.push(promise); + } + + async awaitAll(): Promise { + return Promise.all(this.promises); + } + + /** + * Handles the next task in the queue. + */ + private next(): void { + if (this.queue.length > 0 && this.currentRunning < this.maxConcurrent) { + const nextRun = this.queue.shift()!; + nextRun(); + } + } +} diff --git a/src/libs/setting-item.svelte b/src/libs/setting-item.svelte deleted file mode 100644 index ef1a964..0000000 --- a/src/libs/setting-item.svelte +++ /dev/null @@ -1,105 +0,0 @@ - - - diff --git a/src/libs/setting-utils.ts b/src/libs/setting-utils.ts index 948dc2f..ae316e2 100644 --- a/src/libs/setting-utils.ts +++ b/src/libs/setting-utils.ts @@ -3,7 +3,7 @@ * @Author : frostime * @Date : 2023-12-17 18:28:19 * @FilePath : /src/libs/setting-utils.ts - * @LastEditTime : 2024-04-30 16:42:23 + * @LastEditTime : 2024-05-01 17:44:16 * @Description : */ @@ -105,7 +105,7 @@ export class SettingUtils { args.callback(data); } this.plugin.data[this.name] = data; - this.save(); + this.save(data); }, destroyCallback: () => { //Restore the original value @@ -128,8 +128,8 @@ export class SettingUtils { return data; } - async save() { - let data = this.dump(); + async save(data?: any) { + data = data ?? this.dump(); await this.plugin.saveData(this.file, this.dump()); console.debug('Save config:', data); return data; @@ -285,6 +285,13 @@ export class SettingUtils { createDefaultElement(item: ISettingUtilsItem) { let itemElement: HTMLElement; + //阻止思源内置的回车键确认 + const preventEnterConfirm = (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + e.stopImmediatePropagation(); + } + } switch (item.type) { case 'checkbox': let element: HTMLInputElement = document.createElement('input'); @@ -330,7 +337,7 @@ export class SettingUtils { textInputElement.value = item.value; textInputElement.onchange = item.action?.callback ?? (() => { }); itemElement = textInputElement; - + textInputElement.addEventListener('keydown', preventEnterConfirm); break; case 'textarea': let textareaElement: HTMLTextAreaElement = document.createElement('textarea'); @@ -345,6 +352,7 @@ export class SettingUtils { numberElement.className = 'b3-text-field fn__flex-center fn__size200'; numberElement.value = item.value; itemElement = numberElement; + numberElement.addEventListener('keydown', preventEnterConfirm); break; case 'button': let buttonElement: HTMLButtonElement = document.createElement('button'); diff --git a/src/setting-example.svelte b/src/setting-example.svelte index 4bafee5..2a2c809 100644 --- a/src/setting-example.svelte +++ b/src/setting-example.svelte @@ -1,11 +1,11 @@