這個系列是「將 IE-Only 網站翻修到 Chrome/Edge 相容」過程的瑣碎筆記,有參與古蹟維護的朋友請進。

今天說說網頁裡若出現只剩 IE 支援的 showModalDialog() 要怎麼修改?從 Chrome 43 跟 Firefox 56 之後,showModalDialog API 已被主流瀏覽器移除,但它有些獨家特性,無法輕易用 window.open() 另開新視窗取代,包含:

  1. 會阻擋呼叫端原有網頁操作,直到使用者關閉 showModalDialog 開啟網頁
  2. 呼叫程式碼需等待 showModalDialog() 視窗關閉才會繼續往下執行

前者的替代方案不少,主流網頁元件程式庫幾乎都支援強制對話框,像是 jQueryUI dialogKendo UI DialogBootstrap Modal,更不用說三大前端框架 Vue.js、Angular、React.js (VAR) 也都有自己的解決方案,但如果你已上手 VAR,應該不需要參考這篇筆記就知如何動手。這篇採行的策略是 - 避免砍掉重練,以最小幅度修改 IE-Only 網頁使其相容 Chrome/Edge 等主流瀏覽器。所以先不考慮 VAR,而 jQueryUI、Kendo UI、Bootstrap 三者相比,Bootstrap Dialog 有點陽春;Kendo UI Dialog 包含在免費版 Kendo UI Core的範圍,但程式庫較龐大,如果網頁還有用到其他 Kendo UI 元件時才是我的首選,這篇會用 jQuery UI Dialog 示範,但要抽換元件不是難事。

至於阻擋程式直到對話視窗結束再執行,這是瀏覽器特權,想用 JavaScript 實現基本上無解,建議比照 jQuery.ajax Promise 做法,改寫成 .done(function() { ...後續執行程式碼... })。

我寫了一個簡單 showModalDialog 範例。

index.html

<!DOCTYPE html>

<html>
<body>
	<input type="text" id="name" value="Jeffrey" size="16" />
	<input type="button" onclick="testModalDialog()" value="TEST" />
	<script>
		function testModalDialog() {
			var result = window.showModalDialog(
				'select.html', //url
				document.all('name').value, //dialogArguments
				'dialogWidth=300px;dialogHeight=80px');
			alert('Team=' + result);
		}
	</script>
</body>
</html>

select.html

<!DOCTYPE html>

<html>
<body style="padding:12px">
	<span id='name'></span>, please select team:
	<select id='team'>
		<option selected>Blue</option>
		<option>Red</option>
	</select>
	<input type="button" onclick="sendResult()" value="OK" />
	<script>
		document.getElementById('name').innerText = window.dialogArguments;
		function sendResult() {
			var team = document.getElementById('team');		
			window.returnValue = team.options[team.selectedIndex].innerText;
			window.close();
		}
	</script>
</body>
</html>

執行結果如下,index.html 以 showModalDialog() 帶出,使用者姓名以 window.dialogArguments 傳入,在 select.html 按 OK 會設定 window.returnValue 並 window.close() 傳回結果。index.html 由 showModalDialog() 回傳值取得使用者選取結果,若直接關閉視窗,showModalDialog() 則傳回 undefined。


(原程式有個低級錯誤沒正確抓到下拉選取值,導致選 Red 傳 Blue,請大家當作沒看到... orz)

評估之後,我打算寫一個 jQuery.showModalDialog 取代 window.showModalDialog,傳入參數包含 url、原本的 dialogArguments 參數、視窗標題、視窗寬度、視窗高度,背後呼叫 jQuery UI dialog 顯示(若要抽換 Dialog 元件,改這裡就好)。程式寫成 jquery.showModalDialog.js 方便引用,由於網站原本不一定有引用 jQuery UI,我加入主動偵測並自動從 CDN 載入 jquery-ui.css 及 jquery-ui.min.js 的貼心小功能。(企業內部應用則建議將 jQuery UI css/js 放在同網站,Intranet 比 CDN 下載速度更快)

jQuery.showModalDialog 邏輯不複雜,動態加入 iframe 包成 jQuery UI Dialog,用 $.dialogArguments、$.dialogReturnValue 與對話框溝通,開啟及縮放事件時調整 iframe 大小,關閉事件時從 DOM 清除 Dialog 元件並呼叫 Deferred.resolve() 觸發 Promise .done() 事件:

if (window.jQuery && !jQuery.showModalDialog) {
	let $ = jQuery;
	// Load jQuery UI dynamically if not ready
	if (!$.fn.dialog) {
		$('<link />').attr({
			type: 'text/css', 
			rel: 'stylesheet', 
			href: 'https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css'
		}).appendTo('head');
		$.getScript('https://code.jquery.com/ui/1.12.1/jquery-ui.min.js');
	}
	$.dialogArguments = undefined;
	$.dialogReturnValue = undefined;
	$.dialogInstance = null;
	$.showModalDialog = function (url, diagArgs, title, width, height) {
		$.dialogReturnValue = undefined;
		$.dialogArguments = diagArgs;
		let dfd = $.Deferred();
		$.dialogInstance = $('<iframe></iframe>')
			.attr('src', url)
			.css({ width: width, height: height, border: 'none', padding: 0 })
			.dialog({
				title: title || '',
				width: width, 
				height: height + 50, 
				modal: true,
				resize: function(e, ui) { $(e.target).width(ui.size.width); },
				open: function(e, ui) { $(e.target).width(width); },
				close: function () { 
			        $(this).dialog('destroy').remove();
			        setTimeout(function() { dfd.resolve(); }, 0); //after closed
				}
			});
		return dfd.promise();
	}
	$.closeModalDialog = function() { 
		$.dialogInstance && $.dialogInstance.dialog('close');	
	}
}

index.html 進行小幅改寫,引用 jQuery、jquery.showModalDialog.js,改呼叫 $.showModalDialog(),接受回應結果寫在 done() 函式,由 $.dialogReturnValue 取值:

<!DOCTYPE html>

<html>
	<head>
	<script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
	<script src="jquery.showModalDialog.js"></script>
	</head>
<body>
	<input type="text" id="name" value="Jeffrey" size="16" />
	<input type="button" onclick="testModalDialog()" value="TEST" />
	<script>
		function testModalDialog() {
			$.showModalDialog(
				'select.html', //url
				document.getElementById('name').value, //dialogArguments
				'Select Team', //title
				350, //width
				80 //height
			).done(function() {
				alert('Team=' + $.dialogReturnValue);
			});
		}
	</script>
</body>
</html>

select.html 的修改更單純,window.dialogArguments 改 parent.jQuery.dialogArguments,window.returnValue 改 parent.jQuery.dialogReturnValue,window.close() 改 parent.jQuery.closeModalDialog():

//document.getElementById('name').innerText = window.dialogArguments;
document.getElementById('name').innerText = parent.jQuery.dialogArguments;
function sendResult() {
	var team = document.getElementById('team');
	//window.returnValue = team.options[team.selectedIndex].innerText;
	parent.jQuery.dialogReturnValue = team.options[team.selectedIndex].innerText;
	//window.close();
	parent.jQuery.closeModalDialog();
}

實測成功!

Exmaple of replacing showModalDialog() of IE-only page for Chrom/Edge.


Comments

# by Alex

選 red 卻顯示 blue?

# by 小海

黑哥最高~

# by Jeffrey

to Alex, 啊啊啊(抱頭),好低級的錯誤,已修正程式,謝謝提醒。

# by Alex hsu

黑大,這種情況是在單一視窗下的頁面可以正常回饋訊息 如果是一個頁面有兩個iframe,其中一個iframe的頁面用此功能 會彈到最上層嗎?以及return value會回到該Iframe嗎?

# by Jeffrey

to Alex hsu, 在 IFrame 呼叫 $.showMadalDialog(),其顯示範圍最大只到該 IFrame 的大小,你說的情境我傾向從最上層視窗呼叫,用 top.jQuery.showModalDialog() 開啟,傳回值改透過 top.jQuery.dialogReturnValue 傳遞。

# by joe

請問黑大,我改用伺服器控制項執行 ScriptManager.RegisterStartupScript(Page, Page.GetType(), "onclick", "testModalDialog();", True) 會發生 JavaScript 執行階段錯誤: 物件沒有支援這個屬性或方法 'dialog' ~ 是否寫法錯誤~~謝謝指教

# by Jeffrey

to joe, 感覺是動態載入 jQuery UI 部分不成功,建議在網頁載入 https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css 跟 https://code.jquery.com/ui/1.12.1/jquery-ui.min.js 看看是否能排除問題。 另外,推薦更方便的升級寫法:https://blog.darkthread.net/blog/showmodaldialog-polyfill/

Post a comment