UNPKG

atom-nuclide

Version:

A unified developer experience for web and mobile development, built as a suite of features on top of Atom to provide hackability and the support of an active community.

315 lines (268 loc) 13.8 kB
Object.defineProperty(exports, '__esModule', { value: true }); var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } /* * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ var _atom2; function _atom() { return _atom2 = require('atom'); } var _commonsNodeDebounce2; function _commonsNodeDebounce() { return _commonsNodeDebounce2 = _interopRequireDefault(require('../../commons-node/debounce')); } var _commonsNodeString2; function _commonsNodeString() { return _commonsNodeString2 = require('../../commons-node/string'); } var _nuclideAnalytics2; function _nuclideAnalytics() { return _nuclideAnalytics2 = require('../../nuclide-analytics'); } var _nuclideLogging2; function _nuclideLogging() { return _nuclideLogging2 = require('../../nuclide-logging'); } var VALID_NUX_POSITIONS = new Set(['top', 'bottom', 'left', 'right', 'auto']); // The maximum number of times the NuxView will attempt to attach to the DOM. var ATTACHMENT_ATTEMPT_THRESHOLD = 5; var ATTACHMENT_RETRY_TIMEOUT = 500; // milliseconds var RESIZE_EVENT_DEBOUNCE_DURATION = 100; // milliseconds // The frequency with which to poll the element that the NUX is bound to. var POLL_ELEMENT_TIMEOUT = 100; // milliseconds var logger = (0, (_nuclideLogging2 || _nuclideLogging()).getLogger)(); function validatePlacement(position) { return VALID_NUX_POSITIONS.has(position); } var NuxView = (function () { /** * Constructor for the NuxView. * * @param {number} tourId - The ID of the associated NuxTour * @param {?string} selectorString - The query selector to use to find an element on the DOM to attach to. If null, will use `selectorFunction` instead. * @param {?Function} selectorFunction - The function to execute to query an item on the DOM to attach to. If this is null, will use `selectorString` inside a call to `document.querySelector`. * @param {string} position - The position relative to the DOM element that the NUX should show. * @param {string} content - The content to show in the NUX. * @param {?(() => boolean)} completePredicate - Will be used when determining whether * the NUX has been completed/viewed. The NUX will only be completed if this returns true. * If null, the predicate used will always return true. * @param {number} indexInTour - The index of the NuxView in the associated NuxTour * @param {number} tourSize - The number of NuxViews in the associated tour * * @throws Errors if both `selectorString` and `selectorFunction` are null. */ function NuxView(tourId, selectorString, selectorFunction, position, content, completePredicate, indexInTour, tourSize) { if (completePredicate === undefined) completePredicate = null; _classCallCheck(this, NuxView); this._tourId = tourId; if (selectorFunction != null) { this._selector = selectorFunction; } else if (selectorString != null) { this._selector = function () { return document.querySelector(selectorString); }; } else { throw new Error('Either the selector or selectorFunction must be non-null!'); } this._content = content; this._position = validatePlacement(position) ? position : 'auto'; this._completePredicate = completePredicate; this._index = indexInTour; this._finalNuxInTour = indexInTour === tourSize - 1; this._disposables = new (_atom2 || _atom()).CompositeDisposable(); } _createClass(NuxView, [{ key: '_createNux', value: function _createNux() { var _this = this; var creationAttempt = arguments.length <= 0 || arguments[0] === undefined ? 1 : arguments[0]; if (creationAttempt > ATTACHMENT_ATTEMPT_THRESHOLD) { this._onNuxComplete(false); // An error is logged and tracked instead of simply throwing an error since this function // will execute outside of the parent scope's execution and cannot be caught. var error = 'NuxView #' + this._index + ' for NUX#"' + this._tourId + '" ' + 'failed to succesfully attach to the DOM.'; logger.error('ERROR: ' + error); this._track(error, error); return; } var elem = this._selector(); if (elem == null) { var _ret = (function () { var attachmentTimeout = setTimeout(_this._createNux.bind(_this, creationAttempt + 1), ATTACHMENT_RETRY_TIMEOUT); _this._disposables.add(new (_atom2 || _atom()).Disposable(function () { if (attachmentTimeout !== null) { clearTimeout(attachmentTimeout); } })); return { v: undefined }; })(); if (typeof _ret === 'object') return _ret.v; } // A reference to the element we decorate with classes and listeners is retained // for easy cleanup when the NUX is destroyed. this._modifiedElem = elem; this._tooltipDiv = document.createElement('div'); this._tooltipDiv.className = 'nuclide-nux-tooltip-helper'; this._modifiedElem.classList.add('nuclide-nux-tooltip-helper-parent'); this._modifiedElem.appendChild(this._tooltipDiv); this._createDisposableTooltip(); var debouncedWindowResizeListener = (0, (_commonsNodeDebounce2 || _commonsNodeDebounce()).default)(this._handleWindowResize.bind(this), RESIZE_EVENT_DEBOUNCE_DURATION, false); window.addEventListener('resize', debouncedWindowResizeListener); // Destroy the NUX if the element it is bound to is no longer visible. var tryDismissTooltip = function tryDismissTooltip(element) { // ヽ༼ຈل͜ຈ༽/ Yay for documentation! ᕕ( ᐛ )ᕗ // According to https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent, // `offsetParent` returns `null` if the parent or element is hidden. // However, it also returns null if the `position` CSS of the element is // `fixed`. This case requires a much slower operation `getComputedStyle`, // so try and avoid it if possible. var isHidden = undefined; if (element.style.position !== 'fixed') { isHidden = element.offsetParent === null; } else { isHidden = window.getComputedStyle(element).display === 'none'; } if (isHidden) { // Consider the NUX to be dismissed and mark it as completed. _this._handleDisposableClick(false); } }; // The element is polled every `POLL_ELEMENT_TIMEOUT` milliseconds instead // of using a MutationObserver. When an element such as a panel is closed, // it may not mutate but simply be removed from the DOM - a change which // would not be captured by the MutationObserver. var pollElementTimeout = setInterval(tryDismissTooltip.bind(this, elem), POLL_ELEMENT_TIMEOUT); this._disposables.add(new (_atom2 || _atom()).Disposable(function () { if (pollElementTimeout !== null) { clearTimeout(pollElementTimeout); } })); var boundClickListener = this._handleDisposableClick.bind(this, true /* continue to the next NUX in the NuxTour */ ); this._modifiedElem.addEventListener('click', boundClickListener); this._disposables.add(new (_atom2 || _atom()).Disposable(function () { _this._modifiedElem.removeEventListener('click', boundClickListener); window.removeEventListener('resize', debouncedWindowResizeListener); })); } }, { key: '_handleWindowResize', value: function _handleWindowResize() { this._tooltipDisposable.dispose(); this._createDisposableTooltip(); } }, { key: '_createDisposableTooltip', value: function _createDisposableTooltip() { var _this2 = this; var LINK_ENABLED = 'nuclide-nux-link-enabled'; var LINK_DISABLED = 'nuclide-nux-link-disabled'; // Let the link to the next NuxView be enabled iff // a) it is not the last NuxView in the tour AND // b) there is no condition for completion var nextLinkStyle = !this._finalNuxInTour && this._completePredicate == null ? LINK_ENABLED : LINK_DISABLED; // Additionally, the `Next` button may be disabled if an action must be completed. // In this case we show a hint to the user. var nextLinkButton = ' <span\n class="nuclide-nux-link ' + nextLinkStyle + ' nuclide-nux-next-link-' + this._index + '"\n ' + (nextLinkStyle === LINK_DISABLED ? 'title="Interact with the indicated UI element to proceed."' : '') + '>\n Continue\n </span>\n '; // The next NUX in the tour can be created and added before this NUX // has completed its disposal. So, we attach an index to the classname // of the navigation links to specificy which specific NUX the event listener // should be attached to. // Also, we don't show the var content = ' <span class="nuclide-nux-content-container">\n <div class="nuclide-nux-content">\n ' + this._content + '\n </div>\n <div class="nuclide-nux-navigation">\n <span class="nuclide-nux-link ' + LINK_ENABLED + ' nuclide-nux-dismiss-link-' + this._index + '">\n ' + (!this._finalNuxInTour ? 'Dismiss' : 'Complete') + ' Tour\n </span>\n ' + (!this._finalNuxInTour ? nextLinkButton : '') + '\n </div>\n </span>'; this._tooltipDisposable = atom.tooltips.add(this._tooltipDiv, { title: content, trigger: 'manual', placement: this._position, html: true, template: '<div class="tooltip nuclide-nux-tooltip">\n <div class="tooltip-arrow"></div>\n <div class="tooltip-inner"></div>\n </div>' }); this._disposables.add(this._tooltipDisposable); if (nextLinkStyle === LINK_ENABLED) { (function () { var nextElementClickListener = _this2._handleDisposableClick.bind(_this2, true /* continue to the next NUX in the tour */); var nextElement = document.querySelector('.nuclide-nux-next-link-' + _this2._index); nextElement.addEventListener('click', nextElementClickListener); _this2._disposables.add(new (_atom2 || _atom()).Disposable(function () { return nextElement.removeEventListener('click', nextElementClickListener); })); })(); } // Record the NUX as dismissed iff it is not the last NUX in the tour. // Clicking "Complete Tour" on the last NUX should be tracked as succesful completion. var dismissElementClickListener = !this._finalNuxInTour ? this._handleDisposableClick.bind(this, false /* skip to the end of the tour */) : this._handleDisposableClick.bind(this, true /* continue to the next NUX in the tour */); var dismissElement = document.querySelector('.nuclide-nux-dismiss-link-' + this._index); dismissElement.addEventListener('click', dismissElementClickListener); this._disposables.add(new (_atom2 || _atom()).Disposable(function () { return dismissElement.removeEventListener('click', dismissElementClickListener); })); } }, { key: '_handleDisposableClick', value: function _handleDisposableClick() { var success = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; // If a completion predicate exists, only consider the NUX as complete // if the completion condition has been met. // Use `success` to short circuit the check and immediately dispose of the NUX. if (success && this._completePredicate != null && !this._completePredicate()) { return; } // Cleanup changes made to the DOM. this._modifiedElem.classList.remove('nuclide-nux-tooltip-helper-parent'); this._tooltipDiv.remove(); this._onNuxComplete(success); } }, { key: 'showNux', value: function showNux() { this._createNux(); } }, { key: 'setNuxCompleteCallback', value: function setNuxCompleteCallback(callback) { this._callback = callback; } }, { key: '_onNuxComplete', value: function _onNuxComplete() { var success = arguments.length <= 0 || arguments[0] === undefined ? true : arguments[0]; if (this._callback) { this._callback(success); // Avoid the callback being invoked again. this._callback = null; } this.dispose(); return success; } }, { key: 'dispose', value: function dispose() { this._disposables.dispose(); } }, { key: '_track', value: function _track(message, error) { (0, (_nuclideAnalytics2 || _nuclideAnalytics()).track)('nux-view-action', { tourId: this._tourId, message: '' + message, error: (0, (_commonsNodeString2 || _commonsNodeString()).maybeToString)(error) }); } }]); return NuxView; })(); exports.NuxView = NuxView;