UNPKG

@atlaskit/editor-plugin-interactivity

Version:

Interactivity plugin for @atlaskit/editor-core

369 lines (354 loc) 15.9 kB
import _classCallCheck from "@babel/runtime/helpers/classCallCheck"; import _createClass from "@babel/runtime/helpers/createClass"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } import { getDocument } from '@atlaskit/browser-apis'; import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled'; import { EditorEventObserver } from './editor-event-observer'; import { InteractionObserver } from './interaction-observer'; import { InteractivitySession } from './interactivity-session'; import { SCHEMA_VERSION } from './bucket-boundaries'; import { LifecycleObserver } from './lifecycle-observer'; import { LongAnimationFrameObserver } from './long-animation-frame-observer'; import { SnapshotScheduler } from './snapshot-scheduler'; /** * Collects interaction latencies for one editor mount and emits session-to-date snapshots. * * Every interaction is counted in `page`, and the ones with the editor again in one of the editor * groups, which is what tells a slow editor apart from a slow page around it. * * `start` and `stop` bound the collecting, which happens once. Within it there can be several * sessions, because a session covers one document in one mode: a Confluence live page * navigates and switches between reading and editing without ever remounting the editor, and * each of those closes the current session and opens the next. */ export var InteractivityCollector = /*#__PURE__*/function () { function InteractivityCollector(options) { var _this = this; _classCallCheck(this, InteractivityCollector); _defineProperty(this, "started", false); _defineProperty(this, "stopped", false); this.emit = options.emit; this.getEditorDomSize = options.getEditorDomSize; this.getNodeSize = options.getNodeSize; this.getObjectId = options.getObjectId; this.getSessionMode = options.getSessionMode; this.interactionObserver = new InteractionObserver(function (entries) { return _this.recordEntries(entries); }); this.longAnimationFrameObserver = isExperimentEnabled('platform_editor_editor_interactivity_slowest') ? new LongAnimationFrameObserver(function (frames) { return _this.recordFrames(frames); }) : undefined; this.editorEvents = new EditorEventObserver(function (event) { return _this.recordEditorEvent(event); }); this.snapshotScheduler = new SnapshotScheduler(function () { return _this.onTimer(); }); this.lifecycleObserver = new LifecycleObserver({ onHidden: function onHidden() { return _this.onHidden(); }, onVisible: function onVisible() { return _this.onVisible(); }, onPageHide: function onPageHide() { return _this.onPageHide(); }, onPageShow: function onPageShow() { return _this.onPageShow(); } }); this.session = this.createSession(0); } /** * Starts collecting. Calling it again while collecting changes nothing. * * @returns whether collecting is running; `false` when the browser cannot support it, or * when it has already been stopped. */ return _createClass(InteractivityCollector, [{ key: "start", value: function start() { var _this$longAnimationFr; // Collecting is over for good once stopped: the session it covered has been reported. if (this.stopped) { return false; } if (this.started) { return true; } if (!InteractionObserver.isSupported()) { return false; } // Opened again so the session's timings start with the collecting, not with whenever // this object was constructed. this.session = this.createSession(0); this.interactionObserver.start(); (_this$longAnimationFr = this.longAnimationFrameObserver) === null || _this$longAnimationFr === void 0 || _this$longAnimationFr.start(); this.editorEvents.observe(this.editorRoot); this.snapshotScheduler.start(); this.lifecycleObserver.start(); this.started = true; return true; } /** Reports the session as ended by the editor unmounting, and stops collecting. */ }, { key: "stop", value: function stop() { var _this$longAnimationFr2; if (this.stopped) { return; } this.takeSnapshot('unmount'); this.stopped = true; this.interactionObserver.stop(); (_this$longAnimationFr2 = this.longAnimationFrameObserver) === null || _this$longAnimationFr2 === void 0 || _this$longAnimationFr2.stop(); this.editorEvents.stop(); this.snapshotScheduler.stop(); this.lifecycleObserver.stop(); } /** * The element the editor renders itself into is what makes an interaction one of the editor's. * The session is not tied to it: interactions from before it arrives are counted in `page`. */ }, { key: "setEditorRoot", value: function setEditorRoot(root) { this.editorRoot = root !== null && root !== void 0 ? root : undefined; if (this.started && !this.stopped) { this.editorEvents.observe(this.editorRoot); } } /** * Rotates the session when the editor is pointed at different content. * * An unknown object id is no information, never a change. The provider resolves * asynchronously after mount, and `contextIdentifierPlugin` resets its state to the * configured provider on transactions that do not carry a new one, so the id read here goes * missing for a moment on a document that never changed. */ }, { key: "onObjectIdChanged", value: function onObjectIdChanged() { if (this.stopped) { return; } var objectId = this.getObjectId(); if (objectId === undefined || objectId === this.session.objectId) { return; } if (this.session.objectId === undefined) { this.session.objectId = objectId; return; } this.rotateSession('navigation'); } /** * Rotates the session when the editor switches between reading and editing. Interactions * with a read-only page are a different population from interactions while editing, so one * session never covers both. */ }, { key: "onViewModeChanged", value: function onViewModeChanged() { if (this.stopped) { return; } var mode = this.getSessionMode(); if (mode === undefined || mode === this.session.mode) { return; } if (this.session.mode === undefined) { this.session.mode = mode; return; } this.rotateSession('modeChange'); } /** * @param startsAfterInteractionId the highest interaction the previous session saw, so * entries still arriving for it are not counted here as well. */ }, { key: "createSession", value: function createSession(startsAfterInteractionId) { var _getDocument; return new InteractivitySession({ objectId: this.getObjectId(), mode: this.getSessionMode(), hidden: ((_getDocument = getDocument()) === null || _getDocument === void 0 ? void 0 : _getDocument.visibilityState) === 'hidden', startsAfterInteractionId: startsAfterInteractionId }); } /** Reports the current session as ended by `reason` and puts a new one in its place. */ }, { key: "rotateSession", value: function rotateSession(reason) { this.takeSnapshot(reason); this.session = this.createSession(this.session.tracker.lastInteractionId); this.snapshotScheduler.restart(); } }, { key: "onTimer", value: function onTimer() { this.takeSnapshot('timer'); } }, { key: "onHidden", value: function onHidden() { if (this.session.hiddenSince === undefined) { this.session.hiddenSince = performance.now(); } this.takeSnapshot('hidden'); } }, { key: "onVisible", value: function onVisible() { if (this.session.hiddenSince !== undefined) { this.session.hiddenMs += performance.now() - this.session.hiddenSince; this.session.hiddenSince = undefined; } this.session.lifecycleSnapshotEmitted = false; } }, { key: "onPageHide", value: function onPageHide() { this.takeSnapshot('pagehide'); } /** * The page came back from the back/forward cache, which never raises a visibility change. * The session continues, and the signal that suspended it has been reported, so the next * one is due. */ }, { key: "onPageShow", value: function onPageShow() { this.session.lifecycleSnapshotEmitted = false; } /** * Counts an interaction with the editor, including the ones Event Timing never reports because * they were faster than its threshold — which is why a count alone moves the session on. */ }, { key: "recordEditorEvent", value: function recordEditorEvent(event) { if (this.stopped) { return; } // An event a script dispatched is no interaction: the browser counts none of them either. if (!event.isTrusted) { return; } var group = this.session.tracker.recordEditorEvent(event); if (!group) { return; } // The session names its editor groups after the fields they are reported in. this.session[group].countTotal(); this.session.revision += 1; } /** Long Animation Frames say where the latency of a slow interaction went. */ }, { key: "recordFrames", value: function recordFrames(frames) { var _this$session$slowest; if (this.stopped) { return; } if ((_this$session$slowest = this.session.slowest) !== null && _this$session$slowest !== void 0 && _this$session$slowest.trackLongAnimationFrames(frames)) { this.session.revision += 1; } } }, { key: "recordEntries", value: function recordEntries(entries) { var _iterator = _createForOfIteratorHelper(entries), _step; try { for (_iterator.s(); !(_step = _iterator.n()).done;) { var entry = _step.value; var _iterator2 = _createForOfIteratorHelper(this.session.tracker.merge(entry)), _step2; try { for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { var _this$session$slowest2; var update = _step2.value; var groupsChanged = this.session.page.trackInteractionUpdate(update); if (update.group) { groupsChanged = this.session[update.group].trackInteractionUpdate(update) || groupsChanged; } var slowestChanged = (_this$session$slowest2 = this.session.slowest) === null || _this$session$slowest2 === void 0 ? void 0 : _this$session$slowest2.trackInteractionUpdate(entry, update); if (groupsChanged || slowestChanged) { this.session.revision += 1; } } } catch (err) { _iterator2.e(err); } finally { _iterator2.f(); } } } catch (err) { _iterator.e(err); } finally { _iterator.f(); } } }, { key: "takeSnapshot", value: function takeSnapshot(reason) { var _this$longAnimationFr3, _session$slowest; if (this.stopped) { return; } var session = this.session; // Checked before anything is consumed: `visibilitychange` and `pagehide` fire back to // back on the same transition and it is reported once, and draining first would take // entries out of the browser's queue only to suppress the snapshot carrying them. var lifecycleSignal = reason === 'hidden' || reason === 'pagehide'; if (lifecycleSignal && session.lifecycleSnapshotEmitted) { return; } // Must run before the change check: the browser may be holding entries and frames that have // not reached the observer callbacks yet, and on `pagehide` there is no later chance to // pick them up. Entries first, so a record the frames answer for exists by then. this.interactionObserver.drain(); (_this$longAnimationFr3 = this.longAnimationFrameObserver) === null || _this$longAnimationFr3 === void 0 || _this$longAnimationFr3.drain(); if (session.revision === session.emittedRevision) { return; } if (lifecycleSignal) { session.lifecycleSnapshotEmitted = true; } session.seq += 1; session.emittedRevision = session.revision; var now = performance.now(); var hiddenMs = session.hiddenMs + (session.hiddenSince === undefined ? 0 : now - session.hiddenSince); var pageTotalCount = InteractionObserver.readPageInteractionCount() - session.interactionCountAtStart; var slowest = (_session$slowest = session.slowest) === null || _session$slowest === void 0 ? void 0 : _session$slowest.snapshot(); this.emit(_objectSpread({ schema: SCHEMA_VERSION, interactivitySessionId: session.id, objectId: session.objectId, seq: session.seq, reason: reason, sessionMode: session.mode, activeMs: Math.round(Math.max(0, now - session.startedAt - hiddenMs)), hiddenMs: Math.round(hiddenMs), nodeSize: this.getNodeSize(), editorDomSize: this.getEditorDomSize(), // Only `page` is told its total; each editor group has counted its own. page: session.page.snapshot(pageTotalCount), editorTyping: session.editorTyping.snapshot(), editorPointer: session.editorPointer.snapshot(), editorOther: session.editorOther.snapshot() }, slowest && { slowest: slowest })); } }]); }();