[js] 快捷键移动列表节点

image.png

各位以后要注意先看编辑选项里有没有想要的功能 😂

没仔细看直接想当然的让 AI 搓了 10 分钟搓了个弱化版,当反向示例了这下doge


类似代码编辑器中移动代码行

移动块演示.gif

可能有神奇的特性,个人暂时只打算用于列表节点的临时交换。

(function() { const listSwapper = { // 获取当前文档编辑区 getActiveEditor: function() { return document.querySelector('.protyle:not(.fn__none) .protyle-wysiwyg'); }, // 判断节点是否为列表项 isListItem: function(node) { return node && node.classList && node.classList.contains('li'); }, // 判断节点是否为列表容器 isList: function(node) { return node && node.classList && node.classList.contains('list'); }, // 从当前选择的节点查找最近的列表项节点 findListItemNode: function(node) { // 如果是文本节点,先获取其父元素 if (node.nodeType === 3) { node = node.parentElement; } // 向上查找,直到找到列表项节点 while (node) { if (this.isListItem(node)) { return node; } node = node.parentElement; // 如果超出编辑区范围则停止 if (!node || node.classList.contains('protyle-wysiwyg')) { return null; } } return null; }, // 获取同级的前一个列表项 getPreviousListItem: function(listItem) { if (!this.isListItem(listItem)) return null; // 直接获取前一个兄弟元素 let prev = listItem.previousElementSibling; // 确保是列表项 if (prev && this.isListItem(prev)) { return prev; } return null; }, // 获取同级的后一个列表项 getNextListItem: function(listItem) { if (!this.isListItem(listItem)) return null; // 直接获取后一个兄弟元素 let next = listItem.nextElementSibling; // 确保是列表项 if (next && this.isListItem(next)) { return next; } return null; }, // 交换两个列表项节点的位置 swapListItems: function(item1, item2) { if (!item1 || !item2 || !this.isListItem(item1) || !this.isListItem(item2)) { return false; } try { const parent = item1.parentNode; // 确保两个节点在同一个父列表中 if (parent !== item2.parentNode) { console.log('列表项不在同一个父容器中,无法交换'); return false; } // 保存当前项的ID用于后续恢复焦点 const itemId = item1.getAttribute('data-node-id'); // 判断相对位置并交换 const next = item1.nextElementSibling; if (item2 === next) { // item2紧跟在item1后面,直接调整顺序 parent.insertBefore(item2, item1); } else { // 创建节点引用的副本,用于判断顺序 const item1Clone = item1; const item2Clone = item2; // 标记item2的下一个节点 const item2Next = item2.nextElementSibling; // 如果item1在item2之前 if (Array.from(parent.children).indexOf(item1Clone) < Array.from(parent.children).indexOf(item2Clone)) { // 将item2移到item1之前 parent.insertBefore(item2, item1); // 将item1移到item2原来的位置 if (item2Next) { parent.insertBefore(item1, item2Next); } else { parent.appendChild(item1); } } else { // item2在item1之前 // 先将item1移到item2的位置 parent.insertBefore(item1, item2); // 再将item2移到item1原来的位置 if (next) { parent.insertBefore(item2, next); } else { parent.appendChild(item2); } } } console.log('列表项交换成功'); return itemId; } catch (e) { console.error('交换列表项失败:', e); return false; } }, // 向上移动列表项 moveListItemUp: function() { // 获取当前选中内容所在的列表项 const selection = window.getSelection(); if (!selection.rangeCount) return; const listItem = this.findListItemNode(selection.getRangeAt(0).startContainer); if (!listItem) { console.log('当前光标不在列表项内'); return; } console.log('当前列表项:', listItem); // 获取同级的前一个列表项 const prevItem = this.getPreviousListItem(listItem); if (!prevItem) { console.log('已经是第一个列表项,无法上移'); return; } console.log('前一个列表项:', prevItem); // 交换列表项 const itemId = this.swapListItems(listItem, prevItem); // 恢复焦点 if (itemId) { setTimeout(() => { this.focusListItemById(itemId); }, 10); } }, // 向下移动列表项 moveListItemDown: function() { // 获取当前选中内容所在的列表项 const selection = window.getSelection(); if (!selection.rangeCount) return; const listItem = this.findListItemNode(selection.getRangeAt(0).startContainer); if (!listItem) { console.log('当前光标不在列表项内'); return; } console.log('当前列表项:', listItem); // 获取同级的后一个列表项 const nextItem = this.getNextListItem(listItem); if (!nextItem) { console.log('已经是最后一个列表项,无法下移'); return; } console.log('后一个列表项:', nextItem); // 交换列表项 const itemId = this.swapListItems(listItem, nextItem); // 恢复焦点 if (itemId) { setTimeout(() => { this.focusListItemById(itemId); }, 10); } }, // 通过ID聚焦到列表项 focusListItemById: function(itemId) { if (!itemId) return; // 查找具有特定ID的列表项 const item = document.querySelector(`[data-node-id="${itemId}"]`); if (!item) return; // 查找列表项中的可编辑段落 const paragraph = item.querySelector('[contenteditable="true"]'); if (!paragraph) return; // 聚焦并将光标设置到段落末尾 paragraph.focus(); const range = document.createRange(); const selection = window.getSelection(); // 将光标设置到段落内容末尾 if (paragraph.childNodes.length > 0) { const lastChild = paragraph.childNodes[paragraph.childNodes.length - 1]; if (lastChild.nodeType === Node.TEXT_NODE) { range.setStart(lastChild, lastChild.length); range.setEnd(lastChild, lastChild.length); } else { range.selectNodeContents(lastChild); range.collapse(false); } } else { range.selectNodeContents(paragraph); range.collapse(false); } selection.removeAllRanges(); selection.addRange(range); }, // 初始化键盘事件监听 init: function() { document.addEventListener('keydown', (event) => { // 检查是否是Alt+箭头组合键 if (event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) { // Alt + 上箭头:向上移动列表项 if (event.key === 'ArrowUp') { event.preventDefault(); this.moveListItemUp(); return false; } // Alt + 下箭头:向下移动列表项 else if (event.key === 'ArrowDown') { event.preventDefault(); this.moveListItemDown(); return false; } } }); console.log('思源笔记列表项交换脚本已加载,使用 Alt+↑/↓ 来交换列表项位置'); } }; // 初始化脚本 listSwapper.init(); })();
  • 思源笔记

    思源笔记是一款隐私优先的个人知识管理系统,支持完全离线使用,同时也支持端到端加密同步。

    融合块、大纲和双向链接,重构你的思维。

    25893 引用 • 107282 回帖
  • 代码片段

    代码片段分为 CSS 与 JS 两种代码,添加在 [设置 - 外观 - 代码片段] 中,这些代码会在思源笔记加载时自动执行,用于改善笔记的样式或功能。

    用户在该标签下分享代码片段时需在帖子标题前添加 [css] [js] 用于区分代码片段类型。

    177 引用 • 1255 回帖
1 操作
xqh042 在 2025-03-23 16:30:31 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • 话说代码片段除了不会保存,跟思源内置的功能有什么区别吗?

    image.png

    1 回复
  • 其他回帖
  • xqh042

    甚至还更弱了 😂 主打一个没仔细看设置乱折腾


    好吧,其实我还是搜了一下设置的,但是关键词(移动、块)都没命中,就以为没这功能了doge

推荐标签 标签

  • Sandbox

    如果帖子标签含有 Sandbox ,则该帖子会被视为“测试帖”,主要用于测试社区功能,排查 bug 等,该标签下内容不定期进行清理。

    434 引用 • 1238 回帖 • 592 关注
  • Caddy

    Caddy 是一款默认自动启用 HTTPS 的 HTTP/2 Web 服务器。

    10 引用 • 54 回帖 • 177 关注
  • Tomcat

    Tomcat 最早是由 Sun Microsystems 开发的一个 Servlet 容器,在 1999 年被捐献给 ASF(Apache Software Foundation),隶属于 Jakarta 项目,现在已经独立为一个顶级项目。Tomcat 主要实现了 JavaEE 中的 Servlet、JSP 规范,同时也提供 HTTP 服务,是市场上非常流行的 Java Web 容器。

    162 引用 • 529 回帖 • 4 关注
  • WiFiDog

    WiFiDog 是一套开源的无线热点认证管理工具,主要功能包括:位置相关的内容递送;用户认证和授权;集中式网络监控。

    1 引用 • 7 回帖 • 612 关注
  • danl
    174 关注
  • ZeroNet

    ZeroNet 是一个基于比特币加密技术和 BT 网络技术的去中心化的、开放开源的网络和交流系统。

    1 引用 • 21 回帖 • 653 关注
  • 反馈

    Communication channel for makers and users.

    120 引用 • 906 回帖 • 279 关注
  • Elasticsearch

    Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

    117 引用 • 99 回帖 • 199 关注
  • 新人

    让我们欢迎这对新人。哦,不好意思说错了,让我们欢迎这位新人!
    新手上路,请谨慎驾驶!

    52 引用 • 228 回帖
  • FFmpeg

    FFmpeg 是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。

    23 引用 • 32 回帖
  • 脑图

    脑图又叫思维导图,是表达发散性思维的有效图形思维工具 ,它简单却又很有效,是一种实用性的思维工具。

    32 引用 • 99 回帖
  • React

    React 是 Facebook 开源的一个用于构建 UI 的 JavaScript 库。

    192 引用 • 291 回帖 • 370 关注
  • 创业

    你比 99% 的人都优秀么?

    82 引用 • 1395 回帖
  • 资讯

    资讯是用户因为及时地获得它并利用它而能够在相对短的时间内给自己带来价值的信息,资讯有时效性和地域性。

    56 引用 • 85 回帖
  • OpenResty

    OpenResty 是一个基于 NGINX 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

    17 引用 • 51 关注
  • LeetCode

    LeetCode(力扣)是一个全球极客挚爱的高质量技术成长平台,想要学习和提升专业能力从这里开始,充足技术干货等你来啃,轻松拿下 Dream Offer!

    209 引用 • 72 回帖 • 3 关注
  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    284 引用 • 248 回帖
  • Dubbo

    Dubbo 是一个分布式服务框架,致力于提供高性能和透明化的 RPC 远程服务调用方案,是 [阿里巴巴] SOA 服务化治理方案的核心框架,每天为 2,000+ 个服务提供 3,000,000,000+ 次访问量支持,并被广泛应用于阿里巴巴集团的各成员站点。

    60 引用 • 82 回帖 • 615 关注
  • 招聘

    哪里都缺人,哪里都不缺人。

    188 引用 • 1057 回帖 • 1 关注
  • GitLab

    GitLab 是利用 Ruby 一个开源的版本管理系统,实现一个自托管的 Git 项目仓库,可通过 Web 界面操作公开或私有项目。

    46 引用 • 72 回帖 • 2 关注
  • Facebook

    Facebook 是一个联系朋友的社交工具。大家可以通过它和朋友、同事、同学以及周围的人保持互动交流,分享无限上传的图片,发布链接和视频,更可以增进对朋友的了解。

    4 引用 • 15 回帖 • 451 关注
  • 面试

    面试造航母,上班拧螺丝。多面试,少加班。

    326 引用 • 1395 回帖
  • 印象笔记
    3 引用 • 16 回帖 • 1 关注
  • OAuth

    OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 oAuth 是安全的。oAuth 是 Open Authorization 的简写。

    36 引用 • 103 回帖 • 34 关注
  • 国际化

    i18n(其来源是英文单词 internationalization 的首末字符 i 和 n,18 为中间的字符数)是“国际化”的简称。对程序来说,国际化是指在不修改代码的情况下,能根据不同语言及地区显示相应的界面。

    8 引用 • 26 回帖
  • V2EX

    V2EX 是创意工作者们的社区。这里目前汇聚了超过 400,000 名主要来自互联网行业、游戏行业和媒体行业的创意工作者。V2EX 希望能够成为创意工作者们的生活和事业的一部分。

    16 引用 • 236 回帖 • 253 关注
  • OpenCV
    15 引用 • 36 回帖 • 7 关注