定位 iframe
在写一个划词翻译扩展 Saladict 时,有一个需求:用户选择一段文本之后,会在鼠标附近显示一些元素。
这个初看很简单,监听一个 mouseup 事件,获取 clientX 和 clientY 就行。这也是 Saladict 前几版用的方法。
但这个方法有个缺陷:iframe 里的鼠标事件不会传到父窗口上。
解决方法也很简单,就难在把它们都联系起来。
iframe 里插入脚本
在 manifest.json 里,content_scripts 有个选项 all_frames,可以让脚本插入到所有的 frame 里。
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | {"content_scripts": [
 {
 "js": ["selection.js"],
 "matches": ["<all_urls>"],
 "all_frames": true
 }
 ]
 }
 
 | 
 
检测点击
现在可以检测 iframe 里的点击事件
| 12
 
 | document.addEventListener('mouseup', handleMouseUp)
 
 | 
 
上传坐标
当点击发生在 iframe 里时,获取的坐标是相对于 iframe 窗口的,所以把这个坐标交给上层,再加上 iframe 本身的坐标,就可以算出点击相对上层的坐标。
Chrome 里可以放心使用 postMessage
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | function handleMouseUp (evt) {
 if (window.parent === window) {
 
 doAwesomeThings(evt.clientX,evt.clientY)
 } else {
 
 window.parent.postMessage({
 msg: 'SALADICT_CLICK',
 mouseX: evt.clientX,
 mouseY: evt.clientY
 }, '*')
 }
 }
 
 | 
 
计算偏移
上层怎么知道是哪个 iframe 传来坐标?很简单,message 事件里携带了 iframe 的 window,对比一下就可以。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 
 | window.addEventListener('message', evt => {
 if (evt.data.msg !== 'SALADICT_CLICK') { return }
 
 let iframe = Array.from(document.querySelectorAll('iframe'))
 .filter(f => f.contentWindow === evt.source)
 [0]
 if (!iframe) { return }
 
 
 let pos = iframe.getBoundingClientRect()
 let mouseX = evt.data.mouseX + pos.left
 let mouseY = evt.data.mouseY + pos.top
 
 if (window.parent === window) {
 
 doAwesomeThings(mouseX, mouseY)
 } else {
 
 window.parent.postMessage({
 msg: 'SALADICT_CLICK',
 mouseX,
 mouseY
 }, '*')
 }
 })
 
 | 
 
拖动 iframe
Saladict 另外一个需求就是拖动一个 iframe 查词面板。
实现拖动的常识
实现拖动的一种常用方式就是检测 mousedown, mousemove 和 mouseup。分别对应开始、拖动、结束。然后计算偏移值应用到 left 和 top 上。
第一次实现很容易犯的一个错误就是监听元素本身的 mousemove。当然这个也可以正确计算出偏移,问题在于如果鼠标移动稍快超出了元素,拖动就卡掉了。所以应该监听全局的 mousemove 获取偏移值。
iframe 特色的拖动
iframe 的拖动同理,只是因为发生在 iframe 里的事件不能传到上层,需要手动打包一下。
iframe 部分
拖动由 iframe 里的某个元素触发,为了节省资源,在触发的时候才监听拖动和结束,并在结束的时候解绑。
在 iframe 里监听 mousemove 就是为了把偏移值传回上层,因为上层的 mousemove 事件到这里中断了。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 
 | var baseMouseX, baseMouseY
 
 $dragArea.addEventListener('mousedown', handleDragStart)
 
 function handleDragStart (evt) {
 baseMouseX = evt.clientX
 baseMouseY = evt.clientY
 
 window.parent.postMessage({
 msg: 'SALADICT_DRAG_START',
 mouseX: baseMouseX,
 mouseY: baseMouseY
 }, '*')
 
 document.addEventListener('mouseup', handleDragEnd)
 document.addEventListener('mousemove', handleMousemove)
 }
 
 function handleMousemove (evt) {
 window.parent.postMessage({
 msg: 'SALADICT_DRAG_MOUSEMOVE',
 offsetX: evt.clientX - baseMouseX,
 offsetY: evt.clientY - baseMouseY
 }, '*')
 }
 
 function handleDragEnd () {
 window.parent.postMessage({
 msg: 'SALADICT_DRAG_END'
 }, '*')
 
 document.removeEventListener('mouseup', handleDragEnd)
 document.removeEventListener('mousemove', handleMousemove)
 }
 
 | 
 
上层部分
主要增加了handleFrameMousemove 补上中断的偏移。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 
 | var pageMouseX, pageMouseY
 
 var frameTop = 0
 var frameLeft = 0
 $iframe.style.top = frameTop + 'px'
 $iframe.style.left = frameLeft + 'px'
 
 window.addEventListener('message', evt => {
 const data = evt.data
 
 switch (data.msg) {
 case 'SALADICT_DRAG_START':
 handleDragStart(data.mouseX, data.mouseY)
 break
 case 'SALADICT_DRAG_MOUSEMOVE':
 handleFrameMousemove(data.offsetX, data.offsetY)
 break
 case 'SALADICT_DRAG_END':
 handleDragEnd()
 break
 }
 })
 
 function handleDragStart (mouseX, mouseY) {
 
 pageMouseX = frameLeft + mouseX
 pageMouseY = frameTop + mouseY
 
 document.addEventListener('mouseup', handleDragEnd)
 document.addEventListener('mousemove', handlePageMousemove)
 }
 
 function handleDragEnd () {
 document.removeEventListener('mouseup', handleDragEnd)
 document.removeEventListener('mousemove', handlePageMousemove)
 }
 
 function handleFrameMousemove (offsetX, offsetY) {
 frameTop += offsetY
 frameLeft += offsetX
 $iframe.style.top = frameTop + 'px'
 $iframe.style.left = frameLeft + 'px'
 
 
 pageMouseX += offsetX
 pageMouseY += offsetY
 }
 
 function handlePageMousemove (evt) {
 frameTop += evt.clientX - pageMouseX
 frameLeft += evt.clientY - pageMouseY
 $iframe.style.top = frameTop + 'px'
 $iframe.style.left = frameLeft + 'px'
 
 
 pageMouseX = evt.clientX
 pageMouseY = evt.clientY
 }
 
 | 
 
例子
这里实现了一个例子,下面的正方形 iframe 是可以拖动的:
兼容性
可以看到,这里主要就是传鼠标的坐标偏移值。所以需要兼容老浏览器的话,用繁琐的旧方式与 iframe 交流就行。如果是同域的话也可以直接从 iframe 里获取偏移。