UNPKG

clever-onboarding

Version:

Framework agnostic onboarding widget for your web apps.

302 lines (255 loc) 6.98 kB
import style from "./Onboard.css"; import {select, selectAll} from "d3-selection"; import debounce from "lodash-es/debounce.js"; import * as Defaults from "./OnboardDefaults"; /** * MaskRenderer is reponsible for rendering mask / spotlight * @param {OnboardOptions} options * @param {OnboardModel} model */ export default class MaskRenderer { constructor(options, model) { /** * @private * Onboard options */ this._options = options; /** * @private * DOM container of this widget */ this._containerEl = null; /** * @private * Mask Element */ this._svgEl = null; /** * @private * Step elements */ this._stepElements = []; /** * @private * true if Onboard has been rendered */ this._rendered = false; /** * @private */ this._model = model; this._onStartBinding = this._model.on("start", this._onStart.bind(this)); this._onStepBinding = this._model.on("step", this._onStep.bind(this)); this._onStopBinding = this._model.on("stop", this._onStop.bind(this)); } /** * Returns whether MaskRenderer has been rendered or not * @returns {boolean} true if MaskRenderer has been rendered */ isRendered() { return this._rendered; } /** * Render logic of this widget * @param {String|HTMLElement} selector selector or DOM element * @returns {MaskRenderer} returns this widget instance */ render(selector) { // get container element using selector or given element this._containerEl = select(selector || document.body); this._renderMask(); this._rendered = true; return this; } /** * @private * Returns view size * @return {Object} viewSize * @return {Object} viewSize.width * @return {Object} viewSize.height */ _getViewSize(){ return { width: Math.max(document.documentElement.offsetWidth, document.documentElement.clientWidth), height: Math.max(document.documentElement.offsetHeight, document.documentElement.clientHeight) } } /** * @private * Render logic for the mask */ _renderMask(){ var size = this._getViewSize(); // render SVG this._svgEl = this._containerEl.append("svg") .attr("class", style["svg"]) .attr("width", size.width) .attr("height", size.height) // defs el this._defsEl = this._svgEl.append("defs"); this._maskEl = this._defsEl.append("mask") .attr("class", style["mask"]) .attr("id", "onboarding-mask") .attr("width", "100%") .attr("height", "100%") .attr("x", 0) .attr("y", 0) this._maskBg = this._maskEl.append("rect") .attr("class", style["bg"]) .attr("x", 0) .attr("y", 0) .attr("width", "100%") .attr("height", "100%") .attr("fill", "white") this._bgEl = this._svgEl.append("rect") .attr("class", style["bg"]) .attr("width", "100%") .attr("height", "100%") .attr("x", 0) .attr("y", 0) .attr("mask", "url(#onboarding-mask)") .attr("fill", this._options.fillColor) .attr("fill-opacity", this._options.fillOpacity) this._onWindowResize = debounce(() => { var size = this._getViewSize(); this._svgEl.attr("width", size.width); this._svgEl.attr("height", size.height); if (this._step){ this._onStep(this._step); } }, Defaults.WINDOW_RESIZE_DEBOUNCE_TIME); window.addEventListener("resize", this._onWindowResize); } _clearSteps(){ this._stepElements.forEach(element=>element.remove()); } _renderStep(step){ if (!step.selector) return; var selection = selectAll(step.selector); selection.nodes().forEach(element=>{ this._stepElements.push(this._renderStepElement(element, step)); }); } _renderStepElement(element, step){ var shape = step.shape || { type:"rectangle" }; if (element.tagName == "path"){ return this._renderPathMask(element, step); } else if (shape.type == "circle"){ return this._renderCircleMask(element, step); } else { return this._renderRectangleMask(element, step); } } /** * Returns border radius for given element * @private * @return {number} * @param {HTMLElement} el */ _getBorderRadius(el){ return parseFloat(window.getComputedStyle(el, null).getPropertyValue("border-top-left-radius")); } /** * @private * Returns box for given element * @param {HTMLElement} element * @return {Box} box */ _getBox(element){ var box = element.getBoundingClientRect(); return { top:box.top + + document.body.scrollTop, left:box.left + + document.body.scrollLeft, width: box.width, height:box.height } } _renderRectangleMask(element, step){ var box = this._getBox(element); var borderRadius = this._getBorderRadius(element); var shape = step.shape || {}; var offset = shape.offset || [0,0]; if (step.shape && step.shape.radius){ borderRadius = step.shape.radius; } var stepEl = this._maskEl .append("rect") .attr("fill", "black") .attr("x", box.left + offset[0]) .attr("y", box.top + offset[1]) .attr("rx", borderRadius) .attr("ry", borderRadius) .attr("width", shape.width || box.width) .attr("fill-opacity", 1) .attr("stroke-opacity", 1) .attr("stroke-width", shape.strokeWidth || 0) .attr("stroke", "black") .attr("height", shape.height || box.height) return stepEl; } _renderCircleMask(element, step){ var box = this._getBox(element); var cx = box.left + box.width / 2; var cy = box.top + box.height / 2; var shape = step.shape || {}; var offset = shape.offset || [0,0]; var stepEl = this._maskEl .append("circle") .attr("r", step.shape.radius || box.width / 2) .attr("fill", "black") .attr("fill-opacity", 1) .attr("stroke-width", shape.strokeWidth || 0) .attr("stroke-opacity", 1) .attr("stroke", "black") .attr("cx", cx + offset[0]) .attr("cy", cy + offset[1]) return stepEl; } _renderPathMask(element, step){ var svgElement = element.parentElement; while (svgElement && svgElement.tagName != "svg"){ svgElement = svgElement.parentElement; } var box = this._getBox(svgElement); var stepEl = this._maskEl .append("g") .attr("transform", "translate("+box.left+", "+box.top+")") .append("path") .attr("fill", "black") .attr("fill-opacity", 1) .attr("stroke-width", step.shape?step.shape.strokeWidth||0:0) .attr("stroke", "black") .attr("d", select(element).attr("d")) return stepEl; } _onStart() { this._svgEl.style("display", "block"); } _onStep(step) { this._step = step; this._clearSteps(); this._renderStep(step); } _onStop() { this._step = null; this._svgEl.style("display", "none"); } /** * Destorys MaskRenderer * @return {MaskRenderer} mask renderer instance */ destroy() { if (this._rendered){ window.removeEventListener("resize", this._onWindowResize); this._svgEl.remove(); } this._clearSteps(); this._step = null; this._onStartBinding.destroy(); this._onStepBinding.destroy(); this._onStopBinding.destroy(); return this; } }