<commit_before>/* global sauce, Strava */

sauce.ns('locale', ns => {
    'use strict';

    const metersPerMile = 1609.344;
    const msgCache = new Map();
    const warned = new Set();

    let initialized;
    let hdUnits;

    function warnOnce(msg) {
        if (!warned.has(msg)) {
            warned.add(msg);
            console.warn(msg);
        }
    }

    function isRoughlyEqual(a, b, sameness) {
        sameness = sameness || 0.01;
        const delta = Math.abs(a - b);
        return delta < sameness;
    }

    function _getCacheEntry(args) {
        const hashKey = JSON.stringify(args);
        const entry = {
            hashKey,
            args
        };
        if (!msgCache.has(hashKey)) {
            entry.miss = true;
        } else {
            entry.hit = true;
            entry.value = msgCache.get(hashKey);
        }
        return entry;
    }

    function _formatArgs(args) {
        if (typeof args === 'string') {
            return [args];
        } else {
            return Array.from(args);
        }
    }

    async function getMessage(...args) {
        const entry = _getCacheEntry(_formatArgs(args));
        if (entry.hit) {
            return entry.value;
        } else {
            await sauce.proxy.connected;
            const value = await sauce.locale._getMessage(...args);
            if (!value) {
                warnOnce(`Locale message not found: ${entry.hashKey}`);
            }
            msgCache.set(entry.hashKey, value);
            return value;
        }
    }

    async function getMessages(batch) {
        const missing = [];
        const hashKeys = [];
        for (const args of batch.map(_formatArgs)) {
            const entry = _getCacheEntry(args);
            if (entry.miss) {
                missing.push(entry);
            }
            hashKeys.push(entry.hashKey);
        }
        if (missing.length) {
            await sauce.proxy.connected;
            const values = await sauce.locale._getMessages(missing.map(x => x.args[0]));
            for (let i = 0; i < missing.length; i++) {
                const value = values[i];
                if (!value) {
                    warnOnce(`Locale message not found: ${missing[i].hashKey}`);
                }
                msgCache.set(missing[i].hashKey, value);
            }
        }
        return hashKeys.map(x => msgCache.get(x));
    }

    async function getMessagesObject(keys, ns) {
        const prefix = ns ? `${ns}_` : '';
        const msgs = await getMessages(keys.map(x => `${prefix}${x}`));
        return msgs.reduce((acc, x, i) => (acc[keys[i]] = x, acc), {});
    }

    let _initing;
    async function init() {
        if (initialized) {
            return;
        }
        if (!_initing) {
            _initing = _init();
        }
        await _initing;
    }

    async function _init() {
        const units = ['year', 'week', 'day', 'hour', 'min', 'sec',
                       'years', 'weeks', 'days', 'hours', 'mins', 'secs',
                       'ago', 'in', 'now', 'today'];
        hdUnits = await getMessagesObject(units, 'time');
        ns.elevationFormatter = new Strava.I18n.ElevationFormatter();
        ns.hrFormatter = new Strava.I18n.HeartRateFormatter();
        ns.tempFormatter = new Strava.I18n.TemperatureFormatter();
        Strava.I18n.FormatterTranslations.swim_cadence = {
            abbr: "%{value} spm",
            "long": {
                one: "%{count} strokes per minute",
                other: "%{count} strokes per minute"
            },
            "short": "%{value} spm",
            name_long: "strokes per minute",
            name_short: "spm"
        };
        ns.cadenceFormatter = new Strava.I18n.CadenceFormatter();
        ns.cadenceFormatterRun = new Strava.I18n.DoubledStepCadenceFormatter();
        ns.cadenceFormatterSwim = new Strava.I18n.CadenceFormatter();
        ns.cadenceFormatterSwim.key = 'swim_cadence';
        ns.timeFormatter = new Strava.I18n.TimespanFormatter();
        ns.weightFormatter = new Strava.I18n.WeightFormatter();
        ns.distanceFormatter = new Strava.I18n.DistanceFormatter();
        ns.paceFormatter = new Strava.I18n.PaceFormatter();
        ns.speedFormatter = new Strava.I18n.DistancePerTimeFormatter();
        ns.swimPaceFormatter = new Strava.I18n.SwimPaceFormatter;
        ns.imperialDistanceFormatter = new Strava.I18n.DistanceFormatter(
            Strava.I18n.UnitSystemSource.IMPERIAL);
        ns.metricDistanceFormatter = new Strava.I18n.DistanceFormatter(
            Strava.I18n.UnitSystemSource.METRIC);
        initialized = true;
    }

    function assertInit() {
        if (!initialized) {
            throw new TypeError('init() must be called first');
        }
    }

    function getPaceFormatter(type) {
        return {
            swim: ns.swimPaceFormatter,
            speed: ns.speedFormatter,
            pace: ns.paceFormatter
        }[type || 'pace'];
    }

    function humanDuration(elapsed, options={}) {
        assertInit();
        const min = 60;
        const hour = min * 60;
        const day = hour * 24;
        const week = day * 7;
        const year = day * 365;
        const units = [
            ['year', year],
            ['week', week],
            ['day', day],
            ['hour', hour],
            ['min', min],
            ['sec', 1]
        ].filter(([, period]) =>
            (options.maxPeriod ? period <= options.maxPeriod : true) &&
            (options.minPeriod ? period >= options.minPeriod : true));
        const stack = [];
        const precision = options.precision || 1;
        elapsed = Math.round(elapsed / precision) * precision;
        for (let [key, period] of units) {
            if (precision > period) {
                break;
            }
            if (elapsed >= period) {
                if (elapsed >= 2 * period) {
                    key += 's';
                }
                const suffix = hdUnits[key];
                let val;
                if (options.digits && units[units.length - 1][1] === period) {
                    val = humanNumber(elapsed / period, options.digits);
                } else {
                    val = humanNumber(Math.floor(elapsed / period));
                }
                stack.push(`${val} ${suffix}`);
                elapsed %= period;
            }
        }
        return stack.slice(0, 2).join(', ');
    }

    function humanRaceDistance(value) {
        let label;
        if (value < 1000) {
            label = `${value} m`;
        } else {
            const miles = value / metersPerMile;
            if (isRoughlyEqual(miles, 13.1) ||
                isRoughlyEqual(miles, 26.2) ||
                isRoughlyEqual(miles, Math.round(miles))) {
                label = ns.imperialDistanceFormatter.formatShort(value);
            } else {
                label = ns.metricDistanceFormatter.formatShort(value);
            }
        }
        return label.replace(/\.0 /, ' ');
    }

    function humanRelTime(date, options={}) {
        assertInit();
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const elapsed = (Date.now() - date.getTime()) / 1000;
        const duration = humanDuration(Math.abs(elapsed), options);
        if (duration) {
            if (elapsed > 0) {
                return `${duration} ${hdUnits.ago}`;
            } else {
                return `${hdUnits.in} ${duration}`;
            }
        } else {
            if (options.precision && options.precision >= 86400) {
                return hdUnits.today;
            } else {
                return hdUnits.now;
            }
        }
    }

    function humanWeight(kg) {
        return humanNumber(ns.weightFormatter.convert(kg), 1);
    }

    function humanTimer(seconds) {
        assertInit();
        return ns.timeFormatter.display(seconds);
    }

    const _intlDateFormats = {
        'long': new Intl.DateTimeFormat([], {year: 'numeric', month: 'long', day: 'numeric'}),
        'default': new Intl.DateTimeFormat([], {year: 'numeric', month: 'short', day: 'numeric'}),
        'short': new Intl.DateTimeFormat([], {year: 'numeric', month: 'numeric', day: 'numeric'}),
        'shortDay': new Intl.DateTimeFormat([], {month: 'numeric', day: 'numeric'}),
        'monthYear': new Intl.DateTimeFormat([], {year: 'numeric', month: 'short'}),
        'month': new Intl.DateTimeFormat([], {month: 'short'}),
        'monthDay': new Intl.DateTimeFormat([], {month: 'short', day: 'numeric'}),
        'weekday': new Intl.DateTimeFormat([], {weekday: 'short', month: 'short', day: 'numeric'}),
        'weekdayYear': new Intl.DateTimeFormat([], {weekday: 'short', year: 'numeric', month: 'short', day: 'numeric'}),
    };
    function humanDate(date, options={}) {
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const style = options.style || 'default';
        return _intlDateFormats[style].format(date);
    }

    const _intlTimeFormats = {
        'default': new Intl.DateTimeFormat([], {hour: 'numeric', minute: 'numeric', second: 'numeric'}),
    };
    function humanTime(date, options={}) {
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const style = options.style || 'default';
        return _intlTimeFormats[style].format(date);
    }

    const _intlDateTimeFormats = {
        'long': new Intl.DateTimeFormat([], {
            year: 'numeric', month: 'long', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
        'default': new Intl.DateTimeFormat([], {
            year: 'numeric', month: 'short', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
        'short': new Intl.DateTimeFormat([], {
            year: 'numeric', month: 'numeric', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
        'weekday': new Intl.DateTimeFormat([], {
            weekday: 'short',
            year: 'numeric', month: 'short', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
    };
    function humanDateTime(date, options={}) {
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const now = new Date();
        if (now.getDay() === date.getDay() &&
            now.getMonth() === date.getMonth() &&
            now.getFullYear() === date.getFullYear()) {
            return humanTime(date, {...options, style: 'default'});
        }
        const style = options.style || 'default';
        return _intlDateTimeFormats[style].format(date);
    }

    function humanDistance(meters, precision) {
        assertInit();
        return ns.distanceFormatter.format(meters, precision || 2);
    }

    function humanPace(raw, options={}) {
        assertInit();
        const mps = options.velocity ? raw : 1 / raw;
        const formatter = getPaceFormatter(options.type);
        const value = formatter.key === 'distance_per_time' ? mps * 3600 : mps;
        const minPace = 0.1;  // About 4.5 hours / mile
        if (options.suffix) {
            if (options.html) {
                if (value < minPace) {
                    return '-';
                }
                return formatter.abbreviated(value);
            } else {
                if (value < minPace) {
                    return '-';
                }
                return formatter.formatShort(value);
            }
        } else {
            if (value < minPace) {
                return '-';
            }
            return formatter.format(value);
        }
    }

    function humanNumber(value, precision=0) {
        assertInit();
        if (value == null || value === '') {
            return '';
        }
        const n = Number(value);
        if (isNaN(n)) {
            console.warn("Value is not a number:", value);
            return value;
        }
        if (precision === null) {
            return n.toLocaleString();
        } else if (precision === 0) {
            return Math.round(n).toLocaleString();
        } else {
            return Number(n.toFixed(precision)).toLocaleString();
        }
    }

    function humanElevation(meters, options={}) {
        assertInit();
        if (options.suffix) {
            if (options.longSuffix) {
                return ns.elevationFormatter.formatLong(meters);
            } else {
                return ns.elevationFormatter.formatShort(meters);
            }
        } else {
            return ns.elevationFormatter.format(meters);
        }
    }

    function humanStride(meters) {
        assertInit();
        const metric = ns.weightFormatter.unitSystem === 'metric';
        if (metric) {
            return humanNumber(meters, 2);
        } else {
            const feet = meters / metersPerMile * 5280;
            return humanNumber(feet, 1);
        }
    }

    function weightUnconvert(localeWeight) {
        assertInit();
        return ns.weightFormatter.unitSystem === 'metric' ? localeWeight : localeWeight / 2.20462;
    }

    function elevationUnconvert(localeEl) {
        assertInit();
        return ns.elevationFormatter.unitSystem === 'metric' ? localeEl : localeEl * 0.3048;
    }

    function velocityUnconvert(localeV, options={}) {
        assertInit();
        const f = getPaceFormatter(options.type);
        return (f.unitSystem === 'metric' ? localeV * 1000 : localeV * metersPerMile) / 3600;
    }

    function distanceUnconvert(localeDist) {
        assertInit();
        return ns.distanceFormatter.unitSystem === 'metric' ? localeDist * 1000 : localeDist * metersPerMile;
    }

    return {
        init,
        getMessage,
        getMessages,
        getMessagesObject,
        getPaceFormatter,
        weightUnconvert,
        elevationUnconvert,
        velocityUnconvert,
        distanceUnconvert,
        human: {
            duration: humanDuration,
            relTime: humanRelTime,
            weight: humanWeight,
            elevation: humanElevation,
            number: humanNumber,
            pace: humanPace,
            distance: humanDistance,
            raceDistance: humanRaceDistance,
            timer: humanTimer,
            date: humanDate,
            datetime: humanDateTime,
            time: humanTime,
            stride: humanStride,
        },
        templateHelpers: {
            humanDuration,
            humanRelTime,
            humanWeight,
            humanElevation,
            humanNumber,
            humanPace,
            humanDistance,
            humanRaceDistance,
            humanTimer,
            humanDate,
            humanDateTime,
            humanTime,
            humanStride,
        },
    };
});
<commit_msg>Add "Today" to humanDateTime when it's only doing the time.<commit_after>/* global sauce, Strava */

sauce.ns('locale', ns => {
    'use strict';

    const metersPerMile = 1609.344;
    const msgCache = new Map();
    const warned = new Set();

    let initialized;
    let hdUnits;

    function warnOnce(msg) {
        if (!warned.has(msg)) {
            warned.add(msg);
            console.warn(msg);
        }
    }

    function isRoughlyEqual(a, b, sameness) {
        sameness = sameness || 0.01;
        const delta = Math.abs(a - b);
        return delta < sameness;
    }

    function _getCacheEntry(args) {
        const hashKey = JSON.stringify(args);
        const entry = {
            hashKey,
            args
        };
        if (!msgCache.has(hashKey)) {
            entry.miss = true;
        } else {
            entry.hit = true;
            entry.value = msgCache.get(hashKey);
        }
        return entry;
    }

    function _formatArgs(args) {
        if (typeof args === 'string') {
            return [args];
        } else {
            return Array.from(args);
        }
    }

    async function getMessage(...args) {
        const entry = _getCacheEntry(_formatArgs(args));
        if (entry.hit) {
            return entry.value;
        } else {
            await sauce.proxy.connected;
            const value = await sauce.locale._getMessage(...args);
            if (!value) {
                warnOnce(`Locale message not found: ${entry.hashKey}`);
            }
            msgCache.set(entry.hashKey, value);
            return value;
        }
    }

    async function getMessages(batch) {
        const missing = [];
        const hashKeys = [];
        for (const args of batch.map(_formatArgs)) {
            const entry = _getCacheEntry(args);
            if (entry.miss) {
                missing.push(entry);
            }
            hashKeys.push(entry.hashKey);
        }
        if (missing.length) {
            await sauce.proxy.connected;
            const values = await sauce.locale._getMessages(missing.map(x => x.args[0]));
            for (let i = 0; i < missing.length; i++) {
                const value = values[i];
                if (!value) {
                    warnOnce(`Locale message not found: ${missing[i].hashKey}`);
                }
                msgCache.set(missing[i].hashKey, value);
            }
        }
        return hashKeys.map(x => msgCache.get(x));
    }

    async function getMessagesObject(keys, ns) {
        const prefix = ns ? `${ns}_` : '';
        const msgs = await getMessages(keys.map(x => `${prefix}${x}`));
        return msgs.reduce((acc, x, i) => (acc[keys[i]] = x, acc), {});
    }

    let _initing;
    async function init() {
        if (initialized) {
            return;
        }
        if (!_initing) {
            _initing = _init();
        }
        await _initing;
    }

    async function _init() {
        const units = ['year', 'week', 'day', 'hour', 'min', 'sec',
                       'years', 'weeks', 'days', 'hours', 'mins', 'secs',
                       'ago', 'in', 'now', 'today'];
        hdUnits = await getMessagesObject(units, 'time');
        ns.elevationFormatter = new Strava.I18n.ElevationFormatter();
        ns.hrFormatter = new Strava.I18n.HeartRateFormatter();
        ns.tempFormatter = new Strava.I18n.TemperatureFormatter();
        Strava.I18n.FormatterTranslations.swim_cadence = {
            abbr: "%{value} spm",
            "long": {
                one: "%{count} strokes per minute",
                other: "%{count} strokes per minute"
            },
            "short": "%{value} spm",
            name_long: "strokes per minute",
            name_short: "spm"
        };
        ns.cadenceFormatter = new Strava.I18n.CadenceFormatter();
        ns.cadenceFormatterRun = new Strava.I18n.DoubledStepCadenceFormatter();
        ns.cadenceFormatterSwim = new Strava.I18n.CadenceFormatter();
        ns.cadenceFormatterSwim.key = 'swim_cadence';
        ns.timeFormatter = new Strava.I18n.TimespanFormatter();
        ns.weightFormatter = new Strava.I18n.WeightFormatter();
        ns.distanceFormatter = new Strava.I18n.DistanceFormatter();
        ns.paceFormatter = new Strava.I18n.PaceFormatter();
        ns.speedFormatter = new Strava.I18n.DistancePerTimeFormatter();
        ns.swimPaceFormatter = new Strava.I18n.SwimPaceFormatter;
        ns.imperialDistanceFormatter = new Strava.I18n.DistanceFormatter(
            Strava.I18n.UnitSystemSource.IMPERIAL);
        ns.metricDistanceFormatter = new Strava.I18n.DistanceFormatter(
            Strava.I18n.UnitSystemSource.METRIC);
        initialized = true;
    }

    function assertInit() {
        if (!initialized) {
            throw new TypeError('init() must be called first');
        }
    }

    function getPaceFormatter(type) {
        return {
            swim: ns.swimPaceFormatter,
            speed: ns.speedFormatter,
            pace: ns.paceFormatter
        }[type || 'pace'];
    }

    function humanDuration(elapsed, options={}) {
        assertInit();
        const min = 60;
        const hour = min * 60;
        const day = hour * 24;
        const week = day * 7;
        const year = day * 365;
        const units = [
            ['year', year],
            ['week', week],
            ['day', day],
            ['hour', hour],
            ['min', min],
            ['sec', 1]
        ].filter(([, period]) =>
            (options.maxPeriod ? period <= options.maxPeriod : true) &&
            (options.minPeriod ? period >= options.minPeriod : true));
        const stack = [];
        const precision = options.precision || 1;
        elapsed = Math.round(elapsed / precision) * precision;
        for (let [key, period] of units) {
            if (precision > period) {
                break;
            }
            if (elapsed >= period) {
                if (elapsed >= 2 * period) {
                    key += 's';
                }
                const suffix = hdUnits[key];
                let val;
                if (options.digits && units[units.length - 1][1] === period) {
                    val = humanNumber(elapsed / period, options.digits);
                } else {
                    val = humanNumber(Math.floor(elapsed / period));
                }
                stack.push(`${val} ${suffix}`);
                elapsed %= period;
            }
        }
        return stack.slice(0, 2).join(', ');
    }

    function humanRaceDistance(value) {
        let label;
        if (value < 1000) {
            label = `${value} m`;
        } else {
            const miles = value / metersPerMile;
            if (isRoughlyEqual(miles, 13.1) ||
                isRoughlyEqual(miles, 26.2) ||
                isRoughlyEqual(miles, Math.round(miles))) {
                label = ns.imperialDistanceFormatter.formatShort(value);
            } else {
                label = ns.metricDistanceFormatter.formatShort(value);
            }
        }
        return label.replace(/\.0 /, ' ');
    }

    function humanRelTime(date, options={}) {
        assertInit();
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const elapsed = (Date.now() - date.getTime()) / 1000;
        const duration = humanDuration(Math.abs(elapsed), options);
        if (duration) {
            if (elapsed > 0) {
                return `${duration} ${hdUnits.ago}`;
            } else {
                return `${hdUnits.in} ${duration}`;
            }
        } else {
            if (options.precision && options.precision >= 86400) {
                return hdUnits.today;
            } else {
                return hdUnits.now;
            }
        }
    }

    function humanWeight(kg) {
        return humanNumber(ns.weightFormatter.convert(kg), 1);
    }

    function humanTimer(seconds) {
        assertInit();
        return ns.timeFormatter.display(seconds);
    }

    const _intlDateFormats = {
        'long': new Intl.DateTimeFormat([], {year: 'numeric', month: 'long', day: 'numeric'}),
        'default': new Intl.DateTimeFormat([], {year: 'numeric', month: 'short', day: 'numeric'}),
        'short': new Intl.DateTimeFormat([], {year: 'numeric', month: 'numeric', day: 'numeric'}),
        'shortDay': new Intl.DateTimeFormat([], {month: 'numeric', day: 'numeric'}),
        'monthYear': new Intl.DateTimeFormat([], {year: 'numeric', month: 'short'}),
        'month': new Intl.DateTimeFormat([], {month: 'short'}),
        'monthDay': new Intl.DateTimeFormat([], {month: 'short', day: 'numeric'}),
        'weekday': new Intl.DateTimeFormat([], {weekday: 'short', month: 'short', day: 'numeric'}),
        'weekdayYear': new Intl.DateTimeFormat([], {weekday: 'short', year: 'numeric', month: 'short', day: 'numeric'}),
    };
    function humanDate(date, options={}) {
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const style = options.style || 'default';
        return _intlDateFormats[style].format(date);
    }

    const _intlTimeFormats = {
        'default': new Intl.DateTimeFormat([], {hour: 'numeric', minute: 'numeric', second: 'numeric'}),
    };
    function humanTime(date, options={}) {
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const style = options.style || 'default';
        return _intlTimeFormats[style].format(date);
    }

    const _intlDateTimeFormats = {
        'long': new Intl.DateTimeFormat([], {
            year: 'numeric', month: 'long', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
        'default': new Intl.DateTimeFormat([], {
            year: 'numeric', month: 'short', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
        'short': new Intl.DateTimeFormat([], {
            year: 'numeric', month: 'numeric', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
        'weekday': new Intl.DateTimeFormat([], {
            weekday: 'short',
            year: 'numeric', month: 'short', day: 'numeric',
            hour: 'numeric', minute: 'numeric'
        }),
    };
    function humanDateTime(date, options={}) {
        assertInit();
        if (!(date instanceof Date)) {
            date = new Date(date);
        }
        const now = new Date();
        if (now.getDate() === date.getDate() &&
            now.getMonth() === date.getMonth() &&
            now.getFullYear() === date.getFullYear()) {
            const today = hdUnits.today;
            const Today = today.substr(0, 1).toLocaleUpperCase() + today.substr(1);
            return [Today, humanTime(date, {...options, style: 'default'})].join(', ');
        }
        const style = options.style || 'default';
        return _intlDateTimeFormats[style].format(date);
    }

    function humanDistance(meters, precision) {
        assertInit();
        return ns.distanceFormatter.format(meters, precision || 2);
    }

    function humanPace(raw, options={}) {
        assertInit();
        const mps = options.velocity ? raw : 1 / raw;
        const formatter = getPaceFormatter(options.type);
        const value = formatter.key === 'distance_per_time' ? mps * 3600 : mps;
        const minPace = 0.1;  // About 4.5 hours / mile
        if (options.suffix) {
            if (options.html) {
                if (value < minPace) {
                    return '-';
                }
                return formatter.abbreviated(value);
            } else {
                if (value < minPace) {
                    return '-';
                }
                return formatter.formatShort(value);
            }
        } else {
            if (value < minPace) {
                return '-';
            }
            return formatter.format(value);
        }
    }

    function humanNumber(value, precision=0) {
        assertInit();
        if (value == null || value === '') {
            return '';
        }
        const n = Number(value);
        if (isNaN(n)) {
            console.warn("Value is not a number:", value);
            return value;
        }
        if (precision === null) {
            return n.toLocaleString();
        } else if (precision === 0) {
            return Math.round(n).toLocaleString();
        } else {
            return Number(n.toFixed(precision)).toLocaleString();
        }
    }

    function humanElevation(meters, options={}) {
        assertInit();
        if (options.suffix) {
            if (options.longSuffix) {
                return ns.elevationFormatter.formatLong(meters);
            } else {
                return ns.elevationFormatter.formatShort(meters);
            }
        } else {
            return ns.elevationFormatter.format(meters);
        }
    }

    function humanStride(meters) {
        assertInit();
        const metric = ns.weightFormatter.unitSystem === 'metric';
        if (metric) {
            return humanNumber(meters, 2);
        } else {
            const feet = meters / metersPerMile * 5280;
            return humanNumber(feet, 1);
        }
    }

    function weightUnconvert(localeWeight) {
        assertInit();
        return ns.weightFormatter.unitSystem === 'metric' ? localeWeight : localeWeight / 2.20462;
    }

    function elevationUnconvert(localeEl) {
        assertInit();
        return ns.elevationFormatter.unitSystem === 'metric' ? localeEl : localeEl * 0.3048;
    }

    function velocityUnconvert(localeV, options={}) {
        assertInit();
        const f = getPaceFormatter(options.type);
        return (f.unitSystem === 'metric' ? localeV * 1000 : localeV * metersPerMile) / 3600;
    }

    function distanceUnconvert(localeDist) {
        assertInit();
        return ns.distanceFormatter.unitSystem === 'metric' ? localeDist * 1000 : localeDist * metersPerMile;
    }

    return {
        init,
        getMessage,
        getMessages,
        getMessagesObject,
        getPaceFormatter,
        weightUnconvert,
        elevationUnconvert,
        velocityUnconvert,
        distanceUnconvert,
        human: {
            duration: humanDuration,
            relTime: humanRelTime,
            weight: humanWeight,
            elevation: humanElevation,
            number: humanNumber,
            pace: humanPace,
            distance: humanDistance,
            raceDistance: humanRaceDistance,
            timer: humanTimer,
            date: humanDate,
            datetime: humanDateTime,
            time: humanTime,
            stride: humanStride,
        },
        templateHelpers: {
            humanDuration,
            humanRelTime,
            humanWeight,
            humanElevation,
            humanNumber,
            humanPace,
            humanDistance,
            humanRaceDistance,
            humanTimer,
            humanDate,
            humanDateTime,
            humanTime,
            humanStride,
        },
    };
});
<|endoftext|>


<commit_before>import React from 'react';
import { DatePicker } from 'antd';
import { func, number, string, any } from 'prop-types';

import CellStyled from './Cell.styles';
import { getDateFormat } from '../../utils';

const moment = require('moment');

const DateCell = ({ children, onChange, onFocus, row, column, format }) => (
	<CellStyled
		onFocus={() => onFocus(row, column)}
		onBlur={() => {
			onFocus(null, null);
		}}
	>
		<DatePicker
			showTime
			defaultValue={moment(children, getDateFormat(format))}
			format={getDateFormat(format)}
			css={{
				width: '100% !important',
				height: '100%',
				input: { height: 42, borderColor: 'transparent' },
			}}
			onChange={(momentObject, dateString) => {
				if (children !== dateString) {
					// only update value if date string has changed
					onChange(row, column, dateString);
				}
			}}
		/>
	</CellStyled>
);

DateCell.propTypes = {
	row: number.isRequired,
	column: string.isRequired,
	onChange: func.isRequired,
	children: any,
	format: string,
	onFocus: func.isRequired,
};

export default DateCell;
<commit_msg>Fix empty date state<commit_after>import React from 'react';
import { DatePicker } from 'antd';
import { func, number, string, any } from 'prop-types';

import CellStyled from './Cell.styles';
import { getDateFormat } from '../../utils';

const moment = require('moment');

const DateCell = ({ children, onChange, onFocus, row, column, format }) => (
	<CellStyled
		onFocus={() => onFocus(row, column)}
		onBlur={() => {
			onFocus(null, null);
		}}
	>
		<DatePicker
			showTime
			defaultValue={children && moment(children, getDateFormat(format))}
			format={getDateFormat(format)}
			css={{
				width: '100% !important',
				height: '100%',
				input: { height: 42, borderColor: 'transparent' },
			}}
			onChange={(momentObject, dateString) => {
				if (children !== dateString) {
					// only update value if date string has changed
					onChange(row, column, dateString || null);
				}
			}}
		/>
	</CellStyled>
);

DateCell.propTypes = {
	row: number.isRequired,
	column: string.isRequired,
	onChange: func.isRequired,
	children: any,
	format: string,
	onFocus: func.isRequired,
};

export default DateCell;
<|endoftext|>


<commit_before>define(
    'imcms-link-validator-tab-builder',
    ['imcms-super-admin-tab', 'imcms-i18n-texts', 'imcms-components-builder',
        'imcms-field-wrapper', 'imcms-bem-builder', 'jquery', 'imcms-link-validator-rest-api'],
    function (SuperAdminTab, texts, components, fieldWrapper, BEM, $, linksValidatorRestApi) {

        texts = texts.superAdmin.linkValidator;

        let $inputStartId;
        let $inputEndId;
        let $filterOnlyBrokenList;
        let $searchResultContainer;

        function showOnlyBrokenLinks() {

        }

        function buildTitleText() {
            return fieldWrapper.wrap(components.texts.titleText('<div>', texts.name))
        }

        function buildFilterOnlyBrokenLinks() {
            return $filterOnlyBrokenList = components.checkboxes.imcmsCheckbox('<div>', {
                text: texts.titleOnlyBrokenLinks,
                click: showOnlyBrokenLinks
            });
        }

        function buildLinksContainer() {
            let $linkContainer = $('<div>', {
                'class': 'table-links',
                style: 'display: none;'
            });

            return fieldWrapper.wrap([$linkContainer]);
        }

        function buildContainerInputIdsValidation() {

            function buildFieldStartId() {
                return $inputStartId = components.texts.textNumber('<div>', {
                    placeholder: '1001',
                    text: texts.titleStartId,
                });
            }

            function buildFieldEndId() {
                return $inputEndId = components.texts.textNumber('<div>', {
                    placeholder: '1001',
                    text: texts.titleEndId,
                });
            }

            function buildButtonValidation() {
                let $button = components.buttons.positiveButton({
                    text: texts.buttonValidation,
                    click: listValidationLinks
                });

                return components.buttons.buttonsContainer('<div>', [$button]);
            }

            return new BEM({
                block: 'imcms-field-ids-block', // change this name if need!! =)
                elements: {
                    'links-field-start': buildFieldStartId(),
                    'links-field-end': buildFieldEndId(),
                    'button-validation': buildButtonValidation()
                }
            }).buildBlockStructure('<div>');
        }

        return new SuperAdminTab(texts.name, [
            buildTitleText(),
            buildFilterOnlyBrokenLinks(),
            buildContainerInputIdsValidation(),
            $searchResultContainer = buildLinksContainer()
        ]);
    }
);
<commit_msg>IMCMS-334 New design to super admin page: link validator tab - Add builder validation list links<commit_after>define(
    'imcms-link-validator-tab-builder',
    ['imcms-super-admin-tab', 'imcms-i18n-texts', 'imcms-components-builder',
        'imcms-field-wrapper', 'imcms-bem-builder', 'jquery', 'imcms-link-validator-rest-api'],
    function (SuperAdminTab, texts, components, fieldWrapper, BEM, $, linksValidatorRestApi) {

        texts = texts.superAdmin.linkValidator;

        let $inputStartId;
        let $inputEndId;
        let $filterOnlyBrokenList;
        let $searchResultContainer;

        function showOnlyBrokenLinks() {

        }

        function buildTitleText() {
            return fieldWrapper.wrap(components.texts.titleText('<div>', texts.name))
        }

        function buildFilterOnlyBrokenLinks() {
            return $filterOnlyBrokenList = components.checkboxes.imcmsCheckbox('<div>', {
                text: texts.titleOnlyBrokenLinks,
                click: showOnlyBrokenLinks
            });
        }

        function buildLinksContainer() {
            let $linkContainer = $('<div>', {
                'class': 'table-links',
                style: 'display: none;'
            });

            return fieldWrapper.wrap([$linkContainer]);
        }

        function buildContainerInputIdsValidation() {

            function buildFieldStartId() {
                return $inputStartId = components.texts.textNumber('<div>', {
                    placeholder: '1001',
                    text: texts.titleStartId,
                });
            }

            function buildFieldEndId() {
                return $inputEndId = components.texts.textNumber('<div>', {
                    placeholder: '1001',
                    text: texts.titleEndId,
                });
            }

            var linkListBuilder = function ($searchResultContainer) {
                this.$searchResultContainer = $searchResultContainer;
                this.linkAppender = this.appendLinks.bind(this);
            };

            linkListBuilder.prototype = {
                isEmpty: true,
                clearList: function () {
                    this.$searchResultContainer.empty();
                    this.isEmpty = true;

                    return this;
                },
                linkToRow: function (validationLink) {

                    let infoRowAttributes = {
                        id: 'link-id-' + validationLink.id,
                    };

                    return new BEM({
                        block: "link-info-row",
                        elements: {
                            'link-page-alias-': $('<div>', {
                                text: validationLink.alias
                            }),
                            'link-status-': $('<div>', {
                                text: validationLink.documentStatus
                            }),
                            'link-type-': $('<div>', {
                                text: validationLink.type
                            }),
                            // 'link-admin-': $('<div>', {
                            //     text: validationLink.admin
                            // }),
                            // 'link-ref-': $('<div>', {
                            //     text: validationLink.documentStatus
                            // }),
                            'link-name-': $('<div>', {
                                text: validationLink.title
                            }),
                            'link-host-found-': $('<div>', {
                                text: (validationLink.hostFound) ? "V" : components.controls.remove(),
                            }),
                            'link-host-reachable-': $('<div>', {
                                text: (validationLink.hostReachable) ? "V" : components.controls.remove(),
                            }),
                            'link-page-found-': $('<div>', {
                                text: (validationLink.pageFound) ? "V" : components.controls.remove(),
                            }),
                        }
                    }).buildBlockStructure("<div>", infoRowAttributes);
                },
                prepareTitleRow: function () {
                    let titleRow = new BEM({
                        block: 'table-title-row',
                        elements: {
                            'page-alias': $('<div>', {text: texts.linkInfoRow.pageAlias}),
                            'status': $('<div>', {text: texts.linkInfoRow.status}),
                            'type': $('<div>', {text: texts.linkInfoRow.type}),
                            'admin': $('<div>', {text: texts.linkInfoRow.admin}),
                            'reference': $('<div>', {text: texts.linkInfoRow.reference}),
                            'link': $('<div>', {text: texts.linkInfoRow.link}),
                            'host-found': $('<div>', {text: texts.linkInfoRow.hostFound}),
                            'host-reachable': $('<div>', {text: texts.linkInfoRow.hostReachable}),
                            'page-found': $('<div>', {text: texts.linkInfoRow.pageFound})
                        }
                    }).buildBlockStructure('<div>', {
                        'class': 'table-title',

                    });
                    return titleRow;
                },
                addRowsToList: function (linkRows$) {
                    this.$searchResultContainer.css('display', 'block').append(linkRows$);
                    return this;
                },
                appendLinks: function (validationLinks) {
                    this.prepareTitleRow()
                        .addRowsToList(validationLinks.map(this.linkToRow));
                }
            };

            function listValidationLinks() {
                var filterLinks = {
                    filterBrokenLinks: $filterOnlyBrokenList.isChecked(),
                    startDocumentId: $inputStartId.getInput().val(),
                    endDocumentId: $inputEndId.getInput().val()
                };
                var tableBuilder = new linkListBuilder($searchResultContainer).clearList();
                linksValidatorRestApi.validate(filterLinks).done(tableBuilder.linkAppender);
            }

            function buildButtonValidation() {
                let $button = components.buttons.positiveButton({
                    text: texts.buttonValidation,
                    click: listValidationLinks
                });

                return components.buttons.buttonsContainer('<div>', [$button]);
            }

            return new BEM({
                block: 'imcms-field-ids-block', // change this name if need!! =)
                elements: {
                    'links-field-start': buildFieldStartId(),
                    'links-field-end': buildFieldEndId(),
                    'button-validation': buildButtonValidation()
                }
            }).buildBlockStructure('<div>');
        }

        return new SuperAdminTab(texts.name, [
            buildTitleText(),
            buildFilterOnlyBrokenLinks(),
            buildContainerInputIdsValidation(),
            $searchResultContainer = buildLinksContainer()
        ]);
    }
);
<|endoftext|>


<commit_before>/*global window, $, nunjucks*/

(function (factory) {
    window.SahteReact = factory();
}(function () {
    var utils = {
        // Deep merge helper
        merge: (function () {
            if (window.jQuery) {
                return function merge () {
                    var args = [true].concat(Array.from(arguments));
                    return window.jQuery.extend.apply(window.jQuery, args);
                };
            } else { // for mobile
                return function merge (out) {
                    out = out || {};
                    for (var i = 1; i < arguments.length; i += 1) {
                        var obj = arguments[i];
                        if (!obj) {
                            continue;
                        }
                        for (var key in obj) {
                            if (obj.hasOwnProperty(key)) {
                                if (typeof obj[key] === 'object')
                                    out[key] = utils.merge(out[key], obj[key]);
                                else
                                    out[key] = obj[key];
                            }
                        }
                    }
                    return out;
                };
            }
        }()),

        /**
         * Generates an unique alpha-numeric identifier.

         * To get the same permutation as RFC-4122 use len=24.
         * @param [len=10] Length of the UUID.
         * @param [hypenate=false] When set to true, hyphens are added to the UUID.
         * @return {String} The UUID
         * @method uuid
         */
        uuid: function (len, hypenate) {
            var count = 1, id = ('x').repeat(len || 10).replace(/x/g, function () {
                return ((count++ % 5) ? '' : '-') + (Math.random() * 100 % 36 | 0).toString(36);
            });
            return hypenate ? id : id.replace(/-/g, '');
        },

        /**
         * Send in any object (including function, DOM Nodes or whatever) and get a unique id.
         * If you send the object again, the same id will be returned as the last time.
         * This does not leak memory.
         * @method getUID
         */
        getUID: function getUID(obj) {
            if (!obj._uid_) {
                Object.defineProperty(obj, '_uid_', {
                    value: utils.uuid(),
                    enumerable: false
                });
            }
            return obj._uid_;
        },

        /*Dom helpers*/
        /**
         * Converts html string to a document fragment.
         * @param {String} html
         * @return {DocumentFragment}
         * @method dom
         */
        dom: (function () {
            if (window.jQuery) {
                return function dom(html) {
                    var frag = document.createDocumentFragment();
                    window.jQuery.parseHTML(html).forEach(function (node) {
                        frag.appendChild(node);
                    });
                    return frag;
                };
            } else { // for mobile
                return function dom(html) {
                    var templateTag = document.createElement('template');
                    templateTag.innerHTML = html;
                    return templateTag.content.cloneNode(true);
                };
            }
        }()),

        /**
         * Helper to attach event listener to DOM. Uses jQuery if available or falls back to custom implemeentation.
         * @param {String} html
         * @return {DocumentFragment}
         * @method dom
         */
        on: (function () {
            if (window.jQuery) {
                return function on(node, eventName, func, context) {
                    $(node).on(eventName, func.bind(context));
                };
            } else { // for mobile
                return function on(node, eventName, func, context) {
                    node._bindings = node._bindings || {};
                    var key = eventName + '#' + utils.getUID(func);
                    if (context) { // prevent multiple events from being added.
                        key += '#' + utils.getUID(context);
                    }
                    if (!node._bindings[key]) {
                        node._bindings[key] = context ? func.bind(context) : func;
                    }
                    func = node._bindings[key];
                    node.addEventListener(eventName, func);
                };
            }
        }()),

        removeAllEventListeners: (function () {
            if (window.jQuery) {
                return function removeAllEventListeners(node) {
                    $(node).off();
                };
            } else { // for mobile
                return function removeAllEventListeners(node) {
                    if (node._bindings) {
                        Object.keys(node._bindings).forEach(function (key) {
                            var eventName = key.split('#')[0];
                            var func = node._bindings[key];
                            node.removeEventListener(eventName, func);
                        });
                        delete node._bindings;
                    }
                };
            }
        }())
    };

    var SahteReact = function (config) {
        this.id = SahteReact.generateUId();
        this._data = {};
        this.setConfig(config);

        var data = this.data;
        delete this.data;
        Object.defineProperty(this, 'data', {
            configurable: false,
            set: function (data) {
                data.$view = this;
                this._data = data;
                this.render();
            },
            get: function () {
                return this._data;
            }
        });
        this.init();
        //Trigger render.
        if (data) {
            this.data = data;
        }
    };

    // Static methods and properties
    Object.assign(SahteReact, {
        uid: 1,
        utils: utils,
        generateUId: function () {
            var id = 'view-' + SahteReact.uid;
            SahteReact.uid += 1;
            return id;
        },

        /**
         * Compiles the given template using detected template engine.
         *
         * If compiler isn't available, assume 'template' is an id to an already compiled
         * template (stored within some namespace) and return 'template' as is.
         */
        compile: function (template) {
            if (window.nunjucks || window.swig) {
                return template;
            } else if (window.doT) {
                return window.doT.template(template);
            }
        }
    });

    // Methods and properties
    Object.assign(SahteReact.prototype, {
        /**
         * The data object.
         * This is a private variable accessed through this.data
         * setter/getter.
         */
        _data: null,

        /**
         * The name of the preconmpiled nunjucks template to use.
         */
        template: null,

        /**
         * (Optional) The elment to replace (on first render).
         */
        target: null,

        /**
         * Allow overridable view-specific init() method
         */
        init: function () {},

        /**
         * Sets config.
         */
        setConfig: function (config) {
            if (config && typeof config.template === 'string') {
                config.template = SahteReact.compile(config.template);
            }
            Object.assign(this, config);
        },

        /**
         * Set data on this.data (using Object.assign), and re-render.
         */
        assign: function () {
            var args = Array.prototype.slice.call(arguments);
            Object.assign.apply(Object, [this.data].concat(args));
            this.render();
        },

        /**
         * Deep merge data with this.data, and re-render.
         */
        merge: function (data) {
            utils.merge(this.data, data);
            this.render();
        },

        getHTML: function () {
            if (window.nunjucks) {
                return window.nunjucks.render(this.template, this.data);
            } else if (window.swig) {
                return window.swig.run(window.swig._precompiled[this.template], this.data, this.template);
            } else { // assume this.template is a compiled template function that takes data
                return this.template(this.data);
            }
        },

        /**
         * Render view.
         * If this.target or node paramter is specified, then replaces that node and attaches the
         * rendered DOM to document (or document fragment). Else the render is in-memory.
         *
         * @param {Node} [node] The node to replace. If not sepcified, then searches
         * for the selector specified at this.target.
         *
         * @private
         */
        render: function () {
            // Step 1: Remove event listeners
            // Step 2: Note the currently focused element
            // Step 3: Render/Update UI.
            // Step 4: Resolve references
            // Step 5: Re-focus
            // Step 6: Re-attach listeners

            var target = this.el;

            // Step 1: Find input field focus, remember it's id attribute, so that it
            // can be refocused later.
            var focusId = document.activeElement.id;

            // Step 2: Remove event listeners before patch.
            if (target) {
                [target].concat(Array.from(target.querySelectorAll('*')))
                    .forEach(function (node) {
                        if (node.nodeType === 1) {
                            utils.removeAllEventListeners(node);
                        }
                    }, this);
            }

            // Step 3: Render/Update UI
            var view = utils.dom(this.getHTML()),
                el = view.firstElementChild;

            // Update existing DOM.
            if (target) {
                var parent = target.parentNode,
                    childIndex = Array.from(parent.childNodes).indexOf(target);

                //Update UI (using DOM diff & patch).
                window.domPatch(el, target);
                this.el = parent.childNodes[childIndex];
            } else {
                this.el = el;
            }

            this.el.sahteReactInstance = this;

            // Step 4. Resolve element ref and refs.
            // Note:
            // ref creates a reference to the node as property on the view.
            // refs creates an array property on the view, into which the node is pushed.
            Array.from(this.el.querySelectorAll('[ref]')).forEach(function (node) {
                this[node.getAttribute('ref')] = node;
            }, this);

            var refs = Array.from(this.el.querySelectorAll('[refs]'));
            //Empty references first.
            refs.forEach(function (node) {
                this[node.getAttribute('ref')] = [];
            }, this);
            //Create reference.
            refs.forEach(function (node) {
                this[node.getAttribute('ref')].push(node);
            }, this);

            // Step 5: Re-focus
            if (focusId) {
                var focusEl = document.getElementById(focusId);
                if (focusEl) {
                    focusEl.focus();
                }
            }

            // Step 6: Attach event listeners.
            [this.el].concat(Array.from(this.el.querySelectorAll('*')))
                .forEach(function (node) {
                    if (node.nodeType === 1) {
                        Array.from(node.attributes).forEach(function (attr) {
                            if (attr.name.startsWith('on-')) {
                                var eventName = attr.name.replace(/on-/, '');
                                utils.on(node, eventName, this[attr.value], this);
                            }
                        }, this);
                    }
                }, this);
        },

        mount: function (node) {
            if (!node && this.target) {
                if (typeof this.target === 'string') {
                    node = document.querySelector(this.target);
                } else if (this.target instanceof HTMLElement) {
                    node = this.target;
                }
            }

            //Return if already mounted.
            if (this.el && node === this.el) {
                return;
            }

            if (node && node.parentNode) {
                this.el = node;
                this.render();
            }
        },

        append: function (node) {
            if (!this.el) {
                return this.render();
            }
            node.appendChild(this.el);
        }
    });

    return SahteReact;
}));
<commit_msg>support template engines that doesn't support pre-compilation<commit_after>/*global window, $, nunjucks*/

(function (factory) {
    window.SahteReact = factory();
}(function () {
    var utils = {
        // Deep merge helper
        merge: (function () {
            if (window.jQuery) {
                return function merge () {
                    var args = [true].concat(Array.from(arguments));
                    return window.jQuery.extend.apply(window.jQuery, args);
                };
            } else { // for mobile
                return function merge (out) {
                    out = out || {};
                    for (var i = 1; i < arguments.length; i += 1) {
                        var obj = arguments[i];
                        if (!obj) {
                            continue;
                        }
                        for (var key in obj) {
                            if (obj.hasOwnProperty(key)) {
                                if (typeof obj[key] === 'object')
                                    out[key] = utils.merge(out[key], obj[key]);
                                else
                                    out[key] = obj[key];
                            }
                        }
                    }
                    return out;
                };
            }
        }()),

        /**
         * Generates an unique alpha-numeric identifier.

         * To get the same permutation as RFC-4122 use len=24.
         * @param [len=10] Length of the UUID.
         * @param [hypenate=false] When set to true, hyphens are added to the UUID.
         * @return {String} The UUID
         * @method uuid
         */
        uuid: function (len, hypenate) {
            var count = 1, id = ('x').repeat(len || 10).replace(/x/g, function () {
                return ((count++ % 5) ? '' : '-') + (Math.random() * 100 % 36 | 0).toString(36);
            });
            return hypenate ? id : id.replace(/-/g, '');
        },

        /**
         * Send in any object (including function, DOM Nodes or whatever) and get a unique id.
         * If you send the object again, the same id will be returned as the last time.
         * This does not leak memory.
         * @method getUID
         */
        getUID: function getUID(obj) {
            if (!obj._uid_) {
                Object.defineProperty(obj, '_uid_', {
                    value: utils.uuid(),
                    enumerable: false
                });
            }
            return obj._uid_;
        },

        /*Dom helpers*/
        /**
         * Converts html string to a document fragment.
         * @param {String} html
         * @return {DocumentFragment}
         * @method dom
         */
        dom: (function () {
            if (window.jQuery) {
                return function dom(html) {
                    var frag = document.createDocumentFragment();
                    window.jQuery.parseHTML(html).forEach(function (node) {
                        frag.appendChild(node);
                    });
                    return frag;
                };
            } else { // for mobile
                return function dom(html) {
                    var templateTag = document.createElement('template');
                    templateTag.innerHTML = html;
                    return templateTag.content.cloneNode(true);
                };
            }
        }()),

        /**
         * Helper to attach event listener to DOM. Uses jQuery if available or falls back to custom implemeentation.
         * @param {String} html
         * @return {DocumentFragment}
         * @method dom
         */
        on: (function () {
            if (window.jQuery) {
                return function on(node, eventName, func, context) {
                    $(node).on(eventName, func.bind(context));
                };
            } else { // for mobile
                return function on(node, eventName, func, context) {
                    node._bindings = node._bindings || {};
                    var key = eventName + '#' + utils.getUID(func);
                    if (context) { // prevent multiple events from being added.
                        key += '#' + utils.getUID(context);
                    }
                    if (!node._bindings[key]) {
                        node._bindings[key] = context ? func.bind(context) : func;
                    }
                    func = node._bindings[key];
                    node.addEventListener(eventName, func);
                };
            }
        }()),

        removeAllEventListeners: (function () {
            if (window.jQuery) {
                return function removeAllEventListeners(node) {
                    $(node).off();
                };
            } else { // for mobile
                return function removeAllEventListeners(node) {
                    if (node._bindings) {
                        Object.keys(node._bindings).forEach(function (key) {
                            var eventName = key.split('#')[0];
                            var func = node._bindings[key];
                            node.removeEventListener(eventName, func);
                        });
                        delete node._bindings;
                    }
                };
            }
        }())
    };

    var SahteReact = function (config) {
        this.id = SahteReact.generateUId();
        this._data = {};
        this.setConfig(config);

        var data = this.data;
        delete this.data;
        Object.defineProperty(this, 'data', {
            configurable: false,
            set: function (data) {
                data.$view = this;
                this._data = data;
                this.render();
            },
            get: function () {
                return this._data;
            }
        });
        this.init();
        //Trigger render.
        if (data) {
            this.data = data;
        }
    };

    // Static methods and properties
    Object.assign(SahteReact, {
        uid: 1,
        utils: utils,
        generateUId: function () {
            var id = 'view-' + SahteReact.uid;
            SahteReact.uid += 1;
            return id;
        },

        /**
         * Compiles the given template using detected template engine.
         *
         * If compiler isn't available, assume 'template' is an id to an already compiled
         * template (stored within some namespace) and return 'template' as is.
         */
        compile: function (template) {
            if (window.nunjucks || window.swig) {
                return template;
            } else if (window.doT) {
                return window.doT.template(template);
            }
            // doesn't support pre-compilation
            return template;
        }
    });

    // Methods and properties
    Object.assign(SahteReact.prototype, {
        /**
         * The data object.
         * This is a private variable accessed through this.data
         * setter/getter.
         */
        _data: null,

        /**
         * The name of the preconmpiled nunjucks template to use.
         */
        template: null,

        /**
         * (Optional) The elment to replace (on first render).
         */
        target: null,

        /**
         * Allow overridable view-specific init() method
         */
        init: function () {},

        /**
         * Sets config.
         */
        setConfig: function (config) {
            if (config && typeof config.template === 'string') {
                config.template = SahteReact.compile(config.template);
            }
            Object.assign(this, config);
        },

        /**
         * Set data on this.data (using Object.assign), and re-render.
         */
        assign: function () {
            var args = Array.prototype.slice.call(arguments);
            Object.assign.apply(Object, [this.data].concat(args));
            this.render();
        },

        /**
         * Deep merge data with this.data, and re-render.
         */
        merge: function (data) {
            utils.merge(this.data, data);
            this.render();
        },

        getHTML: function () {
            if (window.nunjucks) {
                return window.nunjucks.render(this.template, this.data);
            } else if (window.swig) {
                return window.swig.run(window.swig._precompiled[this.template], this.data, this.template);
            } else { // assume this.template is a compiled template function that takes data
                return this.template(this.data);
            }
        },

        /**
         * Render view.
         * If this.target or node paramter is specified, then replaces that node and attaches the
         * rendered DOM to document (or document fragment). Else the render is in-memory.
         *
         * @param {Node} [node] The node to replace. If not sepcified, then searches
         * for the selector specified at this.target.
         *
         * @private
         */
        render: function () {
            // Step 1: Remove event listeners
            // Step 2: Note the currently focused element
            // Step 3: Render/Update UI.
            // Step 4: Resolve references
            // Step 5: Re-focus
            // Step 6: Re-attach listeners

            var target = this.el;

            // Step 1: Find input field focus, remember it's id attribute, so that it
            // can be refocused later.
            var focusId = document.activeElement.id;

            // Step 2: Remove event listeners before patch.
            if (target) {
                [target].concat(Array.from(target.querySelectorAll('*')))
                    .forEach(function (node) {
                        if (node.nodeType === 1) {
                            utils.removeAllEventListeners(node);
                        }
                    }, this);
            }

            // Step 3: Render/Update UI
            var view = utils.dom(this.getHTML()),
                el = view.firstElementChild;

            // Update existing DOM.
            if (target) {
                var parent = target.parentNode,
                    childIndex = Array.from(parent.childNodes).indexOf(target);

                //Update UI (using DOM diff & patch).
                window.domPatch(el, target);
                this.el = parent.childNodes[childIndex];
            } else {
                this.el = el;
            }

            this.el.sahteReactInstance = this;

            // Step 4. Resolve element ref and refs.
            // Note:
            // ref creates a reference to the node as property on the view.
            // refs creates an array property on the view, into which the node is pushed.
            Array.from(this.el.querySelectorAll('[ref]')).forEach(function (node) {
                this[node.getAttribute('ref')] = node;
            }, this);

            var refs = Array.from(this.el.querySelectorAll('[refs]'));
            //Empty references first.
            refs.forEach(function (node) {
                this[node.getAttribute('ref')] = [];
            }, this);
            //Create reference.
            refs.forEach(function (node) {
                this[node.getAttribute('ref')].push(node);
            }, this);

            // Step 5: Re-focus
            if (focusId) {
                var focusEl = document.getElementById(focusId);
                if (focusEl) {
                    focusEl.focus();
                }
            }

            // Step 6: Attach event listeners.
            [this.el].concat(Array.from(this.el.querySelectorAll('*')))
                .forEach(function (node) {
                    if (node.nodeType === 1) {
                        Array.from(node.attributes).forEach(function (attr) {
                            if (attr.name.startsWith('on-')) {
                                var eventName = attr.name.replace(/on-/, '');
                                utils.on(node, eventName, this[attr.value], this);
                            }
                        }, this);
                    }
                }, this);
        },

        mount: function (node) {
            if (!node && this.target) {
                if (typeof this.target === 'string') {
                    node = document.querySelector(this.target);
                } else if (this.target instanceof HTMLElement) {
                    node = this.target;
                }
            }

            //Return if already mounted.
            if (this.el && node === this.el) {
                return;
            }

            if (node && node.parentNode) {
                this.el = node;
                this.render();
            }
        },

        append: function (node) {
            if (!this.el) {
                return this.render();
            }
            node.appendChild(this.el);
        }
    });

    return SahteReact;
}));
<|endoftext|>


<commit_before>import page  	  from 'page';
import React 	  from 'react';
import Immutable  from 'immutable';
import classNames from 'classnames';

import Action          from '../actions';
import UserAction      from '../actions/user';
import SettingsAction  from '../actions/settings';
import BroadcastAction from '../actions/broadcast';

import UserStore    from '../stores/user';
import Avatar       from '../components/avatar';
import Dropdown     from '../components/dropdown';
import MemberDialog from '../components/dialog/board-members';

import UserVoice from '../components/user-voice';
import InfoView  from './dialog/view-info';
import AboutView from './dialog/view-about';

import Board from '../models/board';

import ActivityStore from '../stores/activity';

import listener    from '../mixins/listener';
import localeMixin from '../mixins/locale';

/**
 *
 */
export default React.createClass({
	mixins: [
		listener(ActivityStore),
		localeMixin()
	],

	propTypes: {
		title: React.PropTypes.string.isRequired,
		showHelp: React.PropTypes.bool,
		reviewActive: React.PropTypes.bool,
		killReview: React.PropTypes.func,
		board: (props) => {
			if(!props.board instanceof Board) throw new Error();
		}
	},

	onChange() {
		this.setState({
			members: this.props.board
				? ActivityStore.getActiveMembers(this.props.board.id)
				: Immutable.List()
		});
	},

	getInitialState() {
		return {
			members: this.props.board
				? ActivityStore.getActiveMembers(this.props.board.id)
				: Immutable.List(),
			dropdown: false, localesDropdown: false,
			feedback: false, infoActive: false,
			aboutActive: false, membersActive: false
		}
	},

	showWorkspace() {
		return page.show('/boards');
	},

	toggleMembersDialog() {
		this.setState({ membersActive: !this.state.membersActive });
	},

	toggleDropdown() {
		this.setState({ dropdown: !this.state.dropdown });
		if(this.state.localesDropdown) {
			this.setState({ localesDropdown: !this.state.localesDropdown });
		}
	},

	toggleInfoView() {
		this.setState({ infoActive: !this.state.infoActive });
	},

	toggleAboutView() {
		this.setState({ aboutActive: !this.state.aboutActive });
	},

	CancelReview(){
		return !this.props.reviewActive ? null : (

 {
				this.props.killReview(false)
			}}
			className="review active">

		);
	},

	boardMembersAmount() {
	   if(!this.props.board) return null;
	   return this.state.members.size;
   },

	render() {
		let infoDialog = null;
		let aboutDialog = null;
		let infoIcon = null;

		if(!this.state.infoActive) {
			infoIcon = 'question';
			infoDialog = null;
		} else {
			infoIcon = 'times';
			infoDialog = <InfoView onDismiss = { this.toggleInfoView }  board={this.props.board}/>;
		}

		if(!this.state.aboutActive) {
			aboutDialog = null;
		} else {
			aboutDialog = <AboutView onDismiss = { this.toggleAboutView } />;
		}

		let infoButtonClass =
			classNames({
				infobutton: true,
				active: this.state.infoActive
			});

		let userButtonClass =
			classNames({
				'avatar-wrapper': true,
				active: this.state.dropdown
			});

		let membersButtonClass =
			classNames({
				members: true,
				active: this.state.membersActive
			});

		let boardMembersDialog = null;

		if (this.state.membersActive) {
			boardMembersDialog = <MemberDialog members={this.state.members} onDismiss={this.toggleMembersDialog}/>
		}

		let showBoardMembers = !this.props.showBoardMembers ? null : (

						{this.boardMembersAmount()}

		);

		let showInfo = !this.props.showHelp ? null : (

				<span className={`fa fa-fw fa-${infoIcon}`}>

			);

		// If userstore is empty then go back to login
		if(!UserStore.getUser()) {
			page.redirect('/login');
		}

		let isProfileDisabled = UserStore.getUser().type === 'standard';
		let items = [
			{
				disabled: true,
				customclass: 'profile-name',
				content: `${this.locale('DROPDOWN_HELLO')}, ${UserStore.getUser().username}`
			},
			{
				icon: 'user',
				content: this.locale('DROPDOWN_PROFILE'),
				disabled: !isProfileDisabled,
				onClick: () => {
					if(isProfileDisabled) {
						return page.show('/profile');
					}
				}
			},
			{
				icon: 'language',
				content: this.locale('DROPDOWN_LOCALE'),
				onClick: () => {
					this.setState({ localesDropdown: !this.state.localesDropdown });
				}
			},
			{
				icon: 'bullhorn',
				nospan: true,
				content: (
					<UserVoice>
						{this.locale('DROPDOWN_FEEDBACK')}
					</UserVoice>
				)
			},
			{
				icon: 'info',
				content: this.locale('DROPDOWN_ABOUT'),
				onClick: () => {
					this.toggleAboutView();
				}
			},
			{
				icon: 'sign-out',
				content: this.locale('DROPDOWN_LOGOUT'),
				onClick: () => {
					UserAction.logout()
						.catch((err) => {
							BroadcastAction.add(err, Action.User.Logout);
						});
				}
			}
		];
		let locales = [
			{
				flag: 'gb',
				content: 'English',
				onClick: () => {
					SettingsAction.setSetting('locale', 'en');
					this.toggleDropdown();
				}
			},
			{
				flag: 'fi',
				content: 'Suomi',
				onClick: () => {
					SettingsAction.setSetting('locale', 'fi');
					this.toggleDropdown();
				}
			},
			{
				flag: 'se',
				content: 'Svenska',
				onClick: () => {
					SettingsAction.setSetting('locale', 'se');
					this.toggleDropdown();
				}
			},
			{
				flag: 'dk',
				content: 'Dansk',
				onClick: () => {
					SettingsAction.setSetting('locale', 'dk');
					this.toggleDropdown();
				}
			},
			{
				flag: 'jp',
				content: '日本語',
				onClick: () => {
					SettingsAction.setSetting('locale', 'jp');
					this.toggleDropdown();
				}
			}
		];

		let user      = UserStore.getUser();
		let name      = user.get('username');
		let avatarURL = user.get('avatar');
		let userType  = user.get('type');

		return (

{this.props.title}

				{this.CancelReview()}
				{showBoardMembers}
				{showInfo}

						<Avatar size={30} name={name}
								imageurl={avatarURL}
								isOnline={true}
								usertype={userType}>
						</Avatar>

				<Dropdown className="options" show={this.state.dropdown} items={items} />
				<Dropdown className="locales" show={this.state.localesDropdown} items={locales} />
				{infoDialog}
				{boardMembersDialog}
				{aboutDialog}

		);
	}
});
<commit_msg>Change member counter to show only active members.<commit_after>import page  	  from 'page';
import React 	  from 'react';
import Immutable  from 'immutable';
import classNames from 'classnames';

import Action          from '../actions';
import UserAction      from '../actions/user';
import SettingsAction  from '../actions/settings';
import BroadcastAction from '../actions/broadcast';

import UserStore    from '../stores/user';
import Avatar       from '../components/avatar';
import Dropdown     from '../components/dropdown';
import MemberDialog from '../components/dialog/board-members';

import UserVoice from '../components/user-voice';
import InfoView  from './dialog/view-info';
import AboutView from './dialog/view-about';

import Board from '../models/board';

import ActivityStore from '../stores/activity';

import listener    from '../mixins/listener';
import localeMixin from '../mixins/locale';

/**
 *
 */
export default React.createClass({
	mixins: [
		listener(ActivityStore),
		localeMixin()
	],

	propTypes: {
		title: React.PropTypes.string.isRequired,
		showHelp: React.PropTypes.bool,
		reviewActive: React.PropTypes.bool,
		killReview: React.PropTypes.func,
		board: (props) => {
			if(!props.board instanceof Board) throw new Error();
		}
	},

	onChange() {
		this.setState({
			members: this.props.board
				? ActivityStore.getActiveMembers(this.props.board.id)
				: Immutable.List()
		});
	},

	getInitialState() {
		return {
			members: this.props.board
				? ActivityStore.getActiveMembers(this.props.board.id)
				: Immutable.List(),
			dropdown: false, localesDropdown: false,
			feedback: false, infoActive: false,
			aboutActive: false, membersActive: false
		}
	},

	showWorkspace() {
		return page.show('/boards');
	},

	toggleMembersDialog() {
		this.setState({ membersActive: !this.state.membersActive });
	},

	toggleDropdown() {
		this.setState({ dropdown: !this.state.dropdown });
		if(this.state.localesDropdown) {
			this.setState({ localesDropdown: !this.state.localesDropdown });
		}
	},

	toggleInfoView() {
		this.setState({ infoActive: !this.state.infoActive });
	},

	toggleAboutView() {
		this.setState({ aboutActive: !this.state.aboutActive });
	},

	CancelReview(){
		return !this.props.reviewActive ? null : (

 {
				this.props.killReview(false)
			}}
			className="review active">

		);
	},

	boardMembersAmount() {
		if(!this.props.board) return null;

		return this.state.members.filter((member) => {
			return member.get('date') > new Date(new Date().getTime() - (5 * 60000));
		}).size;
   },

	render() {
		let infoDialog = null;
		let aboutDialog = null;
		let infoIcon = null;

		if(!this.state.infoActive) {
			infoIcon = 'question';
			infoDialog = null;
		} else {
			infoIcon = 'times';
			infoDialog = <InfoView onDismiss = { this.toggleInfoView }  board={this.props.board}/>;
		}

		if(!this.state.aboutActive) {
			aboutDialog = null;
		} else {
			aboutDialog = <AboutView onDismiss = { this.toggleAboutView } />;
		}

		let infoButtonClass =
			classNames({
				infobutton: true,
				active: this.state.infoActive
			});

		let userButtonClass =
			classNames({
				'avatar-wrapper': true,
				active: this.state.dropdown
			});

		let membersButtonClass =
			classNames({
				members: true,
				active: this.state.membersActive
			});

		let boardMembersDialog = null;

		if (this.state.membersActive) {
			boardMembersDialog = <MemberDialog members={this.state.members} onDismiss={this.toggleMembersDialog}/>
		}

		let showBoardMembers = !this.props.showBoardMembers ? null : (

						{this.boardMembersAmount()}

		);

		let showInfo = !this.props.showHelp ? null : (

				<span className={`fa fa-fw fa-${infoIcon}`}>

			);

		// If userstore is empty then go back to login
		if(!UserStore.getUser()) {
			page.redirect('/login');
		}

		let isProfileDisabled = UserStore.getUser().type === 'standard';
		let items = [
			{
				disabled: true,
				customclass: 'profile-name',
				content: `${this.locale('DROPDOWN_HELLO')}, ${UserStore.getUser().username}`
			},
			{
				icon: 'user',
				content: this.locale('DROPDOWN_PROFILE'),
				disabled: !isProfileDisabled,
				onClick: () => {
					if(isProfileDisabled) {
						return page.show('/profile');
					}
				}
			},
			{
				icon: 'language',
				content: this.locale('DROPDOWN_LOCALE'),
				onClick: () => {
					this.setState({ localesDropdown: !this.state.localesDropdown });
				}
			},
			{
				icon: 'bullhorn',
				nospan: true,
				content: (
					<UserVoice>
						{this.locale('DROPDOWN_FEEDBACK')}
					</UserVoice>
				)
			},
			{
				icon: 'info',
				content: this.locale('DROPDOWN_ABOUT'),
				onClick: () => {
					this.toggleAboutView();
				}
			},
			{
				icon: 'sign-out',
				content: this.locale('DROPDOWN_LOGOUT'),
				onClick: () => {
					UserAction.logout()
						.catch((err) => {
							BroadcastAction.add(err, Action.User.Logout);
						});
				}
			}
		];
		let locales = [
			{
				flag: 'gb',
				content: 'English',
				onClick: () => {
					SettingsAction.setSetting('locale', 'en');
					this.toggleDropdown();
				}
			},
			{
				flag: 'fi',
				content: 'Suomi',
				onClick: () => {
					SettingsAction.setSetting('locale', 'fi');
					this.toggleDropdown();
				}
			},
			{
				flag: 'se',
				content: 'Svenska',
				onClick: () => {
					SettingsAction.setSetting('locale', 'se');
					this.toggleDropdown();
				}
			},
			{
				flag: 'dk',
				content: 'Dansk',
				onClick: () => {
					SettingsAction.setSetting('locale', 'dk');
					this.toggleDropdown();
				}
			},
			{
				flag: 'jp',
				content: '日本語',
				onClick: () => {
					SettingsAction.setSetting('locale', 'jp');
					this.toggleDropdown();
				}
			}
		];

		let user      = UserStore.getUser();
		let name      = user.get('username');
		let avatarURL = user.get('avatar');
		let userType  = user.get('type');

		return (

{this.props.title}

				{this.CancelReview()}
				{showBoardMembers}
				{showInfo}

						<Avatar size={30} name={name}
								imageurl={avatarURL}
								isOnline={true}
								usertype={userType}>
						</Avatar>

				<Dropdown className="options" show={this.state.dropdown} items={items} />
				<Dropdown className="locales" show={this.state.localesDropdown} items={locales} />
				{infoDialog}
				{boardMembersDialog}
				{aboutDialog}

		);
	}
});
<|endoftext|>


<commit_before>
const express = require("express")
const morgan = require("morgan")
const cors = require("cors")
const compression = require("compression")
const helmet = require("helmet")
const config = require("./config.json")
const MongoClient = require("mongodb")
const Path = require("path")
const app = express()

// Define routes
const home  = require("./routes/v1-home")
const info  = require("./routes/v1-info")
const vehicles  = require("./routes/v1-vehicles")
const launchpad  = require("./routes/v1-launchpad")
const launches  = require("./routes/v1-launches")
const upcoming  = require("./routes/v1-upcoming")
const parts  = require("./routes/v1-parts")

// Enable gzip helmet,
// HTTP logging, and CORS
app.use(compression())
app.use(helmet())
app.use(morgan("common"))
app.use(cors())

// Routes
app.use("/v1", home)
app.use("/v1/info", info)
app.use("/v1/vehicles", vehicles)
app.use("/v1/launchpads", launchpad)
app.use("/v1/launches", launches)
app.use("/v1/launches/upcoming", upcoming)
app.use("/v1/parts", parts)

// 404 Error Handler
app.use((req, res) => {
  res.status(404).sendFile(Path.join(__dirname + "/pages/404.html"))
})

// Create MongoDB Connection
MongoClient.connect(config.url, (err, database) => {
  if (err) return console.log(err)
  global.db = database

  app.set("port", (process.env.PORT || 5000))
  app.listen(app.get("port"), "0.0.0.0", () => {
    console.log("SpaceX API listening on port: " + app.get("port"))
    app.emit("ready")
  })
})

module.exports = app
<commit_msg>edited comments for clarity<commit_after>const express = require("express")
const morgan = require("morgan")
const cors = require("cors")
const compression = require("compression")
const helmet = require("helmet")
const config = require("./config.json")
const MongoClient = require("mongodb")
const Path = require("path")
const app = express()

// Define route paths
const home  = require("./routes/v1-home")
const info  = require("./routes/v1-info")
const vehicles  = require("./routes/v1-vehicles")
const launchpad  = require("./routes/v1-launchpad")
const launches  = require("./routes/v1-launches")
const upcoming  = require("./routes/v1-upcoming")
const parts  = require("./routes/v1-parts")

// Enable middleware
app.use(compression())
app.use(helmet())
app.use(morgan("common"))
app.use(cors())

// Routes
app.use("/v1", home)
app.use("/v1/info", info)
app.use("/v1/vehicles", vehicles)
app.use("/v1/launchpads", launchpad)
app.use("/v1/launches", launches)
app.use("/v1/launches/upcoming", upcoming)
app.use("/v1/parts", parts)

// 404 Error Handler
app.use((req, res) => {
  res.status(404).sendFile(Path.join(__dirname + "/pages/404.html"))
})

// Create DB connection & start server
MongoClient.connect(config.url, (err, database) => {
  if (err) return console.log(err)
  global.db = database

  app.set("port", (process.env.PORT || 5000))
  app.listen(app.get("port"), "0.0.0.0", () => {
    console.log("SpaceX API listening on port: " + app.get("port"))
    app.emit("ready")
  })
})

module.exports = app
<|endoftext|>