Typora是我經(jīng)常使用的一款軟件,用來(lái)寫(xiě)MarkDown很舒適送膳,有著非常優(yōu)秀的使用體驗(yàn):
實(shí)時(shí)預(yù)覽
自定義圖片上傳服務(wù)
文檔轉(zhuǎn)換
主題自定義
起因
不過(guò)我遇到一個(gè)非常好玩的事情普碎,當(dāng)我復(fù)制Typora內(nèi)容粘貼到文本編輯器時(shí)吼肥,會(huì)得到MarkDown格式的內(nèi)容;復(fù)制到富文本編輯器時(shí)麻车,可以渲染出富文本效果:
復(fù)制到VS Code:
復(fù)制到其他富文本編輯器:
我很好奇為什么會(huì)出現(xiàn)兩種不同的結(jié)果缀皱,Typora應(yīng)該是使用Electron(或類似技術(shù))開(kāi)發(fā)的,我嘗試用Clipboard API來(lái)進(jìn)行測(cè)試:
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="js" cid="n72" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">// 為什么使用setTimeout:我是在Chrome控制臺(tái)進(jìn)行的測(cè)試动猬,clipboard依托于頁(yè)面啤斗,所以我需要設(shè)置1s延時(shí),以便可以點(diǎn)擊頁(yè)面聚焦
setTimeout(async()=>{
const clipboardItems = await navigator.clipboard.read();
console.log(clipboardItems)
},1000)</pre>
然后看到了剪切板中有兩種不同類型的內(nèi)容:純文本text/plain
和富文本text/html
赁咙。所以不同的內(nèi)容接收者選擇了不同的內(nèi)容作為數(shù)據(jù)钮莲,文本編輯器拿到的是純文本,富文本編輯器獲取的是富文本格式數(shù)據(jù)彼水。
再來(lái)看看獲取到的具體內(nèi)容吧:
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="js" cid="n100" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">setTimeout(async()=>{
const clipboardItems = await navigator.clipboard.read();
console.log(clipboardItems)
for (const clipboardItem of clipboardItems) {
for (const type of clipboardItem.types) {
const contentBlob = await clipboardItem.getType(type)
const text = await contentBlob.text()
console.log(text)
}
}
},1000)</pre>
Clipboard塞入數(shù)據(jù)試一下:
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="js" cid="n127" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">setTimeout(async ()=>{
await navigator.clipboard.write([
new ClipboardItem({
["text/plain"]: new Blob(['# 純文本和富文本'],{type:'text/plain'}),
["text/html"]: new Blob(['<h1 cid="n21" mdtype="heading" class="md-end-block md-heading md-focus" style="box-sizing: border-box; break-after: avoid-page; break-inside: avoid; orphans: 4; font-size: 2.25em; margin-top: 1rem; margin-bottom: 1rem; position: relative; font-weight: bold; line-height: 1.2; cursor: text; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: rgb(238, 238, 238); white-space: pre-wrap; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-family: "Open Sans", "Clear Sans", "Helvetica Neue", Helvetica, Arial, "Segoe UI Emoji", sans-serif; font-style: normal; font-variant-caps: normal; letter-spacing: normal; text-align: start; text-indent: 0px; text-transform: none; word-spacing: 0px; -webkit-text-stroke-width: 0px; background-color: rgb(255, 255, 255); text-decoration: none;"><span md-inline="plain" class="md-plain md-expand" style="box-sizing: border-box;">純文本和富文本</span></h1>'],{type:'text/html'}),
})
]);
},[1000])</pre>
嘗試了幾個(gè)富文本編輯器得到的結(jié)果(不同富文本編輯器的具體實(shí)現(xiàn)可能存在差異):
如果只存在純文本(僅保留上段代碼中的純文本部分), 會(huì)讀取剪切板中純文本內(nèi)容
如果存在純文本和富文本崔拥,會(huì)讀取剪切板中富文本內(nèi)容
那這個(gè)效果是Typora幫我們實(shí)現(xiàn)的嗎?
我們先來(lái)看一下復(fù)制富文本的默認(rèn)行為凤覆,打開(kāi)一個(gè)網(wǎng)頁(yè)链瓦,復(fù)制網(wǎng)頁(yè)文本,然后使用剛才的代碼嘗試一下,看看讀取到的剪切板內(nèi)容慈俯。
我們可以看到渤刃,在復(fù)制富文本的時(shí)候,Chrome實(shí)現(xiàn)的clipboard API都會(huì)生成兩份結(jié)果贴膘,一份是純文本格式text/plain
卖子,一份是富文本格式text/html
。
不同的是:當(dāng)我們?cè)赥ypora復(fù)制時(shí)刑峡,得到的是Markdown格式的純文本和富文本揪胃,是Typora幫我們進(jìn)行了處理。
監(jiān)聽(tīng)復(fù)制氛琢,寫(xiě)入剪切板
監(jiān)聽(tīng)復(fù)制我們可以使用HTMLElement.oncopy實(shí)現(xiàn):
打開(kāi)任意一個(gè)網(wǎng)頁(yè)喊递,切換到控制臺(tái):
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="js" cid="n189" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">document.body.oncopy = function(e){
console.log(e)
var text = e.clipboardData.getData("text");
console.log(text)
}</pre>
復(fù)制頁(yè)面中內(nèi)容,我們就可以的看到打印的結(jié)果了:
本來(lái)為數(shù)據(jù)會(huì)在clipboardData中阳似,但是嘗試了一下并沒(méi)有獲取到內(nèi)容骚勘,看了一下API, 需要在copy事件中通過(guò)setData設(shè)置數(shù)據(jù),在paste時(shí)間中g(shù)etData獲取數(shù)據(jù)撮奏。我們可以通過(guò)Selection API來(lái)獲取選中的內(nèi)容俏讹。
<pre mdtype="fences" cid="n502" lang="js" spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">document.addEventListener('copy', function(e){
e.preventDefault(); // 防止我們篩入的數(shù)據(jù)被覆蓋
const selectionObj = window.getSelection()
const rangeObj = selectionObj.getRangeAt(0)
const fragment = rangeObj.cloneContents() // 獲取Range包含的文檔片段
const wrapper = document.createElement('div')
wrapper.append(fragment)
e.clipboardData.setData('text/plain', wrapper.innerText + '額外的文本');
e.clipboardData.setData('text/html', wrapper.innerHTML+ '<h1>額外的文本</h1>');
});</pre>
或者使用clipboard.write實(shí)現(xiàn)寫(xiě)入:
<pre class="md-fences md-end-block ty-contain-cm modeLoaded" spellcheck="false" lang="js" cid="n218" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-size: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; caret-color: rgb(51, 51, 51); color: rgb(51, 51, 51); font-style: normal; font-variant-caps: normal; font-weight: normal; letter-spacing: normal; orphans: auto; text-indent: 0px; text-transform: none; widows: auto; word-spacing: 0px; -webkit-text-size-adjust: auto; -webkit-text-stroke-width: 0px; text-decoration: none; background-position: inherit inherit; background-repeat: inherit inherit;">document.body.oncopy = function(e){
e.preventDefault();
const selectionObj = window.getSelection()
const rangeObj = selectionObj.getRangeAt(0)
const fragment = rangeObj.cloneContents() // 獲取Range包含的文檔片段
const wrapper = document.createElement('div')
wrapper.append(fragment)
navigator.clipboard.write([
new ClipboardItem({
["text/plain"]: new Blob([wrapper.innerText,'額外的文本'],{type:'text/plain'}),
["text/html"]: new Blob([wrapper.innerHTML,'<h1>額外的富文本</h1>'],{type:'text/html'}),
})
])
}</pre>
監(jiān)聽(tīng)復(fù)制還可以用來(lái)添加版權(quán)信息,比如上面代碼中的額外信息就會(huì)出現(xiàn)在復(fù)制的文本中畜吊。
對(duì)于復(fù)制和粘貼內(nèi)容也可以通過(guò)document.execCommand泽疆,不過(guò)目前屬于已經(jīng)被棄用的API,不建議使用
歡迎關(guān)注微信“混沌前端”