Skip to content

Commit 5bf901a

Browse files
authored
Merge pull request usablica#1621 from usablica/dontShowAgain
feat: "Don't show again" feature
2 parents 6a9a4c6 + 1c3752d commit 5bf901a

31 files changed

+460
-8
lines changed

src/core/dontShowAgain.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { deleteCookie, getCookie, setCookie } from "../util/cookie";
2+
3+
const dontShowAgainCookieValue = "true";
4+
5+
/**
6+
* Set the "Don't show again" state
7+
*
8+
* @api private
9+
* @param {Boolean} dontShowAgain
10+
* @method setDontShowAgain
11+
*/
12+
export function setDontShowAgain(dontShowAgain) {
13+
if (dontShowAgain) {
14+
setCookie(
15+
this._options.dontShowAgainCookie,
16+
dontShowAgainCookieValue,
17+
this._options.dontShowAgainCookieDays
18+
);
19+
} else {
20+
deleteCookie(this._options.dontShowAgainCookie);
21+
}
22+
}
23+
24+
/**
25+
* Get the "Don't show again" state from cookies
26+
*
27+
* @api private
28+
* @method getDontShowAgain
29+
*/
30+
export function getDontShowAgain() {
31+
const dontShowCookie = getCookie(this._options.dontShowAgainCookie);
32+
return dontShowCookie && dontShowCookie === dontShowAgainCookieValue;
33+
}

src/core/introForElement.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import fetchIntroSteps from "./fetchIntroSteps";
1414
* @returns {Boolean} Success or not?
1515
*/
1616
export default function introForElement(targetElm) {
17+
// don't start the tour if the instance is not active
18+
if (!this.isActive()) return;
19+
1720
if (this._introStartCallback !== undefined) {
1821
this._introStartCallback.call(this, targetElm);
1922
}
@@ -38,5 +41,6 @@ export default function introForElement(targetElm) {
3841
//for window resize
3942
DOMEvent.on(window, "resize", onResize, this, true);
4043
}
44+
4145
return false;
4246
}

src/core/showElement.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export default function _showElement(targetElement) {
253253
self._lastShowElementTimer = window.setTimeout(() => {
254254
// set current step to the label
255255
if (oldHelperNumberLayer !== null) {
256-
oldHelperNumberLayer.innerHTML = `${targetElement.step} of ${this._introItems.length}`;
256+
oldHelperNumberLayer.innerHTML = `${targetElement.step} ${this._options.stepNumbersOfLabel} ${this._introItems.length}`;
257257
}
258258

259259
// set current tooltip text
@@ -356,6 +356,30 @@ export default function _showElement(targetElement) {
356356
tooltipHeaderLayer.appendChild(tooltipTitleLayer);
357357
tooltipLayer.appendChild(tooltipHeaderLayer);
358358
tooltipLayer.appendChild(tooltipTextLayer);
359+
360+
// "Do not show again" checkbox
361+
if (this._options.dontShowAgain) {
362+
const dontShowAgainWrapper = createElement("div", {
363+
className: "introjs-dontShowAgain",
364+
});
365+
const dontShowAgainCheckbox = createElement("input", {
366+
type: "checkbox",
367+
id: "introjs-dontShowAgain",
368+
name: "introjs-dontShowAgain",
369+
});
370+
dontShowAgainCheckbox.onchange = (e) => {
371+
this.setDontShowAgain(e.target.checked);
372+
};
373+
const dontShowAgainCheckboxLabel = createElement("label", {
374+
htmlFor: "introjs-dontShowAgain",
375+
});
376+
dontShowAgainCheckboxLabel.innerText = this._options.dontShowAgainLabel;
377+
dontShowAgainWrapper.appendChild(dontShowAgainCheckbox);
378+
dontShowAgainWrapper.appendChild(dontShowAgainCheckboxLabel);
379+
380+
tooltipLayer.appendChild(dontShowAgainWrapper);
381+
}
382+
359383
tooltipLayer.appendChild(_createBullets.call(this, targetElement));
360384
tooltipLayer.appendChild(_createProgressBar.call(this));
361385

@@ -364,7 +388,7 @@ export default function _showElement(targetElement) {
364388

365389
if (this._options.showStepNumbers === true) {
366390
helperNumberLayer.className = "introjs-helperNumberLayer";
367-
helperNumberLayer.innerHTML = `${targetElement.step} of ${this._introItems.length}`;
391+
helperNumberLayer.innerHTML = `${targetElement.step} ${this._options.stepNumbersOfLabel} ${this._introItems.length}`;
368392
tooltipLayer.appendChild(helperNumberLayer);
369393
}
370394

src/index.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import stamp from "./util/stamp";
33
import exitIntro from "./core/exitIntro";
44
import refresh from "./core/refresh";
55
import introForElement from "./core/introForElement";
6+
import { getDontShowAgain, setDontShowAgain } from "./core/dontShowAgain";
67
import { version } from "../package.json";
78
import {
89
populateHints,
@@ -32,6 +33,8 @@ function IntroJs(obj) {
3233
this._introItems = [];
3334

3435
this._options = {
36+
/* Is this tour instance active? Don't show the tour again if this flag is set to false */
37+
isActive: true,
3538
/* Next button label in tooltip box */
3639
nextLabel: "Next",
3740
/* Previous button label in tooltip box */
@@ -58,8 +61,10 @@ function IntroJs(obj) {
5861
exitOnEsc: true,
5962
/* Close introduction when clicking on overlay layer? */
6063
exitOnOverlayClick: true,
61-
/* Show step numbers in introduction? */
64+
/* Display the pagination detail */
6265
showStepNumbers: false,
66+
/* Pagination "of" label */
67+
stepNumbersOfLabel: "of",
6368
/* Let user use keyboard to navigate the tour? */
6469
keyboardNavigation: true,
6570
/* Show tour control buttons? */
@@ -86,6 +91,12 @@ function IntroJs(obj) {
8691
positionPrecedence: ["bottom", "top", "right", "left"],
8792
/* Disable an interaction with element? */
8893
disableInteraction: false,
94+
/* To display the "Don't show again" checkbox in the tour */
95+
dontShowAgain: false,
96+
dontShowAgainLabel: "Don't show this again",
97+
/* "Don't show again" cookie name and expiry (in days) */
98+
dontShowAgainCookie: "introjs-dontShowAgain",
99+
dontShowAgainCookieDays: 365,
89100
/* Set how much padding to be used around helper element */
90101
helperElementPadding: 10,
91102
/* Default hint position */
@@ -149,6 +160,13 @@ introJs.instances = {};
149160

150161
//Prototype
151162
introJs.fn = IntroJs.prototype = {
163+
isActive() {
164+
if (this._options.dontShowAgain && getDontShowAgain.call(this)) {
165+
return false;
166+
}
167+
168+
return this._options.isActive;
169+
},
152170
clone() {
153171
return new IntroJs(this);
154172
},
@@ -210,6 +228,10 @@ introJs.fn = IntroJs.prototype = {
210228
refresh.call(this, refreshSteps);
211229
return this;
212230
},
231+
setDontShowAgain(dontShowAgain) {
232+
setDontShowAgain.call(this, dontShowAgain);
233+
return this;
234+
},
213235
onbeforechange(providedCallback) {
214236
if (typeof providedCallback === "function") {
215237
this._introBeforeChangeCallback = providedCallback;

src/styles/introjs.scss

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,32 @@ tr.introjs-showElement {
175175
padding: 20px;
176176
}
177177

178+
.introjs-dontShowAgain {
179+
padding-left: 20px;
180+
padding-right: 20px;
181+
}
182+
183+
.introjs-dontShowAgain input {
184+
padding: 0;
185+
margin: 0;
186+
margin-bottom: 2px;
187+
display: inline;
188+
width: 10px;
189+
height: 10px;
190+
}
191+
192+
.introjs-dontShowAgain label {
193+
font-size: 14px;
194+
display: inline-block;
195+
font-weight: normal;
196+
display: inline-block;
197+
margin: 0 0 0 5px;
198+
padding: 0;
199+
background-color: $white;
200+
color: $black600;
201+
user-select: none;
202+
}
203+
178204
.introjs-tooltip-title {
179205
font-size: 18px;
180206
margin: 0;

src/util/cookie.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export function setCookie(name, value, days) {
2+
const cookie = { [name]: value, path: "/" };
3+
4+
if (days) {
5+
let date = new Date();
6+
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
7+
cookie.expires = date.toUTCString();
8+
}
9+
10+
let arr = [];
11+
for (let key in cookie) {
12+
arr.push(`${key}=${cookie[key]}`);
13+
}
14+
15+
document.cookie = arr.join("; ");
16+
17+
return getCookie(name);
18+
}
19+
20+
export function getAllCookies() {
21+
let cookie = {};
22+
23+
document.cookie.split(";").forEach((el) => {
24+
let [k, v] = el.split("=");
25+
cookie[k.trim()] = v;
26+
});
27+
28+
return cookie;
29+
}
30+
31+
export function getCookie(name) {
32+
return getAllCookies()[name];
33+
}
34+
35+
export function deleteCookie(name) {
36+
setCookie(name, "", -1);
37+
}

tests/core/dontShowAgain.test.js

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as cookie from "../../src/util/cookie";
2+
import {
3+
setDontShowAgain,
4+
getDontShowAgain,
5+
} from "../../src/core/dontShowAgain";
6+
7+
describe("dontShowAgain", () => {
8+
test("should call set cookie", () => {
9+
const setCookieMock = jest.spyOn(cookie, "setCookie");
10+
11+
setDontShowAgain.call(
12+
{
13+
_options: {
14+
dontShowAgainCookie: "cookie-name",
15+
dontShowAgainCookieDays: 7,
16+
},
17+
},
18+
true
19+
);
20+
21+
expect(setCookieMock).toBeCalledTimes(1);
22+
expect(setCookieMock).toBeCalledWith("cookie-name", "true", 7);
23+
});
24+
25+
test("should call delete cookie", () => {
26+
const setCookieMock = jest.spyOn(cookie, "setCookie");
27+
const deleteCookieMock = jest.spyOn(cookie, "deleteCookie");
28+
29+
setDontShowAgain.call(
30+
{
31+
_options: {
32+
dontShowAgainCookie: "cookie-name",
33+
dontShowAgainCookieDays: 7,
34+
},
35+
},
36+
false
37+
);
38+
39+
expect(setCookieMock).toBeCalledTimes(0);
40+
expect(deleteCookieMock).toBeCalledTimes(1);
41+
expect(deleteCookieMock).toBeCalledWith("cookie-name");
42+
});
43+
44+
test("should return true when cookie is valid", () => {
45+
jest.spyOn(cookie, "getCookie").mockReturnValue("true");
46+
47+
expect(
48+
getDontShowAgain.call({
49+
_options: {
50+
dontShowAgainCookie: "cookie-name",
51+
dontShowAgainCookieDays: 7,
52+
},
53+
})
54+
).toBe(true);
55+
});
56+
57+
test("should return false when cookie is invalid", () => {
58+
jest.spyOn(cookie, "getCookie").mockReturnValue("invalid-state");
59+
60+
expect(
61+
getDontShowAgain.call({
62+
_options: {
63+
dontShowAgainCookie: "cookie-name",
64+
dontShowAgainCookieDays: 7,
65+
},
66+
})
67+
).toBe(false);
68+
});
69+
});

tests/core/introForElement.test.js

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import introForElement from "../../src/core/introForElement";
22
import * as fetchIntroSteps from "../../src/core/fetchIntroSteps";
33
import * as addOverlayLayer from "../../src/core/addOverlayLayer";
44
import * as nextStep from "../../src/core/steps";
5+
import introJs from "../../src";
56

67
describe("introForElement", () => {
78
test("should call the onstart callback", () => {
@@ -11,14 +12,31 @@ describe("introForElement", () => {
1112

1213
const onstartCallback = jest.fn();
1314

14-
const self = {
15-
_options: {},
16-
_introStartCallback: onstartCallback,
17-
};
15+
const context = introJs().setOptions({
16+
isActive: true,
17+
});
1818

19-
introForElement.call(self, document.body);
19+
context._introStartCallback = onstartCallback;
20+
21+
introForElement.call(context, document.body);
2022

2123
expect(onstartCallback).toBeCalledTimes(1);
2224
expect(onstartCallback).toBeCalledWith(document.body);
2325
});
26+
27+
test("should not start the tour if isActive is false", () => {
28+
const fetchIntroStepsMock = jest.spyOn(fetchIntroSteps, "default");
29+
const addOverlayLayerMock = jest.spyOn(addOverlayLayer, "default");
30+
const nextStepMock = jest.spyOn(nextStep, "nextStep");
31+
32+
const context = introJs().setOptions({
33+
isActive: false,
34+
});
35+
36+
introForElement.call(context, document.body);
37+
38+
expect(fetchIntroStepsMock).toBeCalledTimes(0);
39+
expect(addOverlayLayerMock).toBeCalledTimes(0);
40+
expect(nextStepMock).toBeCalledTimes(0);
41+
});
2442
});

0 commit comments

Comments
 (0)