From e07efde75d91a959eabbb29a0c35fc36852165bc Mon Sep 17 00:00:00 2001 From: spalger Date: Sun, 11 Dec 2016 15:24:11 -0700 Subject: [PATCH] [ui/resize_checker] extract from vislib and use resize-observer polyfill --- package.json | 1 + .../console/public/src/sense_editor_resize.js | 2 +- .../__tests__/reflow_watcher.js | 81 ------- .../public/reflow_watcher/reflow_watcher.js | 67 ------ .../__tests__/resize_checker.js | 145 +++++++++++++ src/ui/public/resize_checker/index.js | 1 + .../public/resize_checker/resize_checker.js | 94 ++++++++ src/ui/public/utils/__tests__/sequencer.js | 100 --------- src/ui/public/utils/sequencer.js | 93 -------- .../vislib/__tests__/lib/resize_checker.js | 203 ------------------ src/ui/public/vislib/lib/resize_checker.js | 203 ------------------ src/ui/public/vislib/vis.js | 12 +- 12 files changed, 247 insertions(+), 755 deletions(-) delete mode 100644 src/ui/public/reflow_watcher/__tests__/reflow_watcher.js delete mode 100644 src/ui/public/reflow_watcher/reflow_watcher.js create mode 100644 src/ui/public/resize_checker/__tests__/resize_checker.js create mode 100644 src/ui/public/resize_checker/index.js create mode 100644 src/ui/public/resize_checker/resize_checker.js delete mode 100644 src/ui/public/utils/__tests__/sequencer.js delete mode 100644 src/ui/public/utils/sequencer.js delete mode 100644 src/ui/public/vislib/__tests__/lib/resize_checker.js delete mode 100644 src/ui/public/vislib/lib/resize_checker.js diff --git a/package.json b/package.json index 407db4520f232..4c2bab9110dc8 100644 --- a/package.json +++ b/package.json @@ -149,6 +149,7 @@ "querystring-browser": "1.0.4", "raw-loader": "0.5.1", "request": "2.61.0", + "resize-observer-polyfill": "1.2.1", "rimraf": "2.4.3", "rison-node": "1.0.0", "rjs-repack-loader": "1.0.6", diff --git a/src/core_plugins/console/public/src/sense_editor_resize.js b/src/core_plugins/console/public/src/sense_editor_resize.js index ef989c1b63493..3644c6aef1793 100644 --- a/src/core_plugins/console/public/src/sense_editor_resize.js +++ b/src/core_plugins/console/public/src/sense_editor_resize.js @@ -1,4 +1,4 @@ -import ResizeCheckerProvider from 'ui/vislib/lib/resize_checker' +import { ResizeCheckerProvider } from 'ui/resize_checker' export function useResizeCheckerProvider(Private) { const ResizeChecker = Private(ResizeCheckerProvider); diff --git a/src/ui/public/reflow_watcher/__tests__/reflow_watcher.js b/src/ui/public/reflow_watcher/__tests__/reflow_watcher.js deleted file mode 100644 index 1b1f8bafba505..0000000000000 --- a/src/ui/public/reflow_watcher/__tests__/reflow_watcher.js +++ /dev/null @@ -1,81 +0,0 @@ -import 'angular'; -import $ from 'jquery'; -import _ from 'lodash'; -import expect from 'expect.js'; -import sinon from 'auto-release-sinon'; -import ngMock from 'ng_mock'; -import EventsProvider from 'ui/events'; -import ReflowWatcherProvider from 'ui/reflow_watcher'; -describe('Reflow watcher', function () { - - const $body = $(document.body); - const $window = $(window); - const expectStubbedEventAndEl = function (stub, event, $el) { - expect(stub.getCalls().some(function (call) { - const events = call.args[0].split(' '); - return _.contains(events, event) && $el.is(call.thisValue); - })).to.be(true); - }; - - let EventEmitter; - let reflowWatcher; - let $rootScope; - let $onStub; - - beforeEach(ngMock.module('kibana', function () { - // stub jQuery's $.on method while creating the reflowWatcher - $onStub = sinon.stub($.fn, 'on'); - })); - beforeEach(ngMock.inject(function (Private, $injector) { - $rootScope = $injector.get('$rootScope'); - EventEmitter = Private(EventsProvider); - reflowWatcher = Private(ReflowWatcherProvider); - // setup the reflowWatchers $http watcher - $rootScope.$apply(); - })); - afterEach(function () { - $onStub.restore(); - }); - - it('is an event emitter', function () { - expect(reflowWatcher).to.be.an(EventEmitter); - }); - - describe('listens', function () { - it('to "mouseup" on the body', function () { - expectStubbedEventAndEl($onStub, 'mouseup', $body); - }); - - it('to "resize" on the window', function () { - expectStubbedEventAndEl($onStub, 'resize', $window); - }); - }); - - describe('un-listens in #destroy()', function () { - let $offStub; - - beforeEach(function () { - $offStub = sinon.stub($.fn, 'off'); - reflowWatcher.destroy(); - $offStub.restore(); - }); - - it('to "mouseup" on the body', function () { - expectStubbedEventAndEl($offStub, 'mouseup', $body); - }); - - it('to "resize" on the window', function () { - expectStubbedEventAndEl($offStub, 'resize', $window); - }); - }); - - it('triggers the "reflow" event within a new angular tick', function () { - const stub = sinon.stub(); - reflowWatcher.on('reflow', stub); - reflowWatcher.trigger(); - - expect(stub).to.have.property('callCount', 0); - $rootScope.$apply(); - expect(stub).to.have.property('callCount', 1); - }); -}); diff --git a/src/ui/public/reflow_watcher/reflow_watcher.js b/src/ui/public/reflow_watcher/reflow_watcher.js deleted file mode 100644 index 43b052a5bafc8..0000000000000 --- a/src/ui/public/reflow_watcher/reflow_watcher.js +++ /dev/null @@ -1,67 +0,0 @@ -import angular from 'angular'; -import $ from 'jquery'; -import _ from 'lodash'; -import EventsProvider from 'ui/events'; -export default function ReflowWatcherService(Private, $rootScope, $http) { - - const EventEmitter = Private(EventsProvider); - const $body = $(document.body); - const $window = $(window); - - const MOUSE_EVENTS = 'mouseup'; - const WINDOW_EVENTS = 'resize'; - - _.class(ReflowWatcher).inherits(EventEmitter); - /** - * Watches global activity which might hint at a change in the content, which - * in turn provides a hint to resizers that they should check their size - */ - function ReflowWatcher() { - ReflowWatcher.Super.call(this); - - // bound version of trigger that can be used as a handler - this.trigger = _.bind(this.trigger, this); - this._emitReflow = _.bind(this._emitReflow, this); - - // list of functions to call that will unbind our watchers - this._unwatchers = [ - $rootScope.$watchCollection(function () { - return $http.pendingRequests; - }, this.trigger) - ]; - - $body.on(MOUSE_EVENTS, this.trigger); - $window.on(WINDOW_EVENTS, this.trigger); - } - - /** - * Simply emit reflow, but in a way that can be bound and passed to - * other functions. Using _.bind caused extra arguments to be added, and - * then emitted to other places. No Bueno - * - * @return {void} - */ - ReflowWatcher.prototype._emitReflow = function () { - this.emit('reflow'); - }; - - /** - * Emit the "reflow" event in the next tick of the digest cycle - * @return {void} - */ - ReflowWatcher.prototype.trigger = function () { - $rootScope.$evalAsync(this._emitReflow); - }; - - /** - * Signal to the ReflowWatcher that it should clean up it's listeners - * @return {void} - */ - ReflowWatcher.prototype.destroy = function () { - $body.off(MOUSE_EVENTS, this.trigger); - $window.off(WINDOW_EVENTS, this.trigger); - _.callEach(this._unwatchers); - }; - - return new ReflowWatcher(); -} diff --git a/src/ui/public/resize_checker/__tests__/resize_checker.js b/src/ui/public/resize_checker/__tests__/resize_checker.js new file mode 100644 index 0000000000000..80c8f7e6726df --- /dev/null +++ b/src/ui/public/resize_checker/__tests__/resize_checker.js @@ -0,0 +1,145 @@ +import $ from 'jquery'; +import { delay, fromNode } from 'bluebird'; +import expect from 'expect.js'; +import sinon from 'auto-release-sinon'; + +import ngMock from 'ng_mock'; +import EventsProvider from 'ui/events'; +import NoDigestPromises from 'test_utils/no_digest_promises'; + +import { ResizeCheckerProvider } from '../resize_checker'; + +describe('Resize Checker', () => { + NoDigestPromises.activateForSuite(); + + const teardown = []; + let setup; + + beforeEach(ngMock.module('kibana')); + beforeEach(ngMock.inject(($injector) => { + setup = () => { + const Private = $injector.get('Private'); + const ResizeChecker = Private(ResizeCheckerProvider); + const EventEmitter = Private(EventsProvider); + + const createEl = () => { + const el = $('
').appendTo('body').get(0); + teardown.push(() => $(el).remove()); + return el; + }; + + const createChecker = el => { + const checker = new ResizeChecker(el); + teardown.push(() => checker.destroy()); + return checker; + }; + + const createListener = () => { + let resolveFirstCallPromise; + const listener = sinon.spy(() => resolveFirstCallPromise()); + listener.firstCallPromise = new Promise(resolve => (resolveFirstCallPromise = resolve)); + return listener; + }; + + return { EventEmitter, createEl, createChecker, createListener }; + }; + })); + + afterEach(() => { + teardown.splice(0).forEach(fn => { + fn(); + }); + }); + + describe('contruction', () => { + it('accepts a jQuery wrapped element', () => { + const { createChecker } = setup(); + + createChecker($('
')); + }); + }); + + describe('events', () => { + it('is an event emitter', () => { + const { createEl, createChecker, EventEmitter } = setup(); + + const checker = createChecker(createEl()); + expect(checker).to.be.a(EventEmitter); + }); + + it('emits a "resize" event asynchronously', async () => { + const { createEl, createChecker, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + $(el).height(100); + sinon.assert.notCalled(listener); + await listener.firstCallPromise; + sinon.assert.calledOnce(listener); + }); + }); + + describe('#modifySizeWithoutTriggeringResize()', () => { + it(`does not emit "resize" events caused by the block`, async () => { + const { createChecker, createEl, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + checker.modifySizeWithoutTriggeringResize(() => { + $(el).height(100); + }); + await delay(1000); + sinon.assert.notCalled(listener); + }); + + it('does emit "resize" when modification is made between the block and resize notification', async () => { + const { createChecker, createEl, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + checker.modifySizeWithoutTriggeringResize(() => { + $(el).height(100); + }); + sinon.assert.notCalled(listener); + $(el).height(200); + await listener.firstCallPromise; + sinon.assert.calledOnce(listener); + }); + }); + + describe('#destroy()', () => { + it('destroys internal observer instance', () => { + const { createChecker, createEl, createListener } = setup(); + + const checker = createChecker(createEl()); + const listener = createListener(); + + checker.destroy(); + expect(!checker._observer).to.be(true); + }); + + it('does not emit future resize events', async () => { + const { createChecker, createEl, createListener } = setup(); + + const el = createEl(); + const checker = createChecker(el); + const listener = createListener(); + + checker.on('resize', listener); + checker.destroy(); + + $(el).height(100); + await delay(1000); + sinon.assert.notCalled(listener); + }); + }); +}); diff --git a/src/ui/public/resize_checker/index.js b/src/ui/public/resize_checker/index.js new file mode 100644 index 0000000000000..fbc47d1f9afd7 --- /dev/null +++ b/src/ui/public/resize_checker/index.js @@ -0,0 +1 @@ +export { ResizeCheckerProvider } from './resize_checker'; diff --git a/src/ui/public/resize_checker/resize_checker.js b/src/ui/public/resize_checker/resize_checker.js new file mode 100644 index 0000000000000..df6d7009f8825 --- /dev/null +++ b/src/ui/public/resize_checker/resize_checker.js @@ -0,0 +1,94 @@ +import $ from 'jquery'; +import ResizeObserver from 'resize-observer-polyfill'; +import { uniqueId, isEqual } from 'lodash'; + +import EventsProvider from 'ui/events'; + +export function ResizeCheckerProvider(Private) { + const EventEmitter = Private(EventsProvider); + + function validateElArg(el) { + // the ResizeChecker historically accepted jquery elements, + // so we wrap in jQuery then extract the element + const $el = $(el); + + if ($el.size() !== 1) { + throw new TypeError('ResizeChecker must be constructed with a single DOM element.'); + } + + return $el.get(0); + } + + function getSize(el) { + return [el.clientWidth, el.clientHeight]; + } + + /** + * ResizeChecker receives an element and emits a "resize" + * event every time it changes size. Used by the vislib to re-render + * visualizations on resize as well as the console for the + * same reason, but for the editors. + */ + return class ResizeChecker extends EventEmitter { + constructor(el) { + super(); + + this._el = validateElArg(el); + + // the width and height of the element that we expect to see + // on the next resize notification. If it matches the size at + // the time of the notifications then it we will be ignored. + this._expectedSize = getSize(this._el); + + this._observer = new ResizeObserver(() => { + if (this._expectedSize) { + const sameSize = isEqual(getSize(this._el), this._expectedSize); + this._expectedSize = null; + + if (sameSize) { + // don't trigger resize notification if the size is what we expect + return; + } + } + + this.emit('resize'); + }); + + this._observer.observe(this._el); + } + + /** + * Run a function and ignore all resizes that occur + * while it's running. + * + * @return {undefined} + */ + modifySizeWithoutTriggeringResize(block) { + try { + block(); + } finally { + this._expectedSize = getSize(this._el); + } + } + + /** + * Tell the ResizeChecker to shutdown, stop listenings, and never + * emit another resize event. + * + * Cleans up it's listeners and timers. + * + * @method destroy + * @return {void} + */ + destroy() { + if (this._destroyed) return; + this._destroyed = true; + + this._observer.disconnect(); + this._observer = null; + this._expectedSize = null; + this._el = null; + this.removeAllListeners(); + } + }; +} diff --git a/src/ui/public/utils/__tests__/sequencer.js b/src/ui/public/utils/__tests__/sequencer.js deleted file mode 100644 index 3a163ff6ad76e..0000000000000 --- a/src/ui/public/utils/__tests__/sequencer.js +++ /dev/null @@ -1,100 +0,0 @@ -import _ from 'lodash'; -import sequencer from 'ui/utils/sequencer'; -import expect from 'expect.js'; -describe('sequencer util', function () { - - const opts = [ - { min: 500, max: 7500, length: 1500 }, - { min: 50, max: 500, length: 1000 }, - { min: 5, max: 50, length: 100 } - ]; - - function eachSeqFor(method, fn) { - opts.forEach(function (args) { - fn(method(args.min, args.max, args.length), args); - }); - } - - function getSlopes(seq, count) { - return _.chunk(seq, Math.ceil(seq.length / count)).map(function (chunk) { - return (_.last(chunk) - _.first(chunk)) / chunk.length; - }); - } - - // using expect() here causes massive GC runs because seq can be +1000 elements - function expectedChange(seq, up) { - up = !!up; - - if (seq.length < 2) { - throw new Error('unable to reach change without at least two elements'); - } - - seq.forEach(function (n, i) { - if (i > 0 && (seq[i - 1] < n) !== up) { - throw new Error('expected values to ' + (up ? 'increase' : 'decrease')); - } - }); - } - - function generalTests(seq, args) { - it('obeys the min arg', function () { - expect(Math.min.apply(Math, seq)).to.be(args.min); - }); - - it('obeys the max arg', function () { - expect(Math.max.apply(Math, seq)).to.be(args.max); - }); - - it('obeys the length arg', function () { - expect(seq).to.have.length(args.length); - }); - - it('always creates increasingly larger values', function () { - expectedChange(seq, true); - }); - } - - describe('#createEaseIn', function () { - eachSeqFor(sequencer.createEaseIn, function (seq, args) { - describe('with args: ' + JSON.stringify(args), function () { - generalTests(seq, args); - - it('produces increasing slopes', function () { - expectedChange(getSlopes(seq, 2), true); - expectedChange(getSlopes(seq, 4), true); - expectedChange(getSlopes(seq, 6), true); - }); - }); - }); - }); - - describe('#createEaseOut', function () { - eachSeqFor(sequencer.createEaseOut, function (seq, args) { - describe('with args: ' + JSON.stringify(args), function () { - generalTests(seq, args); - - it('produces decreasing slopes', function () { - expectedChange(getSlopes(seq, 2), false); - expectedChange(getSlopes(seq, 4), false); - expectedChange(getSlopes(seq, 6), false); - }); - - // Flipped version of previous test to ensure that expectedChange() - // and friends are behaving properly - it('doesn\'t produce increasing slopes', function () { - expect(function () { - expectedChange(getSlopes(seq, 2), true); - }).to.throwError(); - - expect(function () { - expectedChange(getSlopes(seq, 4), true); - }).to.throwError(); - - expect(function () { - expectedChange(getSlopes(seq, 6), true); - }).to.throwError(); - }); - }); - }); - }); -}); diff --git a/src/ui/public/utils/sequencer.js b/src/ui/public/utils/sequencer.js deleted file mode 100644 index 5783d1dcb9c08..0000000000000 --- a/src/ui/public/utils/sequencer.js +++ /dev/null @@ -1,93 +0,0 @@ -import _ from 'lodash'; - -function create(min, max, length, mod) { - const seq = new Array(length); - - const valueDist = max - min; - - // range of values that the mod creates - const modRange = [mod(0, length), mod(length - 1, length)]; - - // distance between - const modRangeDist = modRange[1] - modRange[0]; - - _.times(length, function (i) { - const modIPercent = (mod(i, length) - modRange[0]) / modRangeDist; - - // percent applied to distance and added to min to - // produce value - seq[i] = min + (valueDist * modIPercent); - }); - - seq.min = min; - seq.max = max; - - return seq; -} - -export default { - /** - * Create an exponential sequence of numbers. - * - * Creates a curve resembling: - * - * ; - * / - * / - * .-' - * _.-" - * _.-'" - * _,.-'" - * _,..-'" - * _,..-'"" - * _,..-'"" - * ____,..--'"" - * - * @param {number} min - the min value to produce - * @param {number} max - the max value to produce - * @param {number} length - the number of values to produce - * @return {number[]} - an array containing the sequence - */ - createEaseIn: _.partialRight(create, function (i, length) { - // generates numbers from 1 to +Infinity - return i * Math.pow(i, 1.1111); - }), - - /** - * Create an sequence of numbers using sine. - * - * Create a curve resembling: - * - * ____,..--'"" - * _,..-'"" - * _,..-'"" - * _,..-'" - * _,.-'" - * _.-'" - * _.-" - * .-' - * / - * / - * ; - * - * - * @param {number} min - the min value to produce - * @param {number} max - the max value to produce - * @param {number} length - the number of values to produce - * @return {number[]} - an array containing the sequence - */ - createEaseOut: _.partialRight(create, function (i, length) { - // adapted from output of http://www.timotheegroleau.com/Flash/experiments/easing_function_generator.htm - // generates numbers from 0 to 100 - - const ts = (i /= length) * i; - const tc = ts * i; - return 100 * ( - 0.5 * tc * ts + - -3 * ts * ts + - 6.5 * tc + - -7 * ts + - 4 * i - ); - }) -}; diff --git a/src/ui/public/vislib/__tests__/lib/resize_checker.js b/src/ui/public/vislib/__tests__/lib/resize_checker.js deleted file mode 100644 index bafaef16eaf2d..0000000000000 --- a/src/ui/public/vislib/__tests__/lib/resize_checker.js +++ /dev/null @@ -1,203 +0,0 @@ -import $ from 'jquery'; -import _ from 'lodash'; -import Promise from 'bluebird'; -import ngMock from 'ng_mock'; -import expect from 'expect.js'; -import sinon from 'auto-release-sinon'; -import VislibLibResizeCheckerProvider from 'ui/vislib/lib/resize_checker'; -import EventsProvider from 'ui/events'; -import ReflowWatcherProvider from 'ui/reflow_watcher'; - -describe('Vislib Resize Checker', function () { - - require('test_utils/no_digest_promises').activateForSuite(); - - let ResizeChecker; - let EventEmitter; - let checker; - let reflowWatcher; - const reflowSpies = {}; - - beforeEach(ngMock.module('kibana')); - - beforeEach(ngMock.inject(function (Private) { - ResizeChecker = Private(VislibLibResizeCheckerProvider); - EventEmitter = Private(EventsProvider); - reflowWatcher = Private(ReflowWatcherProvider); - reflowSpies.on = sinon.spy(reflowWatcher, 'on'); - reflowSpies.off = sinon.spy(reflowWatcher, 'off'); - - const $el = $(document.createElement('div')) - .appendTo('body') - .css('visibility', 'hidden') - .get(0); - - checker = new ResizeChecker($el); - })); - - afterEach(function () { - checker.$el.remove(); - checker.destroy(); - }); - - describe('basic functionality', function () { - it('is an event emitter', function () { - expect(checker).to.be.a(EventEmitter); - }); - - it('listens for the "reflow" event of the reflowWatchers', function () { - expect(reflowSpies.on).to.have.property('callCount', 1); - const call = reflowSpies.on.getCall(0); - expect(call.args[0]).to.be('reflow'); - }); - - it('emits a "resize" event when the el is resized', function (done) { - checker.on('resize', function () { - done(); - }); - - checker.$el.text('haz contents'); - checker.check(); - }); - }); - - describe('#read', function () { - it('gets the proper dimensions for the element', function () { - const dimensions = checker.read(); - const windowWidth = document.documentElement.clientWidth; - - expect(dimensions.w).to.equal(windowWidth); - expect(dimensions.h).to.equal(0); - }); - }); - - describe('#saveSize', function () { - it('calls #read() when no arg is passed', function () { - const stub = sinon.stub(checker, 'read').returns({}); - - checker.saveSize(); - - expect(stub).to.have.property('callCount', 1); - }); - - it('saves the size of the element', function () { - const football = {}; - checker.saveSize(football); - expect(checker).to.have.property('_savedSize', football); - }); - - it('returns false if the size matches the previous value', function () { - expect(checker.saveSize(checker._savedSize)).to.be(false); - }); - - it('returns true if the size is different than previous value', function () { - expect(checker.saveSize({})).to.be(true); - }); - }); - - describe('#check()', function () { - let emit; - - beforeEach(function () { - emit = sinon.stub(checker, 'emit'); - - // prevent the checker from auto-checking - checker.destroy(); - checker.startSchedule = checker.continueSchedule = _.noop; - }); - - it('does not emit "resize" immediately after a resize, but waits for changes to stop', function () { - expect(checker).to.have.property('_isDirty', false); - - checker.$el.css('height', 100); - checker.check(); - - expect(checker).to.have.property('_isDirty', true); - expect(emit).to.have.property('callCount', 0); - - // no change in el size - checker.check(); - - expect(checker).to.have.property('_isDirty', false); - expect(emit).to.have.property('callCount', 1); - }); - - it('emits "resize" based on MS_MAX_RESIZE_DELAY, even if el\'s constantly changing size', function () { - const steps = _.random(5, 10); - this.slow(steps * 10); - - // we are going to fake the delay using the fake clock - const msStep = Math.floor(ResizeChecker.MS_MAX_RESIZE_DELAY / (steps - 1)); - const clock = sinon.useFakeTimers(); - - _.times(steps, function step(i) { - checker.$el.css('height', 100 + i); - checker.check(); - - expect(checker).to.have.property('_isDirty', true); - expect(emit).to.have.property('callCount', i > steps ? 1 : 0); - - clock.tick(msStep); // move the clock forward one step - }); - - }); - }); - - describe('#destroy()', function () { - it('removes the "reflow" event from the reflowWatcher', function () { - const onCall = reflowSpies.on.getCall(0); - const handler = onCall.args[1]; - - checker.destroy(); - expect(reflowSpies.off).to.have.property('callCount', 1); - expect(reflowSpies.off.calledWith('reflow', handler)).to.be.ok(); - }); - - it('clears the timeout', function () { - const spy = sinon.spy(window, 'clearTimeout'); - checker.destroy(); - expect(spy).to.have.property('callCount', 1); - }); - }); - - describe('scheduling', function () { - let clock; - let schedule; - - beforeEach(function () { - // prevent the checker from running automatically - checker.destroy(); - clock = sinon.useFakeTimers(); - - schedule = []; - _.times(25, function () { - schedule.push(_.random(3, 250)); - }); - }); - - it('walks the schedule, using each value as it\'s next timeout', function () { - let timerId = checker.startSchedule(schedule); - - // start at 0 even though "start" used the first slot, we will still check it - for (let i = 0; i < schedule.length; i++) { - expect(clock.timers[timerId]).to.have.property('callAt', schedule[i]); - timerId = checker.continueSchedule(); - } - }); - - it('repeats the last value in the schedule', function () { - let timerId = checker.startSchedule(schedule); - - // start at 1, and go until there is one left - for (let i = 1; i < schedule.length - 1; i++) { - timerId = checker.continueSchedule(); - } - - const last = _.last(schedule); - _.times(5, function () { - const timer = clock.timers[checker.continueSchedule()]; - expect(timer).to.have.property('callAt', last); - }); - }); - }); -}); diff --git a/src/ui/public/vislib/lib/resize_checker.js b/src/ui/public/vislib/lib/resize_checker.js deleted file mode 100644 index 69fcf343591a9..0000000000000 --- a/src/ui/public/vislib/lib/resize_checker.js +++ /dev/null @@ -1,203 +0,0 @@ -import $ from 'jquery'; -import _ from 'lodash'; -import sequencer from 'ui/utils/sequencer'; -import EventsProvider from 'ui/events'; -import ReflowWatcherProvider from 'ui/reflow_watcher'; -export default function ResizeCheckerFactory(Private, Notifier) { - - const EventEmitter = Private(EventsProvider); - const reflowWatcher = Private(ReflowWatcherProvider); - - const SCHEDULE = ResizeChecker.SCHEDULE = sequencer.createEaseIn( - 100, // shortest delay - 10000, // longest delay - 50 // tick count - ); - - // maximum ms that we can delay emitting 'resize'. This is only used - // to debounce resizes when the size of the element is constantly changing - const MS_MAX_RESIZE_DELAY = ResizeChecker.MS_MAX_RESIZE_DELAY = 500; - - /** - * Checks the size of an element on a regular basis. Provides - * an event that is emited when the element has changed size. - * - * @class ResizeChecker - * @param {HtmlElement} el - the element to track the size of - */ - _.class(ResizeChecker).inherits(EventEmitter); - function ResizeChecker(el) { - ResizeChecker.Super.call(this); - - this.$el = $(el); - this.notify = new Notifier({ location: 'Vislib ResizeChecker ' + _.uniqueId() }); - - this.saveSize(); - - this.check = _.bind(this.check, this); - this.check(); - - this.onReflow = _.bind(this.onReflow, this); - reflowWatcher.on('reflow', this.onReflow); - } - - ResizeChecker.prototype.onReflow = function () { - this.startSchedule(SCHEDULE); - }; - - /** - * Read the size of the element - * - * @method read - * @return {object} - an object with keys `w` (width) and `h` (height) - */ - ResizeChecker.prototype.read = function () { - return { - w: this.$el[0].clientWidth, - h: this.$el[0].clientHeight - }; - }; - - - /** - * Save the element size, preventing it from being considered as an - * update. - * - * @method save - * @param {object} [size] - optional size to save, otherwise #read() is called - * @return {boolean} - true if their was a change in the new - */ - ResizeChecker.prototype.saveSize = function (size) { - if (!size) size = this.read(); - - if (this._equalsSavedSize(size)) { - return false; - } - - this._savedSize = size; - return true; - }; - - - /** - * Determine if a given size matches the currently saved size. - * - * @private - * @method _equalsSavedSize - * @param {object} a - an object that matches the return value of #read() - * @return {boolean} - true if the passed in value matches the saved size - */ - ResizeChecker.prototype._equalsSavedSize = function (a) { - const b = this._savedSize || {}; - return a.w === b.w && a.h === b.h; - }; - - /** - * Read the time that the dirty state last changed. - * - * @method lastDirtyChange - * @return {timestamp} - the unix timestamp (in ms) of the last update - * to the dirty state - */ - ResizeChecker.prototype.lastDirtyChange = function () { - return this._dirtyChangeStamp; - }; - - /** - * Record the dirty state - * - * @method saveDirty - * @param {boolean} val - * @return {boolean} - true if the dirty state changed by this save - */ - ResizeChecker.prototype.saveDirty = function (val) { - val = !!val; - - if (val === this._isDirty) return false; - - this._isDirty = val; - this._dirtyChangeStamp = Date.now(); - return true; - }; - - /** - * The check routine that executes regularly and will reschedule itself - * to run again in the future. It determines the state of the elements - * size and decides when to emit the "update" event. - * - * @method check - * @return {void} - */ - ResizeChecker.prototype.check = function () { - const newSize = this.read(); - const dirty = this.saveSize(newSize); - const dirtyChanged = this.saveDirty(dirty); - - const doneDirty = !dirty && dirtyChanged; - const muchDirty = dirty && (this.lastDirtyChange() - Date.now() > MS_MAX_RESIZE_DELAY); - if (doneDirty || muchDirty) { - this.emit('resize', newSize); - } - - // if the dirty state is unchanged, continue using the previous schedule - if (!dirtyChanged) { - return this.continueSchedule(); - } - - return this.startSchedule(SCHEDULE); - }; - - /** - * Start running a new schedule, using one of the SCHEDULE_* constants. - * - * @method startSchedule - * @param {integer[]} schedule - an array of millisecond times that should - * be used to schedule calls to #check(); - * @return {integer} - the id of the next timer - */ - ResizeChecker.prototype.startSchedule = function (schedule) { - this._tick = -1; - this._currentSchedule = schedule; - return this.continueSchedule(); - }; - - /** - * Continue running the current schedule. MUST BE CALLED AFTER #startSchedule() - * - * @method continueSchedule - * @return {integer} - the id of the next timer - */ - ResizeChecker.prototype.continueSchedule = function () { - clearTimeout(this._timerId); - - if (this._tick < this._currentSchedule.length - 1) { - // at the end of the schedule, don't progress any further but repeat the last value - this._tick += 1; - } - - const check = this.check; // already bound - const ms = this._currentSchedule[this._tick]; - return (this._timerId = setTimeout(function () { - check(); - }, ms)); - }; - - ResizeChecker.prototype.stopSchedule = function () { - clearTimeout(this._timerId); - }; - - /** - * Signal that the ResizeChecker should shutdown. - * - * Cleans up it's listeners and timers. - * - * @method destroy - * @return {void} - */ - ResizeChecker.prototype.destroy = function () { - reflowWatcher.off('reflow', this.onReflow); - clearTimeout(this._timerId); - }; - - return ResizeChecker; -} diff --git a/src/ui/public/vislib/vis.js b/src/ui/public/vislib/vis.js index f32326882665a..5972dc6215056 100644 --- a/src/ui/public/vislib/vis.js +++ b/src/ui/public/vislib/vis.js @@ -3,13 +3,13 @@ import d3 from 'd3'; import Binder from 'ui/binder'; import errors from 'ui/errors'; import EventsProvider from 'ui/events'; +import { ResizeCheckerProvider } from 'ui/resize_checker'; import './styles/main.less'; -import VislibLibResizeCheckerProvider from './lib/resize_checker'; import VisConifgProvider from './lib/vis_config'; import VisHandlerProvider from './lib/handler'; export default function VisFactory(Private) { - const ResizeChecker = Private(VislibLibResizeCheckerProvider); + const ResizeChecker = Private(ResizeCheckerProvider); const Events = Private(EventsProvider); const VisConfig = Private(VisConifgProvider); const Handler = Private(VisHandlerProvider); @@ -90,11 +90,9 @@ export default function VisFactory(Private) { } _runWithoutResizeChecker(method) { - this.resizeChecker.stopSchedule(); - this._runOnHandler(method); - this.resizeChecker.saveSize(); - this.resizeChecker.saveDirty(false); - this.resizeChecker.continueSchedule(); + this.resizeChecker.modifySizeWithoutTriggeringResize(() => { + this._runOnHandler(method); + }); } _runOnHandler(method) {