IE 都更筆記 - 為 Chrome/Edge 加上 showModalDialog Polyfill
9 |
隨著 IE 即將 EOS (野生 IE 將於 2022/6/15 滅絕,企業人工飼養 IE 則到 2029),IE Only 網頁都更如火如荼。而 showModalDialog 問題也來到第三篇,足見這議題還挺煩人的。
先整理之前的研究心得:
- 汰換 showModalDialog()
嘗試用 IFrame 替代 showModalDialog,但有個問題,當 URL 來自其他網站,可能踩到 X-Frame-Options、CSP frame-ancestors 限制無法顯示 - 再談汰換 showModalDialog(),使用 window.open()
改用 window.open 取代 IFrame 以克服第三方網站無法顯示問題,並用了小技巧偵測視窗關閉整合 Promise 執行後續程式碼
基本上第二種方法已大致滿足需求,但逐一改寫所有網頁裡的 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上跑就行了