<commit_before>import { createBrowserHistory as createHistory } from 'history';
import URLSearchParams from '@ungap/url-search-params';

/**
 * Sets up bidirectional synchronisation between a Redux store and window
 * location query parameters.
 *
 * @param {Object} options.store - The redux store object (= an object `{dispatch, getState}`).
 * @param {Object} options.params - The query parameters in the location to keep in sync.
 * @param {function value => action} options.params[].action - The action creator to be invoked with
 *     the parameter value. Should return an action that sets this value in the store.
 * @param {function state => value} options.params[].selector - The function that gets the value
 *     given the state.
 * @param {*} [options.params[].defaultValue] - The value corresponding to absence of the parameter.
 *     You may want this to equal the state's default/initial value. Default: `undefined`.
 * @param {function} [options.params[].valueToString] - The inverse of stringToValue. Specifies how
 *     to cast the value to a string, to be used in the URL. Defaults to javascript's automatic
 *     string conversion.
 * @param {function} [options.params[].stringToValue] - The inverse of valueToString. Specifies how
 *     to parse the parameter's string value to your desired value type. Defaults to the identity
 *     function (i.e. you get the string as it is).
 * @param {string} options.initialTruth - If set, indicates whose values to sync to the other,
 *     initially. Can be either `'location'` or `'store'`. If not set, the first of them that
 *     changes will set the other, which is not recommended. Usually you will want to use
 *     `location`.
 * @param {boolean} [options.replaceState] - If truthy, update location using
 *     history.replaceState instead of history.pushState, to not fill the browser history.
 * @param {Object} [options.history] - If you use the 'history' module, e.g. when using a router,
 *     pass your history object here in order to ensure all code uses the same instance.
 */
function ReduxQuerySync({
    store,
    params,
    replaceState,
    initialTruth,
    history = createHistory(),
}) {
    const { dispatch } = store

    // Two bits of state used to not respond to self-induced updates.
    let ignoreLocationUpdate = false
    let ignoreStateUpdate = false

    // Keeps the last seen values for comparing what has changed.
    let lastQueryValues

    // Helper function to support history module v3 as well as v4.
    function getLocation(history) {
        if (Object.hasOwnProperty.call(history, 'location')) {
            return history.location
        } else if (Object.hasOwnProperty.call(history, 'getCurrentLocation')) {
            return history.getCurrentLocation()
        }
    }

    function getQueryValues(location) {
        const locationParams = new URLSearchParams(location.search);
        const queryValues = {}
        Object.keys(params).forEach(param => {
            const { defaultValue, stringToValue = s => s } = params[param]
            const valueString = locationParams.get(param)
            const value = (valueString === null)
                ? defaultValue
                : stringToValue(valueString)
            queryValues[param] = value
        })
        return queryValues
    }

    function handleLocationUpdate(location) {
        // Ignore the event if the location update was induced by ourselves.
        if (ignoreLocationUpdate) return

        const state = store.getState()

        // Read the values of the watched parameters
        const queryValues = getQueryValues(location)

        // For each parameter value that changed, we dispatch the corresponding action.
        const actionsToDispatch = []
        Object.keys(queryValues).forEach(param => {
            const value = queryValues[param]
            // Process the parameter both on initialisation and if it has changed since last time.
            // (should we just do this unconditionally?)
            if (lastQueryValues === undefined || lastQueryValues[param] !== value) {
                const { selector, action } = params[param]

                // Dispatch the action to update the state if needed.
                // (except on initialisation, this should always be needed)
                if (selector(state) !== value) {
                    actionsToDispatch.push(action(value))
                }
            }
        })

        lastQueryValues = queryValues

        ignoreStateUpdate = true
        actionsToDispatch.forEach(action => {
            dispatch(action)
        })
        ignoreStateUpdate = false

        // Update the location the again: reducers may have e.g. corrected invalid values.
        handleStateUpdate({replaceState: true})
    }

    function handleStateUpdate({replaceState}) {
        if (ignoreStateUpdate) return

        const state = store.getState()
        const location = getLocation(history)

        // Parse the current location's query string.
        const locationParams = new URLSearchParams(location.search);

        // Replace each configured parameter with its value in the state.
        Object.keys(params).forEach(param => {
            const { selector, defaultValue, valueToString = v => `${v}` } = params[param]
            const value = selector(state)
            if (value === defaultValue) {
                locationParams.delete(param)
            } else {
                locationParams.set(param, valueToString(value))
            }
            lastQueryValues[param] = value
        })
        const newLocationSearchString = `?${locationParams}`
        const oldLocationSearchString = location.search || '?'

        // Only update location if anything changed.
        if (newLocationSearchString !== oldLocationSearchString) {
            // Update location (but prevent triggering a state update).
            ignoreLocationUpdate = true
            const newLocation = {
                pathname: location.pathname,
                search: newLocationSearchString,
                hash: location.hash,
                state: location.state,
            }
            replaceState
                ? history.replace(newLocation)
                : history.push(newLocation)
            ignoreLocationUpdate = false
        }
    }

    // Sync location to store on every location change, and vice versa.
    const unsubscribeFromLocation = history.listen(handleLocationUpdate)
    const unsubscribeFromStore = store.subscribe(() => handleStateUpdate({replaceState}))

    // Sync location to store now, or vice versa, or neither.
    if (initialTruth === 'location') {
        handleLocationUpdate(getLocation(history))
    } else {
        // Just set the last seen values to later compare what changed.
        lastQueryValues = getQueryValues(getLocation(history))
    }
    if (initialTruth === 'store') {
        handleStateUpdate({replaceState: true})
    }

    return function unsubscribe() {
        unsubscribeFromLocation()
        unsubscribeFromStore()
    }
}

/**
 * For convenience, one can set up the synchronisation by passing this enhancer to createStore.
 *
 * @example
 *
 *     const storeEnhancer = ReduxQuerySync.enhancer({params, initialTruth: 'location'})
 *     const store = createStore(reducer, initialState, storeEnhancer)
 *
 * Arguments are equal to those of ReduxQuerySync itself, except that `store` can now be omitted.
 */
ReduxQuerySync.enhancer = function makeStoreEnhancer(config) {
    return storeCreator => (reducer, initialState, enhancer) => {
        // Create the store as usual.
        const store = storeCreator(reducer, initialState, enhancer)

        // Hook up our listeners.
        ReduxQuerySync({store, ...config})

        return store
    }
}

export default ReduxQuerySync
<commit_msg>Support history v5<commit_after>import { createBrowserHistory as createHistory } from 'history';
import URLSearchParams from '@ungap/url-search-params';

/**
 * Sets up bidirectional synchronisation between a Redux store and window
 * location query parameters.
 *
 * @param {Object} options.store - The redux store object (= an object `{dispatch, getState}`).
 * @param {Object} options.params - The query parameters in the location to keep in sync.
 * @param {function value => action} options.params[].action - The action creator to be invoked with
 *     the parameter value. Should return an action that sets this value in the store.
 * @param {function state => value} options.params[].selector - The function that gets the value
 *     given the state.
 * @param {*} [options.params[].defaultValue] - The value corresponding to absence of the parameter.
 *     You may want this to equal the state's default/initial value. Default: `undefined`.
 * @param {function} [options.params[].valueToString] - The inverse of stringToValue. Specifies how
 *     to cast the value to a string, to be used in the URL. Defaults to javascript's automatic
 *     string conversion.
 * @param {function} [options.params[].stringToValue] - The inverse of valueToString. Specifies how
 *     to parse the parameter's string value to your desired value type. Defaults to the identity
 *     function (i.e. you get the string as it is).
 * @param {string} options.initialTruth - If set, indicates whose values to sync to the other,
 *     initially. Can be either `'location'` or `'store'`. If not set, the first of them that
 *     changes will set the other, which is not recommended. Usually you will want to use
 *     `location`.
 * @param {boolean} [options.replaceState] - If truthy, update location using
 *     history.replaceState instead of history.pushState, to not fill the browser history.
 * @param {Object} [options.history] - If you use the 'history' module, e.g. when using a router,
 *     pass your history object here in order to ensure all code uses the same instance.
 */
function ReduxQuerySync({
    store,
    params,
    replaceState,
    initialTruth,
    history = createHistory(),
}) {
    const { dispatch } = store

    // Two bits of state used to not respond to self-induced updates.
    let ignoreLocationUpdate = false
    let ignoreStateUpdate = false

    // Keeps the last seen values for comparing what has changed.
    let lastQueryValues

    // Helper function to support history module v3 as well as v4.
    function getLocation(history) {
        if (Object.hasOwnProperty.call(history, 'location')) {
            return history.location
        } else if (Object.hasOwnProperty.call(history, 'getCurrentLocation')) {
            return history.getCurrentLocation()
        }
    }

    function getQueryValues(location) {
        const locationParams = new URLSearchParams(location.search);
        const queryValues = {}
        Object.keys(params).forEach(param => {
            const { defaultValue, stringToValue = s => s } = params[param]
            const valueString = locationParams.get(param)
            const value = (valueString === null)
                ? defaultValue
                : stringToValue(valueString)
            queryValues[param] = value
        })
        return queryValues
    }

    function handleLocationUpdate(location) {
        // Support history v5
        if (location.location != null) {
            location = location.location;
        }

        // Ignore the event if the location update was induced by ourselves.
        if (ignoreLocationUpdate) return

        const state = store.getState()

        // Read the values of the watched parameters
        const queryValues = getQueryValues(location)

        // For each parameter value that changed, we dispatch the corresponding action.
        const actionsToDispatch = []
        Object.keys(queryValues).forEach(param => {
            const value = queryValues[param]
            // Process the parameter both on initialisation and if it has changed since last time.
            // (should we just do this unconditionally?)
            if (lastQueryValues === undefined || lastQueryValues[param] !== value) {
                const { selector, action } = params[param]

                // Dispatch the action to update the state if needed.
                // (except on initialisation, this should always be needed)
                if (selector(state) !== value) {
                    actionsToDispatch.push(action(value))
                }
            }
        })

        lastQueryValues = queryValues

        ignoreStateUpdate = true
        actionsToDispatch.forEach(action => {
            dispatch(action)
        })
        ignoreStateUpdate = false

        // Update the location the again: reducers may have e.g. corrected invalid values.
        handleStateUpdate({replaceState: true})
    }

    function handleStateUpdate({replaceState}) {
        if (ignoreStateUpdate) return

        const state = store.getState()
        const location = getLocation(history)

        // Parse the current location's query string.
        const locationParams = new URLSearchParams(location.search);

        // Replace each configured parameter with its value in the state.
        Object.keys(params).forEach(param => {
            const { selector, defaultValue, valueToString = v => `${v}` } = params[param]
            const value = selector(state)
            if (value === defaultValue) {
                locationParams.delete(param)
            } else {
                locationParams.set(param, valueToString(value))
            }
            lastQueryValues[param] = value
        })
        const newLocationSearchString = `?${locationParams}`
        const oldLocationSearchString = location.search || '?'

        // Only update location if anything changed.
        if (newLocationSearchString !== oldLocationSearchString) {
            // Update location (but prevent triggering a state update).
            ignoreLocationUpdate = true
            const newLocation = {
                pathname: location.pathname,
                search: newLocationSearchString,
                hash: location.hash,
                state: location.state,
            }
            replaceState
                ? history.replace(newLocation)
                : history.push(newLocation)
            ignoreLocationUpdate = false
        }
    }

    // Sync location to store on every location change, and vice versa.
    const unsubscribeFromLocation = history.listen(handleLocationUpdate)
    const unsubscribeFromStore = store.subscribe(() => handleStateUpdate({replaceState}))

    // Sync location to store now, or vice versa, or neither.
    if (initialTruth === 'location') {
        handleLocationUpdate(getLocation(history))
    } else {
        // Just set the last seen values to later compare what changed.
        lastQueryValues = getQueryValues(getLocation(history))
    }
    if (initialTruth === 'store') {
        handleStateUpdate({replaceState: true})
    }

    return function unsubscribe() {
        unsubscribeFromLocation()
        unsubscribeFromStore()
    }
}

/**
 * For convenience, one can set up the synchronisation by passing this enhancer to createStore.
 *
 * @example
 *
 *     const storeEnhancer = ReduxQuerySync.enhancer({params, initialTruth: 'location'})
 *     const store = createStore(reducer, initialState, storeEnhancer)
 *
 * Arguments are equal to those of ReduxQuerySync itself, except that `store` can now be omitted.
 */
ReduxQuerySync.enhancer = function makeStoreEnhancer(config) {
    return storeCreator => (reducer, initialState, enhancer) => {
        // Create the store as usual.
        const store = storeCreator(reducer, initialState, enhancer)

        // Hook up our listeners.
        ReduxQuerySync({store, ...config})

        return store
    }
}

export default ReduxQuerySync
<|endoftext|>


<commit_before>/* globals angular */
angular.module('gc.fastRepeat', []).directive('fastRepeat', ['$compile', '$parse', '$animate', function ($compile, $parse, $animate) {
    'use strict';

    var fastRepeatId = 0,
        showProfilingInfo = false,
        isGteAngular14 = /^(\d+\.\d+)\./.exec(angular.version.full)[1] > 1.3;

    // JSON.stringify replacer function which removes any keys that start with $$.
    // This prevents unnecessary updates when we watch a JSON stringified value.
    function JSONStripper(key, value) {
        if(key.slice && key.slice(0,2) == '$$') { return undefined; }
        return value;
    }

    function getTime() { // For profiling
        if(window.performance && window.performance.now) { return window.performance.now(); }
        else { return (new Date()).getTime(); }
    }

    return {
        restrict: 'A',
        transclude: 'element',
        priority: 1000,
        compile: function(tElement, tAttrs) {
            return function link(listScope, element, attrs, ctrl, transclude) {
                var repeatParts = attrs.fastRepeat.split(' in ');
                var repeatListName = repeatParts[1], repeatVarName = repeatParts[0];
                var getter = $parse(repeatListName); // getter(scope) should be the value of the list.
                var disableOpts = $parse(attrs.fastRepeatDisableOpts)(listScope);
                var currentRowEls = {};
                var t;

                // The rowTpl will be digested once -- want to make sure it has valid data for the first wasted digest.  Default to first row or {} if no rows
                var scope = listScope.$new();
                scope[repeatVarName] = getter(scope)[0] || {};
                scope.fastRepeatStatic = true; scope.fastRepeatDynamic = false;

                // Transclude the contents of the fast repeat.
                // This function is called for every row. It reuses the rowTpl and scope for each row.
                var rowTpl = transclude(scope, function(rowTpl, scope) {
                    if (isGteAngular14) {
                        $animate.enabled(rowTpl, false);
                    } else {
                        $animate.enabled(false, rowTpl);
                    }
                });

                // Create an offscreen div for the template
                var tplContainer = $("<div/>");
                $('body').append(tplContainer);
                scope.$on('$destroy', function() {
                    tplContainer.detach();
                });
                tplContainer.css({position: 'absolute', top: '-100%'});
                var elParent = element.parents().filter(function() { return $(this).css('display') !== 'inline'; }).first();
                tplContainer.width(elParent.width());
                tplContainer.css({visibility: 'hidden'});

                tplContainer.append(rowTpl);

                var updateList = function(rowTpl, scope, forceUpdate) {
                    function render(item) {
                        scope[repeatVarName] = item;
                        scope.$digest();
                        rowTpl.attr('fast-repeat-id', item.$$fastRepeatId);
                        return rowTpl.clone();
                    }

                    var list = getter(scope);
                    // Generate ids if necessary and arrange in a hash map
                    var listByIds = {};
                    angular.forEach(list, function(item) {
                        if(!item.$$fastRepeatId) {
                            if(item.id) { item.$$fastRepeatId = item.id; }
                            else if(item._id) { item.$$fastRepeatId = item._id; }
                            else { item.$$fastRepeatId = ++fastRepeatId; }
                        }
                        listByIds[item.$$fastRepeatId] = item;
                    });

                    // Delete removed rows
                    angular.forEach(currentRowEls, function(row, id) {
                        if(!listByIds[id]) {
                            row.el.detach();
                        }
                    });
                    // Add/rearrange all rows
                    var previousEl = element;
                    angular.forEach(list, function(item) {
                        var id = item.$$fastRepeatId;
                        var row=currentRowEls[id];

                        if(row) {
                            // We've already seen this one
                            if((!row.compiled && (forceUpdate || !angular.equals(row.copy, item))) || (row.compiled && row.item!==item)) {
                                // This item has not been compiled and it apparently has changed -- need to rerender
                                var newEl = render(item);
                                row.el.replaceWith(newEl);
                                row.el = newEl;
                                row.copy = angular.copy(item);
                                row.compiled = false;
                                row.item = item;
                            }
                        } else {
                            // This must be a new node

                            if(!disableOpts) {
                                row = {
                                    copy: angular.copy(item),
                                    item: item,
                                    el: render(item)
                                };
                            } else {
                                // Optimizations are disabled
                                row = {
                                    copy: angular.copy(item),
                                    item: item,
                                    el: $('<div/>'),
                                    compiled: true
                                };

                                renderUnoptimized(item, function(newEl) {
                                    row.el.replaceWith(newEl);
                                    row.el=newEl;
                                });
                            }

                            currentRowEls[id] =  row;
                        }
                        previousEl.after(row.el.last());
                        previousEl = row.el.last();
                    });

                };

                // Here is the main watch. Testing has shown that watching the stringified list can
                // save roughly 500ms per digest in certain cases.
                // JSONStripper is used to remove the $$fastRepeatId that we attach to the objects.
                var busy=false;
                listScope.$watch(function(scp){ return JSON.stringify(getter(scp), JSONStripper); }, function(list) {
                    tplContainer.width(elParent.width());

                    if(busy) { return; }
                    busy=true;

                    if (showProfilingInfo) {
                        t = getTime();
                    }

                    // Rendering is done in a postDigest so that we are outside of the main digest cycle.
                    // This allows us to digest the individual row scope repeatedly without major hackery.
                    listScope.$$postDigest(function() {
                        tplContainer.width(elParent.width());
                        scope.$digest();

                        updateList(rowTpl, scope);
                        if (showProfilingInfo) {
                            t = getTime() - t;
                            console.log("Total time: ", t, "ms");
                            console.log("time per row: ", t/list.length);
                        }
                        busy=false;
                    });
                }, false);

                function renderRows() {
                    listScope.$$postDigest(function() {
                        tplContainer.width(elParent.width());
                        scope.$digest();
                        updateList(rowTpl, scope, true);
                    });
                }
                if(attrs.fastRepeatWatch) {
                    listScope.$watch(attrs.fastRepeatWatch, renderRows);
                }
                listScope.$on('fastRepeatForceRedraw', renderRows);

                function renderUnoptimized(item, cb) {
                    var newScope = scope.$new(false);

                    newScope[repeatVarName] = item;
                    newScope.fastRepeatStatic = false; newScope.fastRepeatDynamic = true;
                    var clone = transclude(newScope, function(clone) {
                        tplContainer.append(clone);
                    });

                    newScope.$$postDigest(function() {
                        cb(clone);
                    });

                    newScope.$digest();

                    return newScope;
                }

                element.parent().on('click', '[fast-repeat-id]', function(evt) {
                    var $target = $(this);
                    if($target.parents().filter('[fast-repeat-id]').length) {
                        return; // This event wasn't meant for us
                    }
                    evt.stopPropagation();

                    var rowId = $target.attr('fast-repeat-id');
                    var item = currentRowEls[rowId].item;

                    // Find index of clicked dom element in list of all children element of the row.
                    // -1 would indicate the row itself was clicked.
                    var elIndex = $target.find('*').index(evt.target);
                    var newScope = renderUnoptimized(item, function(clone) {
                        $target.replaceWith(clone);

                        currentRowEls[rowId] = {
                            compiled: true,
                            el: clone,
                            item: item
                        };

                        setTimeout(function() {
                            if(elIndex >= 0) {
                                clone.find('*').eq(elIndex).trigger('click');
                            } else {
                                clone.trigger('click');
                            }
                        }, 0);
                    });

                    newScope.$digest();
                });

                // Handle resizes
                //
                var onResize = function() {
                    tplContainer.width(elParent.width());
                };

                var jqWindow = $(window);
                jqWindow.on('resize', onResize);
                element.on('$destroy', function() { jqWindow.off('resize', onResize); });

            };
        },
    };
}]);
<commit_msg>fix memory leaks<commit_after>/* globals angular */
angular.module('gc.fastRepeat', []).directive('fastRepeat', ['$compile', '$parse', '$animate', function ($compile, $parse, $animate) {
    'use strict';
    var $ = angular.element;

    var fastRepeatId = 0,
        showProfilingInfo = false,
        isGteAngular14 = /^(\d+\.\d+)\./.exec(angular.version.full)[1] > 1.3;

    // JSON.stringify replacer function which removes any keys that start with $$.
    // This prevents unnecessary updates when we watch a JSON stringified value.
    function JSONStripper(key, value) {
        if(key.slice && key.slice(0,2) == '$$') { return undefined; }
        return value;
    }

    function getTime() { // For profiling
        if(window.performance && window.performance.now) { return window.performance.now(); }
        else { return (new Date()).getTime(); }
    }

    return {
        restrict: 'A',
        transclude: 'element',
        priority: 1000,
        compile: function(tElement, tAttrs) {
            return function link(listScope, element, attrs, ctrl, transclude) {
                var repeatParts = attrs.fastRepeat.split(' in ');
                var repeatListName = repeatParts[1], repeatVarName = repeatParts[0];
                var getter = $parse(repeatListName); // getter(scope) should be the value of the list.
                var disableOpts = $parse(attrs.fastRepeatDisableOpts)(listScope);
                var currentRowEls = {};
                var t;

                // The rowTpl will be digested once -- want to make sure it has valid data for the first wasted digest.  Default to first row or {} if no rows
                var scope = listScope.$new();
                scope[repeatVarName] = getter(scope)[0] || {};
                scope.fastRepeatStatic = true; scope.fastRepeatDynamic = false;

                // Transclude the contents of the fast repeat.
                // This function is called for every row. It reuses the rowTpl and scope for each row.
                var rowTpl = transclude(scope, function(rowTpl, scope) {
                    if (isGteAngular14) {
                        $animate.enabled(rowTpl, false);
                    } else {
                        $animate.enabled(false, rowTpl);
                    }
                });

                // Create an offscreen div for the template
                var tplContainer = $("<div/>");
                $('body').append(tplContainer);
                scope.$on('$destroy', function() {
                    tplContainer.remove();
                    rowTpl.remove();
                });
                tplContainer.css({position: 'absolute', top: '-100%'});
                var elParent = element.parents().filter(function() { return $(this).css('display') !== 'inline'; }).first();
                tplContainer.width(elParent.width());
                tplContainer.css({visibility: 'hidden'});

                tplContainer.append(rowTpl);

                var updateList = function(rowTpl, scope, forceUpdate) {
                    function render(item) {
                        scope[repeatVarName] = item;
                        scope.$digest();
                        rowTpl.attr('fast-repeat-id', item.$$fastRepeatId);
                        return rowTpl.clone();
                    }

                    var list = getter(scope);
                    // Generate ids if necessary and arrange in a hash map
                    var listByIds = {};
                    angular.forEach(list, function(item) {
                        if(!item.$$fastRepeatId) {
                            if(item.id) { item.$$fastRepeatId = item.id; }
                            else if(item._id) { item.$$fastRepeatId = item._id; }
                            else { item.$$fastRepeatId = ++fastRepeatId; }
                        }
                        listByIds[item.$$fastRepeatId] = item;
                    });

                    // Delete removed rows
                    angular.forEach(currentRowEls, function(row, id) {
                        if(!listByIds[id]) {
                            row.el.detach();
                        }
                    });
                    // Add/rearrange all rows
                    var previousEl = element;
                    angular.forEach(list, function(item) {
                        var id = item.$$fastRepeatId;
                        var row=currentRowEls[id];

                        if(row) {
                            // We've already seen this one
                            if((!row.compiled && (forceUpdate || !angular.equals(row.copy, item))) || (row.compiled && row.item!==item)) {
                                // This item has not been compiled and it apparently has changed -- need to rerender
                                var newEl = render(item);
                                row.el.replaceWith(newEl);
                                row.el = newEl;
                                row.copy = angular.copy(item);
                                row.compiled = false;
                                row.item = item;
                            }
                        } else {
                            // This must be a new node

                            if(!disableOpts) {
                                row = {
                                    copy: angular.copy(item),
                                    item: item,
                                    el: render(item)
                                };
                            } else {
                                // Optimizations are disabled
                                row = {
                                    copy: angular.copy(item),
                                    item: item,
                                    el: $('<div/>'),
                                    compiled: true
                                };

                                renderUnoptimized(item, function(newEl) {
                                    row.el.replaceWith(newEl);
                                    row.el=newEl;
                                });
                            }

                            currentRowEls[id] =  row;
                        }
                        previousEl.after(row.el.last());
                        previousEl = row.el.last();
                    });

                };

                // Here is the main watch. Testing has shown that watching the stringified list can
                // save roughly 500ms per digest in certain cases.
                // JSONStripper is used to remove the $$fastRepeatId that we attach to the objects.
                var busy=false;
                listScope.$watch(function(scp){ return JSON.stringify(getter(scp), JSONStripper); }, function(list) {
                    tplContainer.width(elParent.width());

                    if(busy) { return; }
                    busy=true;

                    if (showProfilingInfo) {
                        t = getTime();
                    }

                    // Rendering is done in a postDigest so that we are outside of the main digest cycle.
                    // This allows us to digest the individual row scope repeatedly without major hackery.
                    listScope.$$postDigest(function() {
                        tplContainer.width(elParent.width());
                        scope.$digest();

                        updateList(rowTpl, scope);
                        if (showProfilingInfo) {
                            t = getTime() - t;
                            console.log("Total time: ", t, "ms");
                            console.log("time per row: ", t/list.length);
                        }
                        busy=false;
                    });
                }, false);

                function renderRows() {
                    listScope.$$postDigest(function() {
                        tplContainer.width(elParent.width());
                        scope.$digest();
                        updateList(rowTpl, scope, true);
                    });
                }
                if(attrs.fastRepeatWatch) {
                    listScope.$watch(attrs.fastRepeatWatch, renderRows);
                }
                listScope.$on('fastRepeatForceRedraw', renderRows);

                function renderUnoptimized(item, cb) {
                    var newScope = scope.$new(false);

                    newScope[repeatVarName] = item;
                    newScope.fastRepeatStatic = false; newScope.fastRepeatDynamic = true;
                    var clone = transclude(newScope, function(clone) {
                        tplContainer.append(clone);
                    });

                    newScope.$$postDigest(function() {
                        cb(clone);
                    });

                    newScope.$digest();

                    return newScope;
                }

                var parentClickHandler = function parentClickHandler(evt) {
                    var $target = $(this);
                    if($target.parents().filter('[fast-repeat-id]').length) {
                        return; // This event wasn't meant for us
                    }
                    evt.stopPropagation();

                    var rowId = $target.attr('fast-repeat-id');
                    var item = currentRowEls[rowId].item;

                    // Find index of clicked dom element in list of all children element of the row.
                    // -1 would indicate the row itself was clicked.
                    var elIndex = $target.find('*').index(evt.target);
                    var newScope = renderUnoptimized(item, function(clone) {
                        $target.replaceWith(clone);

                        currentRowEls[rowId] = {
                            compiled: true,
                            el: clone,
                            item: item
                        };

                        setTimeout(function() {
                            if(elIndex >= 0) {
                                clone.find('*').eq(elIndex).trigger('click');
                            } else {
                                clone.trigger('click');
                            }
                        }, 0);
                    });

                    newScope.$digest();
                };

                element.parent().on('click', '[fast-repeat-id]',parentClickHandler);

                // Handle resizes
                //
                var onResize = function() {
                    tplContainer.width(elParent.width());
                };

                var jqWindow = $(window);
                jqWindow.on('resize', onResize);
                scope.$on('$destroy', function() {
                    jqWindow.off('resize', onResize);
                    element.parent().off('click', '[fast-repeat-id]');
                });
            };
        },
    };
}]);
<|endoftext|>


<commit_before>var GUI = (function(){
  var eventOnFileUploaded;
  var eventOnOptMessagesPerPageChanged;
  var eventOnNavButtonClicked;

  var showModal = function(width, height, html){
    var dialog = DOM.id("dialog");
    dialog.innerHTML = html;

    dialog.style.width = width+"px";
    dialog.style.height = height+"px";
    dialog.style.marginLeft = (-width/2)+"px";
    dialog.style.marginTop = (-height/2)+"px";

    DOM.id("modal").classList.add("visible");
    return dialog;
  };

  // -------------
  // Modal dialogs
  // -------------

  var showSettingsModal = function(){
    showModal(560, 256, [
      "<input id='dht-cfg-imgpreviews' type='checkbox'> Image Previews"
    ].join(""));

    // image previews
    var checkImagePreviews = DOM.id("dht-cfg-imgpreviews");

    checkImagePreviews.checked = STATE.settings.enableImagePreviews;

    checkImagePreviews.addEventListener("change", () => {
      STATE.settings.enableImagePreviews = checkImagePreviews.checked;
    });
  };

  var showInfoModal = function(){
    var linkGH = "https://github.com/chylex/Discord-History-Tracker";

    showModal(560, 128, [
      "
Discord History Tracker is developed by chylex as an open source project.
",
      "
Please, report any issues and suggestions to the tracker. If you want to support the development, please spread the word and consider becoming a patron. Any support is appreciated!
",
      "
",
      "Issue Tracker  —  ",
      "GitHub Repository  —  ",
      "Developer's Twitter",
      "
"
    ].join(""));
  };

  return {
    // ---------
    // GUI setup
    // ---------

    /*
     * Hooks all event listeners into the DOM.
     */
    setup: function(){
      var inputUploadedFile = DOM.id("uploaded-file");

      DOM.id("btn-upload-file").addEventListener("click", () => {
        inputUploadedFile.click();
      });

      inputUploadedFile.addEventListener("change", () => {
        if (eventOnFileUploaded && eventOnFileUploaded(inputUploadedFile.files)){
          inputUploadedFile.value = null;
        }
      });

      DOM.id("opt-messages-per-page").addEventListener("change", () => {
        eventOnOptMessagesPerPageChanged && eventOnOptMessagesPerPageChanged();
      });

      Array.prototype.forEach.call(DOM.tag("button", DOM.cls("nav")[0]), button => {
        button.disabled = true;

        button.addEventListener("click", () => {
          eventOnNavButtonClicked && eventOnNavButtonClicked(button.getAttribute("data-nav"));
        });
      });

      DOM.id("btn-settings").addEventListener("click", () => {
        showSettingsModal();
      });

      DOM.id("btn-about").addEventListener("click", () => {
        showInfoModal();
      });

      DOM.id("overlay").addEventListener("click", () => {
        DOM.id("modal").classList.remove("visible");
        DOM.id("dialog").innerHTML = "";
      });
    },

    // -----------------
    // Event registering
    // -----------------

    /*
     * Sets a callback for when a file is uploaded. The callback takes a single argument, which is the file object array, and should return true to reset the input.
     */
    onFileUploaded: function(callback){
      eventOnFileUploaded = callback;
    },

    /*
     * Sets a callback for when the user changes the messages per page option. The callback is not passed any arguments.
     */
    onOptionMessagesPerPageChanged: function(callback){
      eventOnOptMessagesPerPageChanged = callback;
    },

    /*
     * Sets a callback for when the user clicks a navigation button. The callback is passed one of the following strings: first, prev, next, last.
     */
    onNavigationButtonClicked: function(callback){
      eventOnNavButtonClicked = callback;
    },

    // ----------------------
    // Options and navigation
    // ----------------------

    /*
     * Returns the selected amount of messages per page.
     */
    getOptionMessagesPerPage: function(){
      return parseInt(DOM.id("opt-messages-per-page").value, 10);
    },

    /*
     * Updates the navigation text and buttons.
     */
    updateNavigation: function(currentPage, totalPages){
      DOM.id("nav-page-current").innerHTML = currentPage;
      DOM.id("nav-page-total").innerHTML = totalPages || "?";

      DOM.id("nav-first").disabled = currentPage === 1;
      DOM.id("nav-prev").disabled = currentPage === 1;
      DOM.id("nav-next").disabled = currentPage === (totalPages || 1);
      DOM.id("nav-last").disabled = currentPage === (totalPages || 1);
    },

    // ------------
    // Channel list
    // ------------

    /*
     * Updates the channel list and sets up their click events. The callback is triggered whenever a channel is selected, and takes the channel ID as its argument.
     */
    updateChannelList: function(channels, callback){
      var eleChannels = DOM.id("channels");

      if (!channels){
        eleChannels.innerHTML = "";
      }
      else{
        eleChannels.innerHTML = channels.map(channel => DISCORD.getChannelHTML(channel)).join("");

        Array.prototype.forEach.call(eleChannels.children, ele => {
          ele.addEventListener("click", e => {
            var currentChannel = DOM.cls("active", eleChannels)[0];

            if (currentChannel){
              currentChannel.classList.remove("active");
            }

            ele.classList.add("active");
            callback(ele.getAttribute("data-channel"));
          });
        });
      }
    },

    // ------------
    // Message list
    // ------------

    /*
     * Updates the message list.
     */
    updateMessageList: function(messages){
      DOM.id("messages").innerHTML = messages ? messages.map(message => DISCORD.getMessageHTML(message)).join("") : "";
    },

    /*
     * Scrolls the message div to the top.
     */
    scrollMessagesToTop: function(){
      DOM.id("messages").scrollTop = 0;
    }
  };
})();<commit_msg>Refactor scr.gui.js to make checkboxes easier to setup in renderer settings<commit_after>var GUI = (function(){
  var eventOnFileUploaded;
  var eventOnOptMessagesPerPageChanged;
  var eventOnNavButtonClicked;

  var showModal = function(width, height, html){
    var dialog = DOM.id("dialog");
    dialog.innerHTML = html;

    dialog.style.width = width+"px";
    dialog.style.height = height+"px";
    dialog.style.marginLeft = (-width/2)+"px";
    dialog.style.marginTop = (-height/2)+"px";

    DOM.id("modal").classList.add("visible");
    return dialog;
  };

  // -------------
  // Modal dialogs
  // -------------

  var showSettingsModal = function(){
    showModal(560, 256, [
      "<input id='dht-cfg-imgpreviews' type='checkbox'> Image Previews"
    ].join(""));

    var setupCheckBox = function(id, settingName){
      var ele = DOM.id(id);
      ele.checked = STATE.settings[settingName];
      ele.addEventListener("change", () => STATE.settings[settingName] = ele.checked);
    };

    setupCheckBox("dht-cfg-imgpreviews", "enableImagePreviews");
  };

  var showInfoModal = function(){
    var linkGH = "https://github.com/chylex/Discord-History-Tracker";

    showModal(560, 128, [
      "
Discord History Tracker is developed by chylex as an open source project.
",
      "
Please, report any issues and suggestions to the tracker. If you want to support the development, please spread the word and consider becoming a patron. Any support is appreciated!
",
      "
",
      "Issue Tracker  —  ",
      "GitHub Repository  —  ",
      "Developer's Twitter",
      "
"
    ].join(""));
  };

  return {
    // ---------
    // GUI setup
    // ---------

    /*
     * Hooks all event listeners into the DOM.
     */
    setup: function(){
      var inputUploadedFile = DOM.id("uploaded-file");

      DOM.id("btn-upload-file").addEventListener("click", () => {
        inputUploadedFile.click();
      });

      inputUploadedFile.addEventListener("change", () => {
        if (eventOnFileUploaded && eventOnFileUploaded(inputUploadedFile.files)){
          inputUploadedFile.value = null;
        }
      });

      DOM.id("opt-messages-per-page").addEventListener("change", () => {
        eventOnOptMessagesPerPageChanged && eventOnOptMessagesPerPageChanged();
      });

      Array.prototype.forEach.call(DOM.tag("button", DOM.cls("nav")[0]), button => {
        button.disabled = true;

        button.addEventListener("click", () => {
          eventOnNavButtonClicked && eventOnNavButtonClicked(button.getAttribute("data-nav"));
        });
      });

      DOM.id("btn-settings").addEventListener("click", () => {
        showSettingsModal();
      });

      DOM.id("btn-about").addEventListener("click", () => {
        showInfoModal();
      });

      DOM.id("overlay").addEventListener("click", () => {
        DOM.id("modal").classList.remove("visible");
        DOM.id("dialog").innerHTML = "";
      });
    },

    // -----------------
    // Event registering
    // -----------------

    /*
     * Sets a callback for when a file is uploaded. The callback takes a single argument, which is the file object array, and should return true to reset the input.
     */
    onFileUploaded: function(callback){
      eventOnFileUploaded = callback;
    },

    /*
     * Sets a callback for when the user changes the messages per page option. The callback is not passed any arguments.
     */
    onOptionMessagesPerPageChanged: function(callback){
      eventOnOptMessagesPerPageChanged = callback;
    },

    /*
     * Sets a callback for when the user clicks a navigation button. The callback is passed one of the following strings: first, prev, next, last.
     */
    onNavigationButtonClicked: function(callback){
      eventOnNavButtonClicked = callback;
    },

    // ----------------------
    // Options and navigation
    // ----------------------

    /*
     * Returns the selected amount of messages per page.
     */
    getOptionMessagesPerPage: function(){
      return parseInt(DOM.id("opt-messages-per-page").value, 10);
    },

    /*
     * Updates the navigation text and buttons.
     */
    updateNavigation: function(currentPage, totalPages){
      DOM.id("nav-page-current").innerHTML = currentPage;
      DOM.id("nav-page-total").innerHTML = totalPages || "?";

      DOM.id("nav-first").disabled = currentPage === 1;
      DOM.id("nav-prev").disabled = currentPage === 1;
      DOM.id("nav-next").disabled = currentPage === (totalPages || 1);
      DOM.id("nav-last").disabled = currentPage === (totalPages || 1);
    },

    // ------------
    // Channel list
    // ------------

    /*
     * Updates the channel list and sets up their click events. The callback is triggered whenever a channel is selected, and takes the channel ID as its argument.
     */
    updateChannelList: function(channels, callback){
      var eleChannels = DOM.id("channels");

      if (!channels){
        eleChannels.innerHTML = "";
      }
      else{
        eleChannels.innerHTML = channels.map(channel => DISCORD.getChannelHTML(channel)).join("");

        Array.prototype.forEach.call(eleChannels.children, ele => {
          ele.addEventListener("click", e => {
            var currentChannel = DOM.cls("active", eleChannels)[0];

            if (currentChannel){
              currentChannel.classList.remove("active");
            }

            ele.classList.add("active");
            callback(ele.getAttribute("data-channel"));
          });
        });
      }
    },

    // ------------
    // Message list
    // ------------

    /*
     * Updates the message list.
     */
    updateMessageList: function(messages){
      DOM.id("messages").innerHTML = messages ? messages.map(message => DISCORD.getMessageHTML(message)).join("") : "";
    },

    /*
     * Scrolls the message div to the top.
     */
    scrollMessagesToTop: function(){
      DOM.id("messages").scrollTop = 0;
    }
  };
})();<|endoftext|>


<commit_before>_html2canvas.Renderer.Canvas = function(options) {
  options = options || {};

  var doc = document,
  safeImages = [],
  testCanvas = document.createElement("canvas"),
  testctx = testCanvas.getContext("2d"),
  Util = _html2canvas.Util,
  canvas = options.canvas || doc.createElement('canvas');

  function createShape(ctx, args) {
    ctx.beginPath();
    args.forEach(function(arg) {
      ctx[arg.name].apply(ctx, arg['arguments']);
    });
    ctx.closePath();
  }

  function safeImage(item) {
    if (safeImages.indexOf(item['arguments'][0].src ) === -1) {
      testctx.drawImage(item['arguments'][0], 0, 0);
      try {
        testctx.getImageData(0, 0, 1, 1);
      } catch(e) {
        testCanvas = doc.createElement("canvas");
        testctx = testCanvas.getContext("2d");
        return false;
      }
      safeImages.push(item['arguments'][0].src);
    }
    return true;
  }

  function renderItem(ctx, item) {
    switch(item.type){
      case "variable":
        ctx[item.name] = item['arguments'];
        break;
      case "function":
        switch(item.name) {
          case "createPattern":
            if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) {
              try {
                ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat");
              }
              catch(e) {
                Util.log("html2canvas: Renderer: Error creating pattern", e.message);
              }
            }
            break;
          case "drawShape":
            createShape(ctx, item['arguments']);
            break;
          case "drawImage":
            if (item['arguments'][8] > 0 && item['arguments'][7] > 0) {
              if (!options.taintTest || (options.taintTest && safeImage(item))) {
                ctx.drawImage.apply( ctx, item['arguments'] );
              }
            }
            break;
          default:
            ctx[item.name].apply(ctx, item['arguments']);
        }
        break;
    }
  }

  return function(parsedData, options, document, queue, _html2canvas) {
    var ctx = canvas.getContext("2d"),
    newCanvas,
    bounds,
    fstyle,
    zStack = parsedData.stack;

    canvas.width = canvas.style.width =  options.width || zStack.ctx.width;
    canvas.height = canvas.style.height = options.height || zStack.ctx.height;

    fstyle = ctx.fillStyle;
    ctx.fillStyle = (Util.isTransparent(zStack.backgroundColor) && options.background !== undefined) ? options.background : parsedData.backgroundColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = fstyle;

    queue.forEach(function(storageContext) {
      // set common settings for canvas
      ctx.textBaseline = "bottom";
      ctx.save();

      if (storageContext.transform.matrix) {
        ctx.translate(storageContext.transform.origin[0], storageContext.transform.origin[1]);
        ctx.transform.apply(ctx, storageContext.transform.matrix);
        ctx.translate(-storageContext.transform.origin[0], -storageContext.transform.origin[1]);
      }

      if (storageContext.clip){
        ctx.beginPath();
        ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height);
        ctx.clip();
      }

      if (storageContext.ctx.storage) {
        storageContext.ctx.storage.forEach(function(item) {
          renderItem(ctx, item);
        });
      }

      ctx.restore();
    });

    Util.log("html2canvas: Renderer: Canvas renderer done - returning canvas obj");

    if (options.elements.length === 1) {
      if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") {
        // crop image to the bounds of selected (single) element
        bounds = _html2canvas.Util.Bounds(options.elements[0]);
        newCanvas = document.createElement('canvas');
        newCanvas.width = Math.ceil(bounds.width);
        newCanvas.height = Math.ceil(bounds.height);
        ctx = newCanvas.getContext("2d");

        ctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);
        canvas = null;
        return newCanvas;
      }
    }

    return canvas;
  };
};<commit_msg>Fix missing background color bug<commit_after>_html2canvas.Renderer.Canvas = function(options) {
  options = options || {};

  var doc = document,
  safeImages = [],
  testCanvas = document.createElement("canvas"),
  testctx = testCanvas.getContext("2d"),
  Util = _html2canvas.Util,
  canvas = options.canvas || doc.createElement('canvas');

  function createShape(ctx, args) {
    ctx.beginPath();
    args.forEach(function(arg) {
      ctx[arg.name].apply(ctx, arg['arguments']);
    });
    ctx.closePath();
  }

  function safeImage(item) {
    if (safeImages.indexOf(item['arguments'][0].src ) === -1) {
      testctx.drawImage(item['arguments'][0], 0, 0);
      try {
        testctx.getImageData(0, 0, 1, 1);
      } catch(e) {
        testCanvas = doc.createElement("canvas");
        testctx = testCanvas.getContext("2d");
        return false;
      }
      safeImages.push(item['arguments'][0].src);
    }
    return true;
  }

  function renderItem(ctx, item) {
    switch(item.type){
      case "variable":
        ctx[item.name] = item['arguments'];
        break;
      case "function":
        switch(item.name) {
          case "createPattern":
            if (item['arguments'][0].width > 0 && item['arguments'][0].height > 0) {
              try {
                ctx.fillStyle = ctx.createPattern(item['arguments'][0], "repeat");
              }
              catch(e) {
                Util.log("html2canvas: Renderer: Error creating pattern", e.message);
              }
            }
            break;
          case "drawShape":
            createShape(ctx, item['arguments']);
            break;
          case "drawImage":
            if (item['arguments'][8] > 0 && item['arguments'][7] > 0) {
              if (!options.taintTest || (options.taintTest && safeImage(item))) {
                ctx.drawImage.apply( ctx, item['arguments'] );
              }
            }
            break;
          default:
            ctx[item.name].apply(ctx, item['arguments']);
        }
        break;
    }
  }

  return function(parsedData, options, document, queue, _html2canvas) {
    var ctx = canvas.getContext("2d"),
    newCanvas,
    bounds,
    fstyle,
    zStack = parsedData.stack;

    canvas.width = canvas.style.width =  options.width || zStack.ctx.width;
    canvas.height = canvas.style.height = options.height || zStack.ctx.height;

    fstyle = ctx.fillStyle;
    ctx.fillStyle = (Util.isTransparent(parsedData.backgroundColor) && options.background !== undefined) ? options.background : parsedData.backgroundColor;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    ctx.fillStyle = fstyle;

    queue.forEach(function(storageContext) {
      // set common settings for canvas
      ctx.textBaseline = "bottom";
      ctx.save();

      if (storageContext.transform.matrix) {
        ctx.translate(storageContext.transform.origin[0], storageContext.transform.origin[1]);
        ctx.transform.apply(ctx, storageContext.transform.matrix);
        ctx.translate(-storageContext.transform.origin[0], -storageContext.transform.origin[1]);
      }

      if (storageContext.clip){
        ctx.beginPath();
        ctx.rect(storageContext.clip.left, storageContext.clip.top, storageContext.clip.width, storageContext.clip.height);
        ctx.clip();
      }

      if (storageContext.ctx.storage) {
        storageContext.ctx.storage.forEach(function(item) {
          renderItem(ctx, item);
        });
      }

      ctx.restore();
    });

    Util.log("html2canvas: Renderer: Canvas renderer done - returning canvas obj");

    if (options.elements.length === 1) {
      if (typeof options.elements[0] === "object" && options.elements[0].nodeName !== "BODY") {
        // crop image to the bounds of selected (single) element
        bounds = _html2canvas.Util.Bounds(options.elements[0]);
        newCanvas = document.createElement('canvas');
        newCanvas.width = Math.ceil(bounds.width);
        newCanvas.height = Math.ceil(bounds.height);
        ctx = newCanvas.getContext("2d");

        ctx.drawImage(canvas, bounds.left, bounds.top, bounds.width, bounds.height, 0, 0, bounds.width, bounds.height);
        canvas = null;
        return newCanvas;
      }
    }

    return canvas;
  };
};<|endoftext|>


<commit_before>var FreeStyle = require('free-style')
var insertStyles = require('insert-styles')
var window = require('global/window')

var styleInstancesById = {}
var DEFAULT_STYLE_ID = '__stylin__'

// Support multiple instances of the module in the same document.
module.exports = window.__stylin_data__ = window.__stylin_data__ || {
  STYLE_ID: DEFAULT_STYLE_ID,
  getStyles: getStyles,
  reset: reset,
  registerStyle: createStyleFunction('registerStyle', 1),
  registerKeyframes: createStyleFunction('registerKeyframes', 1),
  registerRule: createStyleFunction('registerRule', 2)
}

function getStyles (styleId) {
  var byId = styleInstancesById[styleId || DEFAULT_STYLE_ID]
  return byId && byId.Style ? byId.Style.getStyles() : ''
}

function reset () {
  styleInstancesById = {}
}

function createStyleFunction (methodName, arity) {
  return function styleFn () {
    var args = Array.prototype.slice.call(arguments)
    var opts = args.length > arity ? args.pop() : null
    var styleId = opts && opts.styleId ? opts.styleId : DEFAULT_STYLE_ID

    var byId = styleInstancesById[styleId] = styleInstancesById[styleId] || {
      Style: FreeStyle.create(),
      changeId: null
    }
    var Style = byId.Style
    var changeId = byId.changeId

    var result = Style[methodName].apply(Style, args)
    if (Style.changeId !== changeId) {
      insertStyles(Style.getStyles(), {id: styleId})
      changeId = Style.changeId
    }

    return result
  }
}
<commit_msg>fix(stylin): set changeId after changing<commit_after>var FreeStyle = require('free-style')
var insertStyles = require('insert-styles')
var window = require('global/window')

var styleInstancesById = {}
var DEFAULT_STYLE_ID = '__stylin__'

// Support multiple instances of the module in the same document.
module.exports = window.__stylin_data__ = window.__stylin_data__ || {
  STYLE_ID: DEFAULT_STYLE_ID,
  getStyles: getStyles,
  reset: reset,
  registerStyle: createStyleFunction('registerStyle', 1),
  registerKeyframes: createStyleFunction('registerKeyframes', 1),
  registerRule: createStyleFunction('registerRule', 2)
}

function getStyles (styleId) {
  var byId = styleInstancesById[styleId || DEFAULT_STYLE_ID]
  return byId && byId.Style ? byId.Style.getStyles() : ''
}

function reset () {
  styleInstancesById = {}
}

function createStyleFunction (methodName, arity) {
  return function styleFn () {
    var args = Array.prototype.slice.call(arguments)
    var opts = args.length > arity ? args.pop() : null
    var styleId = opts && opts.styleId ? opts.styleId : DEFAULT_STYLE_ID

    var byId = styleInstancesById[styleId] = styleInstancesById[styleId] || {
      Style: FreeStyle.create(),
      changeId: null
    }
    var Style = byId.Style
    var changeId = byId.changeId

    var result = Style[methodName].apply(Style, args)
    if (Style.changeId !== changeId) {
      insertStyles(Style.getStyles(), {id: styleId})
      byId.changeId = Style.changeId
    }

    return result
  }
}
<|endoftext|>


<commit_before>(function (Flow, window, document, undefined) {
  'use strict';

  var extend = Flow.extend;
  var each = Flow.each;

  function addEvent(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  }

  function removeEvent(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
  }

  function removeElement(element) {
    element.parentNode.removeChild(element);
  }

  function isFunction(functionToCheck) {
    var getType = {};
    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
  }

  /**
   * Not resumable file upload library, for IE7-IE9 browsers
   * @name FustyFlow
   * @param [opts]
   * @param {bool} [opts.singleFile]
   * @param {string} [opts.fileParameterName]
   * @param {Object|Function} [opts.query]
   * @param {Object} [opts.headers]
   * @param {string} [opts.target]
   * @param {Function} [opts.generateUniqueIdentifier]
   * @param {bool} [opts.matchJSON]
   * @constructor
   */
  function FustyFlow(opts) {
    // Shortcut of "r instanceof Flow"
    this.support = false;

    this.files = [];
    this.events = [];
    this.defaults = {
      simultaneousUploads: 3,
      fileParameterName: 'file',
      query: {},
      target: '/',
      generateUniqueIdentifier: null,
      matchJSON: false
    };

    var $ = this;

    this.inputChangeEvent = function (event) {
      var input = event.srcElement;
      removeEvent(input, 'change', $.inputChangeEvent);
      var newClone = input.cloneNode(false);
      // change current input with new one
      input.parentNode.replaceChild(newClone, input);
      // old input will be attached to hidden form
      $.addFile(input, event);
      // reset new input
      newClone.value = '';
      addEvent(newClone, 'change', $.inputChangeEvent);
    };

    this.opts = Flow.extend({}, this.defaults, opts || {});
  }

  FustyFlow.prototype = {
    on: Flow.prototype.on,
    fire: Flow.prototype.fire,
    cancel: Flow.prototype.cancel,
    assignBrowse: function (domNodes) {
      if (typeof domNodes.length == 'undefined') {
        domNodes = [domNodes];
      }
      each(domNodes, function (domNode) {
        var input;
        if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
          input = domNode;
        } else {
          input = document.createElement('input');
          input.setAttribute('type', 'file');

          extend(domNode.style, {
            display: 'inline-block',
            position: 'relative',
            overflow: 'hidden',
            verticalAlign: 'top'
          });

          extend(input.style, {
            position: 'absolute',
            top: 0,
            right: 0,
            fontFamily: 'Arial',
            // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
            fontSize: '118px',
            margin: 0,
            padding: 0,
            opacity: 0,
            filter: 'alpha(opacity=0)',
            cursor: 'pointer'
          });

          domNode.appendChild(input);
        }
        // When new files are added, simply append them to the overall list
        addEvent(input, 'change', this.inputChangeEvent);
      }, this);
    },
    assignDrop: function () {
      // not supported
    },
    unAssignDrop: function () {
      // not supported
    },
    isUploading: function () {
      var uploading = false;
      each(this.files, function (file) {
        if (file.isUploading()) {
          uploading = true;
          return false;
        }
      });
      return uploading;
    },
    upload: function () {
      // Kick off the queue
      var files = 0;
      each(this.files, function (file) {
        if (file.progress() == 1) {
          return;
        }
        if (file.isUploading()) {
          files++;
          return;
        }
        if (files++ >= this.opts.simultaneousUploads) {
          return false;
        }
        if (files == 1) {
          this.fire('uploadStart');
        }
        file.send();
      }, this);
      if (!files) {
        this.fire('complete');
      }
    },
    pause: function () {
      each(this.files, function (file) {
        file.abort();
      });
    },
    resume: function () {
      this.upload();
    },
    progress: function () {
      var totalDone = 0;
      var totalFiles = 0;
      each(this.files, function (file) {
        totalDone += file.progress();
        totalFiles++;
      });
      return totalFiles > 0 ? totalDone / totalFiles : 0;
    },
    addFiles: function (elementsList, event) {
      var files = [];
      each(elementsList, function (element) {
        // is domElement ?
        if (element.nodeType === 1 && element.value) {
          var f = new FustyFlowFile(this, element);
          if (this.fire('fileAdded', f, event)) {
            files.push(f);
          }
        }
      }, this);
      if (this.fire('filesAdded', files, event)) {
        each(files, function (file) {
          if (this.opts.singleFile && this.files.length > 0) {
            this.removeFile(this.files[0]);
          }
          this.files.push(file);
        }, this);
      }
      this.fire('filesSubmitted', files, event);
    },
    addFile: function (file, event) {
      this.addFiles([file], event);
    },
    generateUniqueIdentifier: function (element) {
      var custom = this.opts.generateUniqueIdentifier;
      if (typeof custom === 'function') {
        return custom(element);
      }
      return 'xxxxxxxx-xxxx-yxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    },
    getFromUniqueIdentifier: function (uniqueIdentifier) {
      var ret = false;
      each(this.files, function (f) {
        if (f.uniqueIdentifier == uniqueIdentifier) ret = f;
      });
      return ret;
    },
    removeFile: function (file) {
      for (var i = this.files.length - 1; i >= 0; i--) {
        if (this.files[i] === file) {
          this.files.splice(i, 1);
        }
      }
    },
    getSize: function () {
      // undefined
    },
    timeRemaining: function () {
      // undefined
    },
    sizeUploaded: function () {
      // undefined
    }
  };

  function FustyFlowFile(flowObj, element) {
    this.flowObj = flowObj;
    this.element = element;
    this.name = element.value && element.value.replace(/.*(\/|\\)/, "");
    this.relativePath = this.name;
    this.uniqueIdentifier = flowObj.generateUniqueIdentifier(element);
    this.iFrame = null;

    this.finished = false;
    this.error = false;

    var $ = this;
    this.iFrameLoaded = function (event) {
      // when we remove iframe from dom
      // the request stops, but in IE load
      // event fires
      if (!$.iFrame || !$.iFrame.parentNode) {
        return;
      }
      $.finished = true;
      try {
        // fixing Opera 10.53
        if ($.iFrame.contentDocument &&
          $.iFrame.contentDocument.body &&
          $.iFrame.contentDocument.body.innerHTML == "false") {
          // In Opera event is fired second time
          // when body.innerHTML changed from false
          // to server response approx. after 1 sec
          // when we upload file with iframe
          return;
        }
      } catch (error) {
        //IE may throw an "access is denied" error when attempting to access contentDocument
        $.error = true;
        $.abort();
        $.flowObj.fire('fileError', $, error);
        return;
      }
      // iframe.contentWindow.document - for IE<7
      var doc = $.iFrame.contentDocument || $.iFrame.contentWindow.document;
      var innerHtml = doc.body.innerHTML;
      if ($.flowObj.opts.matchJSON) {
        innerHtml = /(\{.*\})/.exec(innerHtml)[0];
      }

      $.abort();
      $.flowObj.fire('fileSuccess', $, innerHtml);
      $.flowObj.upload();
    };
    this.bootstrap();
  }

  FustyFlowFile.prototype = {
    getExtension: Flow.FlowFile.prototype.getExtension,
    getType: function () {
      // undefined
    },
    send: function () {
      if (this.finished) {
        return;
      }
      var o = this.flowObj.opts;
      var form = this.createForm();
      var params = o.query;
      if (isFunction(params)) {
        params = params(this);
      }
      params[o.fileParameterName] = this.element;
      params['flowFilename'] = this.name;
      params['flowRelativePath'] = this.relativePath;
      params['flowIdentifier'] = this.uniqueIdentifier;

      this.addFormParams(form, params);
      addEvent(this.iFrame, 'load', this.iFrameLoaded);
      form.submit();
      removeElement(form);
    },
    abort: function () {
      if (this.iFrame) {
        this.iFrame.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;');
        removeElement(this.iFrame);
        this.iFrame = null;
      }
    },
    cancel: function () {
      this.abort();
      this.flowObj.removeFile(this);
    },
    retry: function () {
      this.bootstrap();
      this.flowObj.upload();
    },
    bootstrap: function () {
      this.abort();
      this.error = false;
    },
    timeRemaining: function () {
      // undefined
    },
    sizeUploaded: function () {
      // undefined
    },
    resume: function () {
      this.flowObj.upload();
    },
    pause: function () {
      this.abort();
    },
    isUploading: function () {
      return this.iFrame !== null;
    },
    isComplete: function () {
      return this.progress() === 1;
    },
    progress: function () {
      if (this.error) {
        return 1;
      }
      return this.finished ? 1 : 0;
    },

    createIframe: function () {
      var iFrame = (/MSIE (6|7|8)/).test(navigator.userAgent) ?
        document.createElement('<iframe name="' + this.uniqueIdentifier + '_iframe' + '">') :
        document.createElement('iframe');

      iFrame.setAttribute('id', this.uniqueIdentifier + '_iframe_id');
      iFrame.setAttribute('name', this.uniqueIdentifier + '_iframe');
      iFrame.style.display = 'none';
      document.body.appendChild(iFrame);
      return iFrame;
    },
    createForm: function() {
      var target = this.flowObj.opts.target;
      var form = document.createElement('form');
      form.encoding = "multipart/form-data";
      form.method = "POST";
      form.setAttribute('action', target);
      if (!this.iFrame) {
        this.iFrame = this.createIframe();
      }
      form.setAttribute('target', this.iFrame.name);
      form.style.display = 'none';
      document.body.appendChild(form);
      return form;
    },
    addFormParams: function(form, params) {
      var input;
      each(params, function (value, key) {
        if (value && value.nodeType === 1) {
          input = value;
        } else {
          input = document.createElement('input');
          input.setAttribute('value', value);
        }
        input.setAttribute('name', key);
        form.appendChild(input);
      });
    }
  };

  FustyFlow.FustyFlowFile = FustyFlowFile;

  if (typeof module !== 'undefined') {
    module.exports = FustyFlow;
  } else if (typeof define === "function" && define.amd) {
    // AMD/requirejs: Define the module
    define(function(){
      return FustyFlow;
    });
  } else {
    window.FustyFlow = FustyFlow;
  }
})(window.Flow, window, document);

<commit_msg>fix retry bug<commit_after>(function (Flow, window, document, undefined) {
  'use strict';

  var extend = Flow.extend;
  var each = Flow.each;

  function addEvent(element, type, handler) {
    if (element.addEventListener) {
      element.addEventListener(type, handler, false);
    } else if (element.attachEvent) {
      element.attachEvent("on" + type, handler);
    } else {
      element["on" + type] = handler;
    }
  }

  function removeEvent(element, type, handler) {
    if (element.removeEventListener) {
      element.removeEventListener(type, handler, false);
    } else if (element.detachEvent) {
      element.detachEvent("on" + type, handler);
    } else {
      element["on" + type] = null;
    }
  }

  function removeElement(element) {
    element.parentNode.removeChild(element);
  }

  function isFunction(functionToCheck) {
    var getType = {};
    return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
  }

  /**
   * Not resumable file upload library, for IE7-IE9 browsers
   * @name FustyFlow
   * @param [opts]
   * @param {bool} [opts.singleFile]
   * @param {string} [opts.fileParameterName]
   * @param {Object|Function} [opts.query]
   * @param {Object} [opts.headers]
   * @param {string} [opts.target]
   * @param {Function} [opts.generateUniqueIdentifier]
   * @param {bool} [opts.matchJSON]
   * @constructor
   */
  function FustyFlow(opts) {
    // Shortcut of "r instanceof Flow"
    this.support = false;

    this.files = [];
    this.events = [];
    this.defaults = {
      simultaneousUploads: 3,
      fileParameterName: 'file',
      query: {},
      target: '/',
      generateUniqueIdentifier: null,
      matchJSON: false
    };

    var $ = this;

    this.inputChangeEvent = function (event) {
      var input = event.srcElement;
      removeEvent(input, 'change', $.inputChangeEvent);
      var newClone = input.cloneNode(false);
      // change current input with new one
      input.parentNode.replaceChild(newClone, input);
      // old input will be attached to hidden form
      $.addFile(input, event);
      // reset new input
      newClone.value = '';
      addEvent(newClone, 'change', $.inputChangeEvent);
    };

    this.opts = Flow.extend({}, this.defaults, opts || {});
  }

  FustyFlow.prototype = {
    on: Flow.prototype.on,
    fire: Flow.prototype.fire,
    cancel: Flow.prototype.cancel,
    assignBrowse: function (domNodes) {
      if (typeof domNodes.length == 'undefined') {
        domNodes = [domNodes];
      }
      each(domNodes, function (domNode) {
        var input;
        if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
          input = domNode;
        } else {
          input = document.createElement('input');
          input.setAttribute('type', 'file');

          extend(domNode.style, {
            display: 'inline-block',
            position: 'relative',
            overflow: 'hidden',
            verticalAlign: 'top'
          });

          extend(input.style, {
            position: 'absolute',
            top: 0,
            right: 0,
            fontFamily: 'Arial',
            // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118
            fontSize: '118px',
            margin: 0,
            padding: 0,
            opacity: 0,
            filter: 'alpha(opacity=0)',
            cursor: 'pointer'
          });

          domNode.appendChild(input);
        }
        // When new files are added, simply append them to the overall list
        addEvent(input, 'change', this.inputChangeEvent);
      }, this);
    },
    assignDrop: function () {
      // not supported
    },
    unAssignDrop: function () {
      // not supported
    },
    isUploading: function () {
      var uploading = false;
      each(this.files, function (file) {
        if (file.isUploading()) {
          uploading = true;
          return false;
        }
      });
      return uploading;
    },
    upload: function () {
      // Kick off the queue
      var files = 0;
      each(this.files, function (file) {
        if (file.progress() == 1) {
          return;
        }
        if (file.isUploading()) {
          files++;
          return;
        }
        if (files++ >= this.opts.simultaneousUploads) {
          return false;
        }
        if (files == 1) {
          this.fire('uploadStart');
        }
        file.send();
      }, this);
      if (!files) {
        this.fire('complete');
      }
    },
    pause: function () {
      each(this.files, function (file) {
        file.abort();
      });
    },
    resume: function () {
      this.upload();
    },
    progress: function () {
      var totalDone = 0;
      var totalFiles = 0;
      each(this.files, function (file) {
        totalDone += file.progress();
        totalFiles++;
      });
      return totalFiles > 0 ? totalDone / totalFiles : 0;
    },
    addFiles: function (elementsList, event) {
      var files = [];
      each(elementsList, function (element) {
        // is domElement ?
        if (element.nodeType === 1 && element.value) {
          var f = new FustyFlowFile(this, element);
          if (this.fire('fileAdded', f, event)) {
            files.push(f);
          }
        }
      }, this);
      if (this.fire('filesAdded', files, event)) {
        each(files, function (file) {
          if (this.opts.singleFile && this.files.length > 0) {
            this.removeFile(this.files[0]);
          }
          this.files.push(file);
        }, this);
      }
      this.fire('filesSubmitted', files, event);
    },
    addFile: function (file, event) {
      this.addFiles([file], event);
    },
    generateUniqueIdentifier: function (element) {
      var custom = this.opts.generateUniqueIdentifier;
      if (typeof custom === 'function') {
        return custom(element);
      }
      return 'xxxxxxxx-xxxx-yxxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
        return v.toString(16);
      });
    },
    getFromUniqueIdentifier: function (uniqueIdentifier) {
      var ret = false;
      each(this.files, function (f) {
        if (f.uniqueIdentifier == uniqueIdentifier) ret = f;
      });
      return ret;
    },
    removeFile: function (file) {
      for (var i = this.files.length - 1; i >= 0; i--) {
        if (this.files[i] === file) {
          this.files.splice(i, 1);
        }
      }
    },
    getSize: function () {
      // undefined
    },
    timeRemaining: function () {
      // undefined
    },
    sizeUploaded: function () {
      // undefined
    }
  };

  function FustyFlowFile(flowObj, element) {
    this.flowObj = flowObj;
    this.element = element;
    this.name = element.value && element.value.replace(/.*(\/|\\)/, "");
    this.relativePath = this.name;
    this.uniqueIdentifier = flowObj.generateUniqueIdentifier(element);
    this.iFrame = null;

    this.finished = false;
    this.error = false;

    var $ = this;
    this.iFrameLoaded = function (event) {
      // when we remove iframe from dom
      // the request stops, but in IE load
      // event fires
      if (!$.iFrame || !$.iFrame.parentNode) {
        return;
      }
      $.finished = true;
      try {
        // fixing Opera 10.53
        if ($.iFrame.contentDocument &&
          $.iFrame.contentDocument.body &&
          $.iFrame.contentDocument.body.innerHTML == "false") {
          // In Opera event is fired second time
          // when body.innerHTML changed from false
          // to server response approx. after 1 sec
          // when we upload file with iframe
          return;
        }
      } catch (error) {
        //IE may throw an "access is denied" error when attempting to access contentDocument
        $.error = true;
        $.abort();
        $.flowObj.fire('fileError', $, error);
        return;
      }
      // iframe.contentWindow.document - for IE<7
      var doc = $.iFrame.contentDocument || $.iFrame.contentWindow.document;
      var innerHtml = doc.body.innerHTML;
      if ($.flowObj.opts.matchJSON) {
        innerHtml = /(\{.*\})/.exec(innerHtml)[0];
      }

      $.abort();
      $.flowObj.fire('fileSuccess', $, innerHtml);
      $.flowObj.upload();
    };
    this.bootstrap();
  }

  FustyFlowFile.prototype = {
    getExtension: Flow.FlowFile.prototype.getExtension,
    getType: function () {
      // undefined
    },
    send: function () {
      if (this.finished) {
        return;
      }
      var o = this.flowObj.opts;
      var form = this.createForm();
      var params = o.query;
      if (isFunction(params)) {
        params = params(this);
      }
      params[o.fileParameterName] = this.element;
      params['flowFilename'] = this.name;
      params['flowRelativePath'] = this.relativePath;
      params['flowIdentifier'] = this.uniqueIdentifier;

      this.addFormParams(form, params);
      addEvent(this.iFrame, 'load', this.iFrameLoaded);
      form.submit();
      removeElement(form);
    },
    abort: function () {
      if (this.iFrame) {
        this.iFrame.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;');
        removeElement(this.iFrame);
        this.iFrame = null;
      }
    },
    cancel: function () {
      this.abort();
      this.flowObj.removeFile(this);
    },
    retry: function () {
      this.bootstrap();
      this.flowObj.upload();
    },
    bootstrap: function () {
      this.abort();
      this.finished = false;
      this.error = false;
    },
    timeRemaining: function () {
      // undefined
    },
    sizeUploaded: function () {
      // undefined
    },
    resume: function () {
      this.flowObj.upload();
    },
    pause: function () {
      this.abort();
    },
    isUploading: function () {
      return this.iFrame !== null;
    },
    isComplete: function () {
      return this.progress() === 1;
    },
    progress: function () {
      if (this.error) {
        return 1;
      }
      return this.finished ? 1 : 0;
    },

    createIframe: function () {
      var iFrame = (/MSIE (6|7|8)/).test(navigator.userAgent) ?
        document.createElement('<iframe name="' + this.uniqueIdentifier + '_iframe' + '">') :
        document.createElement('iframe');

      iFrame.setAttribute('id', this.uniqueIdentifier + '_iframe_id');
      iFrame.setAttribute('name', this.uniqueIdentifier + '_iframe');
      iFrame.style.display = 'none';
      document.body.appendChild(iFrame);
      return iFrame;
    },
    createForm: function() {
      var target = this.flowObj.opts.target;
      var form = document.createElement('form');
      form.encoding = "multipart/form-data";
      form.method = "POST";
      form.setAttribute('action', target);
      if (!this.iFrame) {
        this.iFrame = this.createIframe();
      }
      form.setAttribute('target', this.iFrame.name);
      form.style.display = 'none';
      document.body.appendChild(form);
      return form;
    },
    addFormParams: function(form, params) {
      var input;
      each(params, function (value, key) {
        if (value && value.nodeType === 1) {
          input = value;
        } else {
          input = document.createElement('input');
          input.setAttribute('value', value);
        }
        input.setAttribute('name', key);
        form.appendChild(input);
      });
    }
  };

  FustyFlow.FustyFlowFile = FustyFlowFile;

  if (typeof module !== 'undefined') {
    module.exports = FustyFlow;
  } else if (typeof define === "function" && define.amd) {
    // AMD/requirejs: Define the module
    define(function(){
      return FustyFlow;
    });
  } else {
    window.FustyFlow = FustyFlow;
  }
})(window.Flow, window, document);

<|endoftext|>


<commit_before>var _ = require('lodash'),
    utils = require('./../utils'),
    driver = utils.getRuntimeDriver(),
    C = driver.constants;

var runtimeData, intents, register, globals;

exports.make = function(_runtimeData, _intents, _register, _globals) {

    runtimeData = _runtimeData;
    intents = _intents;
    register = _register;
    globals = _globals;

    if (globals.Store) {
        return;
    }

    var Store = register.wrapFn(function(object) {

        Object.entries(object.store).forEach(([resourceType, resourceAmount]) => {
            if(resourceAmount) {
                this[resourceType] = resourceAmount;
            }
        });

        Object.defineProperties(this, {
            getCapacity: {
                value: function getCapacity(resource) {
                    if(!resource) {
                        return object.storeCapacityResource ? null : object.storeCapacity || null;
                    }
                    return utils.capacityForResource(object, resource) || null;
                }
            },
            getUsedCapacity: {
                value: function getUsedCapacity(resource) {
                    if(!resource) {
                        if(!!object.storeCapacityResource && (utils.capacityForResource(object, resource) === 0)) {
                            return null;
                        }
                        if(this._sum === undefined) {
                            Object.defineProperty(this, '_sum', {
                                value: _.sum(object.store)
                            });
                        }
                        return this._sum;
                    }
                    return object.store[resource] || (!!object.storeCapacityResource && utils.capacityForResource(object, resource) === 0 ? null : 0);
                }
            },
            getFreeCapacity: {
                value: function getFreeCapacity(resource) {
                    if(utils.capacityForResource(object, resource) === 0) {
                        return null;
                    }

                    if(!object.storeCapacity) {
                        return this.getCapacity(resource) - this.getUsedCapacity(resource);
                    }

                    const capacity = this.getCapacity(resource);
                    if(!capacity) {
                        return null;
                    }

                    if(this._sum === undefined) {
                        Object.defineProperty(this, '_sum', {
                            value: _.sum(object.store)
                        });
                    }

                    return capacity - this._sum;
                }
            },
            toString: {
                value: function toString() {
                    return `[store]`;
                }
            }
        });

        return new Proxy(this, {
            get(target, name) {
                if(target[name] !== undefined) {
                    return target[name];
                }
                if(C.RESOURCES_ALL.indexOf(name) !== -1) {
                    return 0;
                }
            }
        });
    });

    Object.defineProperty(globals, 'Store', {enumerable: true, value: Store});

};
<commit_msg>🚑 StructureLab.store.getFreeCapacity edge case<commit_after>var _ = require('lodash'),
    utils = require('./../utils'),
    driver = utils.getRuntimeDriver(),
    C = driver.constants;

var runtimeData, intents, register, globals;

exports.make = function(_runtimeData, _intents, _register, _globals) {

    runtimeData = _runtimeData;
    intents = _intents;
    register = _register;
    globals = _globals;

    if (globals.Store) {
        return;
    }

    var Store = register.wrapFn(function(object) {

        Object.entries(object.store).forEach(([resourceType, resourceAmount]) => {
            if(resourceAmount) {
                this[resourceType] = resourceAmount;
            }
        });

        Object.defineProperties(this, {
            getCapacity: {
                value: function getCapacity(resource) {
                    if(!resource) {
                        return object.storeCapacityResource ? null : object.storeCapacity || null;
                    }
                    return utils.capacityForResource(object, resource) || null;
                }
            },
            getUsedCapacity: {
                value: function getUsedCapacity(resource) {
                    if(!resource) {
                        if(!!object.storeCapacityResource && (utils.capacityForResource(object, resource) === 0)) {
                            return null;
                        }
                        if(this._sum === undefined) {
                            Object.defineProperty(this, '_sum', {
                                value: _.sum(object.store)
                            });
                        }
                        return this._sum;
                    }
                    return object.store[resource] || (!!object.storeCapacityResource && utils.capacityForResource(object, resource) === 0 ? null : 0);
                }
            },
            getFreeCapacity: {
                value: function getFreeCapacity(resource) {
                    if(utils.capacityForResource(object, resource) === 0) {
                        return null;
                    }

                    if(!object.storeCapacity) {
                        return this.getCapacity(resource) - this.getUsedCapacity(resource);
                    }

                    const capacity = this.getCapacity(resource);
                    if(!capacity) {
                        return null;
                    }

                    if(object.storeCapacityResource) {
                        return capacity - this.getUsedCapacity(resource);
                    }

                    if(this._sum === undefined) {
                        Object.defineProperty(this, '_sum', {
                            value: _.sum(object.store)
                        });
                    }

                    return capacity - this._sum;
                }
            },
            toString: {
                value: function toString() {
                    return `[store]`;
                }
            }
        });

        return new Proxy(this, {
            get(target, name) {
                if(target[name] !== undefined) {
                    return target[name];
                }
                if(C.RESOURCES_ALL.indexOf(name) !== -1) {
                    return 0;
                }
            }
        });
    });

    Object.defineProperty(globals, 'Store', {enumerable: true, value: Store});

};
<|endoftext|>