為第一次使用網頁顯示「新手提示」之懶人工具
4 | 4,937 |
寫網頁的人總夢想著自己寫的介面夠簡單夠直覺,不需說明文件,使用者模索兩下就能上手。但事與願違,網頁上一些自以為夠明顯一定會被使用者發現的得意設計,上線半年還乏人問津,常令設計者一陣鼻酸。
手機 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, 謝謝分享。