DragHandle.vue 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370
  1. <script setup lang="ts">
  2. import { NPopover, NElement, useThemeVars } from "naive-ui";
  3. import {
  4. lockDragHandle,
  5. unlockDragHandle,
  6. useDragHandle,
  7. useDragHandleData,
  8. } from "./drag-handle";
  9. import { onMounted, ref, watch } from "vue";
  10. import { Range } from "@tiptap/core";
  11. import { injectExtension, useEditorInstance } from "@lib/core/extension/utils/common";
  12. const vars = useThemeVars();
  13. const root = ref<any>();
  14. const showSlash = ref(false);
  15. const showPop = ref(false);
  16. const editorInstance = useEditorInstance();
  17. const data = useDragHandleData();
  18. const container = document.createElement("div");
  19. injectExtension(useDragHandle({ element: container }));
  20. onMounted(() => {
  21. container.append(root.value);
  22. });
  23. const items = ref([
  24. {
  25. name: "插入段落",
  26. cmd: "/paragraph",
  27. icon: "paragraph",
  28. action: (range: Range) => {
  29. if (!data.value.range) {
  30. return;
  31. }
  32. editorInstance.value.chain().focus().insertContentAt(range.to, "<p></p>").run();
  33. },
  34. },
  35. {
  36. name: "插入链接",
  37. cmd: "/link",
  38. icon: "link",
  39. action: (range: Range) => {
  40. if (!data.value.range) {
  41. return;
  42. }
  43. editorInstance.value.chain().focus().insertContentAt(range.to, "<p></p>").run();
  44. editorInstance.value.storage.link.openLink();
  45. },
  46. },
  47. {
  48. name: "插入图片",
  49. cmd: "/img",
  50. icon: "image-line",
  51. action: (range: Range) => {
  52. editorInstance.value.chain().insertContentAt(range.to, "<p></p>").focus().run();
  53. editorInstance.value.storage.image.openUploader();
  54. },
  55. },
  56. {
  57. name: "插入视频",
  58. cmd: "/video",
  59. icon: "video-line",
  60. action: (range: Range) => {
  61. editorInstance.value.chain().focus().insertContentAt(range.to, "<p></p>").run();
  62. editorInstance.value.storage.video.openUploader();
  63. },
  64. },
  65. {
  66. name: "引用",
  67. cmd: "/b",
  68. icon: "double-quotes-l",
  69. action: (range: Range) =>
  70. editorInstance.value
  71. .chain()
  72. .focus()
  73. .insertContentAt(range.to, "<p></p>")
  74. .toggleBlockquote()
  75. .run(),
  76. },
  77. {
  78. name: "标题1",
  79. cmd: "/h1",
  80. icon: "h-1",
  81. action: (range: Range) =>
  82. editorInstance.value
  83. .chain()
  84. .focus()
  85. .insertContentAt(range.to, "<p></p>")
  86. .setHeading({ level: 1 })
  87. .run(),
  88. },
  89. {
  90. name: "标题2",
  91. cmd: "/h2",
  92. icon: "h-2",
  93. action: (range: Range) =>
  94. editorInstance.value
  95. .chain()
  96. .insertContentAt(range.to, "<p></p>")
  97. .setHeading({ level: 2 })
  98. .run(),
  99. },
  100. {
  101. name: "标题3",
  102. cmd: "/h3",
  103. icon: "h-3",
  104. action: (range: Range) =>
  105. editorInstance.value
  106. .chain()
  107. .insertContentAt(range.to, "<p></p>")
  108. .setHeading({ level: 3 })
  109. .run(),
  110. },
  111. {
  112. name: "列表",
  113. cmd: "/list",
  114. icon: "list-unordered",
  115. action: (range: Range) =>
  116. editorInstance.value
  117. .chain()
  118. .focus()
  119. .insertContentAt(range.to, "<p></p>")
  120. .toggleBulletList()
  121. .run(),
  122. },
  123. {
  124. name: "数学公式",
  125. cmd: "/math",
  126. icon: "functions",
  127. action: (range: Range) => {
  128. editorInstance.value.chain().focus().insertContentAt(range.to, "<p></p>").run();
  129. editorInstance.value.storage["hb-math"].openEditor();
  130. },
  131. },
  132. {
  133. name: "代码",
  134. cmd: "/code",
  135. icon: "brackets-line",
  136. action: (range: Range) =>
  137. editorInstance.value
  138. .chain()
  139. .focus()
  140. .insertContentAt(range.to, "<p></p>")
  141. .toggleCode()
  142. .run(),
  143. },
  144. {
  145. name: "代码块",
  146. cmd: "/codeblock",
  147. icon: "code-view",
  148. action: (range: Range) =>
  149. editorInstance.value
  150. .chain()
  151. .focus()
  152. .insertContentAt(range.to, "<p></p>")
  153. .toggleCodeBlock()
  154. .run(),
  155. },
  156. ]);
  157. const items2 = ref([
  158. {
  159. name: "上移一行",
  160. icon: "arrow-up-s-line",
  161. action: (range: Range) => {
  162. if (!data.value.preRange) {
  163. return;
  164. }
  165. const editor = editorInstance.value;
  166. const state = editorInstance.value.state;
  167. const tr = state.tr;
  168. const range1 = { ...data.value.preRange }; // 第二个 range 的位置
  169. const range2 = { ...range }; // 第一个 range 的位置
  170. const fromNode = state.doc.cut(range1.from, range1.to);
  171. const toNode = state.doc.cut(range2.from, range2.to);
  172. tr.replaceRangeWith(range2.from, range2.to, fromNode);
  173. tr.replaceRangeWith(range1.from, range1.to, toNode);
  174. editor.view.dispatch(tr);
  175. },
  176. },
  177. {
  178. name: "删除本行",
  179. icon: "close-line",
  180. action: (range: Range) => {
  181. const editor = editorInstance.value;
  182. const state = editorInstance.value.state;
  183. const tr = state.tr;
  184. tr.delete(range.from, range.to).scrollIntoView();
  185. editorInstance.value.view.dispatch(tr);
  186. },
  187. },
  188. {
  189. name: "复制本行",
  190. icon: "file-copy-line",
  191. action: (range: Range) => {
  192. const editor = editorInstance.value;
  193. if (data.value.node) {
  194. editor.commands.copyRange(range, data.value.node);
  195. }
  196. },
  197. },
  198. {
  199. name: "清除格式",
  200. icon: "format-clear",
  201. action: (range: Range) => {
  202. const editor = editorInstance.value;
  203. editor.chain().setNodeSelection(range.from).unsetAllMarks().run()
  204. },
  205. },
  206. {
  207. name: "下移一行",
  208. icon: "arrow-down-s-line",
  209. action: (range: Range) => {
  210. if (!data.value.nextRange) {
  211. return;
  212. }
  213. const editor = editorInstance.value;
  214. const state = editorInstance.value.state;
  215. const tr = state.tr;
  216. const range1 = { ...range }; // 第一个 range 的位置
  217. const range2 = { ...data.value.nextRange }; // 第二个 range 的位置
  218. const fromNode = state.doc.cut(range1.from, range1.to);
  219. const toNode = state.doc.cut(range2.from, range2.to);
  220. tr.replaceRangeWith(range2.from, range2.to, fromNode);
  221. tr.replaceRangeWith(range1.from, range1.to, toNode);
  222. editor.view.dispatch(tr);
  223. },
  224. },
  225. ]);
  226. function doAction(e: any) {
  227. if (data.value.rect) {
  228. e.action(data.value.range);
  229. showSlash.value = false;
  230. showPop.value = false;
  231. }
  232. }
  233. watch([showSlash, showPop], () => {
  234. if (showSlash.value || showPop.value) {
  235. lockDragHandle();
  236. } else {
  237. unlockDragHandle();
  238. }
  239. });
  240. </script>
  241. <template>
  242. <div style="display: none">
  243. <div class="drag-handle" ref="root">
  244. <n-popover
  245. :z-index="99999"
  246. style="padding: 0; border-radius: 10px"
  247. v-model:show="showSlash"
  248. trigger="click"
  249. placement="bottom-start"
  250. :show-arrow="false"
  251. >
  252. <template #trigger>
  253. <n-element class="drag-button">
  254. <i class="ri-add-fill"></i>
  255. </n-element>
  256. </template>
  257. <slot name="drag-handle-slash">
  258. <n-element class="slash-command">
  259. <div class="slash-item" v-for="(e, i) in items" @click="doAction(e)" :key="e.cmd">
  260. <div class="slash-name">
  261. <div class="slash-icon">
  262. <i :class="`ri-${e.icon}`"></i>
  263. </div>
  264. <span>{{ e.name }}</span>
  265. </div>
  266. </div>
  267. </n-element>
  268. </slot>
  269. </n-popover>
  270. <n-popover
  271. :z-index="99999"
  272. style="padding: 0; border-radius: 10px"
  273. v-model:show="showPop"
  274. trigger="click"
  275. placement="bottom-start"
  276. :show-arrow="false"
  277. >
  278. <template #trigger>
  279. <n-element class="drag-button">
  280. <i class="ri-draggable"></i>
  281. </n-element>
  282. </template>
  283. <slot name="drag-handle-select">
  284. <n-element class="slash-command">
  285. <div class="slash-item" v-for="(e, i) in items2" @click="doAction(e)" :key="e.name">
  286. <div class="slash-name">
  287. <div class="slash-icon">
  288. <i :class="`ri-${e.icon}`"></i>
  289. </div>
  290. <span>{{ e.name }}</span>
  291. </div>
  292. </div>
  293. </n-element>
  294. </slot>
  295. </n-popover>
  296. </div>
  297. </div>
  298. </template>
  299. <style scoped>
  300. .slash-command {
  301. width: 160px;
  302. box-sizing: border-box;
  303. display: flex;
  304. flex-direction: column;
  305. outline: none;
  306. border: none;
  307. user-select: none;
  308. border-radius: 10px;
  309. overflow: hidden;
  310. }
  311. .slash-item {
  312. display: flex;
  313. justify-content: space-between;
  314. align-items: center;
  315. padding: 6px 10px;
  316. transition: all 0.5s;
  317. }
  318. .slash-item:hover {
  319. background: var(--hover-color);
  320. }
  321. .slash-name {
  322. display: flex;
  323. gap: 10px;
  324. align-items: center;
  325. font-size: 14px;
  326. }
  327. .slash-icon {
  328. border: 1px solid var(--border-color);
  329. display: flex;
  330. align-items: center;
  331. justify-content: center;
  332. width: 24px;
  333. height: 24px;
  334. border-radius: 5px;
  335. }
  336. .drag-handle {
  337. align-items: center;
  338. gap: 2px;
  339. display: flex;
  340. }
  341. .drag-button {
  342. width: 24px;
  343. height: 24px;
  344. border-radius: 5px;
  345. box-sizing: border-box;
  346. display: flex;
  347. align-items: center;
  348. justify-content: center;
  349. color: var(--text-color3);
  350. }
  351. .drag-button:hover {
  352. background: var(--hover-color);
  353. border: 1px solid var(--border-color);
  354. }
  355. </style>