隨著 IE 即將 EOS (野生 IE 將於 2022/6/15 滅絕,企業人工飼養 IE 則到 2029),IE Only 網頁都更如火如荼。而 showModalDialog 問題也來到第三篇,足見這議題還挺煩人的。

先整理之前的研究心得:

基本上第二種方法已大致滿足需求,但逐一改寫所有網頁裡的 showModalDialog 有點麻煩,於是我想到 Pollyfill

Polyfill 原指為舊瀏覽器實現或模擬新版瀏覽器才有的函式,讓程式不需修改也能在舊瀏覽器運行。但這回我們反向操作,試著為新式瀏覽器加上古董功能,減少古蹟網頁支援新瀏覽器的難度。

我的構想是偵測 window.showModalDialog 是否已存在,若不存在就加上自製 showModalDialog 函式。自製 showModalDialog 函式將遮蔽原網頁禁止操作,用 window.open 開啟網址,等新視窗關閉再移除遮罩。為了盡可能貼近 IE showModalDialog 行為,自製版也接收 url、dialogArguments、options 三組參數,並識別 dialogWidth/dialogHeight/dialogTop/dialogLeft 等設定,讓新視窗與原本 IE showModalDialog 顯示的大小及位置相近。

另外,由於 window.open 開啟視窗無法永遠保持在最上方,可能會被藏到後面(而且還很難找),往留被遮罩無法操作的原網頁不知如何是好。我動了一點手腳,當滑鼠點選遮罩時呼叫 newWin.focus() 將新視窗重新推到最上層,稍稍彌補無法設定永遠最上層的缺憾。至於原本透過 dialogArguments、returnValue 傳遞資料,以及阻斷程式執行直到新視窗關閉的行為,這部分非得改寫不可。採行做法是將要傳遞參數存入 window._dialogArguments,讓新視窗透過 opener._dialogArguments 抓取,要傳回結果將放入 opener._returnValue,新視窗關閉後要執行的程式寫在 Promise.then() 裡,_returnValue 則會是 onFullfilled 事件的傳入參數,讓程式更直覺。(請看範例 TEST 6 示範) 另外,為了讓程式能在 IE 編譯,要避免 let、() => 等 IE 不支援的寫法。

範例程式如下:

<html>
<head>
	<meta charset="utf-8">
	<style>
		button { margin: 3px; }
	</style>
</head>
<body>
	<script>
		var u = 'newwin.html';
		function test(dialogFeatures, dialogArguments) {
			return window.showModalDialog(u, dialogArguments, dialogFeatures);
		}
	</script>
	<div>
		<button onclick="test('')">TEST 1</button>
		no options
	</div>
	<div>
		<button onclick="test('dialogWidth:480px ;dialogHeight= 320px')">TEST 2</button>
		dialogWidth:480px ;dialogHeight= 320px
	</div>
	<div>
		<button onclick="test('dialogWidth:480px;dialogHeight:320px;center:no')">TEST 3</button>
		dialogWidth:480px;dialogHeight:320px;center:no
	</div>
	<div>
		<button onclick="test('dialogWidth:480px;dialogHeigth:320px;dialogTop:50px')">TEST 4</button>
		dialogWidth:480px;dialogHeigth:320px;dialogTop:50px
	</div>
	<div>
		<button onclick="test('dialogWidth:480px;dialogHeigth:320px;dialogTop:50px;dialogLeft:50px')">TEST 5</button>
		dialogWidth:480px;dialogHeigth:320px;dialogTop:50px;dialogLeft:50px
	</div>
	<div>
		<button onclick="test('','FROM OPENER').then(function(res) { alert(res); })">TEST 6</button>
		pass "FROM OPENER" and get return value (for Chrome/Edge)
	</div>
	<script>
		if (!window.showModalDialog) {
			window.showModalDialog = function (url, dialogArguments, options) {
				//using var instead of let for IE compatible
				var mT = /dialogTop[=:]\s*(\d+)/i.exec(options);
				var mL = /dialogLeft[=:]\s*(\d+)/i.exec(options);
				var mW = /dialogWidth[=:]\s*(\d+)/i.exec(options);
				var mH = /dialogHeight[=:]\s*(\d+)/i.exec(options);
				var mC = /center[=:]\s*(off|no|0)/i.exec(options);
				options = {
					width: mW && parseInt(mW[1]), height: mH && parseInt(mH[1]),
					top: mT && parseInt(mT[1]), left: mL && parseInt(mL[1]),
					center: mC ? false : true
				};
				window._dialogArguments = dialogArguments;
				var maskLayer = null;
				maskLayer = document.createElement('div');
				maskLayer.style.position = 'absolute';
				maskLayer.style.zIndex = 65535;
				maskLayer.style.opacity = 0.5;
				maskLayer.style.top = 0;
				maskLayer.style.left = 0;
				maskLayer.style.width = '100vw';
				maskLayer.style.height = Math.max(window.innerHeight, document.body.scrollHeight) + 'px';
				maskLayer.style.backgroundColor = '#444';
				document.body.appendChild(maskLayer);
				var promise = new Promise(function (resolve, reject) {
					var w = Math.round(options.width || 500);
					var h = Math.round(options.height || 500);
					var t = (options.top || (options.center ? (window.screen.availHeight - h) / 2 : 10)) + window.screen.availTop;
					var l = (options.left || (options.center ? (window.screen.availWidth - w) / 2 : 10)) + window.screen.availLeft;
					window._dialogReturnValue = undefined;
					var newWin = window.open(url, '_blank', 'width=' + w + ',height=' + h + ',top=' + t + ',left=' + l);
					maskLayer.addEventListener('click',function() { newWin.focus(); });
					var hnd = setInterval(function () {
						try {
							if (newWin.closed) {
								clearInterval(hnd);
								//popup window can assing opener._dialogReturnValue for return value
								resolve(window._dialogReturnValue);
								maskLayer && maskLayer.remove();
							}
						}
						catch (e) {
							clearInterval(hnd);
							reject();
							maskLayer && maskLayer.remove();
						}
					}, 100);
				});
				return promise;
			}
		}
	</script>
</body>

</html>

為展示 dialogArguments 與 returnValue 傳遞,我寫了一個簡單的 newwin.html 負責傳收及回傳結果。

<!DOCTYPE html>
<html>
<body>
	dialogArguments = <span id=a></span> <br />
	Return Value = <span id=s></span> <button onclick='closeWin()'>Close</button>
	<script>
		try {
			document.getElementById('a').innerText = opener._dialogArguments;
		} catch(e) { }
		var t = new Date().getTime();
		document.getElementById('s').innerText = t;
		function closeWin() {
			try {
				opener._dialogReturnValue = t;
			}
			catch(e) { }
			window.close();
		}
	</script>
</body>
</html>

展示影片:(先用 IE 示範原生 showModalDialog,後改用 Edge 測試對照 Polyfill 效果,最後示範若新視窗被其他視窗覆蓋,點一下遮罩會跳回最上層)

操作展示

這個小程式估計可節省一些翻修 IE Only 網頁的時間,提供從事維護古蹟的同學參考並歡迎回饋意見。

A simple showModalDialog ployfill to make it easier to migrage IE only website.


Comments

# by 水星

如果開延伸螢幕,位置會錯

# by Jeffrey

to 水星,我知道 IE 在延伸螢幕定位會錯,所以 Polyfill 有用 window.screen.availTop、window.screen.availLeft 修正,有特別測試過位置正確。你是用 Edge/Chrome 測試出錯?

# by Alex

開窗前是否要先將父視窗的 window._returnValue值清除 避免執行過一次後,再次開窗後使用者不是點按鈕正常關閉而是按視窗右上角X關閉視窗 會抓到前一次執行的結果 window._dialogReturnValue = null var newWin = window.open(url, '_blank', 'width=' + w + ',height=' + h + ',top=' + t + ',left=' + l);

# by Jeffrey

to Alex, 有理,這樣更嚴謹,我加上 window._dialogReturnValue = undefined; 意義上應該再比 null 更精準一些。謝謝補充。

# by Alex

to Jeffrey, 加上判斷回傳是否是undefined 來決定resolve還是reject,不正常關閉不觸發resolve, 讓父視窗收到的回傳能確定是正常操作回傳, 但例外是子視窗真的沒有回傳值會造成沒有resolve的狀況,可能子視窗一執行建議都先給opener._dialogReturnValue一個default值 if(window._dialogReturnValue !== undefined) { //popup window can assing opener._dialogReturnValue for return value resolve(window._dialogReturnValue); } else{ reject(); }

# by Jeffrey

to Alex, 有兩種 API 設計方式:新視窗關閉時一律 resolve(),呼叫端由 _dialogReturnValue === undefined 與否判斷是否正常結束;或是如你所說,正常結束 resolve()、強制關閉 reject()。後者呼叫端 then() 要寫成兩個 function (一個接 resolve(), 一個接 reject()),我個人偏好前者。

# by Wayne

Jeffrey大大, 你好, 我用你寫的程式在Edge(119.0.2151.72 (官方組建) (64 位元))或Chrome(版本 119.0.6045.160 (正式版本) (64 位元))按TEST6都不能傳值給newwin.html, 也不能從newwin.html傳時間值給父視窗,都顯示undefined, 是那裡出錯了呢?

# by Jeffrey

to Wayne, 你是從檔案總管直接點選開啟網頁嗎?可以上傳測試網站再測看看,本機開啟網頁有許多資安限制,有些資訊無法存取。

# by Wayne

可以了,謝謝,原因是我是在local跑,會導致發生CORS問題,把它們放到web server上跑就行了

Post a comment