catarse/catarse.js

View on GitHub
legacy/src/h.ts

Summary

Maintainability
F
1 wk
Test Coverage
import _ from 'underscore';
import moment from 'moment';
import * as Sentry from '@sentry/browser';
import m from 'mithril';
import prop from 'mithril/stream';
import { catarse } from './api';
import contributionVM from './vms/contribution-vm';
import generativeTrust from '../vendor/mithril-generative-trust';

function getCallStack() {
    const callStackStr = new Error().stack;
    const callStackLines = callStackStr.split('\n');
    const callStackTrimmedLines = callStackLines.map(d => d.trim());
    const realCallStack = callStackTrimmedLines.filter((k, i) => i > 0);
    return realCallStack;
}

function RedrawScheduler() {
    let redrawsRequestCounter = 0;
    const markedCallStack = {};
    const requestAnimationFramePolyfill = (function() {
        if (window.requestAnimationFrame !== undefined) {
            return window.requestAnimationFrame;
        } else {
            return function requesterTimeout(functionToCall) {
                setTimeout(functionToCall, 100);
            };
        }
    })();

    RedrawScheduler.schedule = () => {
        redrawsRequestCounter++;
        /// #if DEBUG
        markedCallStack[redrawsRequestCounter] = getCallStack();
        /// #endif
    };

    function start() {
        if (redrawsRequestCounter > 0) {
            /// #if DEBUG
            const callStack = markedCallStack[redrawsRequestCounter];
            try {
            /// #endif

                if (redrawsRequestCounter === 1) {
                    m.redraw();
                }

            /// #if DEBUG
            } catch(e) {
                console.log('redraw error:', e);
                console.log('callstack', callStack);
            }
            /// #endif

            redrawsRequestCounter = Math.max(0, --redrawsRequestCounter);
        }

        requestAnimationFramePolyfill(start);
    }

    start();
}

RedrawScheduler();

const { CatarseAnalytics, $ } = window;
const _dataCache = {},
    autoRedrawProp = startData => {
        const p = prop(startData);

        function dataUpdater(newData) {
            if (newData !== undefined) {
                p(newData);
                //m.redraw();
                RedrawScheduler.schedule();
            }

            return p();
        }

        dataUpdater.prototype = p;
        return dataUpdater;
    },
    hashMatch = str => window.location.hash === str,
    mobileScreen = () => window.screen && window.screen.width <= 767,
    paramByName = name => {
        const normalName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]'),
            regex = new RegExp(`[\\?&]${normalName}=([^&#]*)`),
            results = regex.exec(location.search);
        return results === null ? '' : decodeURIComponent(results[1].replace(/\+/g, ' '));
    },
    reduceSearchString = (/** @type {(keyValue:string) => any} */callback, initial) => window.location.search.replace('?', '').split('&').reduce(callback, initial),
    objectToSearchString = (obj) => '?' + Object.keys(obj).map(key => `${key}=${obj[key]}`).join('&'),
    setParamByName = (name, value) => {        
        const keysAndValues = reduceSearchString((finalQueryObject, keyValue) => {
            const [key, value] = keyValue.split('=');
            if (key) {
                finalQueryObject[key] = value;
            }
            return finalQueryObject;
        }, {});

        keysAndValues[name] = value;

        const queryString = objectToSearchString(keysAndValues);

        const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + queryString + (window.location.hash === '#' ? '' : window.location.hash);
        m.route.set(newurl);
    },
    setMultParams = (/** @type {{[key : string] : string}} */ objectParams) => {
        const keysAndValues = reduceSearchString((finalQueryObject, keyValue) => {
            const [key, value] = keyValue.split('=');
            if (key) {
                finalQueryObject[key] = value;
            }
            return finalQueryObject;
        }, {});

        const queryString = objectToSearchString(Object.assign(keysAndValues, objectParams));

        const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + queryString + (window.location.hash === '#' ? '' : window.location.hash);
        m.route.set(newurl);
    },
    setAndResetMultParams = function (/** @type {{[key : string] : string}} */ setParams) {
        const argumentsArray = Array.from(arguments);
        const resetParams = argumentsArray.length > 1 ? argumentsArray.slice(1) : [];
        const keysAndValues = reduceSearchString((finalQueryObject, keyValue) => {
            const [key, value] = keyValue.split('=');
            if (!resetParams.includes(key) && key) {
                finalQueryObject[key] = value;
            }
            return finalQueryObject;
        }, {});

        const queryString = objectToSearchString(Object.assign(keysAndValues, setParams));

        const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + queryString + (window.location.hash === '#' ? '' : window.location.hash);
        m.route.set(newurl);
    },
    setAndResetMultParamsArray = (/** @type {{[key : string] : string | number}} */ setParams, /** @type {string[]}*/ removeQuery = []) => {
        
        /** @type {Object} */ 
        const query = m.parseQueryString(window.location.search);
        
        removeQuery.forEach(param => {
            if (param in query) {
                delete query[param];
            }
        });

        const queryString = objectToSearchString(Object.assign(query, setParams));

        const newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + queryString + (window.location.hash === '#' ? '' : window.location.hash);
        m.route.set(newurl);
    },
    removeParamByName = (name) => {

        const keysAndValues = reduceSearchString((finalQueryObject, keyValue) => {

            const [key, value] = keyValue.split('=');
            if (name !== key && key) {
                finalQueryObject[key] = value;
            }
            return finalQueryObject;
        }, {});

        const queryString = objectToSearchString(keysAndValues);

        const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + queryString + (window.location.hash === '#' ? '' : window.location.hash);
        m.route.set(newurl);
    },
    removeMultParams = function () {
        const paramsNamesToRemove = Array.from(arguments);

        const keysAndValues = reduceSearchString((finalQueryObject, keyValue) => {

            const [key, value] = keyValue.split('=');            
            if (!paramsNamesToRemove.includes(key) && key) {
                finalQueryObject[key] = value;
            }
            return finalQueryObject;
        }, {});
        
        const queryString = objectToSearchString(keysAndValues);

        const newurl = window.location.protocol + "//" + window.location.host + window.location.pathname + queryString + (window.location.hash === '#' ? '' : window.location.hash);
        m.route.set(newurl);
    },
    selfOrEmpty = (obj, emptyState = '') => obj || emptyState,
    setMomentifyLocale = () => {
        moment.locale('pt', {
            months: 'Janeiro_Fevereiro_Março_Abril_Maio_Junho_Julho_Agosto_Setembro_Outubro_Novembro_Dezembro'.split('_'),
            monthsShort: 'jan_fev_mar_abr_mai_jun_jul_ago_set_out_nov_dez'.split('_'),
            relativeTime: {
                future: 'em %s',
                past: 'há %s',
                s: 'segundos',
                m: 'um minuto',
                mm: '%d minutos',
                h: 'uma hora',
                hh: '%d horas',
                d: 'um dia',
                dd: '%d dias',
                M: 'um mês',
                MM: '%d meses',
                y: 'um ano',
                yy: '%d anos',
            },
        });
    },
    lastDayOfNextMonth = () =>
        moment()
            .add(1, 'months')
            .format('D/MMMM'),
    existy = x => x != null,
    slugify = str =>
        window.replaceDiacritics(
            str
                .toLowerCase()
                .replace(/ /g, '-')
                .replace(/[^\w-]+/g, '')
        ),
    momentify = (date, format) => {
        format = format || 'DD/MM/YYYY';
        return date
            ? moment(date)
                  .locale('pt')
                  .format(format)
            : 'no date';
    },
    getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min,
    storeAction = (action, value) => {
        if (!localStorage.getItem(action)) {
            return localStorage.setItem(action, String(value));
        }
    },
    storeObject = (sessionKey, obj) => sessionStorage.setItem(sessionKey, JSON.stringify(obj)),
    getStoredObject = sessionKey => {
        if (sessionStorage.getItem(sessionKey)) {
            return JSON.parse(String(sessionStorage.getItem(sessionKey)));
        }
        return null;
    },
    callStoredAction = action => {
        const item = localStorage.getItem(action);

        if (item) {
            localStorage.removeItem(action);
            return item;
        }
        return null;
    },
    capitalize = string => string.charAt(0).toUpperCase() + string.slice(1),
    discuss = (page, identifier) => {
        const d = document,
            s = d.createElement('script');
        window.disqus_config = function() {
            this.page.url = page;
            this.page.identifier = identifier;
        };
        s.src = '//catarseflex.disqus.com/embed.js';
        s.setAttribute('data-timestamp', String(+new Date()));
        (d.head || d.body).appendChild(s);
        return m('');
    },
    validateEmail = email => {
        const re = /^(([^<>()[\]\.,;:\s@\"]+(\.[^<>()[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
        return re.test(email);
    },
    validateCnpj = cnpjStr => {
        let tamanho, numeros, digitos, soma, pos, resultado;
        const cnpj = cnpjStr.replace(/[^\d]+/g, '');

        if (cnpj == '') {
            return false;
        }

        if (cnpj.length != 14) {
            return false;
        }

        if (
            cnpj == '00000000000000' ||
            cnpj == '11111111111111' ||
            cnpj == '22222222222222' ||
            cnpj == '33333333333333' ||
            cnpj == '44444444444444' ||
            cnpj == '55555555555555' ||
            cnpj == '66666666666666' ||
            cnpj == '77777777777777' ||
            cnpj == '88888888888888' ||
            cnpj == '99999999999999'
        ) {
            return false;
        }

        tamanho = cnpj.length - 2;
        numeros = cnpj.substring(0, tamanho);
        digitos = cnpj.substring(tamanho);
        soma = 0;
        pos = tamanho - 7;

        for (let i = tamanho; i >= 1; i--) {
            soma += Number(numeros.charAt(tamanho - i)) * pos--;
            if (pos < 2) {
                pos = 9;
            }
        }
        resultado = soma % 11 < 2 ? 0 : 11 - (soma % 11);
        if (String(resultado) != digitos.charAt(0)) {
            return false;
        }

        tamanho += 1;
        numeros = cnpj.substring(0, tamanho);
        soma = 0;
        pos = tamanho - 7;
        for (let i = tamanho; i >= 1; i--) {
            soma += Number(numeros.charAt(tamanho - i)) * pos--;
            if (pos < 2) {
                pos = 9;
            }
        }
        resultado = soma % 11 < 2 ? 0 : 11 - (soma % 11);
        if (String(resultado) != digitos.charAt(1)) {
            return false;
        }

        return true;
    },
    validateCpf = strCPF => {
        let sum = 0,
            remainder;

        if (strCPF == '00000000000') return false;

        for (let i = 1; i <= 9; i++) {
            sum += parseInt(strCPF.substring(i - 1, i)) * (11 - i);
        }
        remainder = (sum * 10) % 11;

        if (remainder == 10 || remainder == 11) {
            remainder = 0;
        }

        if (remainder != parseInt(strCPF.substring(9, 10))) {
            return false;
        }

        sum = 0;

        for (let i = 1; i <= 10; i++) {
            sum += parseInt(strCPF.substring(i - 1, i)) * (12 - i);
        }

        remainder = (sum * 10) % 11;

        if (remainder == 10 || remainder == 11) {
            remainder = 0;
        }

        if (remainder != parseInt(strCPF.substring(10, 11))) {
            return false;
        }

        return true;
    },
    validationErrors = prop([]),
    resetValidations = () => validationErrors([]),
    validate = () => {
        const errorFields = prop([]);

        return {
            submit(fields, fn) {
                return () => {
                    resetValidations();

                    _.map(fields, field => {
                        if (field.rule === 'email') {
                            if (!validateEmail(field.prop())) {
                                validationErrors().push({
                                    field: field.prop,
                                    message: 'E-mail inválido.',
                                });
                            }
                        }

                        if (field.rule === 'text') {
                            if (field.prop().trim() === '') {
                                validationErrors().push({
                                    field: field.prop,
                                    message: 'O campo não pode ser vazio.',
                                });
                            }
                        }
                    });

                    return !validationErrors().length > 0 ? fn() : false;
                };
            },
            hasError(fieldProp) {
                return _.reduce(validationErrors(), (memo, fieldError) => fieldError.field() === fieldProp() || memo, false);
            },
        };
    },
    momentFromString = (date, format) => {
        const european = moment(date, format || 'DD/MM/YYYY');
        return european.isValid() ? european : moment(date);
    },
    translatedTimeUnits = {
        days: 'dias',
        minutes: 'minutos',
        hours: 'horas',
        seconds: 'segundos',
    },
    // Object manipulation helpers
    translatedTime = time => {
        const translatedTime = translatedTimeUnits,
            unit = () => {
                const projUnit = translatedTime[time.unit || 'seconds'];

                return Number(time.total) <= 1 ? projUnit.slice(0, -1) : projUnit;
            };

        return {
            unit: unit(),
            total: time.total,
        };
    },
    // Number formatting helpers
    generateFormatNumber = (s, c) => (number, n, x) => {
        if (!_.isNumber(number)) {
            return null;
        }

        const re = `\\d(?=(\\d{${x || 3}})+${n > 0 ? '\\D' : '$'})`,
            num = number.toFixed(Math.max(0, ~~n));
        return (c ? num.replace('.', c) : num).replace(new RegExp(re, 'g'), `$&${s || ','}`);
    },
    formatNumber = generateFormatNumber('.', ','),
    toggleProp = (defaultState, alternateState) => {
        const p = prop(defaultState);
        p.toggle = () => p(p() === alternateState ? defaultState : alternateState);

        return p;
    },
    idVM = catarse.filtersVM({
        id: 'eq',
    }),
    isDevEnv = () => {
        const root = document.getElementById('catarse_bootstrap'),
            data = root && root.getAttribute('data-environment');

        return data && data == 'development';
    },
    getCurrentProject = () => {
        if (_dataCache.currentProject) {
            return _dataCache.currentProject;
        }

        const root = document.getElementById('application'),
            data = root && root.getAttribute('data-parameters');
        if (data) {
            return (_dataCache.currentProject = JSON.parse(data));
        }
        return null;
    },
    getRdToken = () => {
        if (_dataCache.rdToken) {
            return _dataCache.rdToken;
        }

        const meta = _.first(document.querySelectorAll('[name=rd-token]'));
        return meta ? (_dataCache.rdToken = meta.getAttribute('content')) : null;
    },
    getNewsletterUrl = () => {
        if (_dataCache.newsletterUrl) {
            return _dataCache.newsletterUrl;
        }

        const meta = _.first(document.querySelectorAll('[name=newsletter-url]'));
        return meta ? (_dataCache.newsletterUrl = meta.getAttribute('content')) : null;
    },
    getUser = () => {
        if (_dataCache.user) {
            return _dataCache.user;
        }

        const body = document.getElementsByTagName('body'),
            data = _.first(body).getAttribute('data-user');
        if (data) {
            return (_dataCache.user = JSON.parse(data));
        }
        return null;
    },
    getUserID = () => {
        const user = getUser();
        return user == null || user.user_id == null ? null : user.user_id;
    },
    getUserCommonID = () => {
        const user = getUser();
        return user && user.common_id
    },
    userSignedIn = () => !_.isNull(getUserID()),
    getBlogPosts = () => {
        if (_dataCache.blogPosts) {
            return _dataCache.blogPosts;
        }

        const posts = _.first(document.getElementsByTagName('body')).getAttribute('data-blog');

        if (posts) {
            return (_dataCache.blogPosts = JSON.parse(posts));
        }
        return null;
    },
    getApiHost = () => {
        if (_dataCache.apiHost) {
            return _dataCache.apiHost;
        }

        const el = document.getElementById('api-host');
        return (_dataCache.apiHost = el && el.getAttribute('content'));
    },
    locationActionMatch = action => {
        const act = window.location.pathname.split('/').slice(-1)[0];
        return action === act;
    },
    useAvatarOrDefault = avatarPath => avatarPath || '/assets/catarse_bootstrap/user.jpg',
    // Templates
    loader = () => m('.u-text-center.u-margintop-30.u-marginbottom-30', [m('img[alt="Loader"][src="https://s3.amazonaws.com/catarse.files/loader.gif"]')]),
    loaderWithSize = (width, height) => m(`img[alt="Loader"][width=${width}][height=${height || width}][src="https://s3.amazonaws.com/catarse.files/loader.gif"]`),
    newFeatureBadge = () => m('span.badge.badge-success.margin-side-5', window.I18n.t('projects.new_feature_badge')),
    fbParse = () => {
        const tryParse = () => {
            try {
                window.FB.XFBML.parse();
            } catch (e) {
                // console.log(e);
            }
        };

        return window.setTimeout(tryParse, 500); // use timeout to wait async of facebook
    },
    pluralize = (count, s, p) => (count > 1 ? count + p : count + s),
    strip = html => {
        const tmp = document.createElement('div');
        tmp.innerHTML = html;
        return tmp.textContent || tmp.innerText || '';
    },
    simpleFormat = (str = '') => {
        str = str.replace(/\r\n?/, '\n');
        if (str.length > 0) {
            str = str.replace(/\n\n+/g, '</p><p>');
            str = str.replace(/\n/g, '<br />');
            str = `<p>${str}</p>`;
        }
        return str;
    },
    rewardSouldOut = reward => {
        const noRemainingRewards = reward.maximum_contributions > 0 ? reward.paid_count + reward.waiting_payment_count >= reward.maximum_contributions : false;
        return noRemainingRewards || reward.run_out;
    },
    rewardRemaning = reward => reward.maximum_contributions - (reward.paid_count + reward.waiting_payment_count),
    parseUrl = href => {
        const l = document.createElement('a');
        l.href = href;
        return l;
    },
    UIHelper = () => vnode => {
        if (window.$ && window.UIHelper) {
            window.UIHelper.setupResponsiveIframes($(vnode.dom));
        }
    },
    toAnchor = () => vnode => {
        const hash = window.location.hash.substr(1);
        if (hash === vnode.dom.id) {
            window.location.hash = '';
            setTimeout(() => {
                window.location.hash = vnode.dom.id;
            });
        }
    },
    navigateToDevise = params => {
        if (params) {
            window.location.href = `/${window.I18n.locale}/login${params}`;
        } else {
            window.location.href = `/${window.I18n.locale}/login`;
        }

        return false;
    },
    navigateTo = path => {
        window.location.href = path;
        return false;
    },
    cumulativeOffset = element => {
        let top = 0,
            left = 0;
        do {
            top += element.offsetTop || 0;
            left += element.offsetLeft || 0;
            element = element.offsetParent;
        } while (element);

        return {
            top,
            left,
        };
    },
    closeModal = () => {
        // Temp for rails unstyled close links
        const close = (elm, selector) => {
            const all = document.getElementsByClassName(selector);
            let cur = elm.parentNode;
            while (cur && !_.contains(all, cur)) {
                cur = cur.parentNode;
            }
            if (cur) {
                cur.style.display = 'none';
            }
            return cur;
        };

        const elById = document.getElementById('modal-close');
        if (_.isElement(elById)) {
            elById.onclick = event => {
                event.preventDefault();
                close(elById, 'modal-backdrop');
            };
        }

        const els = document.getElementsByClassName('modal-close');
        _.map(els, el => {
            if (_.isElement(el)) {
                el.onclick = event => {
                    event.preventDefault();
                    close(el, 'modal-backdrop');
                };
            }
        });
    },
    closeFlash = () => {
        const el = document.getElementsByClassName('icon-close')[0];
        if (_.isElement(el)) {
            el.onclick = event => {
                event.preventDefault();
                if (el.parentElement) {
                    el.parentElement.remove();
                }
            };
        }
    },
    i18nScope = (scope, obj) => {
        obj = obj || {};
        return _.extend({}, obj, {
            scope,
        });
    },
    redrawHashChange = before => {
        const callback = _.isFunction(before)
            ? () => {
                    before();
                    scrollTop();
                    redraw();
              }
            : () => {
                scrollTop();
                redraw();
            }

        window.addEventListener('hashchange', callback, false);
    },
    authenticityToken = () => {
        const meta = _.first(document.querySelectorAll('[name=csrf-token]'));
        return meta ? meta.getAttribute('content') : null;
    },
    authenticityParam = () => {
        const meta = _.first(document.querySelectorAll('[name=csrf-param]'));
        return meta ? meta.getAttribute('content') : null;
    },
    animateScrollTo = el => {
        let scrolled = window.scrollY;

        const offset = cumulativeOffset(el).top,
            duration = 300,
            dFrame = (offset - scrolled) / duration,
            // EaseInOutCubic easing function. We'll abstract all animation funs later.
            eased = t => (t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1),
            animation = setInterval(() => {
                const pos = eased(scrolled / offset) * scrolled;

                window.scrollTo(0, pos);

                if (scrolled >= offset) {
                    clearInterval(animation);
                }

                scrolled += dFrame;
            }, 1);
    },
    scrollTop = () => window.scrollTo(0, 0),
    scrollTo = () => {
        const setTrigger = (el, anchorId) => {
            el.onclick = () => {
                const anchorEl = document.getElementById(anchorId);

                if (_.isElement(anchorEl)) {
                    animateScrollTo(anchorEl);
                }

                return false;
            };
        };

        return localVnode => {
            if (localVnode.dom.hash) {
                setTrigger(localVnode.dom, localVnode.dom.hash.slice(1));
            }
        };
    },
    projectStateTextClass = (state, has_cancelation_request) => {
        const statusText = {
            online: {
                cssClass: 'text-success',
                text: 'NO AR',
            },
            successful: {
                cssClass: 'text-success',
                text: 'FINANCIADO',
            },
            failed: {
                cssClass: 'text-error',
                text: 'NÃO FINANCIADO',
            },
            waiting_funds: {
                cssClass: 'text-waiting',
                text: 'AGUARDANDO',
            },
            rejected: {
                cssClass: 'text-error',
                text: 'CANCELADO',
            },
            draft: {
                cssClass: '',
                text: 'RASCUNHO',
            },
            in_analysis: {
                cssClass: '',
                text: 'EM ANÁLISE',
            },
            approved: {
                cssClass: 'text-success',
                text: 'APROVADO',
            },
        };

        if (has_cancelation_request) {
            return {
                cssClass: 'text-error',
                text: 'AGUARDANDO CANCELAMENTO',
            };
        }
        return statusText[state];
    },
    RDTracker = eventId => () => {
        const integrationScript = document.createElement('script');
        integrationScript.type = 'text/javascript';
        integrationScript.id = 'RDIntegration';

        if (!document.getElementById(integrationScript.id)) {
            document.body.appendChild(integrationScript);
            integrationScript.onload = () => window.RdIntegration.integrate(getRdToken(), eventId);
            integrationScript.src = 'https://d335luupugsy2.cloudfront.net/js/integration/stable/rd-js-integration.min.js';
        }

        return false;
    },
    analyticsEvent = (eventObj, fn = Function.prototype) => {
        // https://developers.google.com/analytics/devguides/collection/analyticsjs/command-queue-reference#send
        if (!eventObj) {
            return fn;
        }

        return data => {
            try {
                if (!eventObj.project) {
                    eventObj.project = getCurrentProject();
                }
                if (!eventObj.user) {
                    eventObj.user = getUser();
                }
                CatarseAnalytics.event(eventObj);
            } catch (e) {
                // console.error('[h.analyticsEvent] error:', e);
            }
            fn(data);
        };
    },
    _analyticsOneTimeEventFired = {},
    analyticsOneTimeEvent = (eventObj, fn) => {
        if (!eventObj) {
            return fn;
        }

        const eventKey = _.compact([eventObj.cat, eventObj.act]).join('_');
        if (!eventKey) {
            throw new Error('Should inform cat or act');
        }
        return () => {
            if (!_analyticsOneTimeEventFired[eventKey]) {
                // console.log('oneTimeEvent',eventKey);
                _analyticsOneTimeEventFired[eventKey] = true;
                const fireEvent = analyticsEvent(eventObj, fn);
                fireEvent();
            }
        };
    },
    monetaryToFloat = propValue => {
        if (_.isNumber(propValue())) {
            return parseFloat(propValue());
        }

        return parseFloat(
            propValue()
                .replace('.', '')
                .replace(',', '.')
        );
    },
    applyMonetaryMask = number => {
        let onlyNumbers = String(number).replace(/[^0-9]|[.]/g, '');
        let integerPart = onlyNumbers.slice(0, onlyNumbers.length - 2);
        let decimalPart = onlyNumbers.slice(onlyNumbers.length - 2);
        let i = 0;
        for (i = 0; i < integerPart.length; i += 1) {
            if (integerPart.charAt(i) !== '0') {
                break;
            }
        }

        if (i > 0) {
            integerPart = integerPart.slice(i, integerPart.length);
        }

        integerPart = integerPart.length === 0 ? '0' : integerPart;
        integerPart = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');

        return `${integerPart},${decimalPart}`;
    },
    noNumbersMask = value => value.replace(/[0-9]/g, ''),
    numbersOnlyMask = value => value.replace(/[^0-9]/g, ''),
    addChar = (position, maskChar) => char => string => {
        if (string.length === position && char !== maskChar) {
            return string + maskChar;
        }
        return string;
    },
    readMaskDefinition = maskCharDefinitions => maskDefinition =>
        _.compact(_.map(maskDefinition, (letter, index) => (letter in maskCharDefinitions ? null : [index, letter]))),
    isCharAllowed = maskCharDefinitions => maskDefinition => (position, newChar) => {
        if (position >= maskDefinition.length) {
            return false;
        }

        const maskChar = maskDefinition.charAt(position);
        if (maskChar in maskCharDefinitions) {
            return maskCharDefinitions[maskChar].test(newChar);
        }
        return newChar === maskChar || isCharAllowed(maskCharDefinitions)(maskDefinition)(position + 1, newChar);
    },
    applyMask = maskDefinition => {
        const maskFunctions = _.map(maskDefinition, maskChar => addChar(maskChar[0], maskChar[1]));
        return (string, newChar) => {
            const addNewCharFunctions = _.map(maskFunctions, el => el(newChar));
            const applyMaskFunctions = _.reduce(addNewCharFunctions, (memo, f) =>
                _.isFunction(memo)
                    ? _.compose(
                          f,
                          memo
                      )
                    : f
            );
            return applyMaskFunctions(string);
        };
    },
    // Adapted from https://github.com/diogob/jquery.fixedmask
    mask = (maskDefinition, value) => {
        const maskCharDefinitions = {
                9: /\d/, // String key needed to avoid flowType error
                A: /[a-zA-Z]/,
            },
            readMask = readMaskDefinition(maskCharDefinitions),
            isStrCharAllowed = isCharAllowed(maskCharDefinitions),
            applyValueMask = applyMask(readMask(maskDefinition)),
            restrictInput = isStrCharAllowed(maskDefinition);

        return _.reduce(
            value,
            (memo, chr) => {
                if (restrictInput(memo.length, chr)) {
                    memo = applyValueMask(memo, chr) + chr;
                }
                return memo;
            },
            ''
        );
    },
    removeStoredObject = sessionKey => sessionStorage.removeItem(sessionKey),
    currentProject = prop(),
    setProject = project => {
        currentProject(project);
    },
    getProject = () => currentProject,
    currentReward = prop(),
    setReward = reward => {
        currentReward(reward);
    },
    getReward = () => currentReward,
    buildLink = (link, refStr) => `/${link}${refStr ? `?ref=${refStr}` : ''}`,
    analyticsWindowScroll = eventObj => {
        if (eventObj) {
            setTimeout(() => {
                const u = window.location.href;
                let fired = false;
                window.addEventListener('scroll', function sc(e) {
                    //console.log('windowScroll');
                    const same = window.location.href === u;
                    if (same && !fired && window.$ && $(document).scrollTop() > $(window).height() / 2) {
                        fired = true;
                        const fireEvent = analyticsEvent(eventObj);
                        fireEvent();
                        window.removeEventListener('scroll', sc);
                    } else if (!same) {
                        window.removeEventListener('scroll', sc);
                    }
                });
            }, 1000);
        }
    },
    analytics = {
        event: analyticsEvent,
        oneTimeEvent: analyticsOneTimeEvent,
        windowScroll: analyticsWindowScroll,
    },
    projectFullPermalink = project => {
        let permalink;
        if (typeof project === 'function') {
            permalink = project().permalink;
        } else {
            permalink = project.permalink;
        }

        return `https://www.catarse.me/${permalink}`;
    },
    isHome = () => {
        const path = window.location.pathname;

        return path == '/en' || path == '/';
    },
    isProjectPage = () => {
        const path = window.location.pathname || '',
            isOnInsights = path.indexOf('/insights') > -1,
            isOnFiscal = path.indexOf('/fiscal') > -1,
            isOnEdit = path.indexOf('/edit') > -1,
            isOnContribution = path.indexOf('/contribution') > -1;

        return !isOnEdit && !isOnInsights && !isOnContribution && !isOnFiscal;
    },
    setPageTitle = title => vnode => {
        const titleEl = document.getElementsByTagName('title')[0],
            currentTitle = titleEl.innerText;

        if (currentTitle !== title) {
            return (titleEl.innerText = title);
        }
    },
    checkReminder = () => {
        const reminder = sessionStorage.getItem('reminder');

        if (reminder && isHome()) {
            window.location.href = `/projects/${reminder}`;
        }
    },
    rootUrl = () => {
        if (_dataCache.rootUrl) {
            return _dataCache.rootUrl;
        }

        const meta = _.first(document.querySelectorAll('[name=root-url]'));

        return meta ? (_dataCache.rootUrl = meta.getAttribute('content')) : null;
    },
    redactorConfig = params => ({
        source: false,
        formatting: ['p'],
        formattingAdd: [
            {
                tag: 'blockquote',
                title: 'Citar',
                class: 'fontsize-base quote',
                clear: true,
            },

            {
                tag: 'p',
                title: 'Cabeçalho 1',
                class: 'fontsize-larger fontweight-semibold',
                clear: true,
            },
            {
                tag: 'p',
                title: 'Cabeçalho 2',
                class: 'fontsize-large',
                clear: true,
            },
        ],
        lang: 'pt_br',
        maxHeight: 800,
        minHeight: 300,
        convertVideoLinks: true,
        convertUrlLinks: true,
        convertImageLinks: false,
        // You can specify, which ones plugins you need.
        // If you want to use plugins, you have add plugins to your
        // application.js and application.css files and uncomment the line below:
        // "plugins": ['fontsize', 'fontcolor', 'fontfamily', 'fullscreen', 'textdirection', 'clips'],
        plugins: ['video'],
        imageUpload: `/redactor_rails/pictures?${params}`,
        imageGetJson: '/redactor_rails/pictures',
        path: '/assets/redactor-rails',
        css: 'style.css',
    }),
    setRedactor = (
        prop,
        isInit = false //(el, isInit) => {
    ) => vnode => {
        if (!isInit) {
            const el = vnode.dom;
            const $editor = window.$(el);
            const csrf_token = authenticityToken();
            const csrf_param = authenticityParam();
            let params = '';
            if (csrf_param && csrf_token) {
                params = `${csrf_param}=${encodeURIComponent(csrf_token)}`;
            }
            $editor.redactor(redactorConfig(params));
            $editor.redactor('code.set', prop());
            // If we need to get redactor values and send it to js objects we'll have to add
            // a hook on the change.callback.redactor event. e.g.:
            // $editor.on('change.callback.redactor', () => prop($editor.redactor('code.get')) );
            // TODO: workaround to get redactor data
            window.$('.redactor-editor').on('blur', (event) => {
                prop($editor.redactor('code.get'));
                if (vnode.attrs.onblur) {
                    vnode.attrs.onblur(event);
                }
            });

            window.$('.redactor-editor').on('focus', (event) => {
                if (vnode.attrs.onfocus) {
                    vnode.attrs.onfocus(event);
                }
            });
        }
    },
    redactor = (name, prop, aditionalClasses = '') =>
        m(`textarea.input_field.redactor.w-input.text-field.bottom.jumbo.positive${aditionalClasses}`, {
            name,
            oncreate: setRedactor(prop),
        }),
    setCsrfToken = xhr => {
        if (authenticityToken()) {
            xhr.setRequestHeader('X-CSRF-Token', authenticityToken());
        }
    },
    contributionStatusBadge = contribution => {
        const status = {
            delivered: m('span.fontsize-smallest.badge.badge-success', 'Enviada'),
            received: m('span.fontsize-smallest.badge.badge-success', 'Recebida'),
            undelivered: m('span.fontsize-smallest.badge.badge-light', 'Não enviada'),
            error: m('span.fontsize-smallest.badge.badge-attention', 'Erro no envio'),
        };

        return contributionVM.canBeDelivered(contribution) ? status[contribution.delivery_status] : '';
    },
    getParams = searchKey => {
        const query = window.location.href;
        const queryParams = (/^[?#]/.test(query) ? query.slice(1) : query).split('?');

        return queryParams.length > 1
            ? queryParams[1].split('&').reduce((params, param) => {
                  const [key, value] = param.split('=');
                  params[key] = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : '';
                  return params;
              }, {})[searchKey]
            : null;
    },
    stripScripts = s => {
        const div = document.createElement('div');
        div.innerHTML = s;
        const scripts = div.getElementsByTagName('script');
        let i = scripts.length;
        while (i--) {
            scripts[i].parentNode.removeChild(scripts[i]);
        }
        return div.innerHTML;
    },
    sleep = time => {
        const p = new Promise((resolve, reject) => {
            setTimeout(resolve, time);
        });

        return p;
    },
    createRequestRedrawWithCountdown = countdown => {
        countdown = countdown || 0;
        return () => {
            countdown = Math.max(0, countdown - 1);
            if (countdown <= 0) {
                m.redraw();
            }
        };
    },
    createRequestAutoRedraw = function() {
        return createRequestRedrawWithCountdown(arguments.length);
    },
    redraw = function() {
        RedrawScheduler.schedule();
    },
    createBasicPaginationVMWithAutoRedraw = vmInstance => {
        const error = prop(false);
        const errorMessage = prop('');

        return {
            isLastPage() {
                const loadedAmount = Number(vmInstance.collection().length || 0);
                return loadedAmount >= vmInstance.total();
            },
            isLoading: vmInstance.isLoading,
            collection: vmInstance.collection,
            total() {
                try {
                    return Number(vmInstance.total() || 0);
                } catch(e) {
                    return 0;
                }
            },
            error,
            errorMessage,
            async firstPage(params = {}) {
                error(false);
                errorMessage('');
                try {
                    return await vmInstance.firstPage(params);
                } catch(e) {
                    error(true);
                    errorMessage(e);
                    throw e;
                } finally {
                    redraw();
                }
            },
            async nextPage() {
                error(false);
                errorMessage('');
                try {
                    return await vmInstance.nextPage();
                } catch(e) {
                    error(true);
                    errorMessage(e);
                    throw e;
                } finally {
                    redraw();
                }
            },
        };
    },
    trust = text => generativeTrust(text, { eliminateScriptTags: true }),
    SentryInitSDK = () => {
        const metaSentryUrlDSN = document.querySelector('[name="sentry-public-dsn"]');

        if (metaSentryUrlDSN && metaSentryUrlDSN.getAttribute('content')) {
            Sentry.init({ dsn: metaSentryUrlDSN.getAttribute('content') });
            if (getUserID()) {
                Sentry.configureScope(scope => scope.setUser({ id: getUserID() }));
            }
        }
    },
    captureException = (exception) => {
        try {
            Sentry.captureException(exception);
        } catch (e) {
            Sentry.captureException(e);
        }
    },
    captureMessage = (message) => {
        try {
            Sentry.captureMessage(message);
        } catch (e) {
            Sentry.captureException(e);
        }
    },
    titleCase = (str) => {
        // remove leading and trailing spaces
        let newString = str ? str.trim() : '';
        // remove multiple spaces
        newString = newString.replace(/\s{2,}/g, ' ');
        // lowercase
        newString = newString.toLowerCase();

        return newString.split(' ').map(function(word) {
            if (['de', 'da', 'do', 'das', 'dos'].includes(word)) {
                return word.toLowerCase()
            } else {
                return word ? word.replace(word[0], word[0].toUpperCase()) : '';
            }
        }).join(' ');
    },
    buildCreditCard = (creditCardFields) => {
        let expirationMonth = String(creditCardFields.expMonth());
        expirationMonth = (1 === expirationMonth.length ? '0' : '') + expirationMonth;
        const expirationDate = expirationMonth + String(creditCardFields.expYear()).substr(2, 2);

        return {
            card_number: creditCardFields.number(),
            card_holder_name: titleCase(creditCardFields.name()),
            card_expiration_date: expirationDate,
            card_cvv: creditCardFields.cvv()
        }
    }


/**
 * @param {string} phoneNumberStr
 * @return {string}
 */
const extractPhoneDDD = (phoneNumberStr) => {
    const extractPhoneFieldsRegex = /\(([^)]*)\)(.*)/;
    return phoneNumberStr.match(extractPhoneFieldsRegex)[1].replace(/\D/g, '');
};

/**
 * @param {string} phoneNumberStr
 * @return {string}
 */
const extractPhoneNumber = (phoneNumberStr) => {
    const extractPhoneFieldsRegex = /\(([^)]*)\)(.*)/;
    return phoneNumberStr.match(extractPhoneFieldsRegex)[2].replace(/\D/g, '');
};

/**
 * @param {T} data
 * @template T
 */
function ObservableStream(data) {

    /**
     * @type {Array<function(T):void>}
     */
    const observers = [];
    const privateData = prop(data);

    /**
     * @return {T}
     */
    function get() {
        return privateData();
    }

    /**
     * @param {T} newData
     * @return {T}
     */
    function set(newData) {
        privateData(newData);
        notifyAll();
        return newData;
    }

    /**
     * @param {function(T):void} observeFunction
     */
    function observe(observeFunction) {
        observers.push(observeFunction);
    }

    /**
     * @param {T} newData
     */
    function notifyAll(newData) {
        for (const observeFunction of observers) {
            observeFunction(newData);
        }
    }

    return {
        get,
        set,
        observe,
    }
}

/**
 * @param {T} data
 * @template T
 */
function ObservableRedrawStream(data) {
    const observableStream = ObservableStream(data);
    observableStream.observe(redraw);
    return observableStream;
}

function RedrawStream<T>(data : T, onUpdate = (param : T) => {}) {

    const _data = prop<T>(data);

    /**
     * @param {T} newData
     * @returns {T}
     */
    function streamAccessor(newData : T) {
        if (newData !== undefined) {
            _data(newData);
            onUpdate(newData);
            redraw();
            return newData;
        }
        return _data();
    }

    return streamAccessor;
}

/**
 * @param {T} data
 * @template T
 * @returns {{ (newData : T) => T, toggle() : T }}
 */
function RedrawToggleStream(firstState, secondState) {
    const _data = prop(firstState);

    /**
     * @param {T} newData
     * @returns {T}
     */
    function streamAccessor(newData) {
        if (newData !== undefined) {
            _data(newData);
            redraw();
            return newData;
        }
        return _data();
    }

    streamAccessor.toggle = () => streamAccessor(_data() === firstState ? secondState : firstState);

    return streamAccessor;   
}

function createPropAcessors(obj) {
    return Object.keys(obj).reduce((curObject, prop) => {
        return Object.assign(curObject, {
            [prop]: function (newValue) {
                if (newValue !== undefined) {
                    obj[prop] = newValue;
                    return newValue;
                }
                return obj[prop];
            }
        });
    }, {});
}

setMomentifyLocale();
closeFlash();
closeModal();
checkReminder();

function attachEventsToHistory(type) {
    var orig = history[type];
    return function() {
        var rv = orig.apply(this, arguments);
        var e = new Event(type.toLowerCase());
        e.arguments = arguments;
        window.dispatchEvent(e);
        return rv;
    };
};

function isMobile() {
    return (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase()));
}

function clamp(value : number, min : number, max : number) : number {
    return Math.min(Math.max(value, min), max)
}

/**
 * @typedef VNode
 * @property {Object} attrs
 * @property {Object} state
 */

export default {
    createPropAcessors,
    ObservableStream,
    ObservableRedrawStream,
    RedrawStream,
    RedrawToggleStream,
    extractPhoneDDD,
    extractPhoneNumber,
    SentryInitSDK,
    captureException,
    captureMessage,
    redraw,
    getCallStack,
    createRequestRedrawWithCountdown,
    createBasicPaginationVMWithAutoRedraw,
    createRequestAutoRedraw,
    autoRedrawProp,
    sleep,
    stripScripts,
    authenticityParam,
    authenticityToken,
    buildLink,
    contributionStatusBadge,
    cumulativeOffset,
    discuss,
    existy,
    slugify,
    validateEmail,
    validateCpf,
    validateCnpj,
    momentify,
    momentFromString,
    lastDayOfNextMonth,
    formatNumber,
    idVM,
    getUser,
    getUserID,
    getUserCommonID,
    getApiHost,
    getNewsletterUrl,
    getCurrentProject,
    getParams,
    toggleProp,
    loader,
    loaderWithSize,
    newFeatureBadge,
    fbParse,
    pluralize,
    simpleFormat,
    translatedTime,
    rewardSouldOut,
    rewardRemaning,
    parseUrl,
    hashMatch,
    mobileScreen,
    redrawHashChange,
    useAvatarOrDefault,
    locationActionMatch,
    navigateToDevise,
    navigateTo,
    storeAction,
    callStoredAction,
    UIHelper,
    toAnchor,
    capitalize,
    paramByName,
    setParamByName,
    setMultParams,
    setAndResetMultParams,
    setAndResetMultParamsArray,
    removeParamByName,
    removeMultParams,
    i18nScope,
    RDTracker,
    selfOrEmpty,
    animateScrollTo,
    scrollTo,
    scrollTop,
    getRandomInt,
    projectStateTextClass,
    validationErrors,
    validate,
    analytics,
    strip,
    storeObject,
    getStoredObject,
    removeStoredObject,
    setProject,
    getProject,
    setReward,
    getReward,
    applyMonetaryMask,
    noNumbersMask,
    numbersOnlyMask,
    monetaryToFloat,
    mask,
    projectFullPermalink,
    isProjectPage,
    setPageTitle,
    rootUrl,
    setRedactor,
    redactor,
    setCsrfToken,
    userSignedIn,
    isDevEnv,
    trust,
    attachEventsToHistory,
    titleCase,
    buildCreditCard,
    isMobile,
    clamp,
};