一、前情提要
我们在之前已经实现了自定义菜单和自定义工具栏,虽然都比较绿皮,但是也确实是能够使用。
这次我们来整个复杂点的(为什么整之后就知道了)。
思源本来就能够打开网页,就像这样:
应该都能够看得出来,这是一个 iframe 块吧。
但是有的时候比如说你要对着网页做笔记啥的,这样好像有点不太方便,而且有些网站在 iframe 里面也没有办法正常登录。
所有我们这次来实现一个类似浏览器网页的功能。
就像这样:
二、实现过程
获取原型
首先呢,我们要知道页签(tab)是怎么渲染出来的。
看源码就知道大概是这样:
wnd.addTab(new tab(options))
但是思源并没有把这些暴露出来,那么我们首先就需要能够取到 wnd 和 Tab 的类。
这里鼓捣几个工具函数:
noobApi\util\layouts.js
import {展平树 as flattern } from "../util/common.js"
export function getLayoutByElement(el) {
let array = flattern(window.siyuan.layout.centerLayout)
let target
target = array.filter(
item => {
return item.id == el.firstChild.dataset.id
}
)
return target[0]
}
export function getClosestLayout(el) {
if (getLayoutByElement(el)) {
return getLayoutByElement(el)
}
else {
return getLayoutByElement(el.parentElement)
}
}
export function getWndParentElement(element) {
let parent = element.parentElement
if (parent.dataset && parent.dataset.type == "wnd") {
return parent.parentElement
}
else {
return getWndParentElement(parent)
}
}
这样我们就能够获取到 layout 了。
然后来获取 tab 的类:
function 获取tab原型() {
let array = flatten(window.siyuan.layout.centerLayout)
let tab = array.filter(
item => { return item.headElement }
)[0]
if(tab){
return tab.constructor
}
}
没错基本所有的 tab 都有个 headElement,所以可以靠它来判断一下取到的是不是 tab,但是我们在打开思源的时候它不一定就有打开的标签页嘛,所以还要实现一个“如果拿不到结果我就穷追不舍”的函数。
这个时候可能会想到用 setTimeout 之类来搞,但是因为我们不知道什么时候这个函数会返回,所以咋整呢?
还记得之前我们使用过的 async
和 await
嘛?
如果你看了相关的资料的话,可能知道这俩跟 promise
有关。
JavaScript Promise 对象 | 菜鸟教程 (runoob.com)
这里我们就用 promise
来生成一个工具函数:
export function 重复执行直到返回(函数,间隔){
return new Promise((resolve, reject) => {
if(!间隔){
间隔 = 500
}
let 工具函数 = setInterval(async() => {
try{
let 执行结果 = await 函数()
if(执行结果!==undefined){
clearInterval(工具函数)
resolve(执行结果)
}
}catch(e){
clearInterval(工具函数)
reject(e)
}
}, 间隔);
})
}
这个函数会以传入的间隔不断执行传入的函数,直到取到的结果不是 undefined
为止。
其实这里有个坑,js 是一种非常神奇的语言嘛:
JS 中的 null 和 undefined,undefined 为啥用 void 0 代替? - 掘金 (juejin.cn)
不过这里我们假设你不会闲到蛋疼去支持 ie,也不会坑到胃疼去改 undefined,先就这么着吧。
然后我们就可以实现实际的功能了:
import { 重复执行直到返回 } from '../util/common.js'
let tab
export function 展平Layout() {
let array = []
function flatten(tree) {
if (tree.children && tree.children instanceof Array) {
tree.children.forEach(
item => {
array.push(item)
if (item.children && item.children instanceof Array) {
flatten(item)
}
}
)
}
}
flatten(window.siyuan.layout.centerLayout)
return array
}
function 获取tab原型() {
let array = 展平Layout()
let tab = array.filter(
item => { return item.headElement }
)[0]
if(tab){
return tab.constructor
}
}
tab = await 重复执行直到返回(获取tab原型)
class customTab extends tab{
constructor(options){
let {panel,title,icon,data} = options
let docIcon = JSON.stringify({type:options.type,data:data})
super({panel,title,icon,docIcon})
this.type = options.type
this.data = options.data
}
save(){
this.docIcon = JSON.stringify({type:this.type,data:this.data})
}
}
export {customTab as Tab}
export {注册自定义tab} from './util/hack.js'
import { 展平Layout } from "../index.js";
let tab注册表= {}
export const hackLayout=()=>{
let layouts = 展平Layout()
layouts.forEach(
layout=>{
if(layout.docIcon&&layout.docIcon.indexOf('type:')){
try {
let {type,data} = JSON.parse(layout.docIcon)
//已经挂载好的tab咱就不修改它了
if(tab注册表[type]&&!layout.inited){
let customTab = tab注册表[type]
let tab = new customTab(data)
tab.inited = true
layout.parent.addTab(tab)
layout.parent.removeTab(layout.id)
}
}catch(e){
console.error(e)
}
}
}
)
}
export function 注册自定义tab(类型,构造函数){
tab注册表[类型]=构造函数
hackLayout()
}
document.addEventListener('mouseover',hackLayout)
hackLayout()
这样之后,我们的工作就完成了。。。。。。。。额 三分之一?
实现仿浏览器的 tab 页
首先我们给笔记里面的链接元素绑定一下事件:
import noobApi from '../noobApi/index.js'
import browserTab from './browserTab.js'
//还记得这几个函数嘛?就在上面呢
let {getWndParentElement,getLayoutByElement} = noobApi.layouts.util
document.addEventListener('click',onclick,true)
function onclick(e) {
if (e.target.dataset && e.target.dataset.type == 'a') {
e.preventDefault()
e.stopPropagation()
//找到窗口对象
let wndElement = getWndParentElement(e.target)
//用这个窗口对象拿到wnd
let layout = getLayoutByElement(wndElement, siyuan.layout.centerLayout)
//然后创建一个新的tab
layout.addTab((new browserTab({ url: e.target.dataset.href, title: e.target.dataset.tilte || e.target.innerHTML })))
}
然后来实现一个 tab 类
import noobApi from '../noobApi/index.js'
let { Tab, 注册自定义tab } = noobApi.layouts
let { 核心api } = noobApi
class iframeTab extends Tab {
constructor(option) {
let panel = `
<div class="fn__flex fn__flex-1 fn__flex-column">
<div class="fn__flex" style="padding: 4px 8px;position: relative">
<span style="opacity: 1" class="block__icon fn__flex-center btnBack" data-menu="true">
<svg><use xlink:href="#iconLeft"></use></svg>
</span>
<span style="opacity: 1" class="block__icon fn__flex-center btnForward" data-menu="true">
<svg ><use xlink:href="#iconRight"></use></svg>
</span>
<div class="fn__space"></div>
<input class="b3-text-field fn__flex-1">
<span class="fn__space"></span>
<span
style="opacity: 1"
class="block__icon fn__flex-center b3-tooltips b3-tooltips__w reload"
aria-label="刷新">
<svg><use xlink:href="#iconRefresh"></use></svg>
</span>
<span
style="opacity: 1"
class="block__icon fn__flex-center b3-tooltips b3-tooltips__w debug fn__none"
aria-label="反向链接">
<svg><use xlink:href="#iconLink"></use></svg>
</span>
<div id="searchHistoryList" data-close="false" class="fn__none b3-menu b3-list b3-list--background" style="position: absolute;top: 30px;max-height: 50vh;overflow: auto"></div>
</div>
<div class="fn__flex fn__flex-1 naive_ifrmeContainer" style="max-height:100%" >
<webview
class="fn__flex-1"
style=" max-height:calc(100% - 200px)"
src="${option.url}" data-src="" border="0"
frameborder="no"
framespacing="0"
allowfullscreen="true"
allowpopups="true"
></webview >
<div class="fn__flex fn__flex-column browserBakclink" style="width:20%">
<div class="block__icons block__icons--active">
<div class="block__logo">
<svg><use xlink:href="#iconLink"></use></svg>
反向链接
</div>
<span class="counter listCount fn__none">1</span>
<span class="fn__space"></span>
<label class="b3-form__icon b3-form__icon--small search__label">
<svg class="b3-form__icon-icon"><use xlink:href="#iconSearch"></use></svg>
<input class="b3-text-field b3-text-field--small b3-form__icon-input" placeholder="Enter 搜索">
</label>
<span class="fn__space"></span>
<span data-type="refresh" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="刷新"><svg class=""><use xlink:href="#iconRefresh"></use></svg></span>
<span class="fn__space"></span>
<span data-type="min" class="block__icon b3-tooltips b3-tooltips__sw" aria-label="最小化 Ctrl+W"><svg><use xlink:href="#iconMin"></use></svg></span>
</div>
<div class="backlinkList fn__flex-1">
<ul class="b3-list b3-list--background">
<li class="b3-list--empty">暂无相关内容</li>
</ul>
</div>
</div>
</div>
</div>
</div>
`
super({
panel,
title: option.title || '',
icon: "naiveBrowser",
type: "iframeTab",
data: { url: option.url, title: option.title || '' }
})
this.urlInputter = this.panelElement.querySelector("input")
this.urlInputter.value = option.url
this.frame = this.panelElement.querySelector("webview")
if(!window.require){
this.frame.outerHTML =
`
<iframe
class="fn__flex-1"
style=" max-height:calc(100% - 200px)"
src="${option.url}" data-src="" border="0"
frameborder="no"
framespacing="0"
allowfullscreen="true"
allowpopups="true"
></iframe >
`
this.frame=this.panelElement.querySelector('iframe')
this.frame.reload =()=>{ this.frame.setAttribute("src",this.frame.getAttribute('src'))}
}
this.devButton = this.panelElement.querySelector(".debug")
this.minimalButton = this.panelElement.querySelector('[data-type="min"]')
this.backlinkListElement = this.panelElement.querySelector(".backlinkList .b3-list--background")
this.history={
stack:[this.urlInputter.value],
index:0
}
//绑事件啊绑事件
this.挂载事件()
this.findBacklinks()
}
挂载事件() {
this.浏览记录 = []
this.urlInputter.addEventListener("change", () => {
console.log(this.frame)
if (!this.urlInputter.value.startsWith("http://") && !this.urlInputter.value.startsWith("https://") && !this.urlInputter.value.startsWith("/")) {
this.urlInputter.value = "https://" + this.urlInputter.value
}
this.tilte = ""
this.frame.setAttribute("src", this.urlInputter.value)
this.tilte = this.urlInputter.value
document.querySelector(`li[data-id="${this.id}"] span.item__text`).innerHTML = this.urlInputter.value
this.data = { url: this.urlInputter.value ,title:this.tilte}
this.history.stack.push(this.urlInputter.value)
this.history.index+=1
this.save()
this.findBacklinks()
})
document.addEventListener('mousedown',()=>{this.showOverlay()}, true)
document.addEventListener('mouseup',()=>{this.hideOverlay()}, true)
this.bindButtonEvent()
this.bindframeEvent()
}
hideOverlay(){
this.panelElement.querySelector('.ovelayer').remove()
}
showOverlay() {
if(!this.panelElement.querySelector('.ovelayer')){
let div = document.createElement('div')
div.setAttribute('style', `position:absolute;top:0;left:0;height:100%;width:80%`)
div.setAttribute('class',"ovelayer")
this.panelElement.appendChild(div)
}
}
findBacklinks() {
let url = this.urlInputter.value
let { backlinkListElement } = this
let stmt = `select * from spans`
核心api.sql({ stmt: stmt }, '', (data) => {
let backlinkList = data.filter(item => {
return /\[[\s\S]*?\]\([\s\S]*?\)/gm.test(item.markdown) && item.markdown.indexOf(`(${url})`) > 0
})
backlinkListElement.innerHTML = ""
backlinkList.forEach(
item => {
backlinkListElement.innerHTML += `<li class="b3-list-item" draggable="true" data-node-id="20201225220955-bdl9x01" data-treetype="backlink" data-type="NodeListItem" data-subtype="u">
<span style="padding-left: 16px" class="b3-list-item__toggle">
<svg data-id="%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%981" class="b3-list-item__arrow fn__hidden"><use xlink:href="#iconRight"></use></svg>
</span>
<svg class="b3-list-item__graphic popover__block" data-id="20201225220955-bdl9x01"><use xlink:href="#iconListItem"></use></svg>
<span class="b3-list-item__text"><a href="siyuan://blocks/${item.block_id}/">${item.content}</a></span>
</li>`
}
)
})
}
bindButtonEvent() {
this.devButton.addEventListener('mousedown', () => {
this.panelElement.querySelector(".browserBakclink").classList.remove("fn__none")
this.devButton.classList.add("fn__none")
},true)
this.panelElement.querySelector('.reload').addEventListener('mousedown', () => {
this.frame.reload()
},true)
this.minimalButton.addEventListener('mousedown',()=>{
this.panelElement.querySelector(".browserBakclink").classList.add("fn__none")
this.devButton.classList.remove("fn__none")
},true)
this.panelElement.querySelector('.btnForward').addEventListener(
'mousedown',()=>{
if(this.history.index<this.history.stack.length-1){
this.history.index+=1
this.urlInputter.value = this.history.stack[this.history.index]
this.frame.setAttribute('src',this.urlInputter.value)
this.data = { url: this.urlInputter.value ,title:this.tilte}
this.save()
}
}
)
this.panelElement.querySelector('.btnBack').addEventListener(
'mousedown',()=>{
if(this.history.index>0){
this.history.index-=1
this.urlInputter.value = this.history.stack[this.history.index]
this.frame.setAttribute('src',this.urlInputter.value)
this.data = { url: this.urlInputter.value ,title:this.tilte}
this.save()
}
}
)
}
bindframeEvent() {
let { frame, urlInputter } = this
this.findBacklinks()
frame.addEventListener("dom-ready", () => {
fetch(urlInputter.value).then(
res => {
return res.text()
}
).then(
text => {
let tilte = text.match(/<title>(.*?)<\/title>/);
if (tilte) {
this.tilte = tilte
document.querySelector(`li[data-id="${this.id}"] span.item__text`).innerHTML = tilte
}
}
)
})
frame.addEventListener('will-navigate', (e) => {
e.preventDefault()
const protocol = (new URL(e.url)).protocol
if (protocol === 'http:' || protocol === 'https:') {
frame.src = (e.url)
}
})
frame.addEventListener('page-title-updated', async (e) => {
this.tilte = e.title
document.querySelector(`li[data-id="${this.id}"] span.item__text`).innerHTML = e.title
})
}
}
注册自定义tab("iframeTab", iframeTab)
export default iframeTab
这里面其实就是绑事件的活儿了。
有关 webview
标签可以参考这里:
<webview>
Tag | Electron (electronjs.org)
啊,可以看到在浏览器里面因为没有这玩意所以我们降了个记用 iframe。
if(!window.require){
this.frame.outerHTML =
`
<iframe
class="fn__flex-1"
style=" max-height:calc(100% - 200px)"
src="${option.url}" data-src="" border="0"
frameborder="no"
framespacing="0"
allowfullscreen="true"
allowpopups="true"
></iframe >
`
this.frame=this.panelElement.querySelector('iframe')
//因为iframe根本就没有reload方法嘛
this.frame.reload =()=>{ this.frame.setAttribute("src",this.frame.getAttribute('src'))}
}
然后实现一下简单的浏览记录,实际上主要的就是弄个数组把记录放进去(其实这里你可以再抽象一下):
this.panelElement.querySelector('.btnForward').addEventListener(
'mousedown',()=>{
if(this.history.index<this.history.stack.length-1){
this.history.index+=1
this.urlInputter.value = this.history.stack[this.history.index]
this.frame.setAttribute('src',this.urlInputter.value)
this.data = { url: this.urlInputter.value ,title:this.tilte}
//save方法就挂载在customTab的原型上啦
this.save()
}
}
)
this.panelElement.querySelector('.btnBack').addEventListener(
'mousedown',()=>{
if(this.history.index>0){
this.history.index-=1
this.urlInputter.value = this.history.stack[this.history.index]
this.frame.setAttribute('src',this.urlInputter.value)
this.data = { url: this.urlInputter.value ,title:this.tilte}
this.save()
}
}
)
然后因为是在思源里面用嘛,所以加一个反向链接的获取(其实就是查一下表看看有没有包含这个链接的)
findBacklinks() {
let url = this.urlInputter.value
let { backlinkListElement } = this
//没错通过sql接口我们可以查spans表的,不是只有blocks表能查询
let stmt = `select * from spans`
核心api.sql({ stmt: stmt }, '', (data) => {
//这一步其实你可以在sql里查
let backlinkList = data.filter(item => {
return /\[[\s\S]*?\]\([\s\S]*?\)/gm.test(item.markdown) && item.markdown.indexOf(`(${url})`) > 0
})
backlinkListElement.innerHTML = ""
backlinkList.forEach(
item => {
backlinkListElement.innerHTML += `<li class="b3-list-item" draggable="true" data-node-id="${item.block_id}" data-treetype="backlink" data-type="NodeListItem" data-subtype="u">
<span style="padding-left: 16px" class="b3-list-item__toggle">
<svg data-id="%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%981" class="b3-list-item__arrow fn__hidden"><use xlink:href="#iconRight"></use></svg>
</span>
<svg class="b3-list-item__graphic popover__block" data-id="20201225220955-bdl9x01"><use xlink:href="#iconListItem"></use></svg>
<span class="b3-list-item__text"><a href="siyuan://blocks/${item.block_id}/">${item.content}</a></span>
</li>`
}
)
})
}
这么一通鼓捣之后:效果就像这样了:
啊,因为这个东西的代码现在已经稍微有点复杂了,你从头实现一遍可能会有些麻烦,可以尝试去 github 下载一下看看吧。
leolee9086/snippets (github.com)
本文使用思源笔记编辑生成,更新于:2022-11-27。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于