思源笔记折腾记录 - 更多更多的自定义菜单

本贴最后更新于 722 天前,其中的信息可能已经时过境迁

image

一、前情提要

我们之前实现了对块标菜单进行注入:

但是现在还有几个问题:

1、所有块标都是一样的菜单。

2、文档树啊、文档右上角的导出啊之类的地方并没有菜单弹出。

所以我们现在来解决一下它。

二、实现原理和实现过程

1、更详细的状态记录

我们之前会在每次点击块标的时候获取当前块标菜单对应的块 id,但是显然能够从界面上获取的不止这些。

然后通过类似的做法,我们也可以实现更多的状态记录。

就像这样,之前的注册表长这个样子,用了一个数组来进行注册:

let 带渲染菜单项目注册表 = []

然后遍历它进行渲染:

 待渲染菜单项目数组.forEach(
        菜单项目 => {
            渲染自定义菜单(菜单项目)
        }
    )

显然只需要在这之前加上一步条件匹配就可以实现只渲染我们需要的菜单项目了。

但是在此之前我们需要更多一点的菜单状态

首先我们记录一下最后进行的操作:

let status  = {
    鼠标状态:{
        最后鼠标单击:{},
	最后鼠标双击:{}
        最后鼠标右击:{},
        当前鼠标坐标:{},
        当前鼠标元素:{},
    },
    键盘状态:{
        最后键盘输入元素:{},
        最后键盘输入事件:{},
    },
}

然后我们就能够在其他地方通过读取这个来达到一些目的了对吧,也就不用老是在各种地方加监听器了。

然后我们加载一些全局监听器:

let 鼠标单击回调 = (鼠标事件)=>{
    界面状态.鼠标状态.最后鼠标点击元素=鼠标事件.target
    界面状态.鼠标状态.最后鼠标点击事件=鼠标事件 
}
document.addEventListener(
    "click", (event) => {
        鼠标单击回调(event)
    }, true
)
let 键盘事件回调= (键盘事件)=>{
    界面状态.键盘状态.最后键盘输入元素 = 键盘事件.target
    界面状态.键盘状态.最后键盘输入事件 = 键盘事件
}
document.addEventListener(
    "beforeinput",(键盘事件)=>{
        键盘事件回调(键盘事件)
    },true
)
export { 界面状态 as 界面状态}

这样就方便之后在其他地方调用这些元素啦

然后在之前的生成菜单那里调用它并进行判断就可以了:

function 判断是否块标菜单(判定元素) {
    if(!判定元素){
        判定元素 = 界面状态.鼠标状态.最后鼠标点击元素
    }
    if(判定元素){
        switch (判定元素.tagName){
            case 'use':
            return 判断是否块标菜单(判定元素.parentElement)
            case 'svg':
            return    判断是否块标菜单(判定元素.parentElement)
            case 'BUTTON':
            return    判断是否块标菜单(判定元素.parentElement)
            case  'DIV':
            return 判定元素.classList&&判定元素.classList.contains('protyle-gutters')
        }  
    }
}

嗯,效果基本跟之前的一样。

抽离菜单渲染逻辑方便复用

然后抽离一下原本的菜单渲染逻辑,方便之后复用:

//用来生成和插入元素
import { 生成单个dom元素 } from "../../util/dom.js"
function 生成菜单项目元素(菜单项配置) {
    let 菜单项模板 = `
    <button class="b3-menu__item" data-item-id="${菜单项配置.id}">
        <svg class="b3-menu__icon" style="">
            <use xlink:href="${菜单项配置.图标}"></use>
        </svg>
        <span class="b3-menu__label">${菜单项配置.文字}</span>
    </button>
    `
    if (菜单项配置.点击回调函数) {
        return 生成单个dom元素(菜单项模板, 菜单项配置.点击回调函数)
    } else {
        return 生成单个dom元素(菜单项模板, 菜单项配置.事件配置)
    }
}
function 生成多级菜单项目元素(菜单项配置) {
    let 菜单项元素 = 生成菜单项目元素(菜单项配置)
    if (菜单项配置.子菜单配置 && 菜单项配置.子菜单配置[0]) {
        菜单项元素.insertAdjacentHTML("beforeend", `<div class="b3-menu__submenu"></div>`)
        let 子菜单容器 = 菜单项元素.querySelector('div')
        菜单项配置.子菜单配置.forEach(
            子菜单项配置 => {
                try {
                    子菜单容器.appendChild(生成多级菜单项目元素(子菜单项配置))
                } catch (e) {
                    console.error(e)
                }
            }
        )
    }
    return 菜单项元素
}
function 渲染自定义菜单(菜单项目) {
    let 菜单元素 = 生成多级菜单项目元素(菜单项目)
    插入菜单元素(菜单元素)
}
function 插入菜单元素(菜单项目元素) {
    window.top.siyuan.menus.menu.append(菜单项目元素)
}
export  function 批量渲染自定义菜单(待渲染菜单项目数组) {
    待渲染菜单项目数组.forEach(
        菜单项目 => {
            if(菜单项目.判定函数){
                菜单项目.判定函数()?渲染自定义菜单(菜单项目):null
            }
            else{
                渲染自定义菜单(菜单项目)
            }
        }
    )
}

实现更多种类的菜单

这个很简单,按照之前自定义块标菜单的逻辑,实现一下文档图标菜单和文档树菜单等等就完事儿了:

比如说文档编辑器的菜单:

import { 界面状态 } from '../../status/index.js'
let 待渲染菜单项目数组 = []
let 菜单状态 = {}
function 判断是否编辑器菜单(判定元素) {
    if (!判定元素) {
        判定元素 = 界面状态.鼠标状态.最后鼠标点击元素
    }
    if (判定元素) {
        switch (判定元素.tagName) {
            case 'use':
                return 判断是否编辑器菜单(判定元素.parentElement)
            case 'svg':
                return 判断是否编辑器菜单(判定元素.parentElement)
            case 'SPAN':
                return 判断是否编辑器菜单(判定元素.parentElement)

            case 'BUTTON':
                return 判断是否编辑器菜单(判定元素.parentElement)
            case 'DIV':
                if (判定元素.classList && 判定元素.classList.contains('protyle-title')) {
                    菜单状态.当前块id = 判定元素.parentElement.querySelector('.protyle-background').getAttribute('data-node-id')
                }
                return 判定元素.classList && 判定元素.classList.contains('protyle-title')
        }
    }
}
let 自定义编辑器菜单 = {
    注册自定义菜单项: (菜单项) => { 待渲染菜单项目数组.push(菜单项) },
    注册自定义子菜单项: (查找条件, 子菜单项) => {
        let 目标菜单项 = 待渲染菜单项目数组.find(
            菜单项 => { return 查找条件(菜单项) }
        )
        if (目标菜单项) {
            !目标菜单项.子菜单配置 ? 目标菜单项.子菜单配置 = [] : null
            let 重复子菜单项 = 目标菜单项.子菜单配置.find(
                待检查项子菜单项 => { return 待检查项子菜单项.id == 子菜单项.id }
            )
            console.log(目标菜单项, 子菜单项)
            if (!重复子菜单项) {
                目标菜单项.子菜单配置.push(子菜单项)
            }
        }
        else return
    },
    待渲染菜单项目数组,
    判断函数: 判断是否编辑器菜单,
    菜单状态: 菜单状态,
}
export { 自定义编辑器菜单 as 自定义编辑器菜单 }

另一种写法

也许有人奇怪这里为什么不用 class 来实现,其实确实可以:

let 自定义菜单 = {}
export class  自定义菜单原型{
    菜单注册表=[]
    constructor(){
	//这里在新建的时候自动添加菜单类型到自定义菜单接口
        自定义菜单[this.constructor.name]=this
        console.log(自定义菜单)
    }
    get 待渲染菜单项目数组(){
        return this.菜单注册表.filter(
            菜单项=>{
                if(this.筛选函数){
                    return this.筛选函数(菜单项)
                }
                else{
                    return 菜单项
                }
            }
        )
    }
    菜单状态={}
    判断函数(){}
    注册自定义菜单项(菜单项){ this.菜单注册表.push(菜单项) }
    注册自定义子菜单项(查找条件, 子菜单项){
        let 目标菜单项 = this.菜单注册表.find(
            菜单项 => { return 查找条件(菜单项) }
        )
        if (目标菜单项) {
            !目标菜单项.子菜单配置 ? 目标菜单项.子菜单配置 = [] : null
            let 重复子菜单项 = 目标菜单项.子菜单配置.find(
                待检查项子菜单项 => { return 待检查项子菜单项.id == 子菜单项.id }
            )
            console.log(目标菜单项, 子菜单项)
            if (!重复子菜单项) {
                目标菜单项.子菜单配置.push(子菜单项)
            }
        }
        else return
    }
}
export default 自定义菜单

然后各个菜单的实现确实短了很多:

export class 编辑器菜单 extends 自定义菜单原型{
    constructor(){
        super()
    }
    判断函数(判定元素) {
        if (!判定元素) {
            判定元素 = 界面状态.鼠标状态.最后鼠标点击元素
        }
        if (判定元素) {
            switch (判定元素.tagName) {
                case 'use':
                    return this.判断函数(判定元素.parentElement)
                case 'svg':
                    return this.判断函数(判定元素.parentElement)
                case 'SPAN':
                    return this.判断函数(判定元素.parentElement)
  
                case 'BUTTON':
                    return this.判断函数(判定元素.parentElement)
                case 'DIV':
                    if (判定元素.classList && 判定元素.classList.contains('protyle-title')) {
                        this.菜单状态.当前块id = 判定元素.parentElement.querySelector('.protyle-background').getAttribute('data-node-id')
                    }
                    return 判定元素.classList && 判定元素.classList.contains('protyle-title')
            }
        }
    }
}
export default new 编辑器菜单() 

不过我个人不大喜欢这样啦,所以还是耿直一点的写法算了,每个菜单都提供注册菜单项等等接口就可以了。

文档树上的发布菜单

我们之前对接 wechatSync 做了一个发布菜单,但是显示效果是这样的:

image

其实它更适合显示在文档树上对吧。

所以用刚刚出炉的文档树菜单搞一搞:

import noobApi from "../noobApi/index.js";
if(window.$syncer){
    let allAccounts = []
    window.$syncer.getAccounts(function (resp) {
        console.log('allAccounts', resp)
        allAccounts = resp
        注册菜单项(allAccounts)
    })
}
function 注册菜单项(账户列表){
    let 发布菜单配置 = {
        id:'wechatSync',
        文字:`使用wechatSync发布`,
        图标:'#iconInbox',
    }
    noobApi.自定义菜单.文档树菜单.注册自定义菜单项(发布菜单配置)
    noobApi.自定义菜单.编辑器菜单.注册自定义菜单项(发布菜单配置)
    //注意,这里由于两个菜单项注册的其实是同一个菜单配置,所以在一个地方注册子菜单项
    //就会在两边都显示了,所以不要重复注册
    账户列表.forEach(
        账户=>{
            noobApi.自定义菜单.文档树菜单.注册自定义子菜单项(
                (菜单项) => { return 菜单项.id == "wechatSync" },
                {
                    id:账户.uid,
                    文字:`${账户.title}`,
                    图标:'#iconInbox',
                    点击回调函数:()=>{发布文档到(账户)}
                }
            )
        }
    )
}
async function 发布文档到(账户){
    let 块id = noobApi.自定义菜单.当前菜单.菜单状态.当前块id
    let stmt = `select * from blocks where id in (select root_id from blocks  where id = "${块id}" )`
    let 文档数据 = (await noobApi.核心api.sql({ stmt: stmt }))[0]
    let 文档内容 = await noobApi.核心api.exportPreview(
        {
            "id": 文档数据.id
        }
    )
    let 文档属性 = await noobApi.核心api.getDocInfo(
        {
            "id": 文档数据.id
        }
    )
    let 发布数据 = {}
    发布数据.title = 文档属性.ial.title
    发布数据.markdown = 文档数据.markdown
    发布数据.content = 转换图片地址(文档内容)
    发布数据.desc = 文档属性.ial.memo
    发布数据.thumb = 文档属性.ial['title-img']
    noobApi.核心api.pushMsg({
        "msg": `准备同步${文档数据.hpath}到${账户.title}`,
        "timeout": 1000
    }
        , ""
    )
    添加任务(发布数据,账户)
}
function 转换图片地址(文档内容){
    let div = document.createElement('div')
    div.innerHTML = 文档内容.content ? 文档内容.content : 文档内容.html
    div.querySelectorAll('[src]').forEach(
        el=>{
            if(el.getAttribute('src').startsWith('assets'))
            {
                el.setAttribute('src',window.location.origin+'/'+el.getAttribute('src'))
            }
        }
    )
    div.innerHTML+='<p>本文使用<a href="https://b3log.org/siyuan/">思源笔记</a>写作</p>'
    div.innerHTML+='<p>本文使用<a href="http://publish.chuanchengsheji.com/">椽承设计</a><a href="https://github.com/leolee9086/snippets">小工具</a>配合同步</p>'

    return div.innerHTML
}
function 添加任务(发布数据,账户){
    window.$syncer.addTask(
        {
            post: 生成任务(发布数据,账户),
            accounts: [账户],
        },
        function (status) {
            status.accounts.forEach(account => {
                if (account.editResp) {
                    let a = document.createElement('a')
                    a.setAttribute('href', account.editResp.draftLink)
                    a.setAttribute('target', "_blank")
                    a.setAttribute("referrerPolicy", "no-referrer")
                    a.click()
                    a.remove()
                }
            });
        },
        function () {
            noobApi.核心api.pushMsg({
                "msg": `同步${文档数据.hpath}到${账户.title}完成`,
                "timeout": 1000
            })    
        }
    )
}
function 生成任务(发布数据,账户) {
    var post = {}
    post.title = 发布数据.title
    if (发布数据.content) {
        post.content = 发布数据.content
    } else if (发布数据.markdown) {
        post.markdown = 发布数据.markdown
    }
    if (发布数据.thumb) {
        post.thumb = 发布数据.thumb
    }
    if (发布数据.desc) {
        post.desc = 发布数据.desc

    }
    else {
        post.desc = 发布数据.content ? 发布数据.content.substring(0, 20) : 发布数据.markdown.substring(0, 20)
    }
    return post
}

这样就不会在块标菜单显示发布选项了,不然好怪啊。

能不能再给力一点啊

额,其实我也想不到怎么再给力一点啦。

不过我们之前不是弄了随机文档头图嘛,这回有了新的菜单,可以试试往图片上怼个随机切换来着:

    //这后面的都是图片菜单
    let 标题相关图片配置 = {
      id: '随机标题相关图片',
      文字: '随机标题相关图片',
      图标: '#iconRefresh',
      点击回调函数: (event) => this.获取相关附件('title')
    }
    图片菜单.注册自定义菜单项(标题相关图片配置)
    let 提示文本相关图片配置 = {
      id: '随机提示文本相关图片',
      文字: '随机提示文本相关图片',
      图标: '#iconRefresh',
      点击回调函数: (event) => this.获取相关附件('alt')
    }
    图片菜单.注册自定义菜单项(提示文本相关图片配置)

  async 获取相关附件(属性名) {
    let k = 图片菜单.菜单状态.图片容器.getAttribute(属性名) || ''
    let 相关图片数组 = await noobApi.核心api.searchAsset({ k: k })
    if (相关图片数组[0]) {
      let 随机链接 = 相关图片数组[Math.floor(Math.random() * 相关图片数组.length)].path
      let 扩展名 = 随机链接.split('.').pop()
      if (-1 < ['png', 'jpg', 'svg', 'tiff', 'webp', 'gif'].indexOf(扩展名)) {
        图片菜单.菜单状态.图片容器.setAttribute('data-src', 随机链接)
        图片菜单.菜单状态.图片容器.setAttribute('src', 随机链接)
      }
    }
  }

效果就像这样

更多更多的菜单

有些人可能注意到了,下面有个 unsplash,但是这次我们不说这个,下回分解(再水一篇)。。。。

  • 思源笔记

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

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

    22340 引用 • 89395 回帖 • 1 关注

相关帖子

欢迎来到这里!

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

注册 关于
请输入回帖内容 ...