<commit_before>'use strict';

var WorkerManager = require('web-worker-manager');

var bestResults = require('./bestResults');

module.exports = MFProcessorWorker;

function MFProcessorWorker(options) {
    if (!options.url) {
        throw new Error('URL to the chemcalc extended library is needed');
    }
    options.deps = [options.url];
    this.options = options;
    this.manager = null;
    this.hasExperimental = false;
    this._init();
}

MFProcessorWorker.prototype._init = function () {
    this.hasExperimental = false;
    this.manager = new WorkerManager(insideMFProcessor, this.options);
};

MFProcessorWorker.prototype.callbacks = function (options) {
    options = options || {};
    this.onStep = options.onStep || Function.prototype;
    this.onError = options.onError || Function.prototype;
};

MFProcessorWorker.prototype.init = function (experimental, calculationOptions) {
    this.hasExperimental = true;
    return this.manager.postAll('init', [experimental, calculationOptions]);
};

MFProcessorWorker.prototype.process = function (mfs, experimental, calculationOptions) {
    if (!Array.isArray(mfs)) {
        throw new TypeError('mfs must be an array');
    }
    if (!Array.isArray(experimental)) {
        calculationOptions = experimental;
 //       experimental = null;
    }
    var promise;
    if (experimental) {
        promise = this.init(experimental, calculationOptions);
    } else {
        promise = Promise.resolve();
    }
    var self = this;
    return promise.then(function () {
        return new Promise(function (resolve, reject) {
            var processed = 0;
            var total = mfs.length;
            var results = [];
            mfs.forEach(function (mf) {
                self.manager.post('data', [mf.mf]).then(
                    function (result) {
                        result.parts = mf;
                        results.push(result);
                        if (results.length > calculationOptions.maxResults * 2) {
                            results = bestResults(results, calculationOptions.bestOf, calculationOptions.maxResults, calculationOptions.minSimilarity);
                        }

                    },
                    this.onError
                ).then(function () {
                    processed++;
                    this.onStep(processed, total);
                    if (processed === total) {
                        results = bestResults(results, calculationOptions.bestOf, calculationOptions.maxResults, calculationOptions.minSimilarity);
                        results=results.map(function(a) {
                            var ppm=((a.msem-calculationOptions.mass)/(a.msem))*1e6;
                            a.ppm=Math.round(ppm*100)/100;
                            a.absppm=Math.abs(a.ppm*100)/100;
                            return a;
                        });
                        resolve(results);
                    }
                });
            });
        });
    });
};

MFProcessorWorker.prototype.reset = function () {
    this.manager.terminate();
    this._init();
};

function insideMFProcessor() {
    var processor;
    worker.on('init', function(send, exp, options) {
        processor = new chemcalcExtended.MFProcessor(exp, options);
        send(true);
    });
    worker.on('data', function(send, data){
        send(processor.process(data));
    });
}
<commit_msg>Update<commit_after>'use strict';

var WorkerManager = require('web-worker-manager');

var bestResults = require('./bestResults');

module.exports = MFProcessorWorker;

function MFProcessorWorker(options) {
    if (!options.url) {
        throw new Error('URL to the chemcalc extended library is needed');
    }
    options.deps = [options.url];
    this.options = options;
    this.manager = null;
    this.hasExperimental = false;
    this._init();
}

MFProcessorWorker.prototype._init = function () {
    this.hasExperimental = false;
    this.manager = new WorkerManager(insideMFProcessor, this.options);
};

MFProcessorWorker.prototype.callbacks = function (options) {
    options = options || {};
    this.onStep = options.onStep || Function.prototype;
    this.onError = options.onError || Function.prototype;
};

MFProcessorWorker.prototype.init = function (experimental, calculationOptions) {
    this.hasExperimental = true;
    return this.manager.postAll('init', [experimental, calculationOptions]);
};

MFProcessorWorker.prototype.process = function (mfs, experimental, calculationOptions) {
    if (!Array.isArray(mfs)) {
        throw new TypeError('mfs must be an array');
    }
    if (!Array.isArray(experimental)) {
        calculationOptions = experimental;
        experimental = null;
    }
    var promise;
    if (experimental) {
        promise = this.init(experimental, calculationOptions);
    } else {
        promise = Promise.resolve();
    }
    var self = this;
    return promise.then(function () {
        return new Promise(function (resolve, reject) {
            var processed = 0;
            var total = mfs.length;
            var results = [];
            mfs.forEach(function (mf) {
                self.manager.post('data', [mf.mf]).then(
                    function (result) {
                        result.parts = mf;
                        results.push(result);
                        if (results.length > calculationOptions.maxResults * 2) {
                            results = bestResults(results, calculationOptions.bestOf, calculationOptions.maxResults, calculationOptions.minSimilarity);
                        }

                    },
                    this.onError
                ).then(function () {
                    processed++;
                    this.onStep(processed, total);
                    if (processed === total) {
                        results = bestResults(results, calculationOptions.bestOf, calculationOptions.maxResults, calculationOptions.minSimilarity);
                        results=results.map(function(a) {
                            var ppm=((a.msem-calculationOptions.mass)/(a.msem))*1e6;
                            a.ppm=Math.round(ppm*100)/100;
                            a.absppm=Math.abs(a.ppm*100)/100;
                            return a;
                        });
                        resolve(results);
                    }
                });
            });
        });
    });
};

MFProcessorWorker.prototype.reset = function () {
    this.manager.terminate();
    this._init();
};

function insideMFProcessor() {
    var processor;
    worker.on('init', function(send, exp, options) {
        processor = new chemcalcExtended.MFProcessor(exp, options);
        send(true);
    });
    worker.on('data', function(send, data){
        send(processor.process(data));
    });
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, brackets, $, window */

define(function (require, exports, module) {
    "use strict";

    var AppInit             = brackets.getModule("utils/AppInit"),
        CodeHintManager     = brackets.getModule("editor/CodeHintManager"),
        CSSUtils            = brackets.getModule("language/CSSUtils"),
        HTMLUtils           = brackets.getModule("language/HTMLUtils"),
        LanguageManager     = brackets.getModule("language/LanguageManager"),
        TokenUtils          = brackets.getModule("utils/TokenUtils"),
        StringMatch         = brackets.getModule("utils/StringMatch"),
        CSSProperties       = require("text!CSSProperties.json"),
        properties          = JSON.parse(CSSProperties);

    // Context of the last request for hints: either CSSUtils.PROP_NAME,
    // CSSUtils.PROP_VALUE or null.
    var lastContext,
        stringMatcherOptions = { preferPrefixMatches: true };

    /**
     * @constructor
     */
    function CssPropHints() {
        this.primaryTriggerKeys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-()";
        this.secondaryTriggerKeys = ":";
        this.exclusion = null;
    }

    /**
     * Get the CSS style text of the file open in the editor for this hinting session.
     * For a CSS file, this is just the text of the file. For an HTML file,
     * this will be only the text in the <style> tags.
     *
     * @return {string} the "css" text that can be sent to CSSUtils to extract all named flows.
     */
    CssPropHints.prototype.getCssStyleText = function () {
        if (LanguageManager.getLanguageForPath(this.editor.document.file.fullPath).getId() === "html") {
            // Collect text in all style blocks
            var text = "",
                styleBlocks = HTMLUtils.findBlocks(this.editor, "css");

            styleBlocks.forEach(function (styleBlock) {
                text += styleBlock.text;
            });

            return text;
        } else {
            // css file, just return the text
            return this.editor.document.getText();
        }
    };

    /**
     * Extract all the named flows from any "flow-from" or "flow-into" properties
     * in the current document. If we have the cached list of named flows and the
     * cursor is still on the same line as the cached cursor, then the cached list
     * is returned. Otherwise, we recollect all named flows and update the cache.
     *
     * @return {Array.<string>} All named flows available in the current document.
     */
    CssPropHints.prototype.getNamedFlows = function () {
        if (this.namedFlowsCache) {
            // If the cursor is no longer on the same line, then the cache is stale.
            // Delete cache so we can extract all named flows again.
            if (this.namedFlowsCache.cursor.line !== this.cursor.line) {
                this.namedFlowsCache = null;
            }
        }

        if (!this.namedFlowsCache) {
            this.namedFlowsCache = {};
            this.namedFlowsCache.flows = CSSUtils.extractAllNamedFlows(this.getCssStyleText());
            this.namedFlowsCache.cursor = { line: this.cursor.line, ch: this.cursor.ch };
        }

        return this.namedFlowsCache.flows;
    };

    /**
     * Check whether the exclusion is still the same as text after the cursor.
     * If not, reset it to null.
     *
     * @param {boolean} propNameOnly
     * true to indicate that we update the exclusion only if the cursor is inside property name context.
     * Otherwise, we also update exclusion for property value context.
     */
    CssPropHints.prototype.updateExclusion = function (propNameOnly) {
        var textAfterCursor;
        if (this.exclusion && this.info) {
            if (this.info.context === CSSUtils.PROP_NAME) {
                textAfterCursor = this.info.name.substr(this.info.offset);
            } else if (!propNameOnly && this.info.context === CSSUtils.PROP_VALUE) {
                textAfterCursor = this.info.value.substr(this.info.offset);
            }
            if (!CodeHintManager.hasValidExclusion(this.exclusion, textAfterCursor)) {
                this.exclusion = null;
            }
        }
    };

    /**
     * Determines whether CSS propertyname or -name hints are available in the current editor
     * context.
     *
     * @param {Editor} editor
     * A non-null editor object for the active window.
     *
     * @param {String} implicitChar
     * Either null, if the hinting request was explicit, or a single character
     * that represents the last insertion and that indicates an implicit
     * hinting request.
     *
     * @return {Boolean}
     * Determines whether the current provider is able to provide hints for
     * the given editor context and, in case implicitChar is non- null,
     * whether it is appropriate to do so.
     */
    CssPropHints.prototype.hasHints = function (editor, implicitChar) {
        this.editor = editor;
        var cursor = this.editor.getCursorPos(),
            textAfterCursor;

        lastContext = null;
        this.info = CSSUtils.getInfoAtPos(editor, cursor);

        if (this.info.context !== CSSUtils.PROP_NAME && this.info.context !== CSSUtils.PROP_VALUE) {
            return false;
        }

        if (implicitChar) {
            this.updateExclusion(false);
            if (this.info.context === CSSUtils.PROP_NAME) {
                // Check if implicitChar is the first character typed before an existing property name.
                if (!this.exclusion && this.info.offset === 1 && implicitChar === this.info.name[0]) {
                    this.exclusion = this.info.name.substr(this.info.offset);
                }
            }

            return (this.primaryTriggerKeys.indexOf(implicitChar) !== -1) ||
                   (this.secondaryTriggerKeys.indexOf(implicitChar) !== -1);
        } else if (this.info.context === CSSUtils.PROP_NAME) {
            if (this.info.offset === 0) {
                this.exclusion = this.info.name;
            } else {
                this.updateExclusion(true);
            }
        }

        return true;
    };

    /**
     * Returns a list of availble CSS propertyname or -value hints if possible for the current
     * editor context.
     *
     * @param {Editor} implicitChar
     * Either null, if the hinting request was explicit, or a single character
     * that represents the last insertion and that indicates an implicit
     * hinting request.
     *
     * @return {jQuery.Deferred|{
     *              hints: Array.<string|jQueryObject>,
     *              match: string,
     *              selectInitial: boolean,
     *              handleWideResults: boolean}}
     * Null if the provider wishes to end the hinting session. Otherwise, a
     * response object that provides:
     * 1. a sorted array hints that consists of strings
     * 2. a string match that is used by the manager to emphasize matching
     *    substrings when rendering the hint list
     * 3. a boolean that indicates whether the first result, if one exists,
     *    should be selected by default in the hint list window.
     * 4. handleWideResults, a boolean (or undefined) that indicates whether
     *    to allow result string to stretch width of display.
     */
    CssPropHints.prototype.getHints = function (implicitChar) {
        this.cursor = this.editor.getCursorPos();
        this.info = CSSUtils.getInfoAtPos(this.editor, this.cursor);

        var needle = this.info.name,
            valueNeedle = "",
            context = this.info.context,
            valueArray,
            namedFlows,
            result,
            selectInitial = false;

        // Clear the exclusion if the user moves the cursor with left/right arrow key.
        this.updateExclusion(true);

        if (context === CSSUtils.PROP_VALUE) {

            // Always select initial value
            selectInitial = true;

            // When switching from a NAME to a VALUE context, restart the session
            // to give other more specialized providers a chance to intervene.
            if (lastContext === CSSUtils.PROP_NAME) {
                return true;
            } else {
                lastContext = CSSUtils.PROP_VALUE;
            }

            if (!properties[needle]) {
                return null;
            }

            // Cursor is in an existing property value or partially typed value
            if (!this.info.isNewItem && this.info.index !== -1) {
                valueNeedle = this.info.values[this.info.index].trim();
                valueNeedle = valueNeedle.substr(0, this.info.offset);
            }

            valueArray = properties[needle].values;
            if (properties[needle].type === "named-flow") {
                namedFlows = this.getNamedFlows();

                if (valueNeedle.length === this.info.offset && namedFlows.indexOf(valueNeedle) !== -1) {
                    // Exclude the partially typed named flow at cursor since it
                    // is not an existing one used in other css rule.
                    namedFlows.splice(namedFlows.indexOf(valueNeedle), 1);
                }

                valueArray = valueArray.concat(namedFlows);
            }

            result = $.map(valueArray, function (pvalue, pindex) {
                if (StringMatch.stringMatch(pvalue, valueNeedle, stringMatcherOptions)) {
                    return pvalue;
                }
            });

            var newResult = StringMatch.basicMatchSort(result);

            return {
                hints: result,
                match: valueNeedle,
                selectInitial: selectInitial
            };
        } else if (context === CSSUtils.PROP_NAME) {

            // Select initial property if anything has been typed
            if (this.primaryTriggerKeys.indexOf(implicitChar) !== -1 || needle !== "") {
                selectInitial = true;
            }

            lastContext = CSSUtils.PROP_NAME;
            needle = needle.substr(0, this.info.offset);
            result = $.map(properties, function (pvalues, pname) {
                if (StringMatch.stringMatch(pname, needle, stringMatcherOptions)) {
                    return pname;
                }
            });

            var newResult = StringMatch.basicMatchSort(result);

            return {
                hints: result,
                match: needle,
                selectInitial: selectInitial,
                handleWideResults: false
            };
        }
        return null;
    };

    /**
     * Inserts a given CSS protertyname or -value hint into the current editor context.
     *
     * @param {String} hint
     * The hint to be inserted into the editor context.
     *
     * @return {Boolean}
     * Indicates whether the manager should follow hint insertion with an
     * additional explicit hint request.
     */
    CssPropHints.prototype.insertHint = function (hint) {
        var offset = this.info.offset,
            cursor = this.editor.getCursorPos(),
            start = {line: -1, ch: -1},
            end = {line: -1, ch: -1},
            keepHints = false,
            adjustCursor = false,
            newCursor,
            ctx;

        if (this.info.context !== CSSUtils.PROP_NAME && this.info.context !== CSSUtils.PROP_VALUE) {
            return false;
        }

        start.line = end.line = cursor.line;
        start.ch = cursor.ch - offset;

        if (this.info.context === CSSUtils.PROP_NAME) {
            keepHints = true;
            var textAfterCursor = this.info.name.substr(this.info.offset);
            if (this.info.name.length === 0 || CodeHintManager.hasValidExclusion(this.exclusion, textAfterCursor)) {
                // It's a new insertion, so append a colon and set keepHints
                // to show property value hints.
                hint += ": ";
                end.ch = start.ch;
                end.ch += offset;

                if (this.exclusion) {
                    // Append a space to the end of hint to insert and then adjust
                    // the cursor before that space.
                    hint += " ";
                    adjustCursor = true;
                    newCursor = { line: cursor.line,
                                  ch: start.ch + hint.length - 1 };
                    this.exclusion = null;
                }
            } else {
                // It's a replacement of an existing one or just typed in property.
                // So we need to check whether there is an existing colon following
                // the current property name. If a colon already exists, then we also
                // adjust the cursor position and show code hints for property values.
                end.ch = start.ch + this.info.name.length;
                ctx = TokenUtils.getInitialContext(this.editor._codeMirror, cursor);
                if (ctx.token.string.length > 0 && !ctx.token.string.match(/\S/)) {
                    // We're at the very beginning of a property name. So skip it
                    // before we locate the colon following it.
                    TokenUtils.moveNextToken(ctx);
                }
                if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) && ctx.token.string === ": ") {
                    adjustCursor = true;
                    newCursor = { line: cursor.line,
                                  ch: cursor.ch + (hint.length - this.info.name.length) };
                } else {
                    hint += ": ";
                }
            }
        } else {
            if (!this.info.isNewItem && this.info.index !== -1) {
                // Replacing an existing property value or partially typed value
                end.ch = start.ch + this.info.values[this.info.index].length;
            } else {
                // Inserting a new property value
                end.ch = start.ch;
            }

            var parenMatch = hint.match(/\(.*?\)/);
            if (parenMatch) {
                // value has (...), so place cursor inside opening paren
                // and keep hints open
                adjustCursor = true;
                newCursor = { line: cursor.line,
                              ch: start.ch + parenMatch.index + 1 };
                keepHints = true;
            }
        }

        // HACK (tracking adobe/brackets#1688): We talk to the private CodeMirror instance
        // directly to replace the range instead of using the Document, as we should. The
        // reason is due to a flaw in our current document synchronization architecture when
        // inline editors are open.
        this.editor._codeMirror.replaceRange(hint, start, end);

        if (adjustCursor) {
            this.editor.setCursorPos(newCursor);
        }

        return keepHints;
    };

    AppInit.appReady(function () {
        var cssPropHints = new CssPropHints();
        CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 0);

        // For unit testing
        exports.cssPropHintProvider = cssPropHints;
    });
});
<commit_msg>better match sorting<commit_after>/*
 * Copyright (c) 2013 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */
/*global define, brackets, $, window */

define(function (require, exports, module) {
    "use strict";

    var AppInit             = brackets.getModule("utils/AppInit"),
        CodeHintManager     = brackets.getModule("editor/CodeHintManager"),
        CSSUtils            = brackets.getModule("language/CSSUtils"),
        HTMLUtils           = brackets.getModule("language/HTMLUtils"),
        LanguageManager     = brackets.getModule("language/LanguageManager"),
        TokenUtils          = brackets.getModule("utils/TokenUtils"),
        StringMatch         = brackets.getModule("utils/StringMatch"),
        CSSProperties       = require("text!CSSProperties.json"),
        properties          = JSON.parse(CSSProperties);

    // Context of the last request for hints: either CSSUtils.PROP_NAME,
    // CSSUtils.PROP_VALUE or null.
    var lastContext,
        stringMatcherOptions = { preferPrefixMatches: true };

    /**
     * @constructor
     */
    function CssPropHints() {
        this.primaryTriggerKeys = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-()";
        this.secondaryTriggerKeys = ":";
        this.exclusion = null;
    }

    /**
     * Get the CSS style text of the file open in the editor for this hinting session.
     * For a CSS file, this is just the text of the file. For an HTML file,
     * this will be only the text in the <style> tags.
     *
     * @return {string} the "css" text that can be sent to CSSUtils to extract all named flows.
     */
    CssPropHints.prototype.getCssStyleText = function () {
        if (LanguageManager.getLanguageForPath(this.editor.document.file.fullPath).getId() === "html") {
            // Collect text in all style blocks
            var text = "",
                styleBlocks = HTMLUtils.findBlocks(this.editor, "css");

            styleBlocks.forEach(function (styleBlock) {
                text += styleBlock.text;
            });

            return text;
        } else {
            // css file, just return the text
            return this.editor.document.getText();
        }
    };

    /**
     * Extract all the named flows from any "flow-from" or "flow-into" properties
     * in the current document. If we have the cached list of named flows and the
     * cursor is still on the same line as the cached cursor, then the cached list
     * is returned. Otherwise, we recollect all named flows and update the cache.
     *
     * @return {Array.<string>} All named flows available in the current document.
     */
    CssPropHints.prototype.getNamedFlows = function () {
        if (this.namedFlowsCache) {
            // If the cursor is no longer on the same line, then the cache is stale.
            // Delete cache so we can extract all named flows again.
            if (this.namedFlowsCache.cursor.line !== this.cursor.line) {
                this.namedFlowsCache = null;
            }
        }

        if (!this.namedFlowsCache) {
            this.namedFlowsCache = {};
            this.namedFlowsCache.flows = CSSUtils.extractAllNamedFlows(this.getCssStyleText());
            this.namedFlowsCache.cursor = { line: this.cursor.line, ch: this.cursor.ch };
        }

        return this.namedFlowsCache.flows;
    };

    /**
     * Check whether the exclusion is still the same as text after the cursor.
     * If not, reset it to null.
     *
     * @param {boolean} propNameOnly
     * true to indicate that we update the exclusion only if the cursor is inside property name context.
     * Otherwise, we also update exclusion for property value context.
     */
    CssPropHints.prototype.updateExclusion = function (propNameOnly) {
        var textAfterCursor;
        if (this.exclusion && this.info) {
            if (this.info.context === CSSUtils.PROP_NAME) {
                textAfterCursor = this.info.name.substr(this.info.offset);
            } else if (!propNameOnly && this.info.context === CSSUtils.PROP_VALUE) {
                textAfterCursor = this.info.value.substr(this.info.offset);
            }
            if (!CodeHintManager.hasValidExclusion(this.exclusion, textAfterCursor)) {
                this.exclusion = null;
            }
        }
    };

    /**
     * Determines whether CSS propertyname or -name hints are available in the current editor
     * context.
     *
     * @param {Editor} editor
     * A non-null editor object for the active window.
     *
     * @param {String} implicitChar
     * Either null, if the hinting request was explicit, or a single character
     * that represents the last insertion and that indicates an implicit
     * hinting request.
     *
     * @return {Boolean}
     * Determines whether the current provider is able to provide hints for
     * the given editor context and, in case implicitChar is non- null,
     * whether it is appropriate to do so.
     */
    CssPropHints.prototype.hasHints = function (editor, implicitChar) {
        this.editor = editor;
        var cursor = this.editor.getCursorPos(),
            textAfterCursor;

        lastContext = null;
        this.info = CSSUtils.getInfoAtPos(editor, cursor);

        if (this.info.context !== CSSUtils.PROP_NAME && this.info.context !== CSSUtils.PROP_VALUE) {
            return false;
        }

        if (implicitChar) {
            this.updateExclusion(false);
            if (this.info.context === CSSUtils.PROP_NAME) {
                // Check if implicitChar is the first character typed before an existing property name.
                if (!this.exclusion && this.info.offset === 1 && implicitChar === this.info.name[0]) {
                    this.exclusion = this.info.name.substr(this.info.offset);
                }
            }

            return (this.primaryTriggerKeys.indexOf(implicitChar) !== -1) ||
                   (this.secondaryTriggerKeys.indexOf(implicitChar) !== -1);
        } else if (this.info.context === CSSUtils.PROP_NAME) {
            if (this.info.offset === 0) {
                this.exclusion = this.info.name;
            } else {
                this.updateExclusion(true);
            }
        }

        return true;
    };

    /**
     * Returns a list of availble CSS propertyname or -value hints if possible for the current
     * editor context.
     *
     * @param {Editor} implicitChar
     * Either null, if the hinting request was explicit, or a single character
     * that represents the last insertion and that indicates an implicit
     * hinting request.
     *
     * @return {jQuery.Deferred|{
     *              hints: Array.<string|jQueryObject>,
     *              match: string,
     *              selectInitial: boolean,
     *              handleWideResults: boolean}}
     * Null if the provider wishes to end the hinting session. Otherwise, a
     * response object that provides:
     * 1. a sorted array hints that consists of strings
     * 2. a string match that is used by the manager to emphasize matching
     *    substrings when rendering the hint list
     * 3. a boolean that indicates whether the first result, if one exists,
     *    should be selected by default in the hint list window.
     * 4. handleWideResults, a boolean (or undefined) that indicates whether
     *    to allow result string to stretch width of display.
     */
    CssPropHints.prototype.getHints = function (implicitChar) {
        this.cursor = this.editor.getCursorPos();
        this.info = CSSUtils.getInfoAtPos(this.editor, this.cursor);

        var needle = this.info.name,
            valueNeedle = "",
            context = this.info.context,
            valueArray,
            namedFlows,
            result,
            selectInitial = false;

        // Clear the exclusion if the user moves the cursor with left/right arrow key.
        this.updateExclusion(true);

        if (context === CSSUtils.PROP_VALUE) {

            // Always select initial value
            selectInitial = true;

            // When switching from a NAME to a VALUE context, restart the session
            // to give other more specialized providers a chance to intervene.
            if (lastContext === CSSUtils.PROP_NAME) {
                return true;
            } else {
                lastContext = CSSUtils.PROP_VALUE;
            }

            if (!properties[needle]) {
                return null;
            }

            // Cursor is in an existing property value or partially typed value
            if (!this.info.isNewItem && this.info.index !== -1) {
                valueNeedle = this.info.values[this.info.index].trim();
                valueNeedle = valueNeedle.substr(0, this.info.offset);
            }

            valueArray = properties[needle].values;
            if (properties[needle].type === "named-flow") {
                namedFlows = this.getNamedFlows();

                if (valueNeedle.length === this.info.offset && namedFlows.indexOf(valueNeedle) !== -1) {
                    // Exclude the partially typed named flow at cursor since it
                    // is not an existing one used in other css rule.
                    namedFlows.splice(namedFlows.indexOf(valueNeedle), 1);
                }

                valueArray = valueArray.concat(namedFlows);
            }

            result = $.map(valueArray, function (pvalue, pindex) {
                var result = StringMatch.stringMatch(pvalue, valueNeedle, stringMatcherOptions);
                if (result) {
                    return result;
                }
            });

            StringMatch.basicMatchSort(result);

            result = $.map(result, function (entry) {
                return entry.label;
            });

            return {
                hints: result,
                match: valueNeedle,
                selectInitial: selectInitial
            };
        } else if (context === CSSUtils.PROP_NAME) {

            // Select initial property if anything has been typed
            if (this.primaryTriggerKeys.indexOf(implicitChar) !== -1 || needle !== "") {
                selectInitial = true;
            }

            lastContext = CSSUtils.PROP_NAME;
            needle = needle.substr(0, this.info.offset);

            result = $.map(properties, function (pvalues, pname) {
                var result = StringMatch.stringMatch(pname, needle, stringMatcherOptions);
                if (result) {
                    return result;
                }
            });

            StringMatch.basicMatchSort(result);

            result = $.map(result, function (entry) {
                return entry.label;
            });

            return {
                hints: result,
                match: needle,
                selectInitial: selectInitial,
                handleWideResults: false
            };
        }
        return null;
    };

    /**
     * Inserts a given CSS protertyname or -value hint into the current editor context.
     *
     * @param {String} hint
     * The hint to be inserted into the editor context.
     *
     * @return {Boolean}
     * Indicates whether the manager should follow hint insertion with an
     * additional explicit hint request.
     */
    CssPropHints.prototype.insertHint = function (hint) {
        var offset = this.info.offset,
            cursor = this.editor.getCursorPos(),
            start = {line: -1, ch: -1},
            end = {line: -1, ch: -1},
            keepHints = false,
            adjustCursor = false,
            newCursor,
            ctx;

        if (this.info.context !== CSSUtils.PROP_NAME && this.info.context !== CSSUtils.PROP_VALUE) {
            return false;
        }

        start.line = end.line = cursor.line;
        start.ch = cursor.ch - offset;

        if (this.info.context === CSSUtils.PROP_NAME) {
            keepHints = true;
            var textAfterCursor = this.info.name.substr(this.info.offset);
            if (this.info.name.length === 0 || CodeHintManager.hasValidExclusion(this.exclusion, textAfterCursor)) {
                // It's a new insertion, so append a colon and set keepHints
                // to show property value hints.
                hint += ": ";
                end.ch = start.ch;
                end.ch += offset;

                if (this.exclusion) {
                    // Append a space to the end of hint to insert and then adjust
                    // the cursor before that space.
                    hint += " ";
                    adjustCursor = true;
                    newCursor = { line: cursor.line,
                                  ch: start.ch + hint.length - 1 };
                    this.exclusion = null;
                }
            } else {
                // It's a replacement of an existing one or just typed in property.
                // So we need to check whether there is an existing colon following
                // the current property name. If a colon already exists, then we also
                // adjust the cursor position and show code hints for property values.
                end.ch = start.ch + this.info.name.length;
                ctx = TokenUtils.getInitialContext(this.editor._codeMirror, cursor);
                if (ctx.token.string.length > 0 && !ctx.token.string.match(/\S/)) {
                    // We're at the very beginning of a property name. So skip it
                    // before we locate the colon following it.
                    TokenUtils.moveNextToken(ctx);
                }
                if (TokenUtils.moveSkippingWhitespace(TokenUtils.moveNextToken, ctx) && ctx.token.string === ": ") {
                    adjustCursor = true;
                    newCursor = { line: cursor.line,
                                  ch: cursor.ch + (hint.length - this.info.name.length) };
                } else {
                    hint += ": ";
                }
            }
        } else {
            if (!this.info.isNewItem && this.info.index !== -1) {
                // Replacing an existing property value or partially typed value
                end.ch = start.ch + this.info.values[this.info.index].length;
            } else {
                // Inserting a new property value
                end.ch = start.ch;
            }

            var parenMatch = hint.match(/\(.*?\)/);
            if (parenMatch) {
                // value has (...), so place cursor inside opening paren
                // and keep hints open
                adjustCursor = true;
                newCursor = { line: cursor.line,
                              ch: start.ch + parenMatch.index + 1 };
                keepHints = true;
            }
        }

        // HACK (tracking adobe/brackets#1688): We talk to the private CodeMirror instance
        // directly to replace the range instead of using the Document, as we should. The
        // reason is due to a flaw in our current document synchronization architecture when
        // inline editors are open.
        this.editor._codeMirror.replaceRange(hint, start, end);

        if (adjustCursor) {
            this.editor.setCursorPos(newCursor);
        }

        return keepHints;
    };

    AppInit.appReady(function () {
        var cssPropHints = new CssPropHints();
        CodeHintManager.registerHintProvider(cssPropHints, ["css", "scss", "less"], 0);

        // For unit testing
        exports.cssPropHintProvider = cssPropHints;
    });
});
<|endoftext|>


<commit_before>var exports = {
        push: {}
    };

exports.push.getBranch = function(payload) {
    if(!payload) {
        throw new Error("Invalid payload!");
    } else if(!payload.ref) {
        throw new Error("Payload has no 'ref' attribute!");
    }

    var refParts = payload.ref.split("/");
    var branchIndex = ref.length - 1;
    var branch = refParts[branchIndex];

    if(!branch || branch === "") {
        throw new Error("No branch specified on the given payload reference!");
    }

    return branch;
};

module.exports = exports;
<commit_msg>Added reference to payload.ref to split the parts of reference sent from github.<commit_after>var exports = {
        push: {}
    };

exports.push.getBranch = function(payload) {
    if(!payload) {
        throw new Error("Invalid payload!");
    } else if(!payload.ref) {
        throw new Error("Payload has no 'ref' attribute!");
    }

    var refParts = payload.ref.split("/");
    var branchIndex = refParts.length - 1;
    var branch = refParts[branchIndex];

    if(!branch || branch === "") {
        throw new Error("No branch specified on the given payload reference!");
    }

    return branch;
};

module.exports = exports;
<|endoftext|>


<commit_before>/* This file is part of Indico.
 * Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
 *
 * Indico is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * Indico is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indico; if not, see <http://www.gnu.org/licenses/>.
 */

var ndDirectives = angular.module("ndDirectives", []);

ndDirectives.directive("ndDialog", function($http, $compile, $timeout) {
    return {
        restrict: 'E',
        scope: {
            show: '=',
            heading: '@',
            okButton: '@',
            okCallback: '&',
            okOnly: '=',
            cancelButton: '@',
            cancelCallback: '&',
            validate: "=",
            api: '=',
            data: '=',
            config: "="
        },

        controller: function($scope) {
            $scope.actions = {
                init: function() {},
                cleanup: function() {},
                close: function() {
                    $scope.$apply($scope.validationStarted = false);
                    $scope.actions.cleanup();
                    $scope.show = false;
                    $scope.$apply($scope.show);
                },
                cancel: function() {
                    $scope.cancelCallback({dialogScope: $scope});
                    $scope.actions.close();
                },
                ok: function() {
                    $scope.$apply($scope.validationStarted = true);
                    var resultOkCallback = $scope.okCallback({dialogScope: $scope});
                    if(($scope.validate === true && resultOkCallback === true) || $scope.validate !== true) {
                        $scope.actions.close();
                    }
                }
            };
        },

        link: function(scope, element) {
            var dialog;

            var initDialog = function() {
                dialog = new ExclusivePopupWithButtons(
                    scope.heading,
                    scope.actions.cancel,
                    false,
                    false,
                    true
                );

                dialog._onClose = function() {};

                dialog._getButtons = function() {
                    var buttons = [];
                    var ok = scope.okButton || $T('Ok');
                    var cancel = scope.cancelButton || $T('Cancel');

                    buttons.push([ok, function() {
                        scope.actions.ok();
                    }]);

                    if (!scope.okOnly) {
                        buttons.push([cancel, function() {
                            scope.actions.cancel();
                        }]);
                    }

                    return buttons;
                };

                dialog.draw = function() {
                    return this.ExclusivePopupWithButtons.prototype.draw.call(this, element);
                };

                dialog.postDraw = function() {
                    this.canvas.dialog('option', 'draggable', false);
                    this.canvas.dialog('option', 'position', 'center');
                };
            };

            var openDialog = function() {
                if (dialog) {
                    showDialog();
                } else {
                    $http.get(
                        scope.templateUrl, {}
                    ).success(function(response, status, header, config) {
                        element = angular.element(response);
                        $compile(element)(scope);
                        $timeout(function(){
                            initDialog();
                            showDialog();
                        }, 0);
                    }). error(function(data, status, header, config) {
                        scope.show = false;
                        var msg = $T('The content of this dialog is currently unavailable.');
                        new AlertPopup(scope.heading, msg).open();
                    });
                }
            };

            var showDialog = function() {
                scope.actions.init();
                dialog.open();
            };

            scope.setSelectedTab = function(tab_id) {
                scope.tabSelected = tab_id;
            };

            scope.isTabSelected = function(tab_id) {
                return scope.tabSelected === tab_id;
            };

            scope.$watch("show", function(val) {
                if (scope.show === true) {
                    openDialog();
                } else if (dialog) {
                    dialog.close();
                }
            });
        }
    };
});

ndDirectives.directive("contenteditable", function() {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, elem, attrs, ctrl) {
            scope.edition = false;
            scope.multiline = scope.$eval(attrs.ndMultiline) || false;

            var sanitizeHtml = function() {
                var sanitized = elem.html();

                // Chrome sanitization
                sanitized = sanitized
                    .replace(/

<\/div>/g, '
<\/div>')
                    .replace(/<div[^<]*?>/g, '
')
                    .replace(/<\/div[^<]*?>/g, '');

                // IE sanitization
                sanitized = sanitized
                    .replace(/
 <\/p>/g, '
<\/p>')
                    .replace(/<p[^<]*?>/g, '
')
                    .replace(/<\/p[^<]*?>/g, '');

                // Trimming
                sanitized = sanitized
                    .replace(/ /g, ' ')
                    .replace(/^(( )*(
))*/, '')
                    .replace(/(( )*(
)( )*)*$/, '');

                elem.html(sanitized);
            };

            var updateHtml = function() {
                if (ctrl.$viewValue === '') {
                    elem.html(attrs.placeholder);
                    elem.addClass('empty');
                } else {
                    elem.html(_.unescape(ctrl.$viewValue));
                }
            };

            var getOneLineHeight = function() {
                var html = elem.html();
                var height = elem.html('.').css('height');
                elem.html(html);
                return height;
            };

            var actions = {
                init: function() {
                    elem.actioninput('initSize', elem.css('font-size'), getOneLineHeight());
                    elem.actioninput('setIconsVisibility', 'visible');
                    elem.actioninput('setEmptyValue', elem.html());
                    elem.addClass('focus');
                    elem.removeClass('empty');

                    if (ctrl.$viewValue === '' && !scope.edition) {
                        elem.html('');
                    }

                    scope.edition = true;
                    elem.text(elem.html());
                },
                close: function() {
                    elem.removeClass('focus');
                    elem.actioninput('setIconsVisibility', 'hidden');
                    scope.edition = false;
                    updateHtml();
                }
            };

            // model -> view
            ctrl.$render = function() {
                updateHtml();
            };

            // creation
            elem.actioninput({
                focusAfter: false,
                enterKeyEnabled: !scope.multiline,

                // view -> model
                callback: function() {
                    actions.close();
                },

                // view -> model
                actionCallback: function() {
                    sanitizeHtml();
                    scope.$apply(function() {
                        ctrl.$setViewValue(_.unescape(elem.html()));
                    });
                    actions.close();
                },

                // angular -> jquery
                onchange: function() {
                    elem.val(elem.html());
                }
            });

            // input init
            elem.on('focus', function() {
                actions.init();
            });

            elem.actioninput('setIconsVisibility', 'hidden');
        }
    };
});

ndDirectives.directive('ndDatepicker', function($compile) {
    return {
        restrict: 'E',
        replace: 'true',
        template: '<ng-form name="dateForm"></ng-form>',
        scope: {
            dateFormat: '@',
            hiddenInputs: '@',
            required: '@',
            showTime: '=',
            validation: '=',
            value: '='
        },

        link: function(scope, element) {
            var hiddenInputs =
                scope.$eval(scope.hiddenInputs) ||
                ['day', 'month', 'year', 'hour', 'min'];

            var mapMomentFormat = function(format) {
                format = format.replace('%Y', 'YYYY');
                format = format.replace('%m', 'MM');
                format = format.replace('%d', 'DD');
                format = format.replace('%H', 'HH');
                format = format.replace('%M', 'mm');
                return format;
            };

            var updateModel = function() {
                _.each(element.find('input'), function(e) {
                    $(e).trigger('input');
                });
            };

            var getAttributes = function() {
                var attributes = {};

                attributes['ng-class'] = "{hasError: validation && dateForm.$invalid}";
                if (scope.required) {
                    attributes.required = "required";
                }

                return attributes;
            };

            var getDatetime = function() {
                if (scope.value) {
                    return moment('{0} {1}'.format(scope.value.date, scope.value.time), 'YYYY-MM-DD HH:mm:ss');
                } else {
                    return null;
                }
            };

            var createDatePicker = function() {
                return IndicoUI.Widgets.Generic.dateField(
                    scope.showTime,
                    getAttributes(),
                    hiddenInputs,
                    null,
                    scope.dateFormat,
                    updateModel
                );
            };

            var getInputHtml = function(inputId) {
                var required = '';
                var value = null;

                if (inputId.match('Year') !== null) {
                    required = 'ng-required="{{required}}"';
                }

                if (scope.value) {
                    if (inputId.match('Year') !== null) {
                        value = getDatetime().year();
                    } else if (inputId.match('Month') !== null) {
                        value = getDatetime().month() + 1;
                    } else if (inputId.match('Day') !== null) {
                        value = getDatetime().date();
                    } else if (inputId.match('Hour') !== null) {
                        value = getDatetime().hour();
                    } else if (inputId.match('Min') !== null) {
                        value = getDatetime().minute();
                    }
                }

                return '<input type="text" name="{0}" id="{0}" ng-model="{0}" ng-init="{0}={1}" {2} style="display: none"/>'.format(inputId, value, required);
            };

            var init = function() {
                var datepicker = createDatePicker();

                element.html(datepicker.dom);
                _.each(hiddenInputs, function(inputId) {
                    element.append(getInputHtml(inputId));
                });

                if (scope.value) {
                    datepicker.set(getDatetime().format(mapMomentFormat(scope.dateFormat)));
                }

                $compile(element)(scope);
            };

            scope.$watch('dateFormat', function(newVal, oldVal) {
                init();
            });

            init();
        }
    };
});

ndDirectives.directive('ndConfirmationPopup', function() {
    return {
        restrict: 'E',
        scope: {
            heading: '@',
            content: '@',
            callback: '=',
            show: '='
        },

        link: function(scope) {
            var open = function() {
                var popup = new ConfirmPopup(scope.heading, scope.content, scope.callback, null, null, {'max-width': '300px'});
                popup.open();
            };

            scope.$watch('show', function() {
                if (scope.show === true) {
                    scope.show = false;
                    open();
                }
            });
        }
    };
});

ndDirectives.directive('ndValidFile', function() {
    return {
        require: 'ngModel',
        link: function (scope, el, attrs, ngModel) {
            ngModel.$render = function () {
                ngModel.$setViewValue(el.val());
            };

            el.bind('change', function() {
                scope.$apply(function() {
                    ngModel.$render();
                });
            });
        }
    };
});

ndDirectives.directive('ndRadioExtend', function($rootScope) {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, element) {
            element.bind('click', function() {
                $rootScope.$$phase || $rootScope.$apply();
            });
        }
    };
});

ndDirectives.directive('ndBlacklist', function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
            var blacklist = attrs.ndBlacklist.split(',');

            // DOM -> Model validation
            ngModel.$parsers.unshift(function(value) {
                var valid = blacklist.indexOf(value) === -1;
                ngModel.$setValidity('blacklist', valid);
                return valid ? value : undefined;
            });

            // model -> DOM validation
            ngModel.$formatters.unshift(function(value) {
                ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
                return value;
            });
        }
    };
});

ndDirectives.directive('ndInitFocus', function() {
    var timer;

    return function(scope, element, attrs) {
        if (timer) clearTimeout(timer);

        timer = setTimeout(function() {
            element.focus();
        }, 0);
    };
});
<commit_msg>[FIX] contenteditable html on focus after init<commit_after>/* This file is part of Indico.
 * Copyright (C) 2002 - 2014 European Organization for Nuclear Research (CERN).
 *
 * Indico is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * Indico is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Indico; if not, see <http://www.gnu.org/licenses/>.
 */

var ndDirectives = angular.module("ndDirectives", []);

ndDirectives.directive("ndDialog", function($http, $compile, $timeout) {
    return {
        restrict: 'E',
        scope: {
            show: '=',
            heading: '@',
            okButton: '@',
            okCallback: '&',
            okOnly: '=',
            cancelButton: '@',
            cancelCallback: '&',
            validate: "=",
            api: '=',
            data: '=',
            config: "="
        },

        controller: function($scope) {
            $scope.actions = {
                init: function() {},
                cleanup: function() {},
                close: function() {
                    $scope.$apply($scope.validationStarted = false);
                    $scope.actions.cleanup();
                    $scope.show = false;
                    $scope.$apply($scope.show);
                },
                cancel: function() {
                    $scope.cancelCallback({dialogScope: $scope});
                    $scope.actions.close();
                },
                ok: function() {
                    $scope.$apply($scope.validationStarted = true);
                    var resultOkCallback = $scope.okCallback({dialogScope: $scope});
                    if(($scope.validate === true && resultOkCallback === true) || $scope.validate !== true) {
                        $scope.actions.close();
                    }
                }
            };
        },

        link: function(scope, element) {
            var dialog;

            var initDialog = function() {
                dialog = new ExclusivePopupWithButtons(
                    scope.heading,
                    scope.actions.cancel,
                    false,
                    false,
                    true
                );

                dialog._onClose = function() {};

                dialog._getButtons = function() {
                    var buttons = [];
                    var ok = scope.okButton || $T('Ok');
                    var cancel = scope.cancelButton || $T('Cancel');

                    buttons.push([ok, function() {
                        scope.actions.ok();
                    }]);

                    if (!scope.okOnly) {
                        buttons.push([cancel, function() {
                            scope.actions.cancel();
                        }]);
                    }

                    return buttons;
                };

                dialog.draw = function() {
                    return this.ExclusivePopupWithButtons.prototype.draw.call(this, element);
                };

                dialog.postDraw = function() {
                    this.canvas.dialog('option', 'draggable', false);
                    this.canvas.dialog('option', 'position', 'center');
                };
            };

            var openDialog = function() {
                if (dialog) {
                    showDialog();
                } else {
                    $http.get(
                        scope.templateUrl, {}
                    ).success(function(response, status, header, config) {
                        element = angular.element(response);
                        $compile(element)(scope);
                        $timeout(function(){
                            initDialog();
                            showDialog();
                        }, 0);
                    }). error(function(data, status, header, config) {
                        scope.show = false;
                        var msg = $T('The content of this dialog is currently unavailable.');
                        new AlertPopup(scope.heading, msg).open();
                    });
                }
            };

            var showDialog = function() {
                scope.actions.init();
                dialog.open();
            };

            scope.setSelectedTab = function(tab_id) {
                scope.tabSelected = tab_id;
            };

            scope.isTabSelected = function(tab_id) {
                return scope.tabSelected === tab_id;
            };

            scope.$watch("show", function(val) {
                if (scope.show === true) {
                    openDialog();
                } else if (dialog) {
                    dialog.close();
                }
            });
        }
    };
});

ndDirectives.directive("contenteditable", function() {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, elem, attrs, ctrl) {
            scope.edition = false;
            scope.multiline = scope.$eval(attrs.ndMultiline) || false;

            var sanitizeHtml = function() {
                var sanitized = elem.html();

                // Chrome sanitization
                sanitized = sanitized
                    .replace(/

<\/div>/g, '
<\/div>')
                    .replace(/<div[^<]*?>/g, '
')
                    .replace(/<\/div[^<]*?>/g, '');

                // IE sanitization
                sanitized = sanitized
                    .replace(/
 <\/p>/g, '
<\/p>')
                    .replace(/<p[^<]*?>/g, '
')
                    .replace(/<\/p[^<]*?>/g, '');

                // Trimming
                sanitized = sanitized
                    .replace(/ /g, ' ')
                    .replace(/^(( )*(
))*/, '')
                    .replace(/(( )*(
)( )*)*$/, '');

                elem.html(sanitized);
            };

            var updateHtml = function() {
                if (ctrl.$viewValue === '') {
                    elem.html(attrs.placeholder);
                    elem.addClass('empty');
                } else {
                    elem.html(_.unescape(ctrl.$viewValue));
                }
            };

            var getOneLineHeight = function() {
                var html = elem.html();
                var height = elem.html('.').css('height');
                elem.html(html);
                return height;
            };

            var actions = {
                init: function() {
                    elem.actioninput('initSize', elem.css('font-size'), getOneLineHeight());
                    elem.actioninput('setIconsVisibility', 'visible');
                    elem.actioninput('setEmptyValue', elem.html());
                    elem.addClass('focus');
                    elem.removeClass('empty');

                    if (ctrl.$viewValue === '' && !scope.edition) {
                        elem.html('');
                    }

                    if (!scope.edition) {
                        elem.text(elem.html());
                    }

                    scope.edition = true;
                },
                close: function() {
                    elem.removeClass('focus');
                    elem.actioninput('setIconsVisibility', 'hidden');
                    scope.edition = false;
                    updateHtml();
                }
            };

            // model -> view
            ctrl.$render = function() {
                updateHtml();
            };

            // creation
            elem.actioninput({
                focusAfter: false,
                enterKeyEnabled: !scope.multiline,

                // view -> model
                callback: function() {
                    actions.close();
                },

                // view -> model
                actionCallback: function() {
                    sanitizeHtml();
                    scope.$apply(function() {
                        ctrl.$setViewValue(_.unescape(elem.html()));
                    });
                    actions.close();
                },

                // angular -> jquery
                onchange: function() {
                    elem.val(elem.html());
                }
            });

            // input init
            elem.on('focus', function() {
                actions.init();
            });

            elem.actioninput('setIconsVisibility', 'hidden');
        }
    };
});

ndDirectives.directive('ndDatepicker', function($compile) {
    return {
        restrict: 'E',
        replace: 'true',
        template: '<ng-form name="dateForm"></ng-form>',
        scope: {
            dateFormat: '@',
            hiddenInputs: '@',
            required: '@',
            showTime: '=',
            validation: '=',
            value: '='
        },

        link: function(scope, element) {
            var hiddenInputs =
                scope.$eval(scope.hiddenInputs) ||
                ['day', 'month', 'year', 'hour', 'min'];

            var mapMomentFormat = function(format) {
                format = format.replace('%Y', 'YYYY');
                format = format.replace('%m', 'MM');
                format = format.replace('%d', 'DD');
                format = format.replace('%H', 'HH');
                format = format.replace('%M', 'mm');
                return format;
            };

            var updateModel = function() {
                _.each(element.find('input'), function(e) {
                    $(e).trigger('input');
                });
            };

            var getAttributes = function() {
                var attributes = {};

                attributes['ng-class'] = "{hasError: validation && dateForm.$invalid}";
                if (scope.required) {
                    attributes.required = "required";
                }

                return attributes;
            };

            var getDatetime = function() {
                if (scope.value) {
                    return moment('{0} {1}'.format(scope.value.date, scope.value.time), 'YYYY-MM-DD HH:mm:ss');
                } else {
                    return null;
                }
            };

            var createDatePicker = function() {
                return IndicoUI.Widgets.Generic.dateField(
                    scope.showTime,
                    getAttributes(),
                    hiddenInputs,
                    null,
                    scope.dateFormat,
                    updateModel
                );
            };

            var getInputHtml = function(inputId) {
                var required = '';
                var value = null;

                if (inputId.match('Year') !== null) {
                    required = 'ng-required="{{required}}"';
                }

                if (scope.value) {
                    if (inputId.match('Year') !== null) {
                        value = getDatetime().year();
                    } else if (inputId.match('Month') !== null) {
                        value = getDatetime().month() + 1;
                    } else if (inputId.match('Day') !== null) {
                        value = getDatetime().date();
                    } else if (inputId.match('Hour') !== null) {
                        value = getDatetime().hour();
                    } else if (inputId.match('Min') !== null) {
                        value = getDatetime().minute();
                    }
                }

                return '<input type="text" name="{0}" id="{0}" ng-model="{0}" ng-init="{0}={1}" {2} style="display: none"/>'.format(inputId, value, required);
            };

            var init = function() {
                var datepicker = createDatePicker();

                element.html(datepicker.dom);
                _.each(hiddenInputs, function(inputId) {
                    element.append(getInputHtml(inputId));
                });

                if (scope.value) {
                    datepicker.set(getDatetime().format(mapMomentFormat(scope.dateFormat)));
                }

                $compile(element)(scope);
            };

            scope.$watch('dateFormat', function(newVal, oldVal) {
                init();
            });

            init();
        }
    };
});

ndDirectives.directive('ndConfirmationPopup', function() {
    return {
        restrict: 'E',
        scope: {
            heading: '@',
            content: '@',
            callback: '=',
            show: '='
        },

        link: function(scope) {
            var open = function() {
                var popup = new ConfirmPopup(scope.heading, scope.content, scope.callback, null, null, {'max-width': '300px'});
                popup.open();
            };

            scope.$watch('show', function() {
                if (scope.show === true) {
                    scope.show = false;
                    open();
                }
            });
        }
    };
});

ndDirectives.directive('ndValidFile', function() {
    return {
        require: 'ngModel',
        link: function (scope, el, attrs, ngModel) {
            ngModel.$render = function () {
                ngModel.$setViewValue(el.val());
            };

            el.bind('change', function() {
                scope.$apply(function() {
                    ngModel.$render();
                });
            });
        }
    };
});

ndDirectives.directive('ndRadioExtend', function($rootScope) {
    return {
        require: 'ngModel',
        restrict: 'A',
        link: function(scope, element) {
            element.bind('click', function() {
                $rootScope.$$phase || $rootScope.$apply();
            });
        }
    };
});

ndDirectives.directive('ndBlacklist', function() {
    return {
        require: 'ngModel',
        link: function(scope, element, attrs, ngModel) {
            var blacklist = attrs.ndBlacklist.split(',');

            // DOM -> Model validation
            ngModel.$parsers.unshift(function(value) {
                var valid = blacklist.indexOf(value) === -1;
                ngModel.$setValidity('blacklist', valid);
                return valid ? value : undefined;
            });

            // model -> DOM validation
            ngModel.$formatters.unshift(function(value) {
                ngModel.$setValidity('blacklist', blacklist.indexOf(value) === -1);
                return value;
            });
        }
    };
});

ndDirectives.directive('ndInitFocus', function() {
    var timer;

    return function(scope, element, attrs) {
        if (timer) clearTimeout(timer);

        timer = setTimeout(function() {
            element.focus();
        }, 0);
    };
});
<|endoftext|>


<commit_before>import { createLogic } from 'redux-logic';
import { isKana } from 'wanakana';
import { serializeReviewResponse } from 'common/serializers';
import { SRS_RANGES } from 'common/constants';
import { ANSWER_TYPES } from 'common/components/AddSynonym/AddSynonymForm';

import determineCriticality from 'common/utils/determineCriticality';
import { matchAnswer, increment, decrement, isInputValid, cleanseInput } from './utils';

import { selectQuizSettings } from 'features/user/selectors';
import { selectVocabById } from 'features/vocab/selectors';
import { selectSynonymById } from 'features/synonyms/selectors';
import {
  selectCategory,
  selectIsReviewQuiz,
  selectIsLessonQuiz,
  selectIsFinalQuestion,
  selectWrapUp,
  selectCurrent,
  selectCurrentPreviouslyIncorrect,
} from 'features/quiz/QuizSession/selectors';
import { selectAnswer, selectAnswerIgnored } from './selectors';

import { app } from 'common/actions';
import user from 'features/user/actions';
import quiz from 'features/quiz/actions';
import review from 'features/reviews/actions';
import synonym from 'features/synonyms/actions';

// we set this in quiz.advance and hold onto for clearing in quiz.answer.record
let autoAdvance = {};

const stopAutoAdvance = () => {
  if (autoAdvance.timeoutId != null) {
    clearTimeout(autoAdvance.timeoutId);
    autoAdvance.done();
    autoAdvance = {};
  }
};

export const submitAnswerLogic = createLogic({
  type: quiz.answer.submit,
  latest: true,
  process({ getState, action }, dispatch, done) {
    const { isDisabled } = selectAnswer(getState());
    const answerValue = cleanseInput(action.payload);
    const isValid = isInputValid(answerValue);

    dispatch(
      quiz.answer.update({
        value: answerValue,
        type: isKana(answerValue) ? ANSWER_TYPES.READING : ANSWER_TYPES.WORD,
      })
    );

    if (!isValid) {
      dispatch(quiz.answer.update({ isMarked: true, isValid: false }));
    }

    if (isValid && !isDisabled) {
      dispatch(quiz.answer.check());
    }
    if (isValid && isDisabled) {
      dispatch(quiz.answer.confirm());
    }
    done();
  },
});

export const confirmAnswerLogic = createLogic({
  type: quiz.answer.confirm,
  latest: true,
  process({ getState }, dispatch, done) {
    const { value, isValid, isDisabled } = selectAnswer(getState());
    if (value && isValid && isDisabled) {
      dispatch(quiz.answer.record.request());
    } else {
      // eslint-disable-next-line no-console
      console.warn('Tried to confirm answer, but answer state was invalid');
    }
    done();
  },
});

export const checkAnswerLogic = createLogic({
  type: quiz.answer.check,
  latest: true,
  process({ getState }, dispatch, done) {
    const { value } = selectAnswer(getState());
    const settings = selectQuizSettings(getState());
    let { vocab, synonyms } = selectCurrent(getState());
    vocab = vocab.map((id) => selectVocabById(getState(), { id }));
    synonyms = synonyms.map((id) => selectSynonymById(getState(), { id }));
    const matchedAnswer = matchAnswer(value, [vocab, synonyms]);
    const updatedAnswer = {
      isFocused: false,
      isMarked: true,
      isValid: true,
      isDisabled: true,
      isCorrect: false,
      isIncorrect: false,
    };

    if (matchedAnswer) {
      dispatch(
        quiz.answer.update({
          ...updatedAnswer,
          value: matchedAnswer,
          isCorrect: true,
        })
      );
      dispatch(quiz.answer.correct());
      const isOpen =
        settings.autoExpandAnswerOnSuccess && settings.autoAdvanceOnSuccessDelayMilliseconds > 0;
      dispatch(quiz.info.update({ isDisabled: false, detailLevel: 1, isOpen }));
    }

    if (!matchedAnswer) {
      dispatch(quiz.answer.update({ ...updatedAnswer, isIncorrect: true }));
      dispatch(quiz.answer.incorrect());
      const isOpen = settings.autoExpandAnswerOnFailure;
      dispatch(quiz.info.update({ isDisabled: false, detailLevel: 0, isOpen }));
    }

    done();
  },
});

export const correctAnswerLogic = createLogic({
  type: quiz.answer.correct,
  latest: true,
  process({ getState }, dispatch, done) {
    const settings = selectQuizSettings(getState());
    const current = selectCurrent(getState());
    const previouslyIncorrect = selectCurrentPreviouslyIncorrect(getState());

    if (settings.autoAdvanceOnSuccess) {
      dispatch(quiz.question.advance());
    }

    const newCorrect = !previouslyIncorrect ? increment(current.correct) : current.correct;
    const newStreak = !previouslyIncorrect ? increment(current.streak) : current.streak;
    const updatedReview = {
      ...current,
      correct: newCorrect,
      streak: newStreak,
      critical: determineCriticality(newCorrect, current.incorrect),
    };

    dispatch(quiz.session.current.update(updatedReview));
    done();
  },
});

export const incorrectAnswerLogic = createLogic({
  type: quiz.answer.incorrect,
  latest: true,
  process({ getState, action: { payload = {} } }, dispatch, done) {
    const current = selectCurrent(getState());
    const isLessonQuiz = selectIsLessonQuiz(getState());
    const previouslyIncorrect = selectCurrentPreviouslyIncorrect(getState());
    let { incorrect: newIncorrect } = current;
    let { streak: newStreak } = current;

    if (!isLessonQuiz && !previouslyIncorrect) {
      const isCloseToBurned = [...SRS_RANGES.THREE, ...SRS_RANGES.FOUR].includes(newStreak);

      newIncorrect = increment(newIncorrect);
      newStreak = isCloseToBurned
        ? decrement(current.streak - 1) // double decrement if close to burned
        : Math.max(decrement(current.streak), 1); // guard against dropping streak to "lesson" status
    }

    const updatedReview = {
      ...current,
      incorrect: newIncorrect,
      streak: newStreak,
      critical: determineCriticality(current.correct, newIncorrect),
    };

    dispatch(quiz.session.current.update(updatedReview));
    if (payload.forceIncorrect) {
      setTimeout(() => {
        dispatch(quiz.answer.record.request());
        done();
      }, 1000);
    } else {
      done();
    }
  },
});

export const ignoreAnswerLogic = createLogic({
  type: quiz.answer.ignore,
  validate({ getState, action }, allow, reject) {
    const { isMarked, isDisabled, isCorrect, isIncorrect } = selectAnswer(getState());
    stopAutoAdvance();

    if (isMarked && isDisabled && (isCorrect || isIncorrect)) {
      allow(action);
    } else {
      reject();
    }
  },
  process({ getState }, dispatch, done) {
    const { isCorrect } = selectAnswer(getState());
    if (isCorrect) {
      dispatch(quiz.answer.update({ isCorrect: false, isIncorrect: true }));
      dispatch(quiz.answer.incorrect({ forceIncorrect: true }));
    } else {
      dispatch(quiz.answer.update({ isIgnored: true }));
      dispatch(quiz.question.advance());
    }
    done();
  },
});

export const recordAnswerLogic = createLogic({
  type: quiz.answer.record.request,
  process({ api, history, getState }, dispatch, done) {
    const category = selectCategory(getState());
    const current = selectCurrent(getState());
    const wrapUp = selectWrapUp(getState());
    const isLessonQuiz = selectIsLessonQuiz(getState());
    const isFinalQuestion = selectIsFinalQuestion(getState());
    const { isCorrect } = selectAnswer(getState());
    const previouslyIncorrect = selectCurrentPreviouslyIncorrect(getState());
    stopAutoAdvance();

    // only add to totals if not already present
    if (!previouslyIncorrect) {
      const action = isCorrect ? 'addCorrect' : 'addIncorrect';
      dispatch(quiz.session[action](current.id));
      dispatch(quiz.summary[action](current.id, { category }));
    }

    if (isCorrect) {
      dispatch(quiz.session.addComplete(current.id));
      if (wrapUp.active) {
        dispatch(quiz.session.wrapUp.decrement());
      }
      if (isFinalQuestion) {
        dispatch(quiz.session.queue.clear());
        setTimeout(() => history.push(`/${category}`), 1000);
      }
      dispatch(quiz.session.current.replace());
    } else {
      dispatch(quiz.session.current.rotate());
    }

    dispatch(quiz.answer.reset());
    dispatch(quiz.info.reset());

    if (isLessonQuiz && !isCorrect) {
      dispatch(review.update(current));
      done();
    } else {
      api.quiz
        .record({ id: current.id, isCorrect, previouslyIncorrect })
        .then((response) => {
          const { vocabById, synonymsById, ...updatedReview } = serializeReviewResponse(response);
          dispatch(quiz.answer.record.success(current.id));
          dispatch(review.update(updatedReview));
          if (isFinalQuestion && isCorrect) {
            // wait for review times to be updated on server
            // then reload lesson/review counts
            setTimeout(() => {
              dispatch(user.load.request());
              done();
            }, 5000);
          } else {
            dispatch(quiz.session.queue.load.request());
            done();
          }
        })
        .catch((err) => {
          dispatch(app.captureError(err, { current, isCorrect, previouslyIncorrect }));
          dispatch(quiz.answer.record.failure(err));
          done();
        });
    }
  },
});

export const autoAdvanceLogic = createLogic({
  type: quiz.question.advance,
  warnTimeout: 11000,
  process({ getState }, dispatch, done) {
    const { autoAdvanceOnSuccessDelayMilliseconds } = selectQuizSettings(getState());
    const answerIgnored = selectAnswerIgnored(getState());

    if (answerIgnored) {
      const isReviewQuiz = selectIsReviewQuiz(getState());
      const reviewsQuizDelay = 700;
      const lessonsQuizDelay = 200;
      const msDelay = isReviewQuiz ? reviewsQuizDelay : lessonsQuizDelay;
      // allow time for "ignored" animation to occur in _reviews quiz_
      setTimeout(() => {
        dispatch(quiz.answer.reset());
        dispatch(quiz.info.reset());
        dispatch(quiz.session.current.rotate());
        done();
      }, msDelay);
    } else {
      const timeoutId = setTimeout(() => {
        dispatch(quiz.answer.confirm());
        done();
      }, autoAdvanceOnSuccessDelayMilliseconds);

      autoAdvance = {
        timeoutId,
        done,
      };
    }
  },
});

export const synonymAddLogic = createLogic({
  type: synonym.add.success,
  process({ getState }, dispatch, done) {
    dispatch(quiz.session.setSynonymModal(false));
    const { isIncorrect } = selectAnswer(getState());
    if (isIncorrect) {
      dispatch(quiz.answer.ignore());
    }
    done();
  },
});

export default [
  submitAnswerLogic,
  confirmAnswerLogic,
  checkAnswerLogic,
  ignoreAnswerLogic,
  incorrectAnswerLogic,
  correctAnswerLogic,
  recordAnswerLogic,
  autoAdvanceLogic,
  synonymAddLogic,
];
<commit_msg>fix: select vocab & synonyms directly from review instead of current<commit_after>import { createLogic } from 'redux-logic';
import { isKana } from 'wanakana';
import { serializeReviewResponse } from 'common/serializers';
import { SRS_RANGES } from 'common/constants';
import { ANSWER_TYPES } from 'common/components/AddSynonym/AddSynonymForm';

import determineCriticality from 'common/utils/determineCriticality';
import { matchAnswer, increment, decrement, isInputValid, cleanseInput } from './utils';

import { selectQuizSettings } from 'features/user/selectors';
import { selectReviewById } from 'features/reviews/selectors';
import { selectVocabById } from 'features/vocab/selectors';
import { selectSynonymById } from 'features/synonyms/selectors';
import {
  selectCategory,
  selectIsReviewQuiz,
  selectIsLessonQuiz,
  selectIsFinalQuestion,
  selectWrapUp,
  selectCurrent,
  selectCurrentId,
  selectCurrentPreviouslyIncorrect,
} from 'features/quiz/QuizSession/selectors';
import { selectAnswer, selectAnswerIgnored } from './selectors';

import { app } from 'common/actions';
import user from 'features/user/actions';
import quiz from 'features/quiz/actions';
import review from 'features/reviews/actions';
import synonym from 'features/synonyms/actions';

// we set this in quiz.advance and hold onto for clearing in quiz.answer.record
let autoAdvance = {};

const stopAutoAdvance = () => {
  if (autoAdvance.timeoutId != null) {
    clearTimeout(autoAdvance.timeoutId);
    autoAdvance.done();
    autoAdvance = {};
  }
};

export const submitAnswerLogic = createLogic({
  type: quiz.answer.submit,
  latest: true,
  process({ getState, action }, dispatch, done) {
    const { isDisabled } = selectAnswer(getState());
    const answerValue = cleanseInput(action.payload);
    const isValid = isInputValid(answerValue);

    dispatch(
      quiz.answer.update({
        value: answerValue,
        type: isKana(answerValue) ? ANSWER_TYPES.READING : ANSWER_TYPES.WORD,
      })
    );

    if (!isValid) {
      dispatch(quiz.answer.update({ isMarked: true, isValid: false }));
    }

    if (isValid && !isDisabled) {
      dispatch(quiz.answer.check());
    }
    if (isValid && isDisabled) {
      dispatch(quiz.answer.confirm());
    }
    done();
  },
});

export const confirmAnswerLogic = createLogic({
  type: quiz.answer.confirm,
  latest: true,
  process({ getState }, dispatch, done) {
    const { value, isValid, isDisabled } = selectAnswer(getState());
    if (value && isValid && isDisabled) {
      dispatch(quiz.answer.record.request());
    } else {
      dispatch(
        app.captureError(
          'Tried to confirm answer, but answer state was invalid',
          selectAnswer(getState())
        )
      );
    }
    done();
  },
});

export const checkAnswerLogic = createLogic({
  type: quiz.answer.check,
  latest: true,
  process({ getState }, dispatch, done) {
    const state = getState();
    const { value } = selectAnswer(state);
    const settings = selectQuizSettings(state);
    const currentId = selectCurrentId(state);
    let { vocab, synonyms } = selectReviewById(state, { id: currentId });
    vocab = vocab.map((id) => selectVocabById(state, { id }));
    synonyms = synonyms.map((id) => selectSynonymById(state, { id }));
    const matchedAnswer = matchAnswer(value, [vocab, synonyms]);
    const updatedAnswer = {
      isFocused: false,
      isMarked: true,
      isValid: true,
      isDisabled: true,
      isCorrect: false,
      isIncorrect: false,
    };

    if (matchedAnswer) {
      dispatch(
        quiz.answer.update({
          ...updatedAnswer,
          value: matchedAnswer,
          isCorrect: true,
        })
      );
      dispatch(quiz.answer.correct());
      const isOpen =
        settings.autoExpandAnswerOnSuccess && settings.autoAdvanceOnSuccessDelayMilliseconds > 0;
      dispatch(quiz.info.update({ isDisabled: false, detailLevel: 1, isOpen }));
    }

    if (!matchedAnswer) {
      dispatch(quiz.answer.update({ ...updatedAnswer, isIncorrect: true }));
      dispatch(quiz.answer.incorrect());
      const isOpen = settings.autoExpandAnswerOnFailure;
      dispatch(quiz.info.update({ isDisabled: false, detailLevel: 0, isOpen }));
    }

    done();
  },
});

export const correctAnswerLogic = createLogic({
  type: quiz.answer.correct,
  latest: true,
  process({ getState }, dispatch, done) {
    const settings = selectQuizSettings(getState());
    const current = selectCurrent(getState());
    const previouslyIncorrect = selectCurrentPreviouslyIncorrect(getState());

    if (settings.autoAdvanceOnSuccess) {
      dispatch(quiz.question.advance());
    }

    const newCorrect = !previouslyIncorrect ? increment(current.correct) : current.correct;
    const newStreak = !previouslyIncorrect ? increment(current.streak) : current.streak;
    const updatedReview = {
      ...current,
      correct: newCorrect,
      streak: newStreak,
      critical: determineCriticality(newCorrect, current.incorrect),
    };

    dispatch(quiz.session.current.update(updatedReview));
    done();
  },
});

export const incorrectAnswerLogic = createLogic({
  type: quiz.answer.incorrect,
  latest: true,
  process({ getState, action: { payload = {} } }, dispatch, done) {
    const current = selectCurrent(getState());
    const isLessonQuiz = selectIsLessonQuiz(getState());
    const previouslyIncorrect = selectCurrentPreviouslyIncorrect(getState());
    let { incorrect: newIncorrect } = current;
    let { streak: newStreak } = current;

    if (!isLessonQuiz && !previouslyIncorrect) {
      const isCloseToBurned = [...SRS_RANGES.THREE, ...SRS_RANGES.FOUR].includes(newStreak);

      newIncorrect = increment(newIncorrect);
      newStreak = isCloseToBurned
        ? decrement(current.streak - 1) // double decrement if close to burned
        : Math.max(decrement(current.streak), 1); // guard against dropping streak to "lesson" status
    }

    const updatedReview = {
      ...current,
      incorrect: newIncorrect,
      streak: newStreak,
      critical: determineCriticality(current.correct, newIncorrect),
    };

    dispatch(quiz.session.current.update(updatedReview));
    if (payload.forceIncorrect) {
      setTimeout(() => {
        dispatch(quiz.answer.record.request());
        done();
      }, 1000);
    } else {
      done();
    }
  },
});

export const ignoreAnswerLogic = createLogic({
  type: quiz.answer.ignore,
  validate({ getState, action }, allow, reject) {
    const { isMarked, isDisabled, isCorrect, isIncorrect } = selectAnswer(getState());
    stopAutoAdvance();

    if (isMarked && isDisabled && (isCorrect || isIncorrect)) {
      allow(action);
    } else {
      reject();
    }
  },
  process({ getState }, dispatch, done) {
    const { isCorrect } = selectAnswer(getState());
    if (isCorrect) {
      dispatch(quiz.answer.update({ isCorrect: false, isIncorrect: true }));
      dispatch(quiz.answer.incorrect({ forceIncorrect: true }));
    } else {
      dispatch(quiz.answer.update({ isIgnored: true }));
      dispatch(quiz.question.advance());
    }
    done();
  },
});

export const recordAnswerLogic = createLogic({
  type: quiz.answer.record.request,
  process({ api, history, getState }, dispatch, done) {
    const category = selectCategory(getState());
    const current = selectCurrent(getState());
    const wrapUp = selectWrapUp(getState());
    const isLessonQuiz = selectIsLessonQuiz(getState());
    const isFinalQuestion = selectIsFinalQuestion(getState());
    const { isCorrect } = selectAnswer(getState());
    const previouslyIncorrect = selectCurrentPreviouslyIncorrect(getState());
    stopAutoAdvance();

    // only add to correct/incorrect totals on first attempt
    if (!previouslyIncorrect) {
      const action = isCorrect ? 'addCorrect' : 'addIncorrect';
      dispatch(quiz.session[action](current.id));
      dispatch(quiz.summary[action](current.id, { category }));
    }

    if (isCorrect) {
      dispatch(quiz.session.addComplete(current.id));
      if (wrapUp.active) {
        dispatch(quiz.session.wrapUp.decrement());
      }
      if (isFinalQuestion) {
        dispatch(quiz.session.queue.clear());
        setTimeout(() => history.push(`/${category}`), 1000);
      }
      dispatch(quiz.session.current.replace());
    } else {
      dispatch(quiz.session.current.rotate());
    }

    dispatch(quiz.answer.reset());
    dispatch(quiz.info.reset());

    if (isLessonQuiz && !isCorrect) {
      dispatch(review.update(current));
      done();
    } else {
      api.quiz
        .record({ id: current.id, isCorrect, previouslyIncorrect })
        .then((response) => {
          const { vocabById, synonymsById, ...updatedReview } = serializeReviewResponse(response);
          dispatch(quiz.answer.record.success(current.id));
          dispatch(review.update(updatedReview));
          if (isFinalQuestion && isCorrect) {
            // wait for review times to be updated on server
            // then reload lesson/review counts
            setTimeout(() => {
              dispatch(user.load.request());
              done();
            }, 5000);
          } else {
            dispatch(quiz.session.queue.load.request());
            done();
          }
        })
        .catch((err) => {
          dispatch(app.captureError(err, { current, isCorrect, previouslyIncorrect }));
          dispatch(quiz.answer.record.failure(err));
          done();
        });
    }
  },
});

export const autoAdvanceLogic = createLogic({
  type: quiz.question.advance,
  warnTimeout: 11000,
  process({ getState }, dispatch, done) {
    const { autoAdvanceOnSuccessDelayMilliseconds } = selectQuizSettings(getState());
    const answerIgnored = selectAnswerIgnored(getState());

    if (answerIgnored) {
      const isReviewQuiz = selectIsReviewQuiz(getState());
      const reviewsQuizDelay = 700;
      const lessonsQuizDelay = 200;
      const msDelay = isReviewQuiz ? reviewsQuizDelay : lessonsQuizDelay;
      // allow time for "ignored" animation to occur in _reviews quiz_
      setTimeout(() => {
        dispatch(quiz.answer.reset());
        dispatch(quiz.info.reset());
        dispatch(quiz.session.current.rotate());
        done();
      }, msDelay);
    } else {
      const timeoutId = setTimeout(() => {
        dispatch(quiz.answer.confirm());
        done();
      }, autoAdvanceOnSuccessDelayMilliseconds);

      autoAdvance = {
        timeoutId,
        done,
      };
    }
  },
});

export const synonymAddLogic = createLogic({
  type: synonym.add.success,
  process({ getState }, dispatch, done) {
    dispatch(quiz.session.setSynonymModal(false));
    const { isIncorrect } = selectAnswer(getState());
    if (isIncorrect) {
      dispatch(quiz.answer.ignore());
    }
    done();
  },
});

export default [
  submitAnswerLogic,
  confirmAnswerLogic,
  checkAnswerLogic,
  ignoreAnswerLogic,
  incorrectAnswerLogic,
  correctAnswerLogic,
  recordAnswerLogic,
  autoAdvanceLogic,
  synonymAddLogic,
];
<|endoftext|>


<commit_before>/**
 * # Widget `identity`
 *
 * This widget is identified as `identity@hull`
 * Use it to indicate whether the user is logged in or not, and display login buttons if not.
 *
 */

define({
  type: "Hull",

  /**
   * @name Templates
   * #### `identity/identity`
   *
   * Displays login buttons or the name of the user, depending on its logged in state
   *
   */
  templates: ['identity'],

  // Which events to refresh the widget on.
  refreshEvents: ['model.hull.me.change'],

  initialize: function() {
    this.authServices = _.map(this.sandbox.config.services.types.auth, function(s) { return s.replace(/_app$/, ''); })
    if (_.isEmpty(this.authServices)) {
      console.error("No Auth services configured. please add one to be able to authenticate users.");
    }
  },

  /**
   * Process the resolved datasources before sending them to the template
   *
   * @param {Object} data The resolved requests from the `datasources` object
   * @return {Object|Promise|undefined}     The data after you processed it.
   * @name beforeRender
   */
  beforeRender: function(data) {
    data.authServices = this.authServices || [];
    return data;
  }
});
<commit_msg>Clean up Identity widget.<commit_after>/**
 * # Identity
 *
 * Allow users to log in with auth services that you have hooked on your app.
 *
 * ## Template
 *
 * - `identity`: Show loggin buttons if the user isn't logged, display his name
 *   if he is.
 */
define({
  type: 'Hull',

  templates: [
    'identity'
  ],

  refreshEvents: ['model.hull.me.change'],

  initialize: function() {
    this.authServices = _.map(this.sandbox.config.services.types.auth, function(s) {
      return s.replace(/_app$/, '');
    });

    if (_.isEmpty(this.authServices)) {
      console.error('No Auth services configured. please add one to be able to authenticate users.');
    }
  },

  beforeRender: function(data) {
    data.authServices = this.authServices || [];
    return data;
  }
});
<|endoftext|>