寫網頁的人總夢想著自己寫的介面夠簡單夠直覺,不需說明文件,使用者模索兩下就能上手。但事與願違,網頁上一些自以為夠明顯一定會被使用者發現的得意設計,上線半年還乏人問津,常令設計者一陣鼻酸。

手機 App 有一種不錯的設計概念可供借鏡,做法是第一次開啟 App 時先跳出一段動畫展示,簡要提示新功能或操作技巧,使用者必須看完才能開啟使用,而它只有在首次使用時顯示,之後便不再出現。

我想把這種「新手提示」的概念應用在網頁上 - 第一次開啟網頁時先放一層遮罩擋住 UI 不讓使用者操作,以動畫方式提示操作訣竅,展示結束後在 localStorage 留下已讀註記,下回再開啟時檢查到已讀註記便不再顯示。

為每個網頁大費周章客製專屬新手提示動畫有點辛苦,於是我想到用 jQuery Selector 找在特定元素在旁邊附上一小段文字說明的點子,這樣子用 { "selector1": "說明文字1", "selector2": "說明文字2" ... } 定義出標註的元素及文字,呼呼共用函式播放,如此只需簡單設定就能為網頁加上新手提示了。 這種新手指示的效果當然比不上請視覺設計人製作的精美動畫相比,但比起靜態說明文件或任由使用者自己摸索,已是了不起的大躍進,可以大幅提供企業內部應用系統的操作體驗。

最後我把這個概念寫成共用函式,還加入自訂說明文字位置、自動捲動至元素位置、顯示前後自訂事件... 等功能,並小心翼翼保持 IE 相容,理論上可滿足大部分應用情境。而為了實地展示,我 Fork 了一個網頁欄位驗證 Github 專案當成示範對象,在網頁加入以下程式碼:

<script src="afet.novice-guide.js"></script>
<script>
	afet.ShowNoviceGuide('readFlagName', false, {
		'h1': {
			offsetLeft: 0, offsetTop: -20,
			text: '<div style="padding:12px;width:500px;text-align:center;font-size:2em;color:yellow">\
				簡易式「新手提示」展示</div>'
		},
		'[name=name]': '請在這裡填入您的姓名\n例如:黑鮪魚之夢',
		'[type=checkbox]:eq(1)~span': {
			offsetLeft: -50, offsetTop: 20,
			before: function (el) { el.closest('label').css('border', '1px solid red'); },
			after: function (el) { el.closest('label').css('border', "none"); },
			text: '點選文字也可打勾哦'
		},
		'[type=submit]': {
			offsetLeft: 0, offsetTop: -50,
			text: '按這裡送出表單'
		},
		'.email': {
			offsetLeft: 40, offsetTop: 20,
			before: function (el) { validator.checkField(el[0]); },
			after: function (el) { validator.reset(); },
			text: '檢核失敗將出現紅色警語'
		},
		'.reshow': '新手提示只會顯示一次\n重看請按這裡清localStorage旗標\n並重新整理頁面'
	});
</script>

呈現效果如下,想實地動手的朋友可玩玩線上版

函式庫不大,不到 120 行打發,單一 js 搞定,有 jQuery 就能動。完整程式碼如下:

var afet;
(function (afet) {
	/**
	 * 簡易新手提示產生器 Ver 1.0.0 by Jeffrey https://blog.darkthread.net
	 * @param {any} guideCode 指示代碼,首次顯示後在localStorage下註記以免重複出現
	 * @param {boolean} force 無視 localStorage 已讀註記,一律顯示
	 * @param {any} instructions Key/Value 形式,Key為 Selector,Value 為說明物件
	 * @param {any} instrucStyles 說明區塊自訂樣式
	 * 說明物件格式為 { offsetTop: y, offsetLeft: x, text: '...', before: function(el) {...}, after: function(el) {...}  }
	 * 說明物件也可以字串取代,則說明會寫示在選取元素下方,說明文字以可為 HTML 或純文字,若為純文字支援換行顯示
	  */
	function showNoviceGuide(guideCode, force, instructions, instrucStyles) {
		var zIdx = 9990, bgColor = '#2196f3', key = "$NG_" + guideCode;
		if (localStorage.getItem(key) && !force) return;
		var docW = $(document).width() + 'px', docH = $(document).height() + 'px';
		var mask = $('<div></div>').css({
			position: 'absolute', top: 0, left: 0, width: docW, height: docH, 'z-index': zIdx,
			opacity: 0.2, 'background-color': '#444', cursor: 'pointer'
		});
		mask.appendTo('body');
		function getNumber(s) { return parseInt(s.replace('px', '')); }
		var queue = Object.keys(instructions);
		var lastTip;
		var svg = $('<svg version="1.1"></svg>').css({ position: 'absolute', top: 0, left: 0, zIndex: zIdx + 1, width: docW, height: docH, opacity: 0.5, cursor: 'pointer' });
		svg.appendTo('body');
		//http://chubao4ever.github.io/tech/2015/07/16/jquerys-append-not-working-with-svg-element.html
		function SVG(tag) {
			return document.createElementNS('http://www.w3.org/2000/svg', tag);
		}
		function drawLine(x1, y1, x2, y2, color) {
			$(SVG('line'))
				.attr('x1', x1).attr('y1', y1).attr('x2', x2).attr('y2', y2)
				.attr('stroke', color).attr('stroke-width', '2px')
				.appendTo(svg);
		}
		function drawCircle(x, y, r, color) {
			$(SVG('circle')).attr('cx', x).attr('cy', y).attr('r', r).attr('fill', color)
				.appendTo(svg);
		}
		svg.click(function () {
			if (lastTip) {
				 lastTip.trigger('next').remove();
				 lastTip = null;
			}
			svg.empty();
			if (!queue.length) {
				mask.remove();
				svg.remove();
				window.scrollTo(0, 0);
				localStorage.setItem(key, "Y");
				return;
			}
			var sel = queue.shift();
			var focusElem = $(sel);
			var instruction = instructions[sel];
			if (typeof instruction === "string") {
				instruction = {
					offsetTop: focusElem.height() + 20,
					offsetLeft: 0,
					text: instruction
				};
			}
			if (focusElem.length && focusElem.is(":visible")) {
				$.isFunction(instruction.before) && instruction.before(focusElem);
				var pos = focusElem.offset();
				var tip = $('<div></div>');
				tip.css({
					position: 'absolute',
					top: (pos.top + instruction.offsetTop) + 'px',
					left: (pos.left + instruction.offsetLeft) + 'px',
					"z-index": zIdx + 2,
					opacity: 1,
					backgroundColor: bgColor,
					padding: '6px',
					color: 'white'
				});
				if (instrucStyles) tip.css(instrucStyles);
				var instrText = instruction.text;
				if (instrText.indexOf('<') === 0)
					tip.html(instrText);
				else {
					tip.text(instrText);
					if (instrText.indexOf('\n') > -1)
						tip.html(tip.html().replace(/\n/g, '<br />'));
				}
				var s = getComputedStyle(focusElem[0]);
				tip.attr('data-st', (pos.left + getNumber(s.paddingLeft) + focusElem.width() / 2) + "," 
					+ (pos.top + getNumber(s.paddingTop) + focusElem.height() / 2));
				$.isFunction(instruction.after) && tip.bind("next", function() { 
					instruction.after(focusElem);
				});
			}
			else {
				//if not found, trigger click event to show next tip
				setTimeout(function () { svg.click(); }, 0);
				return;
			}
			lastTip = tip;
			lastTip.appendTo('body');
			var pos = lastTip.offset();
			var st = lastTip.attr("data-st").split(',');
			drawCircle(st[0], st[1], 5, bgColor);
			drawLine(st[0], st[1], pos.left + lastTip.width() / 2, pos.top + lastTip.height() / 2, bgColor);
			var y = st[1] - $(document).scrollTop()
			console.log(y);
			if (y > $(window).height() / 2)	window.scrollTo(0, st[1]);
			else if (y < 30) window.scrollTo(0, 0);
			svg.hide().fadeIn('slow');
			lastTip.hide().fadeIn('slow');
		});
		setTimeout(function () { svg.click(); }, 500);
	}
	afet.ShowNoviceGuide = showNoviceGuide;
})(afet || (afet = {}));

實際套用在幾個網頁上效果不錯,是個實用的好工具。好久沒寫前端程式,這個算是近期的得意之作,野人獻曝分享給大家~

【2021-04-04 更新】已升級 1.1 版 加入上下則控制及跳過導覽功能。

Demostration of how to create simple novice guide for first time webpage user with shared JavaScript functions.


Comments

# by Switch

有點類似玩新遊戲時都會出現的「跳不過的新手教學」的概念嗎? 題外話,Captcha 居然是 1-1

# by null

我記得 Bootstrap 就有現成的功能啦

# by Ben

找了一下 Bootstrap 似乎只有 popovers 好像是以下的 API 比較符合 http://bootstraptour.com/

# by Jeffrey

to Switch,出現 1-1 相當於拉 Bar 拉出 7 7 7 哦,恭喜。 to null, Ben, 謝謝分享。

Post a comment