فهرست منبع

Merge branch 'dev' into dev-font-1

Jeffrey Chen 7 ماه پیش
والد
کامیت
4bf6460cef
100فایلهای تغییر یافته به همراه1045 افزوده شده و 364 حذف شده
  1. 22 0
      API.md
  2. 22 0
      API_zh_CN.md
  3. 2 0
      app/appearance/langs/de_DE.json
  4. 2 0
      app/appearance/langs/en_US.json
  5. 2 0
      app/appearance/langs/es_ES.json
  6. 2 1
      app/appearance/langs/fr_FR.json
  7. 2 0
      app/appearance/langs/he_IL.json
  8. 2 0
      app/appearance/langs/it_IT.json
  9. 2 0
      app/appearance/langs/ja_JP.json
  10. 2 0
      app/appearance/langs/pl_PL.json
  11. 2 0
      app/appearance/langs/ru_RU.json
  12. 3 1
      app/appearance/langs/zh_CHT.json
  13. 3 1
      app/appearance/langs/zh_CN.json
  14. 2 2
      app/appearance/themes/daylight/theme.css
  15. 2 2
      app/appearance/themes/midnight/theme.css
  16. 47 1
      app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p.sy
  17. 5 5
      app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20200924100950-9op5xi1.sy
  18. 47 1
      app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa.sy
  19. 5 5
      app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20200813004551-gm0pbn1.sy
  20. 47 1
      app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq.sy
  21. 5 5
      app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20211226122549-jktxego.sy
  22. 47 1
      app/guide/20240530133126-axarxgx/20240530101000-4qitucx.sy
  23. 5 5
      app/guide/20240530133126-axarxgx/20240530101000-4qitucx/20240530101000-g3ugxml/20240530101000-xsbxokr.sy
  24. 3 3
      app/src/assets/scss/base.scss
  25. 21 7
      app/src/assets/scss/business/_av.scss
  26. 38 8
      app/src/assets/scss/business/_custom.scss
  27. 12 1
      app/src/assets/scss/business/_drag.scss
  28. 1 1
      app/src/assets/scss/business/_export.scss
  29. 5 0
      app/src/assets/scss/business/_history.scss
  30. 8 8
      app/src/assets/scss/business/_layout.scss
  31. 3 3
      app/src/assets/scss/business/_search.scss
  32. 13 2
      app/src/assets/scss/component/_button.scss
  33. 1 0
      app/src/assets/scss/component/_dialog.scss
  34. 2 2
      app/src/assets/scss/component/_list.scss
  35. 1 1
      app/src/assets/scss/component/_snackbar.scss
  36. 21 19
      app/src/assets/scss/component/_switch.scss
  37. 2 2
      app/src/assets/scss/component/_tooltips.scss
  38. 12 4
      app/src/assets/scss/component/_typography.scss
  39. 2 2
      app/src/assets/scss/pdf/_pdf.scss
  40. 3 3
      app/src/assets/scss/protyle/_protyle.scss
  41. 34 43
      app/src/assets/scss/protyle/_wysiwyg.scss
  42. 5 0
      app/src/assets/scss/util/_responsive.scss
  43. 3 3
      app/src/block/Panel.ts
  44. 1 1
      app/src/boot/globalEvent/click.ts
  45. 1 1
      app/src/boot/globalEvent/command/panel.ts
  46. 1 1
      app/src/card/openCard.ts
  47. 1 1
      app/src/config/about.ts
  48. 18 0
      app/src/config/keymap.ts
  49. 3 2
      app/src/config/search.ts
  50. 1 1
      app/src/constants.ts
  51. 2 3
      app/src/dialog/tooltip.ts
  52. 1 0
      app/src/emoji/index.ts
  53. 53 32
      app/src/history/doc.ts
  54. 9 8
      app/src/history/history.ts
  55. 2 0
      app/src/layout/dock/Backlink.ts
  56. 1 1
      app/src/layout/dock/Files.ts
  57. 5 1
      app/src/layout/dock/Inbox.ts
  58. 2 1
      app/src/menus/commonMenuItem.ts
  59. 15 26
      app/src/menus/protyle.ts
  60. 4 1
      app/src/menus/util.ts
  61. 1 1
      app/src/mobile/index.ts
  62. 1 1
      app/src/mobile/settings/about.ts
  63. 24 0
      app/src/plugin/index.ts
  64. 3 2
      app/src/protyle/export/index.ts
  65. 6 2
      app/src/protyle/gutter/index.ts
  66. 6 1
      app/src/protyle/header/Title.ts
  67. 6 0
      app/src/protyle/index.ts
  68. 2 1
      app/src/protyle/preview/index.ts
  69. 3 3
      app/src/protyle/render/av/action.ts
  70. 52 21
      app/src/protyle/render/av/blockAttr.ts
  71. 2 2
      app/src/protyle/render/av/calc.ts
  72. 44 13
      app/src/protyle/render/av/relation.ts
  73. 1 1
      app/src/protyle/render/flowchartRender.ts
  74. 16 2
      app/src/protyle/render/highlightRender.ts
  75. 27 0
      app/src/protyle/render/searchMarkRender.ts
  76. 22 4
      app/src/protyle/toolbar/index.ts
  77. 1 1
      app/src/protyle/ui/initUI.ts
  78. 16 6
      app/src/protyle/util/compatibility.ts
  79. 4 0
      app/src/protyle/util/destroy.ts
  80. 47 6
      app/src/protyle/util/editorCommonEvent.ts
  81. 4 0
      app/src/protyle/util/insertHTML.ts
  82. 28 15
      app/src/protyle/util/paste.ts
  83. 10 8
      app/src/protyle/util/reload.ts
  84. 1 0
      app/src/protyle/wysiwyg/enter.ts
  85. 34 25
      app/src/protyle/wysiwyg/index.ts
  86. 1 0
      app/src/protyle/wysiwyg/input.ts
  87. 7 3
      app/src/protyle/wysiwyg/keydown.ts
  88. 2 0
      app/src/protyle/wysiwyg/remove.ts
  89. 0 1
      app/src/protyle/wysiwyg/transaction.ts
  90. 38 11
      app/src/search/util.ts
  91. 24 0
      app/src/types/index.d.ts
  92. 6 0
      app/src/types/protyle.d.ts
  93. 5 3
      app/src/util/assets.ts
  94. 4 2
      app/src/util/highlightById.ts
  95. 1 1
      app/stage/auth.html
  96. 1 0
      app/stage/protyle/js/flowchart.js/flowchart.min.js
  97. 0 0
      app/stage/protyle/js/lute/lute.min.js
  98. 1 1
      app/stage/service-worker.js
  99. 2 2
      app/webpack.config.js
  100. 2 2
      app/webpack.desktop.js

+ 22 - 0
API.md

@@ -416,6 +416,28 @@ Rename a document by `id`:
     "data": null
   }
   ```
+  
+Remove a document by `id`:
+
+* `/api/filetree/removeDocByID`
+* Parameters
+
+  ```json
+  {
+    "id": "20210902210113-0avi12f"
+  }
+  ```
+
+  * `id`: Document ID
+* Return value
+
+  ```json
+  {
+    "code": 0,
+    "msg": "",
+    "data": null
+  }
+  ```
 
 ### Move documents
 

+ 22 - 0
API_zh_CN.md

@@ -416,6 +416,28 @@
     "data": null
   }
   ```
+  
+通过 `id` 删除文档:
+
+* `/api/filetree/removeDocByID`
+* 参数
+
+  ```json
+  {
+    "id": "20210902210113-0avi12f"
+  }
+  ```
+
+  * `id`:文档 ID
+* 返回值
+
+  ```json
+  {
+    "code": 0,
+    "msg": "",
+    "data": null
+  }
+  ```
 
 ### 移动文档
 

+ 2 - 0
app/appearance/langs/de_DE.json

@@ -1,4 +1,6 @@
 {
+  "empty": "Leer",
+  "newRowInRelation": "Erstellen Sie einen neuen Eintrag in ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Schlüsselinhalt",
   "addDesc": "Beschreibung hinzufügen",
   "dataRepoAutoPurgeIndexRetentionDays": "Daten-Snapshot-Aufbewahrungstage",

+ 2 - 0
app/appearance/langs/en_US.json

@@ -1,4 +1,6 @@
 {
+  "empty": "Empty",
+  "newRowInRelation": "Create a new entry in ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Key content",
   "addDesc": "Add description",
   "dataRepoAutoPurgeIndexRetentionDays": "Data snapshot retention days",

+ 2 - 0
app/appearance/langs/es_ES.json

@@ -1,4 +1,6 @@
 {
+  "empty": "Vacío",
+  "newRowInRelation": "Crear una nueva entrada en ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Contenido de la clave",
   "addDesc": "Agregar descripción",
   "dataRepoAutoPurgeIndexRetentionDays": "Días de retención de instantáneas de datos",

+ 2 - 1
app/appearance/langs/fr_FR.json

@@ -1,4 +1,5 @@
 {
+  "newRowInRelation": "Créer une nouvelle entrée dans ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Contenu de la clé",
   "addDesc": "Ajouter une description",
   "dataRepoAutoPurgeIndexRetentionDays": "Jours de rétention des instantanés de données",
@@ -601,7 +602,7 @@
   "querySyntax": "Syntaxe de la requête",
   "rollback": "Rollback",
   "custom": "Personnalisé",
-  "feedback": "Commentaires",
+  "feedback": "Retour d'information",
   "inbox": "Boîte de réception",
   "turnToStatic": "Texte d'ancrage statique",
   "turnToDynamic": "Texte d'ancrage dynamique",

+ 2 - 0
app/appearance/langs/he_IL.json

@@ -1,4 +1,6 @@
 {
+  "empty": "ריק",
+  "newRowInRelation": "צור ערך חדש ב-${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "תוכן המפתח",
   "addDesc": "הוסף תיאור",
   "dataRepoAutoPurgeIndexRetentionDays": "ימי שמירת תמונות נתונים",

+ 2 - 0
app/appearance/langs/it_IT.json

@@ -1,4 +1,6 @@
 {
+  "empty": "Vuoto",
+  "newRowInRelation": "Crea una nuova voce in ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Contenuto della chiave",
   "addDesc": "Aggiungi descrizione",
   "dataRepoAutoPurgeIndexRetentionDays": "Giorni di conservazione degli snapshot dei dati",

+ 2 - 0
app/appearance/langs/ja_JP.json

@@ -1,4 +1,6 @@
 {
+  "empty": "空白",
+  "newRowInRelation": "${x} に新しい項目を作成 <b class='ft__on-surface'>${y}</b>",
   "keyContent": "キーコンテンツ",
   "addDesc": "説明を追加",
   "dataRepoAutoPurgeIndexRetentionDays": "データスナップショットの保持日数",

+ 2 - 0
app/appearance/langs/pl_PL.json

@@ -1,4 +1,6 @@
 {
+  "empty": "Pusty",
+  "newRowInRelation": "Utwórz nowy wpis w ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Zawartość klucza",
   "addDesc": "Dodaj opis",
   "dataRepoAutoPurgeIndexRetentionDays": "Dni przechowywania migawek danych",

+ 2 - 0
app/appearance/langs/ru_RU.json

@@ -1,4 +1,6 @@
 {
+  "empty": "Пусто",
+  "newRowInRelation": "Создать новую запись в ${x} <b class='ft__on-surface'>${y}</b>",
   "keyContent": "Содержимое ключа",
   "addDesc": "Добавить описание",
   "dataRepoAutoPurgeIndexRetentionDays": "Срок хранения снимков данных",

+ 3 - 1
app/appearance/langs/zh_CHT.json

@@ -1,4 +1,6 @@
 {
+  "empty": "空白",
+  "newRowInRelation": "在 ${x} 中新建條目 <b class='ft__on-surface'>${y}</b>",
   "keyContent": "主鍵內容",
   "addDesc": "添加描述",
   "dataRepoAutoPurgeIndexRetentionDays": "數據快照保留天數",
@@ -601,7 +603,7 @@
   "querySyntax": "查詢語法",
   "rollback": "回復",
   "custom": "自定義",
-  "feedback": "反饋",
+  "feedback": "問題反饋",
   "inbox": "收件箱",
   "turnToStatic": "靜態錨文字",
   "turnToDynamic": "動態錨文字",

+ 3 - 1
app/appearance/langs/zh_CN.json

@@ -1,4 +1,6 @@
 {
+  "empty": "空白",
+  "newRowInRelation": "在 ${x} 中新建条目 <b class='ft__on-surface'>${y}</b>",
   "keyContent": "主键内容",
   "addDesc": "添加描述",
   "dataRepoAutoPurgeIndexRetentionDays": "数据快照保留天数",
@@ -601,7 +603,7 @@
   "querySyntax": "查询语法",
   "rollback": "回滚",
   "custom": "自定义",
-  "feedback": "反馈",
+  "feedback": "问题反馈",
   "inbox": "收集箱",
   "turnToStatic": "静态锚文本",
   "turnToDynamic": "动态锚文本",

+ 2 - 2
app/appearance/themes/daylight/theme.css

@@ -27,7 +27,7 @@
     --b3-font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", "Hiragino Sans", "Segoe UI", "Tahoma", "Arial", "Other Emojis", sans-serif;
     --b3-font-family-protyle: var(--b3-font-family);
     --b3-font-family-code: "Number Glyphs Of Custom Font", "Number Glyphs", "Segoe Good Emoji", "Other SiYuan Emojis", "JetBrainsMono-Regular", mononoki, Consolas, "Liberation Mono", Menlo, Courier, monospace, var(--b3-font-family);
-    --b3-font-family-graph: mononoki;
+    --b3-font-family-graph: arial;
     --b3-font-family-emoji: "Segoe Good Emoji", "Other Emojis";
     --b3-font-family-math: KaTeX_Math;
     --b3-font-size: 14px;
@@ -207,7 +207,7 @@
 }
 
 /* https://github.com/siyuan-note/siyuan/issues/6440 */
-.protyle-action--order:after {
+.protyle-action--order::after {
     mix-blend-mode: multiply;
 }
 

+ 2 - 2
app/appearance/themes/midnight/theme.css

@@ -25,7 +25,7 @@
     --b3-font-family: "Helvetica Neue", "Luxi Sans", "DejaVu Sans", "Hiragino Sans", "Segoe UI", "Tahoma", "Arial", "Other Emojis", sans-serif;
     --b3-font-family-protyle: var(--b3-font-family);
     --b3-font-family-code: "Number Glyphs Of Custom Font", "Number Glyphs", "Segoe Good Emoji", "Other SiYuan Emojis", "JetBrainsMono-Regular", mononoki, Consolas, "Liberation Mono", Menlo, Courier, monospace, var(--b3-font-family);
-    --b3-font-family-graph: mononoki;
+    --b3-font-family-graph: arial;
     --b3-font-family-emoji: "Segoe Good Emoji", "Other Emojis";
     --b3-font-family-math: KaTeX_Math;
     --b3-font-size: 14px;
@@ -205,7 +205,7 @@
 }
 
 /* https://github.com/siyuan-note/siyuan/issues/6440 */
-.protyle-action--order:after {
+.protyle-action--order::after {
     mix-blend-mode: screen;
 }
 

+ 47 - 1
app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p.sy

@@ -8,9 +8,55 @@
 		"title": "Please Start Here",
 		"title-img": "background-color:#556;background-image: linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a);background-size:80px 140px;background-position: 0 0, 0 0, 40px 70px, 40px 70px, 0 0, 40px 70px;",
 		"type": "doc",
-		"updated": "20241018102340"
+		"updated": "20241125224626"
 	},
 	"Children": [
+		{
+			"ID": "20241125224615-dwfqru1",
+			"Type": "NodeBlockquote",
+			"Properties": {
+				"id": "20241125224615-dwfqru1",
+				"updated": "20241125224626"
+			},
+			"Children": [
+				{
+					"Type": "NodeBlockquoteMarker",
+					"Data": "\u003e"
+				},
+				{
+					"ID": "20241125224615-z2fevdf",
+					"Type": "NodeParagraph",
+					"Properties": {
+						"id": "20241125224615-z2fevdf",
+						"updated": "20241125224626"
+					},
+					"Children": [
+						{
+							"Type": "NodeText",
+							"Data": "When you close SiYuan Notes, the User Guide will be automatically closed. You can click "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "Main Menu"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " - "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "User Guide"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " to view it again"
+						}
+					]
+				}
+			]
+		},
 		{
 			"ID": "20210528120135-bznvpp6",
 			"Type": "NodeSuperBlock",

+ 5 - 5
app/guide/20210808180117-6v0mkxr/20200923234011-ieuun1p/20210808180303-xaduj2o/20200924100950-9op5xi1.sy

@@ -7,7 +7,7 @@
 		"id": "20200924100950-9op5xi1",
 		"title": "Shortcuts",
 		"type": "doc",
-		"updated": "20241009230212"
+		"updated": "20241128113606"
 	},
 	"Children": [
 		{
@@ -12329,7 +12329,7 @@
 			"HeadingLevel": 3,
 			"Properties": {
 				"id": "20220619002135-5dfw1yo",
-				"updated": "20241009230212"
+				"updated": "20241128113606"
 			},
 			"Children": [
 				{
@@ -12349,7 +12349,7 @@
 			"Properties": {
 				"colgroup": "||",
 				"id": "20220619002135-s399g7e",
-				"updated": "20241009230212"
+				"updated": "20241128113606"
 			},
 			"Children": [
 				{
@@ -12653,7 +12653,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "Move"
+									"Data": "Generate block ref"
 								}
 							]
 						},
@@ -12692,7 +12692,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "Generate block ref"
+									"Data": "Move"
 								}
 							]
 						},

+ 47 - 1
app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa.sy

@@ -8,9 +8,55 @@
 		"title": "请从这里开始",
 		"title-img": "background-color:#556;background-image: linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a);background-size:80px 140px;background-position: 0 0, 0 0, 40px 70px, 40px 70px, 0 0, 40px 70px;",
 		"type": "doc",
-		"updated": "20241018102134"
+		"updated": "20241125224159"
 	},
 	"Children": [
+		{
+			"ID": "20241125224159-8zf3bos",
+			"Type": "NodeBlockquote",
+			"Properties": {
+				"id": "20241125224159-8zf3bos",
+				"updated": "20241125224159"
+			},
+			"Children": [
+				{
+					"Type": "NodeBlockquoteMarker",
+					"Data": "\u003e"
+				},
+				{
+					"ID": "20241125224159-vcaj2bv",
+					"Type": "NodeParagraph",
+					"Properties": {
+						"id": "20241125224159-vcaj2bv",
+						"updated": "20241125224159"
+					},
+					"Children": [
+						{
+							"Type": "NodeText",
+							"Data": "关闭思源笔记时会自动关闭用户指南,可以点击 "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "主菜单"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " - "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "用户指南"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " 再次查看"
+						}
+					]
+				}
+			]
+		},
 		{
 			"ID": "20210528115012-vst5lwt",
 			"Type": "NodeSuperBlock",

+ 5 - 5
app/guide/20210808180117-czj9bvb/20200812220555-lj3enxa/20210808180321-hbvl5c2/20200813004551-gm0pbn1.sy

@@ -7,7 +7,7 @@
 		"id": "20200813004551-gm0pbn1",
 		"title": "快捷键",
 		"type": "doc",
-		"updated": "20241009225803"
+		"updated": "20241128112739"
 	},
 	"Children": [
 		{
@@ -12388,7 +12388,7 @@
 			"HeadingLevel": 3,
 			"Properties": {
 				"id": "20220619001156-26volkp",
-				"updated": "20241009225803"
+				"updated": "20241128112739"
 			},
 			"Children": [
 				{
@@ -12408,7 +12408,7 @@
 			"Properties": {
 				"colgroup": "||",
 				"id": "20220619001217-e9cwukj",
-				"updated": "20241009225803"
+				"updated": "20241128112739"
 			},
 			"Children": [
 				{
@@ -12711,7 +12711,7 @@
 								},
 								{
 									"Type": "NodeText",
-									"Data": "移动"
+									"Data": "生成块引用"
 								}
 							]
 						},
@@ -12762,7 +12762,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "生成块引用"
+									"Data": "移动"
 								}
 							]
 						},

+ 47 - 1
app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq.sy

@@ -8,9 +8,55 @@
 		"title": "請從這裡開始",
 		"title-img": "background-color:#556;background-image: linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a);background-size:80px 140px;background-position: 0 0, 0 0, 40px 70px, 40px 70px, 0 0, 40px 70px;",
 		"type": "doc",
-		"updated": "20241018105300"
+		"updated": "20241125224503"
 	},
 	"Children": [
+		{
+			"ID": "20241125224503-8vqkbc9",
+			"Type": "NodeBlockquote",
+			"Properties": {
+				"id": "20241125224503-8vqkbc9",
+				"updated": "20241125224503"
+			},
+			"Children": [
+				{
+					"Type": "NodeBlockquoteMarker",
+					"Data": "\u003e"
+				},
+				{
+					"ID": "20241125224503-fqq0875",
+					"Type": "NodeParagraph",
+					"Properties": {
+						"id": "20241125224503-fqq0875",
+						"updated": "20241125224503"
+					},
+					"Children": [
+						{
+							"Type": "NodeText",
+							"Data": "關閉思源筆記時會自動關閉用戶指南,可以點擊 "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "主菜單"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " - "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "用戶指南"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " ​再次查看"
+						}
+					]
+				}
+			]
+		},
 		{
 			"ID": "20211226115745-rorv31l",
 			"Type": "NodeSuperBlock",

+ 5 - 5
app/guide/20211226090932-5lcq56f/20211226115423-d5z1joq/20211226121203-rjjngpz/20211226122549-jktxego.sy

@@ -7,7 +7,7 @@
 		"id": "20211226122549-jktxego",
 		"title": "快捷鍵",
 		"type": "doc",
-		"updated": "20241018105254"
+		"updated": "20241128113116"
 	},
 	"Children": [
 		{
@@ -12564,7 +12564,7 @@
 			"HeadingLevel": 3,
 			"Properties": {
 				"id": "20220619002223-zliquo3",
-				"updated": "20241009230052"
+				"updated": "20241128113116"
 			},
 			"Children": [
 				{
@@ -12584,7 +12584,7 @@
 			"Properties": {
 				"colgroup": "||",
 				"id": "20220619002223-xwob5s8",
-				"updated": "20241009230052"
+				"updated": "20241128113116"
 			},
 			"Children": [
 				{
@@ -12896,7 +12896,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "移動"
+									"Data": "生成塊引用"
 								}
 							]
 						},
@@ -12935,7 +12935,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "生成塊引用"
+									"Data": "移動"
 								}
 							]
 						},

+ 47 - 1
app/guide/20240530133126-axarxgx/20240530101000-4qitucx.sy

@@ -9,9 +9,55 @@
 		"title": "スタートガイド",
 		"title-img": "background-color:#556;background-image: linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(30deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(150deg, #445 12%, transparent 12.5%, transparent 87%, #445 87.5%, #445),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a),linear-gradient(60deg, #99a 25%, transparent 25.5%, transparent 75%, #99a 75%, #99a);background-size:80px 140px;background-position: 0 0, 0 0, 40px 70px, 40px 70px, 0 0, 40px 70px;",
 		"type": "doc",
-		"updated": "20241018102426"
+		"updated": "20241125225136"
 	},
 	"Children": [
+		{
+			"ID": "20241125224702-5a9o34q",
+			"Type": "NodeBlockquote",
+			"Properties": {
+				"id": "20241125224702-5a9o34q",
+				"updated": "20241125225136"
+			},
+			"Children": [
+				{
+					"Type": "NodeBlockquoteMarker",
+					"Data": "\u003e"
+				},
+				{
+					"ID": "20241125225100-s0akx3f",
+					"Type": "NodeParagraph",
+					"Properties": {
+						"id": "20241125225100-s0akx3f",
+						"updated": "20241125225136"
+					},
+					"Children": [
+						{
+							"Type": "NodeText",
+							"Data": "SiYuan を閉じると、ユーザーガイドは自動的に閉じられます。"
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "メインメニュー"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " - "
+						},
+						{
+							"Type": "NodeTextMark",
+							"TextMarkType": "kbd",
+							"TextMarkTextContent": "ユーザーガイド"
+						},
+						{
+							"Type": "NodeText",
+							"Data": " ​をクリックすると、再度表示できます"
+						}
+					]
+				}
+			]
+		},
 		{
 			"ID": "20240530101000-x3xk6o7",
 			"Type": "NodeSuperBlock",

+ 5 - 5
app/guide/20240530133126-axarxgx/20240530101000-4qitucx/20240530101000-g3ugxml/20240530101000-xsbxokr.sy

@@ -9,7 +9,7 @@
 		"id": "20240530101000-xsbxokr",
 		"title": "ショートカット",
 		"type": "doc",
-		"updated": "20241009230427"
+		"updated": "20241128113339"
 	},
 	"Children": [
 		{
@@ -12781,7 +12781,7 @@
 			"Properties": {
 				"ID": "20240530101000-xjv6i4l",
 				"id": "20240530101000-mifghto",
-				"updated": "20241009230427"
+				"updated": "20241128113339"
 			},
 			"Children": [
 				{
@@ -12811,7 +12811,7 @@
 			"Properties": {
 				"colgroup": "||",
 				"id": "20240530101000-4iuhg30",
-				"updated": "20241009230427"
+				"updated": "20241128113339"
 			},
 			"Children": [
 				{
@@ -13134,7 +13134,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "ブロックを移動"
+									"Data": "ブロック参照を生成"
 								}
 							]
 						},
@@ -13173,7 +13173,7 @@
 							"Children": [
 								{
 									"Type": "NodeText",
-									"Data": "ブロック参照を生成"
+									"Data": "ブロックを移動"
 								}
 							]
 						},

+ 3 - 3
app/src/assets/scss/base.scss

@@ -329,8 +329,8 @@ html {
     opacity: .38;
   }
 
-  .dragover__top:after,
-  .dragover__bottom:after {
+  .dragover__top::after,
+  .dragover__bottom::after {
     content: "";
     width: var(--file-toggle-width);
     height: 4px;
@@ -341,7 +341,7 @@ html {
     z-index: 1;
   }
 
-  .dragover__top:after {
+  .dragover__top::after {
     top: -3px;
     bottom: auto;
   }

+ 21 - 7
app/src/assets/scss/business/_av.scss

@@ -29,7 +29,7 @@
     background: var(--b3-border-color);
     border-radius: var(--b3-border-radius);
 
-    &:after {
+    &::after {
       content: " ";
       border-radius: var(--b3-border-radius);
       position: absolute;
@@ -125,14 +125,28 @@
     position: relative;
     font-size: 87.5%;
 
+    &.dragover__top,
     &.dragover__bottom {
-      border-bottom-color: var(--b3-theme-primary-lighter);
-      z-index: 3;
+      box-shadow: none !important;
     }
 
-    &.dragover__top {
+    &.dragover__top::after,
+    &.dragover__bottom::after {
+      content: '';
+      position: absolute;
+      left: 0;
+      right: 0;
+      height: 4px;
+      background-color: var(--b3-theme-primary-lighter);
       z-index: 3;
-      box-shadow: 0 -3px 0 var(--b3-theme-primary-lighter), inset 0 2px 0 var(--b3-theme-primary-lighter) !important;
+    }
+
+    &.dragover__top::after {
+      top: -2.5px;
+    }
+
+    &.dragover__bottom::after {
+      bottom: -2.5px;
     }
 
     &:hover [data-type="block-more"] {
@@ -179,7 +193,7 @@
 
       .av__calc {
         display: flex;
-        align-items: baseline;
+        align-items: center;
         padding: 5px;
         border-right: 1px;
         flex-direction: row-reverse;
@@ -188,6 +202,7 @@
         overflow: hidden;
         font-size: 75%;
         white-space: nowrap;
+        line-height: 1.625em;
         cursor: pointer;
 
         &[data-dtype="lineNumber"] {
@@ -485,7 +500,6 @@
         line-height: 20px;
         padding: 0 4px;
         font-size: 12px;
-        font-family: var(--b3-font-family-emoji);
         align-self: center;
         opacity: 1;
       }

+ 38 - 8
app/src/assets/scss/business/_custom.scss

@@ -2,15 +2,20 @@
 
   &__avvalue {
     cursor: pointer;
-    transition: var(--b3-transition);
+    transition: var(--b3-background-transition);
     border-radius: var(--b3-border-radius);
     padding: 4px 8px;
     line-height: 26px;
     min-height: 26px;
     flex-wrap: wrap;
 
-    &:hover {
-      background-color: var(--b3-theme-background);
+    &:not(.custom-attr__avvalue--readonly):hover,
+    &--active {
+      background-color: var(--b3-theme-surface-light);
+    }
+
+    &--readonly {
+      cursor: default;
     }
 
     .av__checkbox {
@@ -40,14 +45,20 @@
   .block__logo:not(.popover__block) {
     width: 160px;
     color: var(--b3-theme-on-surface);
+    border-radius: var(--b3-border-radius);
+    transition: var(--b3-background-transition);
+    min-height: 34px;
     align-self: flex-start;
-    margin-top: 5px;
 
     span {
       text-overflow: ellipsis;
       white-space: nowrap;
       overflow: hidden;
     }
+
+    &:hover {
+      background-color: var(--b3-theme-surface-light);
+    }
   }
 
   .block__icons {
@@ -65,16 +76,35 @@
         background-color: transparent;
       }
     }
+
+    [placeholder]:empty::after {
+      color: var(--b3-empty-color);
+      content: attr(placeholder);
+    }
+  }
+
+  .b3-button[data-type="addColumn"] {
+    margin-left: 28px;
+
+    svg {
+      width: 16px;
+      height: 16px;
+      margin-right: 8px;
+    }
+
+    &:hover {
+      background-color: var(--b3-theme-surface-light);
+    }
   }
 
   .b3-text-field--text {
-    transition: var(--b3-transition);
+    transition: var(--b3-background-transition);
     min-height: 34px;
     background-color: transparent;
     padding: 7px 8px;
 
     &:hover {
-      background-color: var(--b3-theme-background);
+      background-color: var(--b3-theme-surface-light);
     }
   }
 
@@ -86,7 +116,7 @@
     max-height: 24px;
   }
 
-  .dragover__top.av__row {
-    box-shadow: 0 -2px 0 var(--b3-theme-primary-lighter), inset 0 2px 0 var(--b3-theme-primary-lighter) !important;
+  .av__row.dragover__top::after {
+    top: -1.5px;
   }
 }

+ 12 - 1
app/src/assets/scss/business/_drag.scss

@@ -1,5 +1,16 @@
 .dragover {
-  background-color: var(--b3-theme-primary-lightest) !important;
+
+  &::after {
+    background-color: var(--b3-theme-primary-lightest);
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    content: " ";
+    left: 0;
+    top: 0;
+    z-index: 3;
+    pointer-events: none;
+  }
 
   // 需要 !important,否则拖拽到闪卡无效果
   &__top {

+ 1 - 1
app/src/assets/scss/business/_export.scss

@@ -7,7 +7,7 @@
     position: relative;
     z-index: 1;
 
-    [data-node-id].li[fold="1"] > .protyle-action:after {
+    [data-node-id].li[fold="1"] > .protyle-action::after {
       background-color: transparent;
       border: 1px solid var(--b3-list-hover);
       box-sizing: border-box;

+ 5 - 0
app/src/assets/scss/business/_history.scss

@@ -1,4 +1,9 @@
 .history {
+  &__action {
+    overflow: auto;
+    border-bottom: 1px solid var(--b3-border-color);
+  }
+
   &__text {
     background-color: var(--b3-theme-background);
     padding: 16px;

+ 8 - 8
app/src/assets/scss/business/_layout.scss

@@ -2,7 +2,7 @@
   overflow-y: hidden; // https://github.com/siyuan-note/siyuan/issues/6706
 
   &__wnd--active .layout-tab-bar .item--focus {
-    &:after {
+    &::after {
       background-color: var(--b3-theme-primary);
     }
 
@@ -111,11 +111,11 @@
     position: relative;
     z-index: 4; // 需大于 #sidebarContainer https://github.com/siyuan-note/siyuan/issues/11759
 
-    &:hover:after {
+    &:hover::after {
       background-color: var(--b3-scroll-color);
     }
 
-    &:after {
+    &::after {
       content: "";
       width: 100%;
       height: .5px;
@@ -133,7 +133,7 @@
       margin: 0 -6px 0 0;
       height: auto;
 
-      &:after {
+      &::after {
         top: 0;
         width: .5px;
         left: 0;
@@ -226,7 +226,7 @@
           max-width: none;
           flex: 1;
 
-          &.item--focus:after {
+          &.item--focus::after {
             background-color: var(--b3-theme-primary);
           }
         }
@@ -268,7 +268,7 @@
           }
         }
 
-        &--focus:after {
+        &--focus::after {
           content: "";
           width: 100%;
           height: 3px;
@@ -384,12 +384,12 @@
     padding: 0;
 
     .dock__item {
-      &[data-index="0"]:after {
+      &[data-index="0"]::after {
         top: -32px;
         bottom: auto;
       }
 
-      &[data-index="1"]:after {
+      &[data-index="1"]::after {
         bottom: 100%;
       }
     }

+ 3 - 3
app/src/assets/scss/business/_search.scss

@@ -24,7 +24,7 @@
         margin: 0 -6px 0 0;
         height: auto;
 
-        &:after {
+        &::after {
           width: .5px;
           height: 100%;
         }
@@ -104,7 +104,7 @@
     position: relative;
     z-index: 2;
 
-    &:after {
+    &::after {
       content: "";
       display: block;
       background-color: var(--b3-theme-surface-lighter);
@@ -116,7 +116,7 @@
       height: .5px;
     }
 
-    &:hover:after {
+    &:hover::after {
       background-color: var(--b3-scroll-color);
     }
   }

+ 13 - 2
app/src/assets/scss/component/_button.scss

@@ -66,8 +66,7 @@
     font-size: 16px;
   }
 
-  &--text,
-  &--cancel {
+  &--text {
     color: var(--b3-theme-primary);
     background-color: transparent;
 
@@ -85,6 +84,18 @@
 
   &--cancel {
     color: var(--b3-theme-on-surface);
+    background-color: transparent;
+
+    &:hover,
+    &:focus {
+      background-color: var(--b3-list-hover);
+      box-shadow: none;
+    }
+
+    &:active {
+      background-color: var(--b3-list-icon-hover);
+      box-shadow: none;
+    }
   }
 
   &--outline {

+ 1 - 0
app/src/assets/scss/component/_dialog.scss

@@ -54,6 +54,7 @@
   }
 
   &__header {
+    cursor: default;
     padding: 9px 24px;
     line-height: 24px;
     font-size: 16px;

+ 2 - 2
app/src/assets/scss/component/_list.scss

@@ -8,7 +8,7 @@
     border-radius: var(--b3-border-radius);
 
     &:hover:not(.b3-list-item--focus):not(.dragover):not(.dragover__top):not(.dragover__bottom),
-    &--focus:not(.dragover) {
+    &--focus:not(.dragover):not(.dragover__top):not(.dragover__bottom) {
       background-color: var(--b3-list-hover);
     }
   }
@@ -27,7 +27,7 @@
       position: relative;
     }
 
-    .b3-list__panel:before {
+    .b3-list__panel::before {
       content: "";
       height: 100%;
       top: 0;

+ 1 - 1
app/src/assets/scss/component/_snackbar.scss

@@ -41,7 +41,7 @@
   &--error .b3-snackbar__content {
     padding-left: 47px;
 
-    &:after {
+    &::after {
       content: "❗";
       position: absolute;
       left: 16px;

+ 21 - 19
app/src/assets/scss/component/_switch.scss

@@ -24,7 +24,7 @@
     align-self: center;
   }
 
-  &:after {
+  &::after {
     width: 8px;
     height: 8px;
     border-radius: 50%;
@@ -40,7 +40,7 @@
     transition: left 80ms linear, background-color 80ms linear, width 80ms linear, height 80ms linear;
   }
 
-  &:before {
+  &::before {
     z-index: 2;
     content: "";
     left: 7px;
@@ -59,28 +59,18 @@
     background-color: var(--b3-switch-checked-background);
     border-color: transparent;
 
-    &:after {
+    &::after {
       background-color: var(--b3-switch-checked);
       height: 12px;
       width: 12px;
     }
 
-    &:before,
-    &:after {
+    &::before,
+    &::after {
       left: 17px;
     }
 
-    &:hover:not(:disabled) {
-      &:after {
-        background-color: var(--b3-switch-checked-hover);
-      }
-
-      &:before {
-        background-color: var(--b3-switch-checked-hover2);
-      }
-    }
-
-    &:active:not(:disabled):after {
+    &:active:not(:disabled)::after {
       height: 14px;
       width: 14px;
     }
@@ -91,12 +81,24 @@
     cursor: auto;
   }
 
-  &:active:not(:disabled):after {
+  &:active:not(:disabled)::after {
     height: 10px;
     width: 10px;
   }
+}
+
+label:hover .b3-switch:not(:disabled)::before,
+.b3-switch:hover:not(:disabled)::before {
+  display: inline-block;
+}
+
+label:hover .b3-switch:checked:not(:disabled),
+.b3-switch:checked:hover:not(:disabled) {
+  &::after {
+    background-color: var(--b3-switch-checked-hover);
+  }
 
-  &:hover:not(:disabled):before {
-    display: inline-block;
+  &::before {
+    background-color: var(--b3-switch-checked-hover2);
   }
 }

+ 2 - 2
app/src/assets/scss/component/_tooltips.scss

@@ -197,8 +197,8 @@
 }
 
 @media screen and (max-width: 520px) {
-  .b3-tooltips:before,
-  .b3-tooltips:after {
+  .b3-tooltips::before,
+  .b3-tooltips::after {
     content: none;
   }
 }

+ 12 - 4
app/src/assets/scss/component/_typography.scss

@@ -169,7 +169,7 @@
 
   blockquote,
   .bq {
-    &:before {
+    &::before {
       content: '';
       background-color: var(--b3-theme-surface-lighter);
       width: .25em;
@@ -391,7 +391,7 @@
       display: flex;
       align-items: baseline; // https://ld246.com/article/1645933216334
 
-      &:before {
+      &::before {
         content: "";
         flex: 1;
       }
@@ -484,17 +484,25 @@
       position: relative;
       display: inline-block;
       min-width: 22px;
+
+      &[style^=width] img {
+        width: 100%;
+      }
     }
 
     .protyle-action__title {
-      width: 100%;
-      display: block;
+      display: flex;
       font-size: 85%;
       color: var(--b3-theme-on-surface);
       word-break: break-word;
       white-space: break-spaces;
       line-height: normal;
       padding: 4px 0;
+
+      & > span {
+        flex-grow: 1;
+        width: 0;
+      }
     }
   }
 

+ 2 - 2
app/src/assets/scss/pdf/_pdf.scss

@@ -234,7 +234,7 @@
   cursor: ew-resize;
   right: -6px;
 
-  &:after {
+  &::after {
     content: "";
     width: 1px;
     height: 100%;
@@ -244,7 +244,7 @@
     left: 3px;
   }
 
-  &:hover:after {
+  &:hover::after {
     background-color: var(--b3-scroll-color);
   }
 }

+ 3 - 3
app/src/assets/scss/protyle/_protyle.scss

@@ -390,7 +390,7 @@
     padding: 0 4px 0 2px;
     white-space: break-spaces;
 
-    &:empty:after {
+    &:empty::after {
       content: attr(data-tip);
       color: var(--b3-theme-on-surface-light);
       cursor: text;
@@ -420,11 +420,11 @@ table[contenteditable="true"] + .protyle-action__table {
     cursor: col-resize;
     z-index: 2;
 
-    &:hover:after {
+    &:hover::after {
       background-color: var(--b3-theme-primary-light);
     }
 
-    &:after {
+    &::after {
       top: 0;
       width: 2px;
       left: 2px;

+ 34 - 43
app/src/assets/scss/protyle/_wysiwyg.scss

@@ -5,7 +5,7 @@
   user-select: auto;
   overflow-x: clip;
 
-  &--empty:empty:before {
+  &--empty:empty::before {
     color: var(--b3-empty-color);
     content: attr(placeholder);
   }
@@ -18,6 +18,16 @@
     outline: none;
   }
 
+  &::highlight(search-mark) {
+    background-color: var(--b3-protyle-inline-mark-background);
+    color: var(--b3-protyle-inline-mark-color);
+  }
+
+  &::highlight(search-mark-hl) {
+    background-color: var(--b3-theme-primary-lighter);
+    box-shadow: 0 0 0 .5px var(--b3-theme-on-background);
+  }
+
   [data-node-id] {
     position: relative;
 
@@ -43,7 +53,7 @@
         position: relative;
         height: 26px;
 
-        &:after {
+        &::after {
           position: absolute;
           content: " ";
           height: 1px;
@@ -55,18 +65,6 @@
       }
     }
 
-    .bq {
-      .dragover {
-        &__top:not(.av__row) {
-          box-shadow: 0 -3px 0 var(--b3-theme-primary-lighter), inset 0 1px 0 var(--b3-theme-primary-lighter) !important;
-        }
-
-        &__bottom:not(.av__row) {
-          box-shadow: 0 3px 0 var(--b3-theme-primary-lighter), inset 0 -1px 0 var(--b3-theme-primary-lighter) !important;
-        }
-      }
-    }
-
     &.list {
       padding: 0;
       display: flex;
@@ -86,14 +84,14 @@
       display: flex;
       flex-direction: column;
 
-      &:before {
+      &::before {
         content: "";
         position: absolute;
         border-left: .5px solid var(--b3-theme-background-light);
         left: 17px;
       }
 
-      &:hover:before {
+      &:hover::before {
         border-left-color: var(--b3-scroll-color);
       }
 
@@ -111,11 +109,11 @@
       }
 
       &[fold="1"] {
-        &:before {
+        &::before {
           content: none;
         }
 
-        & > .protyle-action:after {
+        & > .protyle-action::after {
           background-color: var(--b3-list-hover);
         }
 
@@ -147,7 +145,7 @@
           padding-bottom: 0;
         }
 
-        &:after {
+        &::after {
           content: "";
           position: absolute;
           border-radius: 50%;
@@ -163,7 +161,7 @@
           position: relative;
         }
 
-        &--order:after {
+        &--order::after {
           border-radius: var(--b3-border-radius);
         }
       }
@@ -177,11 +175,11 @@
         display: flex;
         flex-wrap: wrap;
         justify-content: space-between;
+        column-gap: 1.5em;
 
         & > div {
-          flex: 1;
+          flex: 1 0 10%;
           box-sizing: border-box;
-          margin-right: 24px;
           min-width: 1px;
 
           &:nth-last-child(2),
@@ -334,7 +332,7 @@
         filter: brightness(.68);
       }
 
-      &--drag > span:after {
+      &--drag > span::after {
         content: "";
         position: absolute;
         top: 0;
@@ -363,8 +361,8 @@
           box-shadow: 0 0 1px 1px var(--b3-theme-on-background);
         }
 
-        &:before,
-        &:after {
+        &::before,
+        &::after {
           position: absolute;
           width: 4px;
           content: "";
@@ -372,7 +370,7 @@
           left: -4px;
         }
 
-        &:after {
+        &::after {
           left: 4px;
           width: 8px
         }
@@ -409,7 +407,7 @@
       }
 
       &--drag {
-        .iframe-content:after {
+        .iframe-content::after {
           content: "";
           position: absolute;
           top: 0;
@@ -433,7 +431,7 @@
         box-shadow: 0 0 1px 1px var(--b3-theme-on-surface);
         box-sizing: border-box;
 
-        &:after {
+        &::after {
           content: "";
           background-color: var(--b3-theme-surface);
           width: 32px;
@@ -449,7 +447,7 @@
         }
 
         &:hover,
-        &:hover:after {
+        &:hover::after {
           background-color: var(--b3-theme-background);
           box-shadow: 0 0 1px 1px var(--b3-theme-on-background);
         }
@@ -459,17 +457,10 @@
 
   &--select,
   &--hl {
-    &:after {
-      background-color: var(--b3-theme-primary-lightest);
-      position: absolute;
-      width: 100%;
-      height: 100%;
-      content: " ";
-      left: 0;
-      top: 0;
+    @extend .dragover;
+
+    &::after {
       border-radius: var(--b3-border-radius);
-      z-index: 3;
-      pointer-events: none;
     }
   }
 
@@ -483,11 +474,11 @@
   }
 
   .dragover {
-    &__top:not(.av__row) {
+    &__top {
       box-shadow: 0 -4px 0 0 var(--b3-theme-primary-lighter) !important;
     }
 
-    &__bottom:not(.av__row) {
+    &__bottom {
       box-shadow: 0 4px 0 0 var(--b3-theme-primary-lighter) !important
     }
   }
@@ -515,7 +506,7 @@
     min-height: 26px !important;
   }
 
-  div[fold="1"][data-type="NodeHeading"]:before {
+  div[fold="1"][data-type="NodeHeading"]::before {
     content: "";
     height: 16px !important;
     width: 16px;
@@ -600,7 +591,7 @@
     cursor: pointer;
 
     &:hover {
-      &:after {
+      &::after {
         background-color: var(--b3-theme-background-light);
       }
 

+ 5 - 0
app/src/assets/scss/util/_responsive.scss

@@ -71,6 +71,11 @@
     &__panel {
       flex-direction: column;
 
+      & > .fn__flex-column {
+        min-height: auto;
+        border-radius: var(--b3-border-radius-b);
+      }
+
       & > .history__side {
         height: 40%;
         overflow: auto;

+ 3 - 3
app/src/block/Panel.ts

@@ -337,8 +337,8 @@ export class BlockPanel {
 
         this.element.querySelector(".block__content").addEventListener("scroll", () => {
             this.editors.forEach(item => {
-                hideElements(["gutter"], item.protyle)
-            })
-        })
+                hideElements(["gutter"], item.protyle);
+            });
+        });
     }
 }

+ 1 - 1
app/src/boot/globalEvent/click.ts

@@ -66,7 +66,7 @@ export const globalClick = (event: MouseEvent & { target: HTMLElement }) => {
     }
     const copyElement = hasTopClosestByClassName(event.target, "protyle-action__copy");
     if (copyElement) {
-        let text = copyElement.parentElement.nextElementSibling.textContent.replace("\n", "");
+        let text = copyElement.parentElement.nextElementSibling.textContent.replace(/\n$/, "");
         text = text.replace(/\u00A0/g, " "); // Replace non-breaking spaces with normal spaces when copying https://github.com/siyuan-note/siyuan/issues/9382
         writeText(text);
         showMessage(window.siyuan.languages.copied, 2000);

+ 1 - 1
app/src/boot/globalEvent/command/panel.ts

@@ -160,7 +160,7 @@ const filterList = (inputElement: HTMLInputElement, listElement: Element) => {
         const elementValue = element.querySelector(".b3-list-item__text").textContent.toLowerCase();
         const command = element.dataset.command;
         if (inputValue.indexOf(elementValue) > -1 || elementValue.indexOf(inputValue) > -1 ||
-            inputValue.indexOf(command) > -1 || command.indexOf(inputValue) > -1) {
+            inputValue.indexOf(command) > -1 || command?.indexOf(inputValue) > -1) {
             if (!hasFocus) {
                 element.classList.add("b3-list-item--focus");
             }

+ 1 - 1
app/src/card/openCard.ts

@@ -99,7 +99,7 @@ export const genCardHTML = (options: {
         <div>🔮</div>
         ${window.siyuan.languages.noDueCard}
     </div>
-    <div class="fn__flex card__action${options.cardsData.cards.length === 0 ? " fn__none" : ""}">
+    <div class="fn__flex card__action fn__none">
         <button class="b3-button b3-button--cancel" disabled="disabled" data-type="-2" style="width: 25%;min-width: 86px;display: flex">
             <svg><use xlink:href="#iconLeft"></use></svg>
             (p / q)

+ 1 - 1
app/src/config/about.ts

@@ -216,7 +216,7 @@ ${checkUpdateHTML}
         <span style="color:var(--b3-theme-background);font-family: cursive;">会泽百家&nbsp;至公天下</span>
     </div>
     <div class='fn__hr'></div>
-    ${window.siyuan.languages.about1}
+    ${window.siyuan.languages.about1} ${"harmony" === window.siyuan.config.system.container? " • " + window.siyuan.languages.feedback + " 845765@qq.com" : ""} 
 </div>`;
     },
     bindEvent: () => {

+ 18 - 0
app/src/config/keymap.ts

@@ -52,6 +52,24 @@ export const keymap = {
     </span>
     <span data-type="update" class="config-keymap__key">${keyValue}</span>
     <input data-key="plugin${Constants.ZWSP}${item.name}${Constants.ZWSP}${command.langKey}" data-value="${command.customHotkey}" data-default="${command.hotkey}" class="b3-text-field fn__none" value="${keyValue}" spellcheck="false">
+</label>`;
+            });
+            item.updateProtyleToolbar([]).forEach(toolbarItem => {
+                if (typeof toolbarItem === "string" || Constants.INLINE_TYPE.concat("|").includes(toolbarItem.name) || !toolbarItem.hotkey) {
+                    return;
+                }
+                const dockKeymap = window.siyuan.config.keymap.plugin[item.name][toolbarItem.name];
+                const keyValue = updateHotkeyTip(dockKeymap.custom);
+                commandHTML += `<label class="b3-list-item b3-list-item--narrow b3-list-item--hide-action">
+    <span class="b3-list-item__text">${toolbarItem.tip||window.siyuan.languages[toolbarItem.lang]}</span>
+    <span data-type="reset" class="b3-list-item__action b3-tooltips b3-tooltips__w" aria-label="${window.siyuan.languages.reset}">
+        <svg><use xlink:href="#iconUndo"></use></svg>
+    </span>
+    <span data-type="clear" class="b3-list-item__action b3-tooltips b3-tooltips__w" aria-label="${window.siyuan.languages.remove}">
+        <svg><use xlink:href="#iconTrashcan"></use></svg>
+    </span>
+    <span data-type="update" class="config-keymap__key">${keyValue}</span>
+    <input data-key="plugin${Constants.ZWSP}${item.name}${Constants.ZWSP}${toolbarItem.name}" data-value="${dockKeymap.custom}" data-default="${dockKeymap.default}" class="b3-text-field fn__none" value="${keyValue}" spellcheck="false">
 </label>`;
             });
             Object.keys(item.docks).forEach(key => {

+ 3 - 2
app/src/config/search.ts

@@ -86,7 +86,7 @@ export const initConfigSearch = (element: HTMLElement, app: App) => {
         getLang(["cloudStorage", "trafficStat", "sync", "backup", "cdn", "total", "sizeLimit", "cloudBackup",
             "cloudBackupTip", "updatePath", "cloudSync", "upload", "download", "syncMode", "syncModeTip",
             "generateConflictDoc", "generateConflictDocTip", "syncProvider", "syncProviderTip",
-            "syncMode1", "syncMode2", "reposTip", "openSyncTip1", "openSyncTip2", "cloudSyncDir", "config"]),
+            "syncMode1", "syncMode2", "reposTip", "openSyncTip1", "openSyncTip2", "cloudSyncDir", "cloudSyncDirTip", "config"]),
 
         // 发布
         getLang(["publishService", "publishServiceTip", "publishServicePort", "publishServicePortTip",
@@ -100,7 +100,8 @@ export const initConfigSearch = (element: HTMLElement, app: App) => {
             "systemLog", "importKey", "genKey", "genKeyByPW", "copyKey", "resetRepo", "systemLogTip", "export",
             "downloadLatestVer", "safeQuit", "directConnection", "siyuanNote", "key", "password", "copied", "resetRepoTip",
             "autoDownloadUpdatePkg", "autoDownloadUpdatePkgTip", "networkProxy", "keyPlaceholder", "initRepoKeyTip",
-            "googleAnalytics", "googleAnalyticsTip"]),
+            "googleAnalytics", "googleAnalyticsTip", "dataRepoPurge", "dataRepoPurgeTip", "dataRepoAutoPurgeIndexRetentionDays",
+            "dataRepoAutoPurgeRetentionIndexesDaily"]),
     ];
     const inputElement = element.querySelector(".b3-form__icon input") as HTMLInputElement;
     /// #if !BROWSER

+ 1 - 1
app/src/constants.ts

@@ -194,10 +194,10 @@ export abstract class Constants {
     public static readonly HELP_PATH = {
         zh_CN: "20210808180117-czj9bvb",
         zh_CHT: "20211226090932-5lcq56f",
+        ja_JP: "20240530133126-axarxgx",
         en_US: "20210808180117-6v0mkxr",
         fr_FR: "20210808180117-6v0mkxr",
         es_ES: "20210808180117-6v0mkxr",
-        ja_JP: "20240530133126-axarxgx",
         it_IT: "20210808180117-6v0mkxr",
         de_DE: "20210808180117-6v0mkxr",
         he_IL: "20210808180117-6v0mkxr",

+ 2 - 3
app/src/dialog/tooltip.ts

@@ -14,13 +14,12 @@ export const showTooltip = (message: string, target: Element, tooltipClass?: str
         document.body.insertAdjacentHTML("beforeend", `<div class="tooltip" id="tooltip">${message}</div>`);
         messageElement = document.getElementById("tooltip");
     } else {
+        messageElement.className = "tooltip";
         messageElement.innerHTML = message;
     }
 
     if (tooltipClass) {
         messageElement.classList.add("tooltip--" + tooltipClass);
-    } else {
-        messageElement.className = "tooltip";
     }
 
     let left = targetRect.left;
@@ -44,7 +43,7 @@ export const showTooltip = (message: string, target: Element, tooltipClass?: str
         left = parentRect.right + 8;
     } else if (position?.endsWith("parentW")) {
         // 数据库属性视图
-        top = parentRect.top + parseInt(position) || 8;
+        top = parentRect.top + (parseInt(position) || 8);
         left = parentRect.left - messageElement.clientWidth;
     }
 

+ 1 - 0
app/src/emoji/index.ts

@@ -62,6 +62,7 @@ ${unicode2Emoji(emoji.unicode)}</button>`;
                 });
                 entrie.target.innerHTML = html;
                 entrie.target.removeAttribute("data-index");
+                entrie.target.style.minHeight = "";
             }
         });
     });

+ 53 - 32
app/src/history/doc.ts

@@ -15,7 +15,6 @@ let isLoading = false;
 const renderDoc = (element: HTMLElement, currentPage: number, id: string) => {
     const previousElement = element.querySelector('[data-type="docprevious"]');
     const nextElement = element.querySelector('[data-type="docnext"]');
-    element.setAttribute("data-page", currentPage.toString());
     if (currentPage > 1) {
         previousElement.removeAttribute("disabled");
     } else {
@@ -23,8 +22,9 @@ const renderDoc = (element: HTMLElement, currentPage: number, id: string) => {
     }
     const opElement = element.querySelector('.b3-select[data-type="opselect"]') as HTMLSelectElement;
     const listElement = element.querySelector(".b3-list--background");
+    element.querySelector(".protyle-title__input").classList.add("fn__none");
     element.querySelector('.history__text[data-type="docPanel"]').classList.add("fn__none");
-    element.querySelector('.history__text[data-type="mdPanel"]').classList.remove("fn__none");
+    element.querySelector('.history__text[data-type="mdPanel"]').classList.add("fn__none");
     fetchPost("/api/history/searchHistory", {
         query: id,
         page: currentPage,
@@ -36,7 +36,16 @@ const renderDoc = (element: HTMLElement, currentPage: number, id: string) => {
         } else {
             nextElement.setAttribute("disabled", "disabled");
         }
-        nextElement.nextElementSibling.nextElementSibling.textContent = `${currentPage}/${response.data.pageCount || 1}`;
+        const pageNumElement = element.querySelector('[data-type="jumpRepoPage"]');
+        if (response.data.pageCount > 1) {
+            pageNumElement.removeAttribute("disabled");
+        } else {
+            pageNumElement.setAttribute("disabled", "disabled");
+        }
+        pageNumElement.setAttribute("data-totalpage", response.data.pageCount.toString());
+        pageNumElement.textContent = currentPage.toString();
+        const pageInfoElement = nextElement.nextElementSibling.nextElementSibling;
+        pageInfoElement.textContent = window.siyuan.languages.pageCountAndHistoryCount.replace("${x}", response.data.pageCount).replace("${y}", response.data.totalCount);
         if (response.data.histories.length === 0) {
             listElement.innerHTML = `<li class="b3-list--empty">${window.siyuan.languages.emptyContent}</li>`;
             return;
@@ -61,40 +70,38 @@ export const openDocHistory = (options: {
     notebookId: string,
     pathString: string
 }) => {
-    const contentHTML = `<div class="fn__flex fn__flex-1 history__panel">
+    const contentHTML = `<div class="history__action">
+    <div class="block__icons">
+        <span data-type="docprevious" class="block__icon block__icon--show b3-tooltips b3-tooltips__e" disabled="disabled" aria-label="${window.siyuan.languages.previousLabel}"><svg><use xlink:href="#iconLeft"></use></svg></span>
+        <button class="b3-button b3-button--text ft__selectnone" data-type="jumpRepoPage" disabled>1</button>
+        <span data-type="docnext" class="block__icon block__icon--show b3-tooltips b3-tooltips__e" disabled="disabled" aria-label="${window.siyuan.languages.nextLabel}"><svg><use xlink:href="#iconRight"></use></svg></span>
+        <span class="fn__space"></span>
+        <span class="ft__on-surface fn__flex-shrink ft__selectnone">${window.siyuan.languages.pageCountAndHistoryCount}</span>
+        <span class="fn__space"></span>
+        <div class="fn__flex-1"></div>
+        <select data-type="opselect" class="b3-select">
+            <option value="all" selected>${window.siyuan.languages.allOp}</option>
+            <option value="update">${window.siyuan.languages.historyUpdate}</option>
+            <option value="format">${window.siyuan.languages.historyFormat}</option>
+            <option value="sync">${window.siyuan.languages.historySync}</option>
+            <option value="replace">${window.siyuan.languages.historyReplace}</option>
+            <option value="outline">${window.siyuan.languages.historyOutline}</option>
+        </select>
+    </div>
+</div>
+<div class="fn__flex fn__flex-1 history__panel">
     <ul class="b3-list b3-list--background history__side" ${isMobile() ? "" : `style="width: ${window.siyuan.storage[Constants.LOCAL_HISTORY].sideDocWidth}"`}>
         <li class="b3-list--empty">${window.siyuan.languages.emptyContent}</li>
     </ul>
     <div class="history__resize"></div>
     <div class="fn__flex-1 fn__flex-column">
-        <div class="protyle-title__input ft__center ft__breakword"></div>
+        <div class="protyle-title__input fn__none ft__center ft__breakword"></div>
         <textarea class="fn__flex-1 history__text fn__none" readonly data-type="mdPanel"></textarea>
         <div class="fn__flex-1 history__text fn__none" style="padding: 0" data-type="docPanel"></div>
     </div>
 </div>`;
     const dialog = new Dialog({
-        title:`<div class="block__icons">
-            ${isMobile() ? "" : options.pathString}
-            <span class="fn__space"></span>
-            <div class="fn__flex-1"></div>
-            <select data-type="opselect" class="b3-select">
-                <option value="all" selected>${window.siyuan.languages.allOp}</option>
-                <option value="clean">${window.siyuan.languages.historyClean}</option>
-                <option value="update">${window.siyuan.languages.historyUpdate}</option>
-                <option value="delete">${window.siyuan.languages.historyDelete}</option>
-                <option value="format">${window.siyuan.languages.historyFormat}</option>
-                <option value="sync">${window.siyuan.languages.historySync}</option>
-                <option value="replace">${window.siyuan.languages.historyReplace}</option>
-                <option value="outline">${window.siyuan.languages.historyOutline}</option>
-            </select>
-            <span class="fn__space"></span>
-            <span data-type="docprevious" class="block__icon block__icon--show b3-tooltips b3-tooltips__s" disabled="disabled" aria-label="${window.siyuan.languages.previousLabel}"><svg><use xlink:href="#iconLeft"></use></svg></span>
-            <span class="fn__space"></span>
-            <span data-type="docnext" class="block__icon block__icon--show b3-tooltips b3-tooltips__s" disabled="disabled" aria-label="${window.siyuan.languages.nextLabel}"><svg><use xlink:href="#iconRight"></use></svg></span>
-            <span class="fn__space"></span>
-            <span>1/1</span>
-            ${isMobile() ? '<span class="fn__space"></span><span data-type="close" class="block__icon block__icon--show"><svg><use xlink:href="#iconClose"></use></svg></span>' : ""}
-        </div>`,
+        title: options.pathString,
         content: contentHTML,
         width: isMobile() ? "100vw" : "90vw",
         height: isMobile() ? "100vh" : "80vh",
@@ -127,13 +134,13 @@ export const openDocHistory = (options: {
         typewriterMode: false,
     });
     disabledProtyle(historyEditor.protyle);
+    const pageNumElement = dialog.element.querySelector('[data-type="jumpRepoPage"]');
+    const titleElement = dialog.element.querySelector(".protyle-title__input");
     dialog.element.addEventListener("click", (event) => {
         let target = event.target as HTMLElement;
         while (target && !target.isEqualNode(dialog.element)) {
             const type = target.getAttribute("data-type");
-            if (type === "close") {
-                dialog.destroy();
-            } else if (type === "rollback" && !isLoading) {
+            if (type === "rollback" && !isLoading) {
                 getHistoryPath(target.parentElement, opElement.value, options.id, (item) => {
                     const dataPath = item.path;
                     isLoading = false;
@@ -168,7 +175,8 @@ export const openDocHistory = (options: {
                                 action: [Constants.CB_GET_HISTORY, Constants.CB_GET_HTML],
                             });
                         }
-                        dialog.element.querySelector(".protyle-title__input").textContent = item.title;
+                        titleElement.textContent = item.title;
+                        titleElement.classList.remove("fn__none");
                         isLoading = false;
                     });
                     target.parentElement.querySelector(".b3-list-item--focus")?.classList.remove("b3-list-item--focus");
@@ -178,11 +186,24 @@ export const openDocHistory = (options: {
                 event.preventDefault();
                 break;
             } else if ((type === "docprevious" || type === "docnext") && target.getAttribute("disabled") !== "disabled") {
-                const currentPage = parseInt(dialog.element.getAttribute("data-page"));
+                const currentPage = parseInt(pageNumElement.textContent);
                 renderDoc(dialog.element, type === "docprevious" ? currentPage - 1 : currentPage + 1, options.id);
                 event.stopPropagation();
                 event.preventDefault();
                 break;
+            } else if (type === "jumpRepoPage") {
+                const totalPage = parseInt(target.getAttribute("data-totalpage") || "1");
+                confirmDialog(
+                    window.siyuan.languages.jumpToPage.replace("${x}", totalPage),
+                    `<input class="b3-text-field fn__block" type="number" min="1" max="${totalPage}" value="${pageNumElement.textContent}">`,
+                    (confirmD) => {
+                        const inputElement = confirmD.element.querySelector(".b3-text-field") as HTMLInputElement;
+                        if (inputElement.value === "") {
+                            return;
+                        }
+                        renderDoc(dialog.element, Math.max(1, Math.min(parseInt(inputElement.value), totalPage)), options.id);
+                    }
+                );
             }
             target = target.parentElement;
         }

+ 9 - 8
app/src/history/history.ts

@@ -354,7 +354,7 @@ export const openHistory = (app: App) => {
     </div>
     <div class="fn__flex-1 fn__flex" id="historyContainer">
         <div data-type="doc" class="history__repo fn__block" data-init="true">
-            <div style="overflow:auto;border-bottom: 1px solid var(--b3-border-color);">
+            <div class="history__action">
                 <div class="block__icons">
                     <span data-type="docprevious" class="block__icon block__icon--show b3-tooltips b3-tooltips__e" disabled="disabled" aria-label="${window.siyuan.languages.previousLabel}"><svg><use xlink:href='#iconLeft'></use></svg></span>
                     <button class="b3-button b3-button--text ft__selectnone" data-type="jumpHistoryPage" data-totalpage="1">1</button>
@@ -409,7 +409,7 @@ export const openHistory = (app: App) => {
             <li class="b3-list--empty">${window.siyuan.languages.emptyContent}</li>
         </ul>
         <div data-type="repo" class="fn__none history__repo">
-            <div style="overflow: auto"">
+            <div class="history__action">
                 <div class="block__icons">
                     <span data-type="previous" class="block__icon block__icon--show b3-tooltips b3-tooltips__e" disabled="disabled" aria-label="${window.siyuan.languages.previousLabel}"><svg><use xlink:href='#iconLeft'></use></svg></span>
                     <button class="b3-button b3-button--text ft__selectnone" data-type="jumpRepoPage" data-totalpage="1">1</button>
@@ -445,6 +445,7 @@ export const openHistory = (app: App) => {
             icon: "iconHistory",
             title: window.siyuan.languages.dataHistory,
             bindEvent(element) {
+                element.firstElementChild.setAttribute("style", "background-color:var(--b3-theme-background);height:100%");
                 bindEvent(app, element.firstElementChild);
             }
         });
@@ -839,9 +840,9 @@ const bindEvent = (app: App, element: Element, dialog?: Dialog) => {
                 if (totalPage > 1) {
                     confirmDialog(
                         window.siyuan.languages.jumpToPage.replace("${x}", totalPage),
-                        `<input style="width: 100%;" class="b3-text-field fn__flex-center" type="number" min="1" max="${totalPage}" value="${currentPage}">`,
-                        (dialog: Dialog) => {
-                            const inputElement = dialog.element.querySelector(".b3-text-field") as HTMLInputElement;
+                        `<input class="b3-text-field fn__block" type="number" min="1" max="${totalPage}" value="${currentPage}">`,
+                        (confirmD) => {
+                            const inputElement = confirmD.element.querySelector(".b3-text-field") as HTMLInputElement;
                             if (inputElement.value === "") {
                                 return;
                             }
@@ -858,9 +859,9 @@ const bindEvent = (app: App, element: Element, dialog?: Dialog) => {
                 if (totalPage > 1) {
                     confirmDialog(
                         window.siyuan.languages.jumpToPage.replace("${x}", totalPage),
-                        `<input style="width: 100%;" class="b3-text-field fn__flex-center" type="number" min="1" max="${totalPage}" value="${currentPage}">`,
-                        (dialog: Dialog) => {
-                            const inputElement = dialog.element.querySelector(".b3-text-field") as HTMLInputElement;
+                        `<input class="b3-text-field fn__block" type="number" min="1" max="${totalPage}" value="${currentPage}">`,
+                        (confirmD) => {
+                            const inputElement = confirmD.element.querySelector(".b3-text-field") as HTMLInputElement;
                             if (inputElement.value === "") {
                                 return;
                             }

+ 2 - 0
app/src/layout/dock/Backlink.ts

@@ -10,6 +10,7 @@ import {openFileById} from "../../editor/util";
 import {Protyle} from "../../protyle";
 import {MenuItem} from "../../menus/Menu";
 import {App} from "../../index";
+import {searchMarkRender} from "../../protyle/render/searchMarkRender";
 
 export class Backlink extends Model {
     public element: HTMLElement;
@@ -456,6 +457,7 @@ export class Backlink extends Model {
                     }
                 });
                 editor.protyle.notebookId = liElement.getAttribute("data-notebook-id");
+                searchMarkRender(editor.protyle, editor.protyle.wysiwyg.element.querySelectorAll('span[data-type~="search-mark"]'));
                 this.editors.push(editor);
             });
         }

+ 1 - 1
app/src/layout/dock/Files.ts

@@ -737,7 +737,7 @@ export class Files extends Model {
     }
 
     private updateDocInfo(data: IWebSocketData) {
-        const liElement = this.element.querySelector(`li[data-node-id="${data.data.rootID}"]`)
+        const liElement = this.element.querySelector(`li[data-node-id="${data.data.rootID}"]`);
         if (liElement) {
             liElement.setAttribute("data-count", data.data.subFileCount);
             liElement.querySelector(".ariaLabel")?.setAttribute("aria-label", this.genDocAriaLabel(data.data, escapeGreat));

+ 5 - 1
app/src/layout/dock/Inbox.ts

@@ -339,11 +339,15 @@ ${data.shorthandContent}
                     id: item
                 }, (response) => {
                     this.data[response.data.oId] = response.data;
+                    let md = response.data.shorthandMd;
+                    if ("" === md && "" === response.data.shorthandContent && "" != response.data.shorthandURL) {
+                        md = "[" + response.data.shorthandTitle + "](" + response.data.shorthandURL + ")";
+                    }
                     fetchPost("/api/filetree/createDoc", {
                         notebook: toNotebook[0],
                         path: pathPosix().join(getDisplayName(toPath[0], false, true), Lute.NewNodeID() + ".sy"),
                         title: replaceFileName(response.data.shorthandTitle),
-                        md: response.data.shorthandMd,
+                        md: md,
                     }, () => {
                         this.remove(item);
                     });

+ 2 - 1
app/src/menus/commonMenuItem.ts

@@ -184,6 +184,7 @@ export const openFileAttr = (attrs: IObject, focusName = "bookmark", protyle?: I
     });
     const dialog = new Dialog({
         width: isMobile() ? "92vw" : "50vw",
+        containerClassName: "b3-dialog__container--theme",
         height: "80vh",
         content: `<div class="fn__flex-column">
     <div class="layout-tab-bar fn__flex" style="flex-shrink:0;border-radius: var(--b3-border-radius-b) var(--b3-border-radius-b) 0 0">
@@ -234,7 +235,7 @@ export const openFileAttr = (attrs: IObject, focusName = "bookmark", protyle?: I
         <div data-type="custom" class="fn__none custom-attr">
            ${customHTML}
            <div class="b3-label">
-               <button data-action="addCustom" class="b3-button b3-button--outline">
+               <button data-action="addCustom" class="b3-button b3-button--cancel">
                    <svg><use xlink:href="#iconAdd"></use></svg>${window.siyuan.languages.addAttr}
                </button>
            </div>

+ 15 - 26
app/src/menus/protyle.ts

@@ -991,7 +991,7 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
     hideElements(["util", "toolbar", "hint"], protyle);
     const id = nodeElement.getAttribute("data-node-id");
     const imgElement = assetElement.querySelector("img");
-    const titleElement = assetElement.querySelector(".protyle-action__title") as HTMLElement;
+    const titleElement = assetElement.querySelector(".protyle-action__title span") as HTMLElement;
     const html = nodeElement.outerHTML;
     if (!protyle.disabled) {
         window.siyuan.menus.menu.append(new MenuItem({
@@ -1173,7 +1173,6 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
                 alignImgLeft(protyle, nodeElement, [assetElement], id, html);
             }
         }).element);
-        const width = imgElement.style.width.endsWith("vw") ? parseInt(imgElement.style.width) : 0;
         let rangeElement: HTMLInputElement;
         window.siyuan.menus.menu.append(new MenuItem({
             label: window.siyuan.languages.width,
@@ -1181,7 +1180,7 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
                 iconHTML: "",
                 type: "readonly",
                 label: `<div class="fn__flex-center">
-<input class="b3-text-field" value="${imgElement.style.width.endsWith("px") ? parseInt(imgElement.style.width) : ""}" type="number" style="margin: 4px" placeholder="${window.siyuan.languages.width}"> px
+<input class="b3-text-field" style="margin: 4px 8px 4px 0" value="${imgElement.parentElement.style.width.endsWith("px") ? parseInt(imgElement.parentElement.style.width) : ""}" type="number" placeholder="${window.siyuan.languages.width}">px
 </div>`,
                 bind(element) {
                     const inputElement = element.querySelector("input");
@@ -1189,11 +1188,11 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
                         rangeElement.value = "0";
                         rangeElement.parentElement.setAttribute("aria-label", inputElement.value ? (inputElement.value + "px") : window.siyuan.languages.default);
 
-                        imgElement.style.width = inputElement.value ? (inputElement.value + "px") : "";
+                        imgElement.parentElement.style.width = inputElement.value ? (inputElement.value + "px") : "";
                         imgElement.style.height = "";
                     });
                     inputElement.addEventListener("blur", () => {
-                        if (inputElement.value === imgElement.style.width.replace("px", "")) {
+                        if (inputElement.value === imgElement.parentElement.style.width.replace("px", "")) {
                             return;
                         }
                         nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
@@ -1213,13 +1212,14 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
                 }, {
                     iconHTML: "",
                     type: "readonly",
-                    label: `<div style="margin: 4px 0;" aria-label="${imgElement.style.width.endsWith("px") ? imgElement.style.width : (imgElement.style.width?.replace("vw", "%") || window.siyuan.languages.default)}" class="b3-tooltips b3-tooltips__n${isMobile() ? "" : " fn__size200"}">
-    <input style="box-sizing: border-box" value="${width}" class="b3-slider fn__block" max="100" min="1" step="1" type="range">
+                    label: `<div style="margin: 4px 0;" aria-label="${imgElement.parentElement.style.width ? imgElement.parentElement.style.width.replace("vw", "%") : window.siyuan.languages.default}" class="b3-tooltips b3-tooltips__n${isMobile() ? "" : " fn__size200"}">
+    <input style="box-sizing: border-box" value="${(imgElement.parentElement.style.width.endsWith("%")||imgElement.parentElement.style.width.endsWith("vw")) ? parseInt(imgElement.parentElement.style.width) : 0}" class="b3-slider fn__block" max="100" min="1" step="1" type="range">
 </div>`,
                     bind(element) {
                         rangeElement = element.querySelector("input");
                         rangeElement.addEventListener("input", () => {
-                            imgElement.style.width = rangeElement.value + "vw";
+                            imgElement.parentElement.style.width = rangeElement.value + "%";
+                            imgElement.style.height = "";
                             rangeElement.parentElement.setAttribute("aria-label", `${rangeElement.value}%`);
                         });
                         rangeElement.addEventListener("change", () => {
@@ -1236,14 +1236,13 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
             ]
         }).element);
         let rangeHeightElement: HTMLInputElement;
-        const height = imgElement.style.height.endsWith("vh") ? parseInt(imgElement.style.height) : 0;
         window.siyuan.menus.menu.append(new MenuItem({
             label: window.siyuan.languages.height,
             submenu: [{
                 iconHTML: "",
                 type: "readonly",
                 label: `<div class="fn__flex-center">
-<input class="b3-text-field" value="${imgElement.style.height.endsWith("px") ? parseInt(imgElement.style.height) : ""}" type="number" style="margin: 4px" placeholder="${window.siyuan.languages.height}"> px
+<input class="b3-text-field" value="${imgElement.style.height.endsWith("px") ? parseInt(imgElement.style.height) : ""}" type="number" style="margin: 4px 8px 4px 0" placeholder="${window.siyuan.languages.height}">px
 </div>`,
                 bind(element) {
                     const inputElement = element.querySelector("input");
@@ -1252,8 +1251,7 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
                         rangeHeightElement.parentElement.setAttribute("aria-label", inputElement.value ? (inputElement.value + "px") : window.siyuan.languages.default);
 
                         imgElement.style.height = inputElement.value ? (inputElement.value + "px") : "";
-                        assetElement.style.width = "";
-                        imgElement.style.width = "";
+                        imgElement.parentElement.style.width = "";
                     });
                     inputElement.addEventListener("blur", () => {
                         if (inputElement.value === imgElement.style.height.replace("px", "")) {
@@ -1276,14 +1274,13 @@ export const imgMenu = (protyle: IProtyle, range: Range, assetElement: HTMLEleme
                 }, {
                     iconHTML: "",
                     type: "readonly",
-                    label: `<div style="margin: 4px 0;" aria-label="${imgElement.style.height.endsWith("vh") ? parseInt(imgElement.style.height) + "%" : (imgElement.style.height || window.siyuan.languages.default)}" class="b3-tooltips b3-tooltips__n${isMobile() ? "" : " fn__size200"}">
-    <input style="box-sizing: border-box" value="${height}" class="b3-slider fn__block" max="100" min="1" step="1" type="range">
+                    label: `<div style="margin: 4px 0;" aria-label="${imgElement.style.height ? imgElement.style.height.replace("vh", "%") : window.siyuan.languages.default}" class="b3-tooltips b3-tooltips__n${isMobile() ? "" : " fn__size200"}">
+    <input style="box-sizing: border-box" value="${imgElement.style.height.endsWith("vh") ? parseInt(imgElement.style.height) : 0}" class="b3-slider fn__block" max="100" min="1" step="1" type="range">
 </div>`,
                     bind(element) {
                         rangeHeightElement = element.querySelector("input");
                         rangeHeightElement.addEventListener("input", () => {
-                            assetElement.style.width = "";
-                            imgElement.style.width = "";
+                            imgElement.parentElement.style.width = "";
                             imgElement.style.height = rangeHeightElement.value + "vh";
                             rangeHeightElement.parentElement.setAttribute("aria-label", `${rangeHeightElement.value}%`);
                         });
@@ -1823,11 +1820,7 @@ const genImageWidthMenu = (label: string, imgElement: HTMLElement, protyle: IPro
         label,
         click() {
             nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
-            if (label === window.siyuan.languages.default) {
-                imgElement.style.width = "";
-            } else {
-                imgElement.style.width = label.replace("%", "vw");
-            }
+            imgElement.parentElement.style.width = label === window.siyuan.languages.default ? "" : label;
             imgElement.style.height = "";
             updateTransaction(protyle, id, nodeElement.outerHTML, html);
             focusBlock(nodeElement);
@@ -1841,11 +1834,7 @@ const genImageHeightMenu = (label: string, imgElement: HTMLElement, protyle: IPr
         label,
         click() {
             nodeElement.setAttribute("updated", dayjs().format("YYYYMMDDHHmmss"));
-            if (label === window.siyuan.languages.default) {
-                imgElement.style.height = "";
-            } else {
-                imgElement.style.height = parseInt(label) + "vh";
-            }
+            imgElement.style.height = label === window.siyuan.languages.default ? "" : parseInt(label) + "vh";
             imgElement.style.width = "";
             updateTransaction(protyle, id, nodeElement.outerHTML, html);
             focusBlock(nodeElement);

+ 4 - 1
app/src/menus/util.ts

@@ -172,9 +172,12 @@ export const openEditorTab = (app: App, ids: string[], notebookId?: string, path
 };
 
 export const copyPNGByLink = (link: string) => {
-    if (isInAndroid() || isInHarmony()) {
+    if (isInAndroid()) {
         window.JSAndroid.writeImageClipboard(link);
         return;
+    } else if (isInHarmony()) {
+        window.JSHarmony.writeImageClipboard(link);
+        return;
     } else {
         const canvas = document.createElement("canvas");
         const tempElement = document.createElement("img");

+ 1 - 1
app/src/mobile/index.ts

@@ -34,7 +34,7 @@ class App {
     public appId: string;
 
     constructor() {
-        if (!window.webkit?.messageHandlers && !window.JSAndroid) {
+        if (!window.webkit?.messageHandlers && !window.JSAndroid && !window.JSHarmony) {
             registerServiceWorker(`${Constants.SERVICE_WORKER_PATH}?v=${Constants.SIYUAN_VERSION}`);
         }
         addBaseURL();

+ 1 - 1
app/src/mobile/settings/about.ts

@@ -169,7 +169,7 @@ export const initAbout = () => {
         </div>
     </div>
     <div style="color:var(--b3-theme-surface);font-family: cursive;">会泽百家&nbsp;至公天下</div>
-    ${window.siyuan.languages.about1}
+    ${window.siyuan.languages.about1} ${"harmony" === window.siyuan.config.system.container? " • " + window.siyuan.languages.feedback + " 845765@qq.com" : ""}
 </div>
 </div>`,
         bindEvent(modelMainElement: HTMLElement) {

+ 24 - 0
app/src/plugin/index.ts

@@ -15,6 +15,7 @@ import {hasClosestByAttribute} from "../protyle/util/hasClosest";
 import {BlockPanel} from "../block/Panel";
 import {Setting} from "./Setting";
 import {clearOBG} from "../layout/dock/util";
+import {Constants} from "../constants";
 
 export class Plugin {
     private app: App;
@@ -75,6 +76,29 @@ export class Plugin {
             value: options.name,
             writable: false,
         });
+
+        this.updateProtyleToolbar([]).forEach(toolbarItem => {
+            if (typeof toolbarItem === "string" || Constants.INLINE_TYPE.concat("|").includes(toolbarItem.name) || !toolbarItem.hotkey) {
+                return;
+            }
+            if (!window.siyuan.config.keymap.plugin) {
+                window.siyuan.config.keymap.plugin = {};
+            }
+            if (!window.siyuan.config.keymap.plugin[options.name]) {
+                window.siyuan.config.keymap.plugin[options.name] = {
+                    [toolbarItem.name]: {
+                        default: toolbarItem.hotkey,
+                        custom: toolbarItem.hotkey,
+                    }
+                };
+            }
+            if (!window.siyuan.config.keymap.plugin[options.name][toolbarItem.name]) {
+                window.siyuan.config.keymap.plugin[options.name][toolbarItem.name] = {
+                    default: toolbarItem.hotkey,
+                    custom: toolbarItem.hotkey,
+                };
+            }
+        });
     }
 
     public onload() {

+ 3 - 2
app/src/protyle/export/index.ts

@@ -64,7 +64,7 @@ export const saveExport = (option: IExportOptions) => {
             wordDialog.destroy();
         });
     } else {
-        getExportPath(option);
+        getExportPath(option, false, true);
     }
     /// #endif
 };
@@ -441,7 +441,8 @@ const renderPDF = async (id: string) => {
                     const linkAddress = target.getAttribute("href");
                     if (linkAddress.startsWith("#")) {
                         // 导出预览模式点击块引转换后的脚注跳转不正确 https://github.com/siyuan-note/siyuan/issues/5700
-                        previewElement.querySelector(linkAddress).scrollIntoView();
+                        const hash = linkAddress.substring(1);
+                        previewElement.querySelector('[data-node-id="' + hash + '"], [id="' + hash + '"]').scrollIntoView();
                         event.stopPropagation();
                         event.preventDefault();
                         return;

+ 6 - 2
app/src/protyle/gutter/index.ts

@@ -1653,8 +1653,10 @@ export class Gutter {
                 label: `${window.siyuan.languages.copy} ${window.siyuan.languages.headings1}`,
                 click() {
                     fetchPost("/api/block/getHeadingChildrenDOM", {id}, (response) => {
-                        if (isInAndroid() || isInHarmony()) {
+                        if (isInAndroid()) {
                             window.JSAndroid.writeHTMLClipboard(protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
+                        } else if (isInHarmony()) {
+                            window.JSHarmony.writeHTMLClipboard(protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
                         } else {
                             writeText(response.data + Constants.ZWSP);
                         }
@@ -1667,8 +1669,10 @@ export class Gutter {
                 label: `${window.siyuan.languages.cut} ${window.siyuan.languages.headings1}`,
                 click() {
                     fetchPost("/api/block/getHeadingChildrenDOM", {id}, (response) => {
-                        if (isInAndroid() || isInHarmony()) {
+                        if (isInAndroid()) {
                             window.JSAndroid.writeHTMLClipboard(protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
+                        } else if (isInHarmony()) {
+                            window.JSHarmony.writeHTMLClipboard(protyle.lute.BlockDOM2StdMd(response.data).trimEnd(), response.data + Constants.ZWSP);
                         } else {
                             writeText(response.data + Constants.ZWSP);
                         }

+ 6 - 1
app/src/protyle/header/Title.ts

@@ -12,7 +12,7 @@ import {MenuItem} from "../../menus/Menu";
 import {openFileAttr,} from "../../menus/commonMenuItem";
 import {Constants} from "../../constants";
 import {matchHotKey} from "../util/hotKey";
-import {isMac, readText, writeText} from "../util/compatibility";
+import {isMac, readText} from "../util/compatibility";
 import * as dayjs from "dayjs";
 import {openFileById} from "../../editor/util";
 import {setTitle} from "../../dialog/processSystem";
@@ -25,6 +25,7 @@ import {hideTooltip} from "../../dialog/tooltip";
 import {commonClick} from "../wysiwyg/commonClick";
 import {openTitleMenu} from "./openTitleMenu";
 import {electronUndo} from "../undo";
+import {enableLuteMarkdownSyntax, restoreLuteMarkdownSyntax} from "../util/paste";
 
 export class Title {
     public element: HTMLElement;
@@ -77,7 +78,9 @@ export class Title {
                 navigator.clipboard.readText().then(textPlain => {
                     // 对 HTML 标签进行内部转义,避免被 Lute 解析以后变为小写 https://github.com/siyuan-note/siyuan/issues/10620
                     textPlain = textPlain.replace(/</g, ";;;lt;;;").replace(/>/g, ";;;gt;;;");
+                    enableLuteMarkdownSyntax(protyle);
                     let content = protyle.lute.BlockDOM2EscapeMarkerContent(protyle.lute.Md2BlockDOM(textPlain));
+                    restoreLuteMarkdownSyntax(protyle);
                     // 移除 ;;;lt;;; 和 ;;;gt;;; 转义及其包裹的内容
                     content = content.replace(/;;;lt;;;[^;]+;;;gt;;;/g, "");
                     document.execCommand("insertText", false, replaceFileName(content));
@@ -229,7 +232,9 @@ export class Title {
                 click: async () => {
                     navigator.clipboard.readText().then(textPlain => {
                         textPlain = textPlain.replace(/</g, ";;;lt;;;").replace(/>/g, ";;;gt;;;");
+                        enableLuteMarkdownSyntax(protyle);
                         let content = protyle.lute.BlockDOM2EscapeMarkerContent(protyle.lute.Md2BlockDOM(textPlain));
+                        restoreLuteMarkdownSyntax(protyle);
                         // 移除 ;;;lt;;; 和 ;;;gt;;; 转义及其包裹的内容
                         content = content.replace(/;;;lt;;;[^;]+;;;gt;;;/g, "");
                         document.execCommand("insertText", false, replaceFileName(content));

+ 6 - 0
app/src/protyle/index.ts

@@ -72,6 +72,12 @@ export class Protyle {
             element: id,
             options: mergedOptions,
             block: {},
+            highlight: {
+                mark: new Highlight(),
+                markHL: new Highlight(),
+                ranges: [],
+                rangeIndex: 0,
+            }
         };
 
         this.protyle.hint = new Hint(this.protyle);

+ 2 - 1
app/src/protyle/preview/index.ts

@@ -77,7 +77,8 @@ export class Preview {
                     const linkAddress = target.getAttribute("href");
                     if (linkAddress.startsWith("#")) {
                         // 导出预览模式点击块引转换后的脚注跳转不正确 https://github.com/siyuan-note/siyuan/issues/5700
-                        previewElement.querySelector(linkAddress).scrollIntoView();
+                        const hash = linkAddress.substring(1);
+                        previewElement.querySelector('[data-node-id="' + hash + '"], [id="' + hash + '"]').scrollIntoView();
                         event.stopPropagation();
                         event.preventDefault();
                         break;

+ 3 - 3
app/src/protyle/render/av/action.ts

@@ -212,9 +212,9 @@ export const avClick = (protyle: IProtyle, event: MouseEvent & { target: HTMLEle
                 if (!rowElement) {
                     return;
                 }
-                const type = getTypeByCellElement(target);
+                const cellType = getTypeByCellElement(target);
                 // TODO 点击单元格的时候, lineNumber 选中整行
-                if (type === "updated" || type === "created" || type === "lineNumber" || (type === "block" && !target.getAttribute("data-detached"))) {
+                if (cellType === "updated" || cellType === "created" || cellType === "lineNumber" || (cellType === "block" && !target.getAttribute("data-detached"))) {
                     selectRow(rowElement.querySelector(".av__firstcol"), "toggle");
                 } else {
                     scrollElement.querySelectorAll(".av__row--select").forEach(item => {
@@ -229,7 +229,7 @@ export const avClick = (protyle: IProtyle, event: MouseEvent & { target: HTMLEle
             event.stopPropagation();
             return true;
         } else if (target.classList.contains("av__calc")) {
-            openCalcMenu(protyle, target);
+            openCalcMenu(protyle, target, undefined, event.clientX - 64);
             event.preventDefault();
             event.stopPropagation();
             return true;

+ 52 - 21
app/src/protyle/render/av/blockAttr.ts

@@ -9,8 +9,12 @@ import {transaction} from "../../wysiwyg/transaction";
 import {openMenuPanel} from "./openMenuPanel";
 import {uploadFiles} from "../../upload";
 import {openLink} from "../../../editor/openLink";
-import {editAssetItem} from "./asset";
+import {dragUpload, editAssetItem} from "./asset";
 import {previewImage} from "../../preview/image";
+/// #if !BROWSER
+import {webUtils} from "electron";
+/// #endif
+import {isBrowser} from "../../../util/functions";
 
 const genAVRollupHTML = (value: IAVCellValue) => {
     let html = "";
@@ -56,13 +60,13 @@ export const genAVValueHTML = (value: IAVCellValue) => {
     let html = "";
     switch (value.type) {
         case "block":
-            html = `<div class="fn__flex-1">${value.block.content}</div>`;
+            html = `<div class="fn__flex-1" placeholder="${window.siyuan.languages.empty}">${value.block.content}</div>`;
             break;
         case "text":
-            html = `<textarea style="resize: vertical" rows="${value.text.content.split("\n").length}" class="b3-text-field b3-text-field--text fn__flex-1">${value.text.content}</textarea>`;
+            html = `<textarea style="resize: vertical" rows="${value.text.content.split("\n").length}" class="b3-text-field b3-text-field--text fn__flex-1" placeholder="${window.siyuan.languages.empty}">${value.text.content}</textarea>`;
             break;
         case "number":
-            html = `<input value="${value.number.isNotEmpty ? value.number.content : ""}" type="number" class="b3-text-field b3-text-field--text fn__flex-1">
+            html = `<input value="${value.number.isNotEmpty ? value.number.content : ""}" type="number" class="b3-text-field b3-text-field--text fn__flex-1" placeholder="${window.siyuan.languages.empty}">
 <span class="fn__space"></span><span class="fn__flex-center ft__on-surface b3-tooltips__w b3-tooltips" aria-label="${window.siyuan.languages.format}">${value.number.formattedContent}</span><span class="fn__space"></span>`;
             break;
         case "mSelect":
@@ -84,7 +88,7 @@ export const genAVValueHTML = (value: IAVCellValue) => {
             });
             break;
         case "date":
-            html = `<span class="av__celltext" data-value='${JSON.stringify(value[value.type])}'>`;
+            html = `<span class="av__celltext" data-value='${JSON.stringify(value[value.type])}' placeholder="${window.siyuan.languages.empty}">`;
             if (value[value.type] && value[value.type].isNotEmpty) {
                 html += dayjs(value[value.type].content).format(value[value.type].isNotTime ? "YYYY-MM-DD" : "YYYY-MM-DD HH:mm");
             }
@@ -100,12 +104,12 @@ export const genAVValueHTML = (value: IAVCellValue) => {
             }
             break;
         case "url":
-            html = `<input value="${value.url.content}" class="b3-text-field b3-text-field--text fn__flex-1">
+            html = `<input value="${value.url.content}" class="b3-text-field b3-text-field--text fn__flex-1" placeholder="${window.siyuan.languages.empty}">
 <span class="fn__space"></span>
 <a href="${value.url.content}" target="_blank" aria-label="${window.siyuan.languages.openBy}" class="block__icon block__icon--show fn__flex-center b3-tooltips__w b3-tooltips"><svg><use xlink:href="#iconLink"></use></svg></a>`;
             break;
         case "phone":
-            html = `<input value="${value.phone.content}" class="b3-text-field b3-text-field--text fn__flex-1">
+            html = `<input value="${value.phone.content}" class="b3-text-field b3-text-field--text fn__flex-1" placeholder="${window.siyuan.languages.empty}">
 <span class="fn__space"></span>
 <a href="tel:${value.phone.content}" target="_blank" aria-label="${window.siyuan.languages.openBy}" class="block__icon block__icon--show fn__flex-center b3-tooltips__w b3-tooltips"><svg><use xlink:href="#iconPhone"></use></svg></a>`;
             break;
@@ -113,10 +117,10 @@ export const genAVValueHTML = (value: IAVCellValue) => {
             html = `<svg class="av__checkbox"><use xlink:href="#icon${value.checkbox.checked ? "Check" : "Uncheck"}"></use></svg>`;
             break;
         case "template":
-            html = `<div class="fn__flex-1">${value.template.content}</div>`;
+            html = `<div class="fn__flex-1" placeholder="${window.siyuan.languages.empty}">${value.template.content}</div>`;
             break;
         case "email":
-            html = `<input value="${value.email.content}" class="b3-text-field b3-text-field--text fn__flex-1">
+            html = `<input value="${value.email.content}" class="b3-text-field b3-text-field--text fn__flex-1" placeholder="${window.siyuan.languages.empty}">
 <span class="fn__space"></span>
 <a href="mailto:${value.email.content}" target="_blank" aria-label="${window.siyuan.languages.openBy}" class="block__icon block__icon--show fn__flex-center b3-tooltips__w b3-tooltips"><svg><use xlink:href="#iconEmail"></use></svg></a>`;
             break;
@@ -188,17 +192,14 @@ export const renderAVAttribute = (element: HTMLElement, id: string, protyle: IPr
         <span>${escapeHtml(item.key.name)}</span>
     </div>
     <div data-av-id="${table.avID}" data-col-id="${item.values[0].keyID}" data-block-id="${item.values[0].blockID}" data-id="${item.values[0].id}" data-type="${item.values[0].type}" 
-data-options="${item.key?.options ? escapeAttr(JSON.stringify(item.key.options)) : "[]"}"
-class="fn__flex-1 fn__flex${["url", "text", "number", "email", "phone", "block"].includes(item.values[0].type) ? "" : " custom-attr__avvalue"}">
-        ${genAVValueHTML(item.values[0])}
-    </div>
+data-options="${item.key?.options ? escapeAttr(JSON.stringify(item.key.options)) : "[]"}" 
+${["text", "number", "date", "url", "phone", "template", "email"].includes(item.values[0].type) ? "" : `placeholder="${window.siyuan.languages.empty}"`}  
+class="fn__flex-1 fn__flex${["url", "text", "number", "email", "phone"].includes(item.values[0].type) ? "" : " custom-attr__avvalue"}${["block", "created", "updated"].includes(item.values[0].type) ? " custom-attr__avvalue--readonly" : ""}">${genAVValueHTML(item.values[0])}</div>
 </div>`;
             });
             innerHTML += `<div class="fn__hr"></div>
-<div class="fn__flex">
-    <div class="fn__space"></div><div class="fn__space"></div>
-    <button data-type="addColumn" class="b3-button b3-button--outline"><svg><use xlink:href="#iconAdd"></use></svg>${window.siyuan.languages.addAttr}</button>
-</div><div class="fn__hr--b"></div>`;
+<button data-type="addColumn" class="b3-button b3-button--cancel"><svg><use xlink:href="#iconAdd"></use></svg>${window.siyuan.languages.newCol}</button>
+<div class="fn__hr--b"></div>`;
             html += `<div data-av-id="${table.avID}" data-node-id="${id}" data-type="NodeAttributeView">${innerHTML}</div>`;
 
             if (element.innerHTML) {
@@ -224,9 +225,13 @@ class="fn__flex-1 fn__flex${["url", "text", "number", "email", "phone", "block"]
                     ghostElement.remove();
                 });
             });
-            element.addEventListener("drop", () => {
+            element.addEventListener("drop", (event) => {
                 counter = 0;
-                window.siyuan.dragElement.style.opacity = "";
+                if (protyle.disabled) {
+                    event.preventDefault();
+                    event.stopPropagation();
+                    return;
+                }
                 const targetElement = element.querySelector(".dragover__bottom, .dragover__top") as HTMLElement;
                 if (targetElement && dragBlockElement) {
                     const isBottom = targetElement.classList.contains("dragover__bottom");
@@ -251,12 +256,38 @@ class="fn__flex-1 fn__flex${["url", "text", "number", "email", "phone", "block"]
                         }
                     }
                     targetElement.classList.remove("dragover__bottom", "dragover__top");
+                } else if (!window.siyuan.dragElement && event.dataTransfer.types[0] === "Files") {
+                    const cellElement = element.querySelector(".custom-attr__avvalue--active") as HTMLElement;
+                    if (cellElement) {
+                        if (event.dataTransfer.types[0] === "Files" && !isBrowser()) {
+                            const files: string[] = [];
+                            for (let i = 0; i < event.dataTransfer.files.length; i++) {
+                                files.push(webUtils.getPathForFile(event.dataTransfer.files[i]));
+                            }
+                            dragUpload(files, protyle, cellElement);
+                        }
+                    }
+                }
+                if (window.siyuan.dragElement) {
+                    window.siyuan.dragElement.style.opacity = "";
+                    window.siyuan.dragElement = undefined;
                 }
-                window.siyuan.dragElement = null;
             });
             element.addEventListener("dragover", (event: DragEvent) => {
                 const target = event.target as HTMLElement;
-                let targetElement = hasClosestByClassName(target, "av__row");
+                let targetElement: HTMLElement | false;
+                if (event.dataTransfer.types.includes("Files")) {
+                    element.querySelectorAll(".custom-attr__avvalue--active").forEach((item: HTMLElement) => {
+                        item.classList.remove("custom-attr__avvalue--active");
+                    });
+                    targetElement = hasClosestByClassName(target, "custom-attr__avvalue");
+                    if (targetElement && targetElement.getAttribute("data-type") === "mAsset") {
+                        targetElement.classList.add("custom-attr__avvalue--active");
+                        event.preventDefault();
+                    }
+                    return;
+                }
+                targetElement = hasClosestByClassName(target, "av__row");
                 if (!targetElement) {
                     targetElement = hasClosestByClassName(document.elementFromPoint(event.clientX, event.clientY - 1), "av__row");
                 }

+ 2 - 2
app/src/protyle/render/av/calc.ts

@@ -79,7 +79,7 @@ export const openCalcMenu = async (protyle: IProtyle, calcElement: HTMLElement,
     data: IAV,
     colId: string,
     blockID: string
-}) => {
+}, x?: number) => {
     let rowElement: HTMLElement | false;
     let type;
     let colId: string;
@@ -403,7 +403,7 @@ export const openCalcMenu = async (protyle: IProtyle, calcElement: HTMLElement,
         });
     }
     const calcRect = calcElement.getBoundingClientRect();
-    menu.open({x: calcRect.left, y: calcRect.bottom, h: calcRect.height});
+    menu.open({x: Math.max(x || 0, calcRect.left), y: calcRect.bottom, h: calcRect.height});
 };
 
 export const getCalcValue = (column: IAVColumn) => {

+ 44 - 13
app/src/protyle/render/av/relation.ts

@@ -8,6 +8,7 @@ import {updateCellsValue} from "./cell";
 import {updateAttrViewCellAnimation} from "./action";
 import {focusBlock} from "../../util/selection";
 import {setPosition} from "../../../util/setPosition";
+import * as dayjs from "dayjs";
 
 const genSearchList = (element: Element, keyword: string, avId?: string, excludes = true, cb?: () => void) => {
     fetchPost("/api/av/searchAttributeView", {
@@ -213,6 +214,11 @@ const genSelectItemHTML = (type: "selected" | "empty" | "unselect", id?: string,
 <svg class="b3-menu__action"><use xlink:href="#iconMin"></use></svg>`;
     }
     if (type === "empty") {
+        if (id) {
+            return `<button class="b3-menu__item" data-type="setRelationCell">
+    <span class="b3-menu__label">${window.siyuan.languages.newRowInRelation.replace("${x}", text).replace("${y}", id)}</span>
+</button>`;
+        }
         return `<button class="b3-menu__item">
     <span class="b3-menu__label">${window.siyuan.languages.emptyContent}</span>
 </button>`;
@@ -243,9 +249,10 @@ const filterItem = (menuElement: Element, cellElement: HTMLElement, keyword: str
                 html += genSelectItemHTML("unselect", item.block.id, item.isDetached, item.block.content || window.siyuan.languages.untitled);
             }
         });
-        menuElement.querySelector(".b3-menu__items").innerHTML = `${selectHTML || genSelectItemHTML("empty")}
+        menuElement.querySelector(".b3-menu__items").innerHTML = `${selectHTML}
 <button class="b3-menu__separator"></button>
-${html || genSelectItemHTML("empty")}`;
+${html}
+${keyword ? genSelectItemHTML("empty", keyword, undefined, menuElement.querySelector(".popover__block").outerHTML) : (html ? "" : genSelectItemHTML("empty"))}`;
         menuElement.querySelector(".b3-menu__items .b3-menu__item:not(.fn__none)").classList.add("b3-menu__item--current");
     });
 };
@@ -273,7 +280,7 @@ export const bindRelationEvent = (options: {
                 html += genSelectItemHTML("unselect", item.block.id, item.isDetached, item.block.content || window.siyuan.languages.untitled);
             }
         });
-        options.menuElement.querySelector(".b3-menu__items").innerHTML = `${selectHTML || genSelectItemHTML("empty")}
+        options.menuElement.querySelector(".b3-menu__items").innerHTML = `${selectHTML}
 <button class="b3-menu__separator"></button>
 ${html || genSelectItemHTML("empty")}`;
         const cellRect = options.cellElements[options.cellElements.length - 1].getBoundingClientRect();
@@ -340,7 +347,7 @@ export const getRelationHTML = (data: IAV, cellElements?: HTMLElement[]) => {
 };
 
 export const setRelationCell = (protyle: IProtyle, nodeElement: HTMLElement, target: HTMLElement, cellElements: HTMLElement[]) => {
-    const menuElement = hasClosestByClassName(target, "b3-menu__items");
+    const menuElement = hasClosestByClassName(target, "b3-menu");
     if (!menuElement) {
         return;
     }
@@ -353,7 +360,7 @@ export const setRelationCell = (protyle: IProtyle, nodeElement: HTMLElement, tar
     }
     if (!nodeElement.contains(cellElements[0])) {
         cellElements[0] = (nodeElement.querySelector(`.av__row[data-id="${rowElement.dataset.id}"] .av__cell[data-col-id="${cellElements[0].dataset.colId}"]`) ||
-            nodeElement.querySelector(`.fn__flex-1[data-col-id="${cellElements[0].dataset.colId}"]`) ) as HTMLElement;
+            nodeElement.querySelector(`.fn__flex-1[data-col-id="${cellElements[0].dataset.colId}"]`)) as HTMLElement;
     }
     const newValue: IAVCellRelationValue = {blockIDs: [], contents: []};
     menuElement.querySelectorAll('[draggable="true"]').forEach(item => {
@@ -371,8 +378,9 @@ export const setRelationCell = (protyle: IProtyle, nodeElement: HTMLElement, tar
     if (target.classList.contains("b3-menu__item")) {
         const targetId = target.getAttribute("data-id");
         const separatorElement = menuElement.querySelector(".b3-menu__separator");
+        const searchValue = menuElement.querySelector("input").value;
         if (target.getAttribute("draggable")) {
-            if (!separatorElement.nextElementSibling.getAttribute("data-id")) {
+            if (!separatorElement.nextElementSibling.getAttribute("data-id") && !searchValue) {
                 separatorElement.nextElementSibling.remove();
             }
             const removeIndex = newValue.blockIDs.indexOf(targetId);
@@ -380,13 +388,7 @@ export const setRelationCell = (protyle: IProtyle, nodeElement: HTMLElement, tar
             newValue.contents.splice(removeIndex, 1);
             separatorElement.after(target);
             target.outerHTML = genSelectItemHTML("unselect", targetId, !target.querySelector(".popover__block"), target.querySelector(".b3-menu__label").textContent);
-            if (!separatorElement.previousElementSibling) {
-                separatorElement.insertAdjacentHTML("beforebegin", genSelectItemHTML("empty"));
-            }
-        } else {
-            if (!separatorElement.previousElementSibling.getAttribute("data-id")) {
-                separatorElement.previousElementSibling.remove();
-            }
+        } else if (targetId) {
             newValue.blockIDs.push(targetId);
             newValue.contents.push({
                 type: "block",
@@ -401,6 +403,35 @@ export const setRelationCell = (protyle: IProtyle, nodeElement: HTMLElement, tar
             if (!separatorElement.nextElementSibling) {
                 separatorElement.insertAdjacentHTML("afterend", genSelectItemHTML("empty"));
             }
+        } else {
+            const blockID = target.querySelector(".popover__block").getAttribute("data-id");
+            const content = target.querySelector("b").textContent;
+            const rowId = Lute.NewNodeID();
+            transaction(protyle, [{
+                action: "insertAttrViewBlock",
+                ignoreFillFilter: true,
+                avID: menuElement.firstElementChild.getAttribute("data-av-id"),
+                srcs: [{
+                    id: rowId,
+                    isDetached: true,
+                    content
+                }],
+                blockID,
+            }, {
+                action: "doUpdateUpdated",
+                id: blockID,
+                data: dayjs().format("YYYYMMDDHHmmss"),
+            }]);
+            newValue.blockIDs.push(rowId);
+            newValue.contents.push({
+                type: "block",
+                block: {
+                    id: rowId,
+                    content
+                },
+                isDetached: true
+            });
+            separatorElement.insertAdjacentHTML("beforebegin", `<button data-id="${rowId}" data-type="setRelationCell" class="b3-menu__item" draggable="true">${genSelectItemHTML("selected", rowId, true, content)}</button>`);
         }
         menuElement.querySelector(".b3-menu__item--current")?.classList.remove("b3-menu__item--current");
         menuElement.querySelector(".b3-menu__items .b3-menu__item:not(.fn__none)").classList.add("b3-menu__item--current");

+ 1 - 1
app/src/protyle/render/flowchartRender.ts

@@ -18,7 +18,7 @@ export const flowchartRender = (element: Element, cdn = Constants.PROTYLE_CDN) =
     if (flowchartElements.length === 0) {
         return;
     }
-    addScript(`${cdn}/js/flowchart.js/flowchart.min.js?v=0.0.0`, "protyleFlowchartScript").then(() => {
+    addScript(`${cdn}/js/flowchart.js/flowchart.min.js?v=1.18.0`, "protyleFlowchartScript").then(() => {
         if (flowchartElements[0].firstElementChild.clientWidth === 0) {
             const observer = new MutationObserver(() => {
                 initFlowchart(flowchartElements);

+ 16 - 2
app/src/protyle/render/highlightRender.ts

@@ -14,7 +14,7 @@ export const highlightRender = (element: Element, cdn = Constants.PROTYLE_CDN) =
             // bazaar reademe
             codeElements = element.querySelectorAll("pre code");
             codeElements.forEach(item => {
-                item.parentElement.setAttribute("lineNumber", "false");
+                item.parentElement.setAttribute("linenumber", "false");
             });
         } else if (element.classList.contains("b3-typography")) {
             // preview & export html markdown
@@ -94,10 +94,12 @@ export const highlightRender = (element: Element, cdn = Constants.PROTYLE_CDN) =
                         block.firstElementChild.className = "protyle-linenumber__rows";
                         block.firstElementChild.setAttribute("contenteditable", "false");
                         lineNumberRender(block);
+                        block.style.display = "";
                     } else {
                         block.firstElementChild.className = "fn__none";
                         block.firstElementChild.innerHTML = "";
                         hljsElement.style.paddingLeft = "";
+                        block.style.display = "block";
                     }
                 }
                 hljsElement.innerHTML = window.hljs.highlight(
@@ -141,7 +143,7 @@ width: ${codeElement.clientWidth}px;
 white-space:${codeElement.style.whiteSpace};
 word-break:${codeElement.style.wordBreak};
 font-variant-ligatures:${codeElement.style.fontVariantLigatures};
-box-sizing: border-box;position: absolute;padding-top:0 !important;padding-bottom:0 !important;min-height:auto !important;`);
+max-height: none;box-sizing: border-box;position: absolute;padding-top:0 !important;padding-bottom:0 !important;min-height:auto !important;`);
     lineNumberTemp.setAttribute("contenteditable", "true");
     block.insertAdjacentElement("afterend", lineNumberTemp);
 
@@ -162,4 +164,16 @@ box-sizing: border-box;position: absolute;padding-top:0 !important;padding-botto
 
     lineNumberTemp.remove();
     block.firstElementChild.innerHTML = lineNumberHTML;
+    // https://github.com/siyuan-note/siyuan/issues/12726
+    if (block.scrollHeight > block.clientHeight) {
+        if (getSelection().rangeCount > 0) {
+            const range = getSelection().getRangeAt(0);
+            if (block.contains(range.startContainer)) {
+                const brElement = document.createElement("br");
+                range.insertNode(brElement);
+                brElement.scrollIntoView({block: "nearest"});
+                brElement.remove();
+            }
+        }
+    }
 };

+ 27 - 0
app/src/protyle/render/searchMarkRender.ts

@@ -0,0 +1,27 @@
+export const searchMarkRender = (protyle: IProtyle, matchElements: NodeListOf<Element>) => {
+    if (matchElements.length === 0) {
+        return;
+    }
+    protyle.highlight.markHL.clear();
+    protyle.highlight.markHL.clear();
+    protyle.highlight.ranges = [];
+    matchElements.forEach((item, index) => {
+        const range = new Range();
+        if (item.getAttribute("data-type") === "search-mark") {
+            const contentElement = item.firstChild;
+            item.replaceWith(contentElement);
+            range.selectNodeContents(contentElement);
+        } else {
+            item.setAttribute("data-type", item.getAttribute("data-type").replace(" search-mark", "").replace("search-mark ", ""));
+            range.selectNodeContents(item);
+        }
+        if (index === protyle.highlight.rangeIndex) {
+            protyle.highlight.markHL.add(range);
+        } else {
+            protyle.highlight.mark.add(range);
+        }
+        protyle.highlight.ranges.push(range);
+    });
+    CSS.highlights.set("search-mark", protyle.highlight.mark);
+    CSS.highlights.set("search-mark-hl", protyle.highlight.markHL);
+};

+ 22 - 4
app/src/protyle/toolbar/index.ts

@@ -69,7 +69,16 @@ export class Toolbar {
         /// #endif
         this.toolbarHeight = 29;
         protyle.app.plugins.forEach(item => {
-            options.toolbar = toolbarKeyToMenu(item.updateProtyleToolbar(options.toolbar));
+            const pluginToolbar = item.updateProtyleToolbar(options.toolbar);
+            pluginToolbar.forEach(toolbarItem => {
+                if (typeof toolbarItem === "string" || Constants.INLINE_TYPE.concat("|").includes(toolbarItem.name) || !toolbarItem.hotkey) {
+                    return;
+                }
+                if (window.siyuan.config.keymap.plugin && window.siyuan.config.keymap.plugin[item.name] && window.siyuan.config.keymap.plugin[item.name][toolbarItem.name]) {
+                    toolbarItem.hotkey = window.siyuan.config.keymap.plugin[item.name][toolbarItem.name].custom;
+                }
+            });
+            options.toolbar = toolbarKeyToMenu(pluginToolbar);
         });
         options.toolbar.forEach((menuItem: IMenuItem) => {
             const itemElement = this.genItem(protyle, menuItem);
@@ -81,7 +90,16 @@ export class Toolbar {
         this.element.innerHTML = "";
         protyle.options.toolbar = toolbarKeyToMenu(Constants.PROTYLE_TOOLBAR);
         protyle.app.plugins.forEach(item => {
-            protyle.options.toolbar = toolbarKeyToMenu(item.updateProtyleToolbar(protyle.options.toolbar));
+            const pluginToolbar = item.updateProtyleToolbar(protyle.options.toolbar);
+            pluginToolbar.forEach(toolbarItem => {
+                if (typeof toolbarItem === "string" || Constants.INLINE_TYPE.concat("|").includes(toolbarItem.name) || !toolbarItem.hotkey) {
+                    return;
+                }
+                if (window.siyuan.config.keymap.plugin && window.siyuan.config.keymap.plugin[item.name] && window.siyuan.config.keymap.plugin[item.name][toolbarItem.name]) {
+                    toolbarItem.hotkey = window.siyuan.config.keymap.plugin[item.name][toolbarItem.name].custom;
+                }
+            });
+            protyle.options.toolbar = toolbarKeyToMenu(pluginToolbar);
         });
         protyle.options.toolbar.forEach((menuItem: IMenuItem) => {
             const itemElement = this.genItem(protyle, menuItem);
@@ -153,7 +171,7 @@ export class Toolbar {
         this.toolbarHeight = this.element.clientHeight;
         const y = rangePosition.top - this.toolbarHeight - 4;
         this.element.setAttribute("data-inity", y + Constants.ZWSP + protyle.contentElement.scrollTop.toString());
-        setPosition(this.element, rangePosition.left - 52, Math.max(y, protyle.element.getBoundingClientRect().top));
+        setPosition(this.element, rangePosition.left - 52, Math.max(y, protyle.element.getBoundingClientRect().top + 30));
         this.element.querySelectorAll(".protyle-toolbar__item--current").forEach(item => {
             item.classList.remove("protyle-toolbar__item--current");
         });
@@ -291,7 +309,7 @@ export class Toolbar {
         )) {
             // 移除
             if (type === "clear") {
-                toolbarElement.querySelectorAll('[data-type="em"],[data-type="u"],[data-type="s"],[data-type="mark"],[data-type="sup"],[data-type="sub"],[data-type="strong"]').forEach(item => {
+                toolbarElement.querySelectorAll('[data-type="strong"],[data-type="em"],[data-type="u"],[data-type="s"],[data-type="mark"],[data-type="sup"],[data-type="sub"],[data-type="kbd"],[data-type="mark"],[data-type="code"]').forEach(item => {
                     item.classList.remove("protyle-toolbar__item--current");
                 });
             } else if (actionBtn) {

+ 1 - 1
app/src/protyle/ui/initUI.ts

@@ -184,7 +184,7 @@ export const setPadding = (protyle: IProtyle) => {
     if (window.siyuan.config.editor.displayBookmarkIcon) {
         const editorAttrElement = document.getElementById("editorAttr");
         if (editorAttrElement) {
-            editorAttrElement.innerHTML = `.protyle-wysiwyg--attr .b3-tooltips:after { max-width: ${protyle.wysiwyg.element.clientWidth - left - right}px; }`;
+            editorAttrElement.innerHTML = `.protyle-wysiwyg--attr .b3-tooltips::after { max-width: ${protyle.wysiwyg.element.clientWidth - left - right}px; }`;
         }
     }
     const oldWidth = protyle.wysiwyg.element.getAttribute("data-realwidth");

+ 16 - 6
app/src/protyle/util/compatibility.ts

@@ -21,16 +21,20 @@ export const openByMobile = (uri: string) => {
                 window.webkit.messageHandlers.openLink.postMessage("https://" + uri);
             }
         }
-    } else if (isInAndroid() || isInHarmony()) {
+    } else if (isInAndroid()) {
         window.JSAndroid.openExternal(uri);
+    } else if (isInHarmony()) {
+        window.JSHarmony.openExternal(uri);
     } else {
         window.open(uri);
     }
 };
 
 export const readText = () => {
-    if (isInAndroid() || isInHarmony()) {
+    if (isInAndroid()) {
         return window.JSAndroid.readClipboard();
+    } else if (isInHarmony()) {
+        return window.JSHarmony.readClipboard();
     }
     return navigator.clipboard.readText();
 };
@@ -42,10 +46,14 @@ export const writeText = (text: string) => {
     }
     try {
         // navigator.clipboard.writeText 抛出异常不进入 catch,这里需要先处理移动端复制
-        if (isInAndroid() || isInHarmony()) {
+        if (isInAndroid()) {
             window.JSAndroid.writeClipboard(text);
             return;
         }
+        if (isInHarmony()) {
+            window.JSHarmony.writeClipboard(text);
+            return;
+        }
         if (isInIOS()) {
             window.webkit.messageHandlers.setClipboard.postMessage(text);
             return;
@@ -54,8 +62,10 @@ export const writeText = (text: string) => {
     } catch (e) {
         if (isInIOS()) {
             window.webkit.messageHandlers.setClipboard.postMessage(text);
-        } else if (isInAndroid() || isInHarmony()) {
+        } else if (isInAndroid()) {
             window.JSAndroid.writeClipboard(text);
+        } else if (isInHarmony()) {
+            window.JSHarmony.writeClipboard(text);
         } else {
             const textElement = document.createElement("textarea");
             textElement.value = text;
@@ -137,8 +147,8 @@ export const isInIOS = () => {
 };
 
 export const isInHarmony = () => {
-    return window.siyuan.config.system.container === "harmony";
-}
+    return window.siyuan.config.system.container === "harmony" && window.JSHarmony;
+};
 
 // Mac,Windows 快捷键展示
 export const updateHotkeyTip = (hotkey: string) => {

+ 4 - 0
app/src/protyle/util/destroy.ts

@@ -5,6 +5,10 @@ export const destroy = (protyle: IProtyle) => {
         return;
     }
     hideElements(["util"], protyle);
+    protyle.highlight.markHL.clear();
+    protyle.highlight.mark.clear();
+    protyle.highlight.ranges = [];
+    protyle.highlight.rangeIndex = 0;
     protyle.observer?.disconnect();
     protyle.observerLoad?.disconnect();
     protyle.element.classList.remove("protyle");

+ 47 - 6
app/src/protyle/util/editorCommonEvent.ts

@@ -31,6 +31,7 @@ import {zoomOut} from "../../menus/protyle";
 /// #if !BROWSER
 import {webUtils} from "electron";
 /// #endif
+import {addDragFill} from "../render/av/cell";
 
 const moveToNew = (protyle: IProtyle, sourceElements: Element[], targetElement: Element, newSourceElement: Element,
                    isSameDoc: boolean, isBottom: boolean, isCopy: boolean) => {
@@ -1070,7 +1071,7 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
         } else if (event.dataTransfer.getData(Constants.SIYUAN_DROP_FILE)?.split("-").length > 1) {
             // 文件树拖拽
             const ids = event.dataTransfer.getData(Constants.SIYUAN_DROP_FILE).split(",");
-            if (event.altKey) {
+            if (!event.altKey) {
                 if (event.y > protyle.wysiwyg.element.lastElementChild.getBoundingClientRect().bottom) {
                     insertEmptyBlock(protyle, "afterend", protyle.wysiwyg.element.lastElementChild.getAttribute("data-node-id"));
                 } else {
@@ -1179,6 +1180,10 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
                 } else {
                     paste(protyle, event);
                 }
+                protyle.wysiwyg.element.querySelectorAll(".av__cell--select, .av__cell--active").forEach(item => {
+                    item.classList.remove("av__cell--select", "av__cell--active");
+                    item.querySelector(".av__drag-fill")?.remove();
+                });
             } else {
                 const cellElement = hasClosestByClassName(event.target, "av__cell");
                 if (cellElement) {
@@ -1214,8 +1219,27 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
                 behavior: "smooth"
             });
         }
+        let targetElement: HTMLElement | false;
         // 设置了的话 drop 就无法监听 shift/control event.dataTransfer.dropEffect = "move";
         if (event.dataTransfer.types.includes("Files")) {
+            targetElement = hasClosestByClassName(event.target, "av__cell");
+            if (targetElement && targetElement.getAttribute("data-dtype") === "mAsset" &&
+                !targetElement.classList.contains("av__cell--header")) {
+                event.preventDefault(); // 不使用导致无法触发 drop
+                if (dragoverElement && targetElement.isSameNode(dragoverElement)) {
+                    return;
+                }
+                const blockElement = hasClosestBlock(targetElement);
+                if (blockElement) {
+                    protyle.wysiwyg.element.querySelectorAll(".av__cell--select, .av__cell--active").forEach(item => {
+                        item.classList.remove("av__cell--select", "av__cell--active");
+                        item.querySelector(".av__drag-fill")?.remove();
+                    });
+                    targetElement.classList.add("av__cell--select");
+                    addDragFill(targetElement);
+                    dragoverElement = targetElement;
+                }
+            }
             // 使用 event.preventDefault(); 会导致无光标 https://github.com/siyuan-note/siyuan/issues/12857
             return;
         }
@@ -1230,7 +1254,8 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
             event.preventDefault();
             return;
         }
-        if (event.shiftKey || event.altKey) {
+        const fileTreeIds = (event.dataTransfer.types.includes(Constants.SIYUAN_DROP_FILE) && window.siyuan.dragElement) ? window.siyuan.dragElement.innerText : "";
+        if (event.shiftKey || (event.altKey && fileTreeIds.indexOf("-") === -1)) {
             const targetElement = hasClosestBlock(event.target);
             if (targetElement) {
                 targetElement.classList.remove("dragover__top", "protyle-wysiwyg--select", "dragover__bottom", "dragover__left", "dragover__right");
@@ -1242,7 +1267,7 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
         }
         // 编辑器内文字拖拽或资源文件拖拽或按住 alt/shift 拖拽反链图标进入编辑器时不能运行 event.preventDefault(), 否则无光标; 需放在 !window.siyuan.dragElement 之后
         event.preventDefault();
-        let targetElement = hasClosestByClassName(event.target, "av__row") || hasClosestByClassName(event.target, "av__row--util") || hasClosestBlock(event.target);
+        targetElement = hasClosestByClassName(event.target, "av__row") || hasClosestByClassName(event.target, "av__row--util") || hasClosestBlock(event.target);
         const point = {x: event.clientX, y: event.clientY, className: ""};
 
         // 超级块中有a,b两个段落块,移动到 ab 之间的间隙 targetElement 会变为超级块,需修正为 a
@@ -1315,7 +1340,6 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
         if (!targetElement) {
             return;
         }
-        const fileTreeIds = (event.dataTransfer.types.includes(Constants.SIYUAN_DROP_FILE) && window.siyuan.dragElement) ? window.siyuan.dragElement.innerText : "";
         if (targetElement && dragoverElement && targetElement.isSameNode(dragoverElement)) {
             // 性能优化,目标为同一个元素不再进行校验
             const nodeRect = targetElement.getBoundingClientRect();
@@ -1324,6 +1348,15 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
                 item.removeAttribute("select-start");
                 item.removeAttribute("select-end");
             });
+            // 文档树拖拽限制
+            if (fileTreeIds.indexOf("-") > -1 && !targetElement.classList.contains("av__row") &&
+                !targetElement.classList.contains("av__row--util")) {
+                if (!event.altKey) {
+                    return;
+                } else if (fileTreeIds.split(",").includes(protyle.block.rootID) && event.altKey) {
+                    return;
+                }
+            }
             if (targetElement.getAttribute("data-type") === "NodeAttributeView" && hasClosestByTag(event.target, "TD")) {
                 return;
             }
@@ -1332,7 +1365,8 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
                 addDragover(targetElement);
                 return;
             }
-            if (targetElement.getAttribute("data-type") === "NodeListItem" || fileTreeIds.indexOf("-") > -1) {
+            // 忘记为什么要限定文档树的拖拽了,先放开 https://github.com/siyuan-note/siyuan/pull/13284#issuecomment-2503853135
+            if (targetElement.getAttribute("data-type") === "NodeListItem") {
                 if (event.clientY > nodeRect.top + nodeRect.height / 2) {
                     targetElement.classList.add("dragover__bottom");
                     addDragover(targetElement);
@@ -1380,9 +1414,16 @@ export const dropEvent = (protyle: IProtyle, editorElement: HTMLElement) => {
             }
             return;
         }
+
         if (fileTreeIds.indexOf("-") > -1) {
-            if (fileTreeIds.split(",").includes(protyle.block.rootID) && !targetElement.classList.contains("av__row")) {
+            if (fileTreeIds.split(",").includes(protyle.block.rootID) && !targetElement.classList.contains("av__row") &&
+                !targetElement.classList.contains("av__row--util") && event.altKey) {
                 dragoverElement = undefined;
+                editorElement.querySelectorAll(".dragover__left, .dragover__right, .dragover__bottom, .dragover__top, .dragover").forEach((item: HTMLElement) => {
+                    item.classList.remove("dragover__top", "dragover__bottom", "dragover__left", "dragover__right", "dragover");
+                    item.removeAttribute("select-start");
+                    item.removeAttribute("select-end");
+                });
             } else {
                 dragoverElement = targetElement;
             }

+ 4 - 0
app/src/protyle/util/insertHTML.ts

@@ -298,6 +298,10 @@ export const insertHTML = (html: string, protyle: IProtyle, isBlock = false,
     if (!isBlock &&
         (isNodeCodeBlock || protyle.toolbar.getCurrentType(range).includes("code"))) {
         range.deleteContents();
+        // 代码块需保持至少一个 \n https://github.com/siyuan-note/siyuan/pull/13271#issuecomment-2502672155
+        if (isNodeCodeBlock && getContenteditableElement(blockElement).textContent === "") {
+            html += "\n";
+        }
         range.insertNode(document.createTextNode(html.replace(/\r\n|\r|\u2028|\u2029/g, "\n")));
         range.collapse(false);
         range.insertNode(document.createElement("wbr"));

+ 28 - 15
app/src/protyle/util/paste.ts

@@ -190,21 +190,9 @@ export const pasteAsPlainText = async (protyle: IProtyle) => {
             textPlain = textPlain.replace(/__@kbd@__/g, "<kbd>").replace(/__@\/kbd@__/g, "</kbd>");
             textPlain = textPlain.replace(/__@u@__/g, "<u>").replace(/__@\/u@__/g, "</u>");
 
-            protyle.lute.SetInlineAsterisk(true);
-            protyle.lute.SetGFMStrikethrough(true);
-            protyle.lute.SetInlineMath(true);
-            protyle.lute.SetSub(true);
-            protyle.lute.SetSup(true);
-            protyle.lute.SetTag(true);
-            protyle.lute.SetInlineUnderscore(true);
+            enableLuteMarkdownSyntax(protyle);
             const content = protyle.lute.BlockDOM2EscapeMarkerContent(protyle.lute.Md2BlockDOM(textPlain));
-            protyle.lute.SetInlineAsterisk(window.siyuan.config.editor.markdown.inlineAsterisk);
-            protyle.lute.SetGFMStrikethrough(window.siyuan.config.editor.markdown.inlineStrikethrough);
-            protyle.lute.SetInlineMath(window.siyuan.config.editor.markdown.inlineMath);
-            protyle.lute.SetSub(window.siyuan.config.editor.markdown.inlineSub);
-            protyle.lute.SetSup(window.siyuan.config.editor.markdown.inlineSup);
-            protyle.lute.SetTag(window.siyuan.config.editor.markdown.inlineTag);
-            protyle.lute.SetInlineUnderscore(window.siyuan.config.editor.markdown.inlineUnderscore);
+            restoreLuteMarkdownSyntax(protyle);
 
             // insertHTML 会进行内部反转义
             insertHTML(content, protyle, false, false, true);
@@ -213,6 +201,26 @@ export const pasteAsPlainText = async (protyle: IProtyle) => {
     }
 };
 
+export const enableLuteMarkdownSyntax = (protyle: IProtyle) => {
+    protyle.lute.SetInlineAsterisk(true);
+    protyle.lute.SetGFMStrikethrough(true);
+    protyle.lute.SetInlineMath(true);
+    protyle.lute.SetSub(true);
+    protyle.lute.SetSup(true);
+    protyle.lute.SetTag(true);
+    protyle.lute.SetInlineUnderscore(true);
+};
+
+export const restoreLuteMarkdownSyntax = (protyle: IProtyle) => {
+    protyle.lute.SetInlineAsterisk(window.siyuan.config.editor.markdown.inlineAsterisk);
+    protyle.lute.SetGFMStrikethrough(window.siyuan.config.editor.markdown.inlineStrikethrough);
+    protyle.lute.SetInlineMath(window.siyuan.config.editor.markdown.inlineMath);
+    protyle.lute.SetSub(window.siyuan.config.editor.markdown.inlineSub);
+    protyle.lute.SetSup(window.siyuan.config.editor.markdown.inlineSup);
+    protyle.lute.SetTag(window.siyuan.config.editor.markdown.inlineTag);
+    protyle.lute.SetInlineUnderscore(window.siyuan.config.editor.markdown.inlineUnderscore);
+};
+
 export const pasteText = (protyle: IProtyle, textPlain: string, nodeElement: Element, toBlockDOM = true) => {
     const range = getEditorRange(protyle.wysiwyg.element);
     if (nodeElement.getAttribute("data-type") === "NodeCodeBlock") {
@@ -452,9 +460,14 @@ export const paste = async (protyle: IProtyle, event: (ClipboardEvent | DragEven
             )) {
                 isHTML = false;
             } else {
-                // 需注意 Edge 中的选不应识别为图片 https://github.com/siyuan-note/siyuan/issues/7021
+                // 需注意 Edge 中的选不应识别为图片 https://github.com/siyuan-note/siyuan/issues/7021
                 isHTML = true;
             }
+
+            if (textPlain && "" !== textPlain.trim() && textHTML.startsWith("<span") && -1 < textHTML.indexOf("white-space: pre;")) {
+                // 豆包复制粘贴问题 https://github.com/siyuan-note/siyuan/issues/13265
+                isHTML = false;
+            }
         }
         if (isHTML) {
             const tempElement = document.createElement("div");

+ 10 - 8
app/src/protyle/util/reload.ts

@@ -4,6 +4,8 @@ import {getDocByScroll, saveScroll} from "../scroll/saveScroll";
 import {renderBacklink} from "../wysiwyg/renderBacklink";
 import {hasClosestByClassName} from "./hasClosest";
 import {preventScroll} from "../scroll/preventScroll";
+import {searchMarkRender} from "../render/searchMarkRender";
+import {restoreLuteMarkdownSyntax} from "./paste";
 
 export const reloadProtyle = (protyle: IProtyle, focus: boolean, updateReadonly?: boolean) => {
     if (!protyle.preview.element.classList.contains("fn__none")) {
@@ -27,13 +29,7 @@ export const reloadProtyle = (protyle: IProtyle, focus: boolean, updateReadonly?
     }
     protyle.lute.SetProtyleMarkNetImg(window.siyuan.config.editor.displayNetImgMark);
     protyle.lute.SetSpellcheck(window.siyuan.config.editor.spellcheck);
-    protyle.lute.SetInlineAsterisk(window.siyuan.config.editor.markdown.inlineAsterisk);
-    protyle.lute.SetInlineUnderscore(window.siyuan.config.editor.markdown.inlineUnderscore);
-    protyle.lute.SetSup(window.siyuan.config.editor.markdown.inlineSup);
-    protyle.lute.SetSub(window.siyuan.config.editor.markdown.inlineSub);
-    protyle.lute.SetTag(window.siyuan.config.editor.markdown.inlineTag);
-    protyle.lute.SetInlineMath(window.siyuan.config.editor.markdown.inlineMath);
-    protyle.lute.SetGFMStrikethrough(window.siyuan.config.editor.markdown.inlineStrikethrough);
+    restoreLuteMarkdownSyntax(protyle);
     protyle.lute.SetGFMStrikethrough1(false);
     addLoading(protyle);
     if (protyle.options.backlinkData) {
@@ -48,6 +44,7 @@ export const reloadProtyle = (protyle: IProtyle, focus: boolean, updateReadonly?
             }, response => {
                 protyle.options.backlinkData = isMention ? response.data.backmentions : response.data.backlinks;
                 renderBacklink(protyle, protyle.options.backlinkData);
+                searchMarkRender(protyle, protyle.wysiwyg.element.querySelectorAll('span[data-type~="search-mark"]'));
             });
         }
     } else {
@@ -56,7 +53,12 @@ export const reloadProtyle = (protyle: IProtyle, focus: boolean, updateReadonly?
             protyle,
             focus,
             scrollAttr: saveScroll(protyle, true) as IScrollAttr,
-            updateReadonly
+            updateReadonly,
+            cb () {
+                if (protyle.query?.key) {
+                    searchMarkRender(protyle, protyle.wysiwyg.element.querySelectorAll('span[data-type~="search-mark"]'));
+                }
+            }
         });
     }
 };

+ 1 - 0
app/src/protyle/wysiwyg/enter.ts

@@ -120,6 +120,7 @@ export const enter = (blockElement: HTMLElement, range: Range, protyle: IProtyle
         editableElement.parentElement.removeAttribute("data-render");
         highlightRender(blockElement);
         updateTransaction(protyle, blockElement.getAttribute("data-node-id"), blockElement.outerHTML, oldHTML);
+        scrollCenter(protyle);
         return true;
     }
 

+ 34 - 25
app/src/protyle/wysiwyg/index.ts

@@ -4,7 +4,8 @@ import {
     hasClosestByAttribute,
     hasClosestByClassName,
     hasClosestByMatchTag,
-    hasTopClosestByClassName, isInEmbedBlock,
+    hasTopClosestByClassName,
+    isInEmbedBlock,
 } from "../util/hasClosest";
 import {
     focusBlock,
@@ -12,7 +13,8 @@ import {
     focusByWbr,
     focusSideBlock,
     getEditorRange,
-    getSelectionOffset, setFirstNodeRange,
+    getSelectionOffset,
+    setFirstNodeRange,
     setLastNodeRange,
 } from "../util/selection";
 import {Constants} from "../../constants";
@@ -23,7 +25,8 @@ import {
     contentMenu,
     enterBack,
     fileAnnotationRefMenu,
-    imgMenu, inlineMathMenu,
+    imgMenu,
+    inlineMathMenu,
     linkMenu,
     refMenu,
     setFold,
@@ -62,7 +65,7 @@ import {openGlobalSearch} from "../../search/util";
 import {popSearch} from "../../mobile/menu/search";
 /// #endif
 import {BlockPanel} from "../../block/Panel";
-import {isInIOS, isOnlyMeta, readText} from "../util/compatibility";
+import {isInIOS, isMac, isOnlyMeta, readText} from "../util/compatibility";
 import {MenuItem} from "../../menus/Menu";
 import {fetchPost} from "../../util/fetch";
 import {onGet} from "../util/onGet";
@@ -668,14 +671,13 @@ export class WYSIWYG {
                 const dragHeight = dragElement.clientHeight;
                 documentSelf.onmousemove = (moveEvent: MouseEvent) => {
                     if (dragElement.tagName === "IMG") {
-                        dragElement.parentElement.parentElement.style.width = "";
                         dragElement.style.height = "";
                     }
                     if (moveEvent.clientX > x - dragWidth + 8 && moveEvent.clientX < mostRight) {
                         if ((dragElement.tagName === "IMG" && !dragElement.parentElement.parentElement.style.minWidth && nodeElement.style.textAlign !== "center") || !isCenter) {
-                            dragElement.style.width = Math.max(17, dragWidth + (moveEvent.clientX - x)) + "px";
+                            dragElement.parentElement.style.width = Math.max(17, dragWidth + (moveEvent.clientX - x)) + "px";
                         } else {
-                            dragElement.style.width = Math.max(17, dragWidth + (moveEvent.clientX - x) * 2) + "px";
+                            dragElement.parentElement.style.width = Math.max(17, dragWidth + (moveEvent.clientX - x) * 2) + "px";
                         }
                     }
                     if (dragElement.tagName !== "IMG") {
@@ -769,11 +771,11 @@ export class WYSIWYG {
             }
 
             // 多选节点
-            let x = event.clientX;
+            let clentX = event.clientX;
             if (event.clientX > mostRight) {
-                x = mostRight;
+                clentX = mostRight;
             } else if (event.clientX < mostLeft) {
-                x = mostLeft;
+                clentX = mostLeft;
             }
             const mostTop = rect.top + (protyle.options.render.breadcrumb ? protyle.breadcrumb.element.parentElement.clientHeight : 0);
 
@@ -877,7 +879,7 @@ export class WYSIWYG {
                 let newLeft = 0;
                 let newWidth = 0;
                 let newHeight = 0;
-                if (moveEvent.clientX < x) {
+                if (moveEvent.clientX < clentX) {
                     if (moveEvent.clientX < mostLeft) {
                         // 向左越界
                         newLeft = mostLeft;
@@ -885,16 +887,16 @@ export class WYSIWYG {
                         // 向左
                         newLeft = moveEvent.clientX;
                     }
-                    newWidth = x - newLeft;
+                    newWidth = clentX - newLeft;
                 } else {
                     if (moveEvent.clientX > mostRight) {
                         // 向右越界
-                        newLeft = x;
+                        newLeft = clentX;
                         newWidth = mostRight - newLeft;
                     } else {
                         // 向右
-                        newLeft = x;
-                        newWidth = moveEvent.clientX - x;
+                        newLeft = clentX;
+                        newWidth = moveEvent.clientX - clentX;
                     }
                 }
 
@@ -2005,15 +2007,16 @@ export class WYSIWYG {
         // 输入法测试点 https://github.com/siyuan-note/siyuan/issues/3027
         let isComposition = false; // for iPhone
         this.element.addEventListener("compositionstart", (event) => {
-            // 搜狗输入法划选输入后无 data https://github.com/siyuan-note/siyuan/issues/4672
+            isComposition = true;
+            // 微软双拼由于 focusByRange 导致无法输入文字,因此不再 keydown 中记录了,但 keyup 会记录拼音字符,因此使用 isComposition 阻止 keyup 记录。
+            // 但搜狗输入法选中后继续输入不走 keydown,isComposition 阻止了 keyup 记录,因此需在此记录。
             const range = getEditorRange(protyle.wysiwyg.element);
             const nodeElement = hasClosestBlock(range.startContainer);
-            if (nodeElement && typeof protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] === "undefined") {
+            if (!isMac() && nodeElement) {
                 range.insertNode(document.createElement("wbr"));
                 protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] = nodeElement.outerHTML;
                 nodeElement.querySelector("wbr").remove();
             }
-            isComposition = true;
             event.stopPropagation();
         });
 
@@ -2085,22 +2088,28 @@ export class WYSIWYG {
         this.element.addEventListener("keyup", (event) => {
             const range = getEditorRange(this.element).cloneRange();
             const nodeElement = hasClosestBlock(range.startContainer);
-            if (event.key !== "PageUp" && event.key !== "PageDown" && event.key !== "Home" && event.key !== "End" && event.key.indexOf("Arrow") === -1 &&
-                event.key !== "Alt" && event.key !== "Shift" && event.key !== "CapsLock" && event.key !== "Escape" && event.key !== "Meta" && !/^F\d{1,2}$/.test(event.key) &&
-                (!event.isComposing || (event.isComposing && range.toString() !== "")) // https://github.com/siyuan-note/siyuan/issues/4341
-            ) {
-                // 搜狗输入法不走 keydown,需重新记录历史状态
-                if (range.toString() === "" &&  // windows 下回车新建块输入abc,选中 bc ctrl+m 后光标错误
-                    nodeElement && typeof protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] === "undefined") {
+
+            if (event.key !== "PageUp" && event.key !== "PageDown" && event.key !== "Home" && event.key !== "End" &&
+                event.key.indexOf("Arrow") === -1 && event.key !== "Escape" && event.key !== "Shift" &&
+                event.key !== "Meta" && event.key !== "Alt" && event.key !== "Control" && event.key !== "CapsLock" &&
+                !event.ctrlKey && !event.shiftKey && !event.metaKey && !event.altKey &&
+                !/^F\d{1,2}$/.test(event.key)) {
+                // 搜狗输入法不走 keydown,没有选中字符后不走 compositionstart,需重新记录历史状态
+                if (!isMac() && nodeElement &&
+                    // 微软双拼 keyup 会记录拼音字符,因此在 compositionstart 记录
+                    !isComposition &&
+                    (typeof protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] === "undefined" || range.toString() !== "" || !this.preventKeyup)) {
                     range.insertNode(document.createElement("wbr"));
                     protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] = nodeElement.outerHTML;
                     nodeElement.querySelector("wbr").remove();
                 }
+                this.preventKeyup = false;
                 return;
             }
 
             // 需放在 lastHTMLs 后,否则 https://github.com/siyuan-note/siyuan/issues/4388
             if (this.preventKeyup) {
+                this.preventKeyup = false;
                 return;
             }
 

+ 1 - 0
app/src/protyle/wysiwyg/input.ts

@@ -298,6 +298,7 @@ const updateInput = (html: string, protyle: IProtyle, id: string) => {
                 data: protyle.wysiwyg.lastHTMLs[id],
                 action: "update"
             });
+            protyle.wysiwyg.lastHTMLs[id] = item.outerHTML;
         } else {
             let firstElement;
             if (index === 0) {

+ 7 - 3
app/src/protyle/wysiwyg/keydown.ts

@@ -166,15 +166,19 @@ export const keydown = (protyle: IProtyle, editorElement: HTMLElement) => {
         // 有可能输入 shift+. ,因此需要使用 event.key 来进行判断
         if (event.key !== "PageUp" && event.key !== "PageDown" && event.key !== "Home" && event.key !== "End" && event.key.indexOf("Arrow") === -1 &&
             event.key !== "Escape" && event.key !== "Shift" && event.key !== "Meta" && event.key !== "Alt" && event.key !== "Control" && event.key !== "CapsLock" &&
-            !isNotEditBlock(nodeElement) &&
-            !/^F\d{1,2}$/.test(event.key) && typeof protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] === "undefined") {
+            !isNotEditBlock(nodeElement) && !/^F\d{1,2}$/.test(event.key) &&
+            // 微软双拼使用 compositionstart,否则 focusByRange 导致无法输入文字
+            event.key !== "Process") {
             const cloneRange = range.cloneRange();
+            range.collapse(false);
             range.insertNode(document.createElement("wbr"));
             protyle.wysiwyg.lastHTMLs[nodeElement.getAttribute("data-node-id")] = nodeElement.outerHTML;
             nodeElement.querySelector("wbr").remove();
             // 光标位于引用结尾后 ctrl+b 偶尔会失效
             range = cloneRange;
-            // 会导致  protyle.toolbar.range 和 range 不一致,先在有问题的地方重置一下 https://github.com/siyuan-note/siyuan/issues/10933
+            focusByRange(cloneRange);
+            protyle.toolbar.range = cloneRange;
+            protyle.wysiwyg.preventKeyup = true;
         }
 
         if (!window.siyuan.menus.menu.element.classList.contains("fn__none") &&

+ 2 - 0
app/src/protyle/wysiwyg/remove.ts

@@ -387,6 +387,8 @@ export const removeBlock = (protyle: IProtyle, blockElement: Element, range: Ran
         // 需先移除 removeElement,否则 side 会选中 removeElement
         removeElement.remove();
         focusBlock(previousLastElement, undefined, false);
+        // https://github.com/siyuan-note/siyuan/issues/13254
+        undoOperations.splice(0, 1);
     } else {
         const previousLastEditElement = getContenteditableElement(previousLastElement);
         if (editableElement && (editableElement.textContent !== "" || editableElement.querySelector(".emoji"))) {

+ 0 - 1
app/src/protyle/wysiwyg/transaction.ts

@@ -1121,7 +1121,6 @@ export const transaction = (protyle: IProtyle, doOperations: IOperation[], undoO
         protyle.transactionTime - time < Constants.TIMEOUT_INPUT) {
         needDebounce = true;
     }
-    protyle.wysiwyg.lastHTMLs = {};
     if (undoOperations) {
         if (window.siyuan.config.fileTree.openFilesUseCurrentTab && protyle.model) {
             protyle.model.headElement.classList.remove("item--unupdate");

+ 38 - 11
app/src/search/util.ts

@@ -51,6 +51,7 @@ import {addClearButton} from "../util/addClearButton";
 import {checkFold} from "../util/noRelyPCFunction";
 import {getUnRefList, openSearchUnRef, unRefMoreMenu} from "./unRef";
 import {getDefaultType} from "./getDefault";
+import {searchMarkRender} from "../protyle/render/searchMarkRender";
 
 export const toggleReplaceHistory = (searchElement: Element) => {
     const list = window.siyuan.storage[Constants.LOCAL_SEARCHKEYS];
@@ -1159,6 +1160,28 @@ const renderNextSearchMark = (options: {
     edit: Protyle,
     target: Element,
 }) => {
+    const contentRect = options.edit.protyle.contentElement.getBoundingClientRect();
+    if (CSS.highlights) {
+        options.edit.protyle.highlight.markHL.clear();
+        options.edit.protyle.highlight.mark.clear();
+        options.edit.protyle.highlight.rangeIndex++;
+        if (options.edit.protyle.highlight.rangeIndex >= options.edit.protyle.highlight.ranges.length) {
+            options.edit.protyle.highlight.rangeIndex = 0;
+        }
+        let rangeTop;
+        options.edit.protyle.highlight.ranges.forEach((item, index) => {
+            if (options.edit.protyle.highlight.rangeIndex === index) {
+                options.edit.protyle.highlight.markHL.add(item);
+                rangeTop = item.getBoundingClientRect().top;
+            } else {
+                options.edit.protyle.highlight.mark.add(item);
+            }
+        });
+        if (typeof rangeTop === "number") {
+            options.edit.protyle.contentElement.scrollTop = options.edit.protyle.contentElement.scrollTop + rangeTop - contentRect.top - contentRect.height / 2;
+        }
+        return;
+    }
     let matchElement;
     const allMatchElements = Array.from(options.edit.protyle.wysiwyg.element.querySelectorAll(`div[data-node-id="${options.id}"] span[data-type~="search-mark"]`));
     allMatchElements.find((item, itemIndex) => {
@@ -1173,7 +1196,6 @@ const renderNextSearchMark = (options: {
     }
     if (matchElement) {
         matchElement.classList.add("search-mark--hl");
-        const contentRect = options.edit.protyle.contentElement.getBoundingClientRect();
         options.edit.protyle.contentElement.scrollTop = options.edit.protyle.contentElement.scrollTop + matchElement.getBoundingClientRect().top - contentRect.top - contentRect.height / 2;
     }
 };
@@ -1209,18 +1231,23 @@ export const getArticle = (options: {
                     updateReadonly: true,
                     data: getResponse,
                     protyle: options.edit.protyle,
-                    action: zoomIn ? [Constants.CB_GET_ALL, Constants.CB_GET_HTML] : [Constants.CB_GET_HL, Constants.CB_GET_HTML],
+                    action: zoomIn ? [Constants.CB_GET_ALL, Constants.CB_GET_HTML] : [Constants.CB_GET_HTML],
                 });
-                const matchElement = options.edit.protyle.wysiwyg.element.querySelector(`div[data-node-id="${options.id}"] span[data-type~="search-mark"]`);
-                if (matchElement) {
-                    matchElement.classList.add("search-mark--hl");
-                    const contentRect = options.edit.protyle.contentElement.getBoundingClientRect();
-                    const matchRectTop = matchElement.getBoundingClientRect().top;  // 需前置,否则代码高亮后会移除该元素
-                    setTimeout(() => {
-                        // 等待 scrollCenter 定位后再滚动
-                        options.edit.protyle.contentElement.scrollTop = options.edit.protyle.contentElement.scrollTop + matchRectTop - contentRect.top - contentRect.height / 2;
-                    });
+                const matchElements = options.edit.protyle.wysiwyg.element.querySelectorAll(`div[data-node-id="${options.id}"] span[data-type~="search-mark"]`);
+                if (matchElements.length === 0) {
+                    return;
+                }
+                const contentRect = options.edit.protyle.contentElement.getBoundingClientRect();
+                let matchRectTop: number;
+                if (CSS.highlights) {
+                    options.edit.protyle.highlight.rangeIndex = 0;
+                    searchMarkRender(options.edit.protyle, matchElements);
+                    matchRectTop = options.edit.protyle.highlight.ranges[0].getBoundingClientRect().top;
+                } else {
+                    matchElements[0].classList.add("search-mark--hl");
+                    matchRectTop = matchElements[0].getBoundingClientRect().top;
                 }
+                options.edit.protyle.contentElement.scrollTop = options.edit.protyle.contentElement.scrollTop + matchRectTop - contentRect.top - contentRect.height / 2;
             });
         });
     });

+ 24 - 0
app/src/types/index.d.ts

@@ -107,8 +107,23 @@ type TAVFilterOperator =
     | "Is relative to today"
     | "Is true"
     | "Is false"
+
 declare module "blueimp-md5"
 
+declare class Highlight {
+    constructor(...range: Range[]);
+
+    add(range: Range): void
+
+    clear(): void
+
+    forEach(callbackfn: (value: Range, key: number) => void): void;
+}
+
+declare namespace CSS {
+    const highlights: Map<string, Highlight>;
+}
+
 interface Window {
     echarts: {
         init(element: HTMLElement, theme?: string, options?: {
@@ -178,6 +193,15 @@ interface Window {
         readClipboard(): string
         getBlockURL(): string
     }
+    JSHarmony: {
+        openExternal(url: string): void
+        changeStatusBarColor(color: string, mode: number): void
+        writeClipboard(text: string): void
+        writeHTMLClipboard(text: string, html: string): void
+        writeImageClipboard(uri: string): void
+        readClipboard(): string
+        getBlockURL(): string
+    }
 
     Protyle: import("../protyle/method").default
 

+ 6 - 0
app/src/types/protyle.d.ts

@@ -482,6 +482,12 @@ interface IProtyleOptions {
 }
 
 interface IProtyle {
+    highlight: {
+        mark: Highlight
+        markHL: Highlight
+        ranges: Range[]
+        rangeIndex: 0
+    }
     getInstance: () => import("../protyle").Protyle,
     observerLoad?: ResizeObserver,
     observer?: ResizeObserver,

+ 5 - 3
app/src/util/assets.ts

@@ -205,9 +205,9 @@ export const setInlineStyle = (set = true) => {
 .b3-typography code:not(.hljs), .protyle-wysiwyg span[data-type~=code] { font-variant-ligatures: ${window.siyuan.config.editor.codeLigatures ? "normal" : "none"} }
 .li > .protyle-action {height:${height + 8}px;line-height: ${height + 8}px}
 .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h1, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h2, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h3, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h4, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h5, .protyle-wysiwyg [data-node-id].li > .protyle-action ~ .h6 {line-height:${height + 8}px;}
-.protyle-wysiwyg [data-node-id].li > .protyle-action:after {height: ${window.siyuan.config.editor.fontSize}px;width: ${window.siyuan.config.editor.fontSize}px;margin:-${window.siyuan.config.editor.fontSize / 2}px 0 0 -${window.siyuan.config.editor.fontSize / 2}px}
+.protyle-wysiwyg [data-node-id].li > .protyle-action::after {height: ${window.siyuan.config.editor.fontSize}px;width: ${window.siyuan.config.editor.fontSize}px;margin:-${window.siyuan.config.editor.fontSize / 2}px 0 0 -${window.siyuan.config.editor.fontSize / 2}px}
 .protyle-wysiwyg [data-node-id].li > .protyle-action svg {height: ${Math.max(14, window.siyuan.config.editor.fontSize - 8)}px}
-.protyle-wysiwyg [data-node-id].li:before {height: calc(100% - ${height + 8}px);top:${(height + 8)}px}
+.protyle-wysiwyg [data-node-id].li::before {height: calc(100% - ${height + 8}px);top:${(height + 8)}px}
 .protyle-wysiwyg [data-node-id] [spellcheck] {min-height:${height}px;}
 .protyle-wysiwyg .p,
 .protyle-wysiwyg .code-block .hljs,
@@ -342,8 +342,10 @@ const updateMobileTheme = (OSTheme: string) => {
             }
             if (isInIOS()) {
                 window.webkit.messageHandlers.changeStatusBar.postMessage((backgroundColor || (mode === 0 ? "#fff" : "#1e1e1e")) + " " + mode);
-            } else if (isInAndroid() || isInHarmony()) {
+            } else if (isInAndroid()) {
                 window.JSAndroid.changeStatusBarColor(backgroundColor, mode);
+            } else if ( isInHarmony()) {
+                window.JSHarmony.changeStatusBarColor(backgroundColor, mode);
             }
         }, 500); // 移动端需要加载完才可以获取到颜色
     }

+ 4 - 2
app/src/util/highlightById.ts

@@ -1,5 +1,5 @@
 import {hasClosestBlock, hasClosestByAttribute} from "../protyle/util/hasClosest";
-import {getEditorRange} from "../protyle/util/selection";
+import {focusByRange, getEditorRange} from "../protyle/util/selection";
 
 export const bgFade = (element: Element) => {
     element.classList.add("protyle-wysiwyg--hl");
@@ -56,7 +56,8 @@ export const scrollCenter = (protyle: IProtyle, nodeElement?: Element, top = fal
                 (blockElement.querySelector(".av__row--header").getAttribute("style")?.indexOf("transform") > -1 || blockElement.querySelector(".av__row--footer").getAttribute("style")?.indexOf("transform") > -1)) {
                 return;
             }
-
+            // 撤销时 br 插入删除会导致 rang 被修改 https://github.com/siyuan-note/siyuan/issues/12679
+            const cloneRange = range.cloneRange();
             const br2Element = document.createElement("br");
             range.insertNode(br2Element);
             const editorElement = protyle.contentElement;
@@ -71,6 +72,7 @@ export const scrollCenter = (protyle: IProtyle, nodeElement?: Element, top = fal
                 editorElement.scroll({top: scrollTop, behavior});
             }
             br2Element.remove();
+            focusByRange(cloneRange);
             return;
         }
     }

+ 1 - 1
app/stage/auth.html

@@ -416,7 +416,7 @@
             const {ipcRenderer} = require('electron')
             ipcRenderer.send('siyuan-quit', window.location.port)
         } catch (e) {
-            if ((window.webkit && window.webkit.messageHandlers) || window.JSAndroid) {
+            if ((window.webkit && window.webkit.messageHandlers) || window.JSAndroid || window.JSHarmony) {
                 window.location.href = 'siyuan://api/system/exit'
             } else {
                 window.location.reload()

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
app/stage/protyle/js/flowchart.js/flowchart.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
app/stage/protyle/js/lute/lute.min.js


+ 1 - 1
app/stage/service-worker.js

@@ -6,7 +6,7 @@ const INITIAL_CACHED_RESOURCES = [
     "/stage/icon-large.png",
     "/stage/icon.png",
     "/stage/loading-pure.svg",
-    "/stage/build/fonts/JetBrainsMono-Regular.woff",
+    "/stage/build/fonts/JetBrainsMono-Regular.woff2",
     "/stage/protyle/js/lute/lute.min.js",
     "/stage/protyle/js/protyle-html.js"
 ];

+ 2 - 2
app/webpack.config.js

@@ -76,10 +76,10 @@ module.exports = (env, argv) => {
                     ],
                 },
                 {
-                    test: /\.woff$/,
+                    test: /\.woff2$/,
                     type: "asset/resource",
                     generator: {
-                        filename: "../fonts/JetBrainsMono-Regular.woff",
+                        filename: "../fonts/JetBrainsMono-Regular.woff2",
                     },
                 },
                 {

+ 2 - 2
app/webpack.desktop.js

@@ -79,10 +79,10 @@ module.exports = (env, argv) => {
                     ],
                 },
                 {
-                    test: /\.woff$/,
+                    test: /\.woff2$/,
                     type: "asset/resource",
                     generator: {
-                        filename: "../fonts/JetBrainsMono-Regular.woff",
+                        filename: "../fonts/JetBrainsMono-Regular.woff2",
                     },
                 },
                 {

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است