前言


這二天在整理一個系統前端時, 發現需要一個可拉動位置的彈出視窗. 最初一直卡在拉動時移動量非常大. 網路上貼的例子雖然都可以正常動作, 可是都沒有明確的說明重點, 套到自己的程式時就是不正常. 最後經過大半天的測試修正, 總算找到問題點, 搞定! 故特記下一點心得.

HTML 文件


這是前端系統的 HTML 框架. 只寫到 myDiv 這一層, 前端 JS 程式執行了之後 myDiv 裡會再填入一個輸入用的表格 (form). 完成之後, 我希望可拉動的部份就是整個輸入表格 (含 myDiv).

<div id='pop' class="msgDiv ui-overlay">
	<div id='myDiv'></div>
</div>

填入輸入用的表格後的 DOM 結構如下:

<div id='pop' class="msgDiv ui-overlay">
	<div id='myDiv'>
		<div class='action'>
			<div class='Header'>...</div>
			<div class='Form_Content'>...</div>
			<div class='Footer'>...</div>
		</div>
	</div>
</div>

程式碼


這段程式的基礎來自 W3School, 我稍微做了一些改變:

  • 需要輸入二個參數: 一是可以被拉動的部份, 一是控制可拉動的部份.
    可被拉動的部份一般是一個 div tag;
    而控制的部份則通常是這個容器的表頭部份.
  • 輸入的參數可以是字串 (用於 querySelector()), 或者是 (querySelector()getElementById() 等等...) 已經找到的元素.
  • 再來就是變數名稱改了一下而已.

程式的架構也很簡單:

  1. 先在控制移動的元件上附加上按下滑鼠右鍵事件 (mousedown) 的事件處理函數 (_evMsDown).
  2. 當按下滑鼠右鍵時則改為: 記下滑鼠目前位置 (稍後計算移動量用的), 並附加上
    放開滑鼠右鍵事件 (mouseup) 的事件處理函數 (_evMsUp)
    以及拉動滑鼠事件 (mousedrag) 的事件處理函數 (_evMsDrag).
  3. 拉動滑鼠時要處理的就是計算滑鼠的移動量, 並經由修改 CSS 更新整個被拉動部份的位置.
  4. 放開滑鼠右鍵時, 則把第二項附加上去的二個事件處理函數拿掉.

不過會發生問題的點並不是 JS 程式, 而是 CSS.

dragElement("#myDiv", "#myDiv .Header");

function dragElement(emCntr, emHdr) {
	var dltaX = 0, dltaY = 0, oPosX = 0, oPosY = 0;
	if (typeof emCntr === 'string')
		emCntr = document.querySelector(emCntr);
	if (typeof emHdr === 'string')
		emHdr = document.querySelector(emHdr);
	emHdr.addEventListener("mousedown", _evMsDown);

	function _evMsDown(ev) {
		ev = ev || window.event;
		ev.preventDefault();
		// 保存滑鼠位置
		oPosX = ev.clientX;
		oPosY = ev.clientY;
		// 附加事件監聽器
		document.addEventListener("mouseup", _evMsUp);
		document.addEventListener("mousemove", _evMsDrag);
	}

	function _evMsDrag(ev) {
		ev = ev || window.event;
		ev.preventDefault();
		// 計算移動量
		dltaX = oPosX - ev.clientX;
		dltaY = oPosY - ev.clientY;
		// 保存滑鼠位置
		oPosX = ev.clientX;
		oPosY = ev.clientY;
		// 更新被拉動元件的位置
		emCntr.style.top  = (emCntr.offsetTop  - dltaY) + "px";
		emCntr.style.left = (emCntr.offsetLeft - dltaX) + "px";
	}

	function _evMsUp() {
		// 移除事件監聽器
		document.removeEventListener("mouseup", _evMsUp, false);
		document.removeEventListener("mousemove", _evMsDrag, false);
	}
}

CSS


前面的 JS 要正常有以下幾個重點:

  1. 被拉動的元件: (第 3 行) 可以有 padding, 但是 margin 必需為 0 (尤其是 margin-topmargin-left). 拉動時是否平順正確就靠它了.
  2. 預設位置: (第 4,5 行) 用於將被拉動的元件的初始位置居中.
  3. 拉動的游標: (第 7 行) 一般我們只要加上 cursor 設定就好, 不過這裡要加上 z-index 把它拉到最上層, 移動的游標才會出現. (因為 #myDiv 是包含在 #pop 內, 並設定 z-index:10 把它向上拉. 因此 .header 必需比它再高一層才會顯現出 cursor:move 的效果.)
  4. 由於拉動的 JS 程式透過是更動 top, left 來改變可被拉動元件的位置 (JS 程式第 30,31 行), 因此它的 position 不可以是不能用 top, left 改變位置的 static (CSS .msgDiv > div 的最後一行).
#pop { position: absolute; }
#myDiv {
	width:50%; margin:0; padding:5px; z-index: 10;
	left:50%; top: 50%;
	transform: translate(-50%, -60%);
}
#myDiv .Header { border-radius:5px 5px 0 0; cursor: move; z-index: 11; background-color:lightblue; }
#myDiv .Footer { border-radius:0 0 5px 5px; background-color:lightblue; }
#myDiv .Form_Content { line-height: 1.5em; padding: 0.8em 0; }
.ui-overlay {
	position: fixed;
	inset: 0px;
	background: rgba(64, 64, 64, 0.7);
	transition: opacity 500ms ease 0s;
	overflow-y: auto;
	margin: 0px !important;
}
.msgDiv {
	width: 100%;
	overflow: hidden;
}
.msgDiv > div {
	background-color: white;
	border: 5px solid rgb(179, 213, 255);
	border-radius: 10px;
	margin: 2em 1em;
	padding: 1.5em 1em 0.2em;
	position: relative;
}

追加功能: 調整視窗大小


調整視窗大小的功能很簡單: 主要是二行 CSS:

	resize:both;
	overflow:hedden;

這裡 overflow 不可以設為 visible, 其他設定值 (auto, scroll, hidden) 應該都沒有問題. 而加了上述 resize 設定的 <div> tag, 其右下角就會出現一個調整大小的 icon. 不過應該把它附加在哪一個 div 呢? 有下面幾種選擇:

  1. 放在最裡層的 div.Form_Content: 這是最簡單的. 這種作法調整大小的 icon 會出現在 div.Form_Content 的右下角, 而不是整個可拉動的 div#myDiv. 不過受限於我原本寫的 CSS, 它無法調整寬度, 只能啟用垂直單向調整.
    #myDiv .Form_Content {
    	resize:vertical;
    	overflow:hedden;
    }
    
  2. 同樣是放在最裡層的 div.Form_Content: 想要擺脫只能啟用垂直單向調整的限制, 則我們需要先把整個可拉動部份 div#myDiv 的寬度設為 width:fit-content (即讓它依其下層內容自動調整大小), 再將 div.Form_Content 的寬度改為某一固定大小 (這裡不能用 % 因為上一層的寬度是 width:fit-content).
    #myDiv {
    	width:fit-content;
    }
    #myDiv .Form_Content {
    	width: 25em;
    	resize:both;
    	overflow:hedden;
    }
    
  3. 放在 div.action 裡: 這組設定幾乎和上一組一樣, 差異只是作用在不同的 <div> tag 上.
    #myDiv {
    	width:fit-content;
    }
    #myDiv .action {
    	width: 25em;
    	resize:both;
    	overflow:hedden;
    }
    
    不過上述設定引發了另一個問題, div.Footer 並不會因 div#myDiv 的大小調整而保持在它的最下方, 同樣這也是因為原先的 CSS 設定造成的). 解決方法是 div.action 改用 Flex 來佈放它的三個下層 <div> tag, 同時讓div.Form_Content 可以自由擴展佔據剩下的空白高度即可. (千萬不要用 position: fixed 的作法把 div.Footer 的位置固定在最下面, 它會多出很多設定, 很麻煩.)
    #myDiv .action {
    	display: flex;
    	flex-flow: column;
    }
    #myDiv .Form_Content {
    	flex-grow: 1;
    }
    
    合併二組設定:
    #myDiv {
    	width:fit-content;
    }
    #myDiv .action {
    	width: 25em;
    	resize:both;
    	overflow:hedden;
    	display: flex;
    	flex-flow: column;
    }
    #myDiv .Form_Content {
    	flex-grow: 1;
    }
    
  4. 放在可移動部份的最外層的 div#myDiv: 有了上一項的改用 Flex, 要把 resize 設定套最外層 div#myDiv 也是很簡單的事情: div#myDiv 不用更動寬度, div.action 需要多設高度佔滿整個 div#myDiv.
    #myDiv {
    	resize:both;
    	overflow:hedden;
    }
    #myDiv .action {
    	height:100%;
    	display: flex;
    	flex-flow: column;
    }
    #myDiv .Form_Content {
    	flex-grow: 1;
    }
    

匯整


最後, 我把上面拉動及調整大小的功能匯整放在 上, 大家可以連過去試一下. 或者, 直接在下面試試拉動及調整大小的效果.

See the Pen Something Dragable by Jack Ting (@magicjack) on CodePen.

arrow
arrow
    創作者介紹
    創作者 MagicJackTing 的頭像
    MagicJackTing

    傑克! 真是太神奇了!

    MagicJackTing 發表在 痞客邦 留言(0) 人氣()