diff options
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/lib/utils/pretty_time.js.es6 | 67 | ||||
-rw-r--r-- | app/assets/javascripts/smart_interval.js.es6 | 130 | ||||
-rw-r--r-- | app/assets/javascripts/subbable_resource.js.es6 | 54 |
3 files changed, 251 insertions, 0 deletions
diff --git a/app/assets/javascripts/lib/utils/pretty_time.js.es6 b/app/assets/javascripts/lib/utils/pretty_time.js.es6 new file mode 100644 index 00000000000..ccaf447eb0b --- /dev/null +++ b/app/assets/javascripts/lib/utils/pretty_time.js.es6 @@ -0,0 +1,67 @@ +(() => { + /* + * TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints, + * stringifyTime condensed or non-condensed, abbreviateTimelengths) + * */ + + class PrettyTime { + + /* + * Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # } + * Seconds can be negative or positive, zero or non-zero. + */ + static parseSeconds(seconds) { + const DAYS_PER_WEEK = 5; + const HOURS_PER_DAY = 8; + const MINUTES_PER_HOUR = 60; + const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR; + const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR; + + const timePeriodConstraints = { + weeks: MINUTES_PER_WEEK, + days: MINUTES_PER_DAY, + hours: MINUTES_PER_HOUR, + minutes: 1, + }; + + let unorderedMinutes = PrettyTime.secondsToMinutes(seconds); + + return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => { + const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod); + + unorderedMinutes -= (periodCount * minutesPerPeriod); + + return periodCount; + }); + } + + /* + * Accepts a timeObject and returns a condensed string representation of it + * (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included. + */ + + static stringifyTime(timeObject) { + const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => { + const isNonZero = !!unitValue; + return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo; + }, '').trim(); + return reducedTime.length ? reducedTime : '0m'; + } + + /* + * Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns + * the first non-zero unit/value pair. + */ + + static abbreviateTime(timeStr) { + return timeStr.split(' ') + .filter(unitStr => unitStr.charAt(0) !== '0')[0]; + } + + static secondsToMinutes(seconds) { + return Math.abs(seconds / 60); + } + } + + gl.PrettyTime = PrettyTime; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/smart_interval.js.es6 b/app/assets/javascripts/smart_interval.js.es6 new file mode 100644 index 00000000000..5eb15dba79b --- /dev/null +++ b/app/assets/javascripts/smart_interval.js.es6 @@ -0,0 +1,130 @@ +/* +* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable +* and controllable by a public API. +* +* */ + +(() => { + class SmartInterval { + /** + * @param { function } callback Function to be called on each iteration (required) + * @param { milliseconds } startingInterval `currentInterval` is set to this initially + * @param { milliseconds } maxInterval `currentInterval` will be incremented to this + * @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor + * @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily + */ + constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) { + this.cfg = { + callback, + startingInterval, + maxInterval, + incrementByFactorOf, + lazyStart, + }; + + this.state = { + intervalId: null, + currentInterval: startingInterval, + pageVisibility: 'visible', + }; + + this.initInterval(); + } + /* public */ + + start() { + const cfg = this.cfg; + const state = this.state; + + state.intervalId = window.setInterval(() => { + cfg.callback(); + + if (this.getCurrentInterval() === cfg.maxInterval) { + return; + } + + this.incrementInterval(); + this.resume(); + }, this.getCurrentInterval()); + } + + // cancel the existing timer, setting the currentInterval back to startingInterval + cancel() { + this.setCurrentInterval(this.cfg.startingInterval); + this.stopTimer(); + } + + // start a timer, using the existing interval + resume() { + this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped + this.start(); + } + + destroy() { + this.cancel(); + $(document).off('visibilitychange').off('page:before-unload'); + } + + /* private */ + + initInterval() { + const cfg = this.cfg; + + if (!cfg.lazyStart) { + this.start(); + } + + this.initVisibilityChangeHandling(); + this.initPageUnloadHandling(); + } + + initVisibilityChangeHandling() { + // cancel interval when tab no longer shown (prevents cached pages from polling) + $(document) + .off('visibilitychange').on('visibilitychange', (e) => { + this.state.pageVisibility = e.target.visibilityState; + this.handleVisibilityChange(); + }); + } + + initPageUnloadHandling() { + // prevent interval continuing after page change, when kept in cache by Turbolinks + $(document).on('page:before-unload', () => this.cancel()); + } + + handleVisibilityChange() { + const state = this.state; + + const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume; + + intervalAction.apply(this); + } + + getCurrentInterval() { + return this.state.currentInterval; + } + + setCurrentInterval(newInterval) { + this.state.currentInterval = newInterval; + } + + incrementInterval() { + const cfg = this.cfg; + const currentInterval = this.getCurrentInterval(); + let nextInterval = currentInterval * cfg.incrementByFactorOf; + + if (nextInterval > cfg.maxInterval) { + nextInterval = cfg.maxInterval; + } + + this.setCurrentInterval(nextInterval); + } + + stopTimer() { + const state = this.state; + + state.intervalId = window.clearInterval(state.intervalId); + } + } + gl.SmartInterval = SmartInterval; +})(window.gl || (window.gl = {})); diff --git a/app/assets/javascripts/subbable_resource.js.es6 b/app/assets/javascripts/subbable_resource.js.es6 new file mode 100644 index 00000000000..932120157a3 --- /dev/null +++ b/app/assets/javascripts/subbable_resource.js.es6 @@ -0,0 +1,54 @@ +//= require vue +//= require vue-resource + +(() => { +/* +* SubbableResource can be extended to provide a pubsub-style service for one-off REST +* calls. Subscribe by passing a callback or render method you will use to handle responses. + * +* */ + + class SubbableResource { + constructor(resourcePath) { + this.endpoint = resourcePath; + + // TODO: Switch to axios.create + this.resource = $.ajax; + this.subscribers = []; + } + + subscribe(callback) { + this.subscribers.push(callback); + } + + publish(newResponse) { + const responseCopy = _.extend({}, newResponse); + this.subscribers.forEach((fn) => { + fn(responseCopy); + }); + return newResponse; + } + + get(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + post(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + put(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + + delete(payload) { + return this.resource(payload) + .then(data => this.publish(data)); + } + } + + gl.SubbableResource = SubbableResource; +})(window.gl || (window.gl = {})); |