<commit_before>/* jshint devel:true, esnext: true */
/* globals nunjucks: true, $ */

/**
 * This file consists of four major pieces of functionality for a file view:
 * 0) Any combination of 1) and 2)
 * 1) Multi-select highlight lines with shift key and update window.location.hash
 * 2) Multi-select highlight lines with command/control key and update window.location.hash
 * 3) Highlight lines when page loads, if window.location.hash exists
 * In addition, we update the permalink and other nav links to keep them
 * synchronized with window.location.
 */

$(function () {
    'use strict';
    var container = $('#line-numbers'),
        navlinks = $('.panel a'), // whenever we update window.location, maybe update these too
        permalink = $('.permalink'), // a subset of navlinks, but it has more specific update rules
        lastModifierKey = null, // use this as a sort of canary/state indicator showing the last user action
        singleLinesArray = [], //track single highlighted lines here
        rangesArray = []; // track ranges of highlighted lines here

    //sort a one dimensional array in Ascending order
    function sortAsc(a, b) {
        return a - b;
    }
    function stringToRange(a) {
        a = a.split('-');
        a[0] = parseInt(a[0],10);
        a[1] = parseInt(a[1],10);
        return a;
    }
    function sortRangeAsc(a, b) {
        // tweak in order to account for inverted ranges like 150-120
        return Math.min(a[0],a[1]) - Math.min(b[0],b[1]);
    }
    function generateSelectedArrays() {
        var line = null,
            rangeMax = null,
            lines = [],
            rangesArray = [],
            singleLinesArray = [];

        var multiSelected = $('.line-number.multihighlight');
        var singleSelected = $('.line-number.highlighted');

        function generateLines(selected, lines) {
            for (var i = 0; i < selected.length; i++) {
                lines.push(parseInt(selected[i].id, 10));
            }
            return lines;
        }

        lines = generateLines(multiSelected, lines);
        lines = generateLines(singleSelected, lines);

        // strip all single lines, e.g. those without an adjacent line+1 == nextLine
        for (var s = lines.length - 1; s >= 0; s--) {
            line = lines[s];
            // this presumes selected is sorted in asc order, if not it won't work
            if (line !== lines[s + 1] - 1 && line !== lines[s - 1] + 1) {
                singleLinesArray.push(line);
                lines.splice(s, 1);
            }
        }

        //this presumes selected is sorted in asc order after single lines have been removed
        while (lines.length > 0) {
            line = lines[0];
            var pos = 1;
            while (line === lines[pos] - pos) {
                rangeMax = lines[pos];
                pos++;
            }
            rangesArray.push([line, rangeMax]);
            lines.splice(0, pos);
        }
        //return sorted arrays
        return [singleLinesArray.sort(sortAsc), rangesArray.sort(sortRangeAsc)];
    }

    //generate the window.location.hash based on singleLinesArray and rangesArray
    function setWindowHash() {
        var windowHash = null,
            s = null,
            r = null,
            reCleanup = /(^#?,|,$)/;
        var selectedArray = generateSelectedArrays(); // generates sorted arrays
        var singleLinesArray = selectedArray[0];
        var rangesArray = selectedArray[1];
        var lastNumber;
        // eliminate duplication
        for (s = 0; s < singleLinesArray.length; s++) {
            for (r = 0; r < rangesArray.length; r++) {
                if (s >= rangesArray[r][0] && s <= rangesArray[r][1]) {
                    singleLinesArray.splice(s,1);
                    s--;
                }
            }
        }
        if (singleLinesArray.length || rangesArray.length) {
            windowHash = '#';
        }
        for (s = 0, r = 0; s < singleLinesArray.length || r < rangesArray.length;) {
            // if no ranges left or singleLine < range add singleLine to hash
            // if no singleLines left or range < singleLine add range to hash
            if ((r == rangesArray.length) || (singleLinesArray[s] < rangesArray[r][0])) {
                windowHash += singleLinesArray[s] + ',';
                lastNumber = singleLinesArray[s];
                s++;
            } else if (( s == singleLinesArray.length) || (rangesArray[r][0] < singleLinesArray[s])) {
                windowHash += rangesArray[r][0] + '-' + rangesArray[r][1] + ',';
                lastNumber = rangesArray[r][1];
                r++;
            }
        }
        if (windowHash) {
            windowHash = windowHash.replace(reCleanup, '');
            updateHash(windowHash, lastNumber);
        }
    }

    //update places where hash location is used: window, permalink, other nav links
    function updateHash(hash, lastNumber) {
        if (permalink.length > 0)
            updatePermalink(hash);
        updateNavLinks(lastNumber);
        history.replaceState(null, '', hash);
    }

    //update the permalink href based on the windowHash.
    function updatePermalink(windowHash) {
        var permalink_href = permalink.attr('href'),
            hash_loc = permalink_href.indexOf('#');
        // If the link already has #, then cut that it off.
        if (hash_loc >= 0)
            permalink_href = permalink_href.substring(0, hash_loc);
        permalink.attr('href', permalink_href + windowHash);
    }

    //replace any occurrence of {{line}} in hrefs with the last-selected line.
    //unless the last-selected line is undefined, then remove {{line}} from the
    //displayed url.
    function updateNavLinks(lastNumber) {
        navlinks.each(function() {
            const $this = $(this);
            if ($this.data('template')) {
                if (lastNumber) {
                    $this.attr('href', $this.data('template').replace(/{{line}}/g, lastNumber));
                } else {
                    $this.attr('href', $this.data('template').replace(/{{line}}/g, ''));
                }
            }
        });
    }

    //parse window.location.hash on new requests into two arrays
    //one of single lines and one multilines
    //use with singleLinesArray and rangesArray for adding/changing new highlights
    function getSortedHashLines() {
        var highlights = window.location.hash.substring(1),
            lineStart = null,
            reRanges = /[0-9]+-[0-9]+/g,
            reCleanup = /[^0-9,]/g,
            ranges = null,
            firstRange = null;
        highlights = highlights.replace(/ /g,''); // clean whitespace
        ranges = highlights.match(reRanges);
        if (ranges !== null) {
            ranges = ranges.map(stringToRange).sort(sortRangeAsc);
            //strip out multiline items like 12-15, so that all that is left are single lines
            //populate rangesArray for reuse if a user selects more ranges later
            for (var i = 0; i < ranges.length; i++) {
                highlights = highlights.replace(ranges[i].join('-'), '');
                ranges[i].sort(sortAsc);
            }
            // add the ordered ranges to the rangesArray
            rangesArray = rangesArray.concat(ranges);
            firstRange = rangesArray[0];
            highlights = highlights.replace(reCleanup ,''); // clean anything other than digits and commas
            highlights = highlights.replace(/,,+/g, ','); // clean multiple commas
            highlights = highlights.replace(/^,|,$/g, ''); // clean leading and tailing comas
        }

        if (highlights.length) {
            //make an array of integers and sort it for the remaining single lines
            highlights = highlights.split(',');
            for (var h = 0; h < highlights.length; h++) {
                highlights[h] = parseInt(highlights[h], 10);
                //in case some unwanted string snuck by remove it
                if (isNaN(highlights[h])) {
                    highlights.splice(h,1);
                    h--;
                }
            }
            highlights = highlights.sort(sortAsc);
            //set the global singleLinesArry for reuse
            singleLinesArray = highlights;
        } else {
            //this happens if there is no single line in a url
            //without setting this the url gets an NaN element in it
            highlights = null;
        }

        //a url can look like foo#12,15,20-25 or foo#12-15,18,20-25 or foo#1,2,3 etc.
        //the lineStart should be the smallest integer in the single or highlighted ranges
        //this ensures a proper position to which to scroll once the page loads
        if (firstRange !== null && highlights !== null) {
            if (highlights[0] < firstRange[0]) {
                lineStart = highlights[0];
            } else if (highlights[0] > firstRange[0]) {
                lineStart = firstRange[0];
            }
        } else if (firstRange !== null && highlights === null) {
            lineStart = firstRange[0];
        } else if (firstRange === null && highlights !== null) {
            lineStart = highlights[0];
        } else {
            lineStart = null;
        }

        return {'lineStart':lineStart, 'highlights':singleLinesArray, 'ranges':rangesArray};
    }

    //first bind to all .line-number click events only
    container.on('click', '.line-number', function (event) {
        var clickedNum = parseInt($(this).attr('id'), 10), // get the clicked line number
            line = $('#' + clickedNum + ', #line-' + clickedNum), // get line-number and code
            lastSelectedLine = $('.line-number.last-selected, code.last-selected'),
            lastSelectedNum = parseInt($('.line-number.last-selected').attr('id'), 10), // get last selected line number as integer
            selectedLineNums = null, // used for selecting elements with class .line-number
            selectedLineCode = null, // used for selecting code elements in .code class
            self = this;

        //multiselect on shiftkey modifier combined with click
        if (event.shiftKey) {
            var classToAdd = 'multihighlight';
            // on shift, find last-selected code element
            // if lastSelectedNum less than clickedNum go back
            // else if lastSelectedNum greater than line id, go forward
            if (lastSelectedNum === clickedNum) {
                //toggle a single shiftclicked line
                line.removeClass('last-selected highlighted clicked multihighlight');
            } else if (lastSelectedNum < clickedNum) {
                //shiftclick descending down the page
                line.addClass('clicked');
                selectedLineNums = $('.line-number.last-selected').nextUntil($('.line-number.clicked'));
                selectedLineCode = $('code.last-selected').nextUntil($('code.clicked'));
                $('.last-selected').removeClass('clicked');
            } else if (lastSelectedNum > clickedNum) {
                //shiftclick ascending up the page
                $('.line-number, code').removeClass('clicked');
                line.addClass('clicked');
                selectedLineNums = $('.line-number.clicked').nextUntil($('.line-number.last-selected'));
                selectedLineCode = $('code.clicked').nextUntil($('code.last-selected'));
            }
            selectedLineNums.addClass(classToAdd);
            selectedLineCode.addClass(classToAdd);

            //set the last used modifier key
            lastModifierKey = 'shift';
            // since all highlighed items are stripped, add one back, mark new last-selected
            lastSelectedLine.addClass(classToAdd)
                            .removeClass('last-selected highlighted');
            //line.removeClass('highlighted');
            line.addClass(classToAdd);
            line.addClass('last-selected');

        } else if (event.shiftKey && lastModifierKey === 'singleSelectKey') {
            //if ctrl/command was last pressed, add multihighlight class to new lines
            $('.line-number, .code code').removeClass('clicked');
            line.addClass('clicked');
            selectedLineNums = $('.line-number.last-selected').nextUntil($('.line-number.clicked'));
            selectedLineNums.addClass('multihighlight')
                            .removeClass('highlighted');
            selectedLineCode = $('code.last-selected').nextUntil($('code.clicked'));
            selectedLineCode.addClass('multihighlight')
                            .removeClass('highlighted');
            line.addClass('multihighlight');

        } else if (event.ctrlKey || event.metaKey) {
            //a single click with ctrl/command highlights one line and preserves existing highlights
            lastModifierKey = 'singleSelectKey';
            $('.highlighted').addClass('multihighlight');
            $('.line-number, .code code').removeClass('last-selected clicked highlighted');
            if (lastSelectedNum !== clickedNum) {
                line.toggleClass('clicked last-selected multihighlight');
            } else {
                line.toggleClass('multihighlight');
                updateHash('#');
            }

        } else {
            removeAllHighlighting();
            //toggle highlighting on for any line that was not previously clicked
            if (lastSelectedNum !== clickedNum) {
                //With this we're one better than github, which doesn't allow toggling single lines
                line.toggleClass('last-selected highlighted');
            } else {
                updateHash('#');
            }
        }
        setWindowHash();
    });

    function removeAllHighlighting() {
        lastModifierKey = null;
        rangesArray = [];
        singleLinesArray = [];
        $('.line-number, .code code').removeClass('last-selected highlighted multihighlight clicked');
    }

    //highlight line(s) if someone visits a url with an #anchor
    function processHash() {
        if (window.location.hash.substring(1)) {
            var toHighlight = getSortedHashLines(),
                jumpPosition = $('#' + toHighlight.lineStart).offset(),
                highlights = toHighlight.highlights,
                ranges = toHighlight.ranges;

            if (highlights !== null) {
                //add single line highlights
                for (var i=0; i < highlights.length; i++) {
                    $('#' + highlights[i] + ', #line-' + highlights[i]).addClass('highlighted');
                }
            }

            if (ranges !== null) {
                //handle multiple sets of multi-line highlights from an incoming url
                for (var j=0; j < ranges.length; j++) {
                    //handle a single set of line ranges here; the c counter must be <= since it is a line id
                    for (var c = ranges[j][0]; c <= ranges[j][1]; c++) {
                        $('#' + c + ', #line-' + c).addClass('highlighted');
                    }
                }
            }

            //for directly linked line(s), scroll to the offset minus 150px for fixed search bar height
            //but only scrollTo if the offset is more than 150px in distance from the top of the page
            jumpPosition = parseInt(jumpPosition.top, 10) - 150;
            if (jumpPosition >= 0) {
                window.scrollTo(0, jumpPosition);
            } else {
                window.scrollTo(0, 0);
            }
            //tidy up an incoming url that might be typed in manually
            setWindowHash();
        }
    }

    // Highlight any lines specified by hash in either a direct page load or a history pop.
    $(document).ready(function() {
        processHash();
    });
    $(window).on('popstate', function() {
        removeAllHighlighting();
        processHash();
    });
});
<commit_msg>Simplify some js logic<commit_after>/* jshint devel:true, esnext: true */
/* globals nunjucks: true, $ */

/**
 * This file consists of four major pieces of functionality for a file view:
 * 0) Any combination of 1) and 2)
 * 1) Multi-select highlight lines with shift key and update window.location.hash
 * 2) Multi-select highlight lines with command/control key and update window.location.hash
 * 3) Highlight lines when page loads, if window.location.hash exists
 * In addition, we update the permalink and other nav links to keep them
 * synchronized with window.location.
 */

$(function () {
    'use strict';
    var container = $('#line-numbers'),
        navlinks = $('.panel a'), // whenever we update window.location, maybe update these too
        permalink = $('.permalink'), // a subset of navlinks, but it has more specific update rules
        lastModifierKey = null, // use this as a sort of canary/state indicator showing the last user action
        singleLinesArray = [], //track single highlighted lines here
        rangesArray = []; // track ranges of highlighted lines here

    //sort a one dimensional array in Ascending order
    function sortAsc(a, b) {
        return a - b;
    }
    function stringToRange(a) {
        a = a.split('-');
        a[0] = parseInt(a[0],10);
        a[1] = parseInt(a[1],10);
        return a;
    }
    function sortRangeAsc(a, b) {
        // tweak in order to account for inverted ranges like 150-120
        return Math.min(a[0],a[1]) - Math.min(b[0],b[1]);
    }
    function generateSelectedArrays() {
        var line = null,
            rangeMax = null,
            lines = [],
            rangesArray = [],
            singleLinesArray = [];

        var multiSelected = $('.line-number.multihighlight');
        var singleSelected = $('.line-number.highlighted');

        function generateLines(selected, lines) {
            for (var i = 0; i < selected.length; i++) {
                lines.push(parseInt(selected[i].id, 10));
            }
            return lines;
        }

        lines = generateLines(multiSelected, lines);
        lines = generateLines(singleSelected, lines);

        // strip all single lines, e.g. those without an adjacent line+1 == nextLine
        for (var s = lines.length - 1; s >= 0; s--) {
            line = lines[s];
            // this presumes selected is sorted in asc order, if not it won't work
            if (line !== lines[s + 1] - 1 && line !== lines[s - 1] + 1) {
                singleLinesArray.push(line);
                lines.splice(s, 1);
            }
        }

        //this presumes selected is sorted in asc order after single lines have been removed
        while (lines.length > 0) {
            line = lines[0];
            var pos = 1;
            while (line === lines[pos] - pos) {
                rangeMax = lines[pos];
                pos++;
            }
            rangesArray.push([line, rangeMax]);
            lines.splice(0, pos);
        }
        //return sorted arrays
        return [singleLinesArray.sort(sortAsc), rangesArray.sort(sortRangeAsc)];
    }

    //generate the window.location.hash based on singleLinesArray and rangesArray
    function setWindowHash() {
        var windowHash = null,
            s = null,
            r = null,
            reCleanup = /(^#?,|,$)/;
        var selectedArray = generateSelectedArrays(); // generates sorted arrays
        var singleLinesArray = selectedArray[0];
        var rangesArray = selectedArray[1];
        var lastNumber;
        // eliminate duplication
        for (s = 0; s < singleLinesArray.length; s++) {
            for (r = 0; r < rangesArray.length; r++) {
                if (s >= rangesArray[r][0] && s <= rangesArray[r][1]) {
                    singleLinesArray.splice(s,1);
                    s--;
                }
            }
        }
        if (singleLinesArray.length || rangesArray.length) {
            windowHash = '#';
        }
        for (s = 0, r = 0; s < singleLinesArray.length || r < rangesArray.length;) {
            // if no ranges left or singleLine < range add singleLine to hash
            // if no singleLines left or range < singleLine add range to hash
            if ((r == rangesArray.length) || (singleLinesArray[s] < rangesArray[r][0])) {
                windowHash += singleLinesArray[s] + ',';
                lastNumber = singleLinesArray[s];
                s++;
            } else if (( s == singleLinesArray.length) || (rangesArray[r][0] < singleLinesArray[s])) {
                windowHash += rangesArray[r][0] + '-' + rangesArray[r][1] + ',';
                lastNumber = rangesArray[r][1];
                r++;
            }
        }
        if (windowHash) {
            windowHash = windowHash.replace(reCleanup, '');
            updateHash(windowHash, lastNumber);
        }
    }

    //update places where hash location is used: window, permalink, other nav links
    function updateHash(hash, lastNumber) {
        if (permalink.length > 0)
            updatePermalink(hash);
        updateNavLinks(lastNumber);
        history.replaceState(null, '', hash);
    }

    //update the permalink href based on the windowHash.
    function updatePermalink(windowHash) {
        var permalink_href = permalink.attr('href'),
            hash_loc = permalink_href.indexOf('#');
        // If the link already has #, then cut that it off.
        if (hash_loc >= 0)
            permalink_href = permalink_href.substring(0, hash_loc);
        permalink.attr('href', permalink_href + windowHash);
    }

    //replace any occurrence of {{line}} in hrefs with the last-selected line.
    //unless the last-selected line is undefined, then remove {{line}} from the
    //displayed url.
    function updateNavLinks(lastNumber) {
        navlinks.each(function() {
            const $this = $(this);
            if ($this.data('template')) {
                $this.attr('href', $this.data('template').replace(/{{line}}/g, lastNumber || ''));
            }
        });
    }

    //parse window.location.hash on new requests into two arrays
    //one of single lines and one multilines
    //use with singleLinesArray and rangesArray for adding/changing new highlights
    function getSortedHashLines() {
        var highlights = window.location.hash.substring(1),
            lineStart = null,
            reRanges = /[0-9]+-[0-9]+/g,
            reCleanup = /[^0-9,]/g,
            ranges = null,
            firstRange = null;
        highlights = highlights.replace(/ /g,''); // clean whitespace
        ranges = highlights.match(reRanges);
        if (ranges !== null) {
            ranges = ranges.map(stringToRange).sort(sortRangeAsc);
            //strip out multiline items like 12-15, so that all that is left are single lines
            //populate rangesArray for reuse if a user selects more ranges later
            for (var i = 0; i < ranges.length; i++) {
                highlights = highlights.replace(ranges[i].join('-'), '');
                ranges[i].sort(sortAsc);
            }
            // add the ordered ranges to the rangesArray
            rangesArray = rangesArray.concat(ranges);
            firstRange = rangesArray[0];
            highlights = highlights.replace(reCleanup ,''); // clean anything other than digits and commas
            highlights = highlights.replace(/,,+/g, ','); // clean multiple commas
            highlights = highlights.replace(/^,|,$/g, ''); // clean leading and tailing comas
        }

        if (highlights.length) {
            //make an array of integers and sort it for the remaining single lines
            highlights = highlights.split(',');
            for (var h = 0; h < highlights.length; h++) {
                highlights[h] = parseInt(highlights[h], 10);
                //in case some unwanted string snuck by remove it
                if (isNaN(highlights[h])) {
                    highlights.splice(h,1);
                    h--;
                }
            }
            highlights = highlights.sort(sortAsc);
            //set the global singleLinesArry for reuse
            singleLinesArray = highlights;
        } else {
            //this happens if there is no single line in a url
            //without setting this the url gets an NaN element in it
            highlights = null;
        }

        //a url can look like foo#12,15,20-25 or foo#12-15,18,20-25 or foo#1,2,3 etc.
        //the lineStart should be the smallest integer in the single or highlighted ranges
        //this ensures a proper position to which to scroll once the page loads
        if (firstRange !== null && highlights !== null) {
            if (highlights[0] < firstRange[0]) {
                lineStart = highlights[0];
            } else if (highlights[0] > firstRange[0]) {
                lineStart = firstRange[0];
            }
        } else if (firstRange !== null && highlights === null) {
            lineStart = firstRange[0];
        } else if (firstRange === null && highlights !== null) {
            lineStart = highlights[0];
        } else {
            lineStart = null;
        }

        return {'lineStart':lineStart, 'highlights':singleLinesArray, 'ranges':rangesArray};
    }

    //first bind to all .line-number click events only
    container.on('click', '.line-number', function (event) {
        var clickedNum = parseInt($(this).attr('id'), 10), // get the clicked line number
            line = $('#' + clickedNum + ', #line-' + clickedNum), // get line-number and code
            lastSelectedLine = $('.line-number.last-selected, code.last-selected'),
            lastSelectedNum = parseInt($('.line-number.last-selected').attr('id'), 10), // get last selected line number as integer
            selectedLineNums = null, // used for selecting elements with class .line-number
            selectedLineCode = null, // used for selecting code elements in .code class
            self = this;

        //multiselect on shiftkey modifier combined with click
        if (event.shiftKey) {
            var classToAdd = 'multihighlight';
            // on shift, find last-selected code element
            // if lastSelectedNum less than clickedNum go back
            // else if lastSelectedNum greater than line id, go forward
            if (lastSelectedNum === clickedNum) {
                //toggle a single shiftclicked line
                line.removeClass('last-selected highlighted clicked multihighlight');
            } else if (lastSelectedNum < clickedNum) {
                //shiftclick descending down the page
                line.addClass('clicked');
                selectedLineNums = $('.line-number.last-selected').nextUntil($('.line-number.clicked'));
                selectedLineCode = $('code.last-selected').nextUntil($('code.clicked'));
                $('.last-selected').removeClass('clicked');
            } else if (lastSelectedNum > clickedNum) {
                //shiftclick ascending up the page
                $('.line-number, code').removeClass('clicked');
                line.addClass('clicked');
                selectedLineNums = $('.line-number.clicked').nextUntil($('.line-number.last-selected'));
                selectedLineCode = $('code.clicked').nextUntil($('code.last-selected'));
            }
            selectedLineNums.addClass(classToAdd);
            selectedLineCode.addClass(classToAdd);

            //set the last used modifier key
            lastModifierKey = 'shift';
            // since all highlighed items are stripped, add one back, mark new last-selected
            lastSelectedLine.addClass(classToAdd)
                            .removeClass('last-selected highlighted');
            //line.removeClass('highlighted');
            line.addClass(classToAdd);
            line.addClass('last-selected');

        } else if (event.shiftKey && lastModifierKey === 'singleSelectKey') {
            //if ctrl/command was last pressed, add multihighlight class to new lines
            $('.line-number, .code code').removeClass('clicked');
            line.addClass('clicked');
            selectedLineNums = $('.line-number.last-selected').nextUntil($('.line-number.clicked'));
            selectedLineNums.addClass('multihighlight')
                            .removeClass('highlighted');
            selectedLineCode = $('code.last-selected').nextUntil($('code.clicked'));
            selectedLineCode.addClass('multihighlight')
                            .removeClass('highlighted');
            line.addClass('multihighlight');

        } else if (event.ctrlKey || event.metaKey) {
            //a single click with ctrl/command highlights one line and preserves existing highlights
            lastModifierKey = 'singleSelectKey';
            $('.highlighted').addClass('multihighlight');
            $('.line-number, .code code').removeClass('last-selected clicked highlighted');
            if (lastSelectedNum !== clickedNum) {
                line.toggleClass('clicked last-selected multihighlight');
            } else {
                line.toggleClass('multihighlight');
                updateHash('#');
            }

        } else {
            removeAllHighlighting();
            //toggle highlighting on for any line that was not previously clicked
            if (lastSelectedNum !== clickedNum) {
                //With this we're one better than github, which doesn't allow toggling single lines
                line.toggleClass('last-selected highlighted');
            } else {
                updateHash('#');
            }
        }
        setWindowHash();
    });

    function removeAllHighlighting() {
        lastModifierKey = null;
        rangesArray = [];
        singleLinesArray = [];
        $('.line-number, .code code').removeClass('last-selected highlighted multihighlight clicked');
    }

    //highlight line(s) if someone visits a url with an #anchor
    function processHash() {
        if (window.location.hash.substring(1)) {
            var toHighlight = getSortedHashLines(),
                jumpPosition = $('#' + toHighlight.lineStart).offset(),
                highlights = toHighlight.highlights,
                ranges = toHighlight.ranges;

            if (highlights !== null) {
                //add single line highlights
                for (var i=0; i < highlights.length; i++) {
                    $('#' + highlights[i] + ', #line-' + highlights[i]).addClass('highlighted');
                }
            }

            if (ranges !== null) {
                //handle multiple sets of multi-line highlights from an incoming url
                for (var j=0; j < ranges.length; j++) {
                    //handle a single set of line ranges here; the c counter must be <= since it is a line id
                    for (var c = ranges[j][0]; c <= ranges[j][1]; c++) {
                        $('#' + c + ', #line-' + c).addClass('highlighted');
                    }
                }
            }

            //for directly linked line(s), scroll to the offset minus 150px for fixed search bar height
            //but only scrollTo if the offset is more than 150px in distance from the top of the page
            jumpPosition = parseInt(jumpPosition.top, 10) - 150;
            if (jumpPosition >= 0) {
                window.scrollTo(0, jumpPosition);
            } else {
                window.scrollTo(0, 0);
            }
            //tidy up an incoming url that might be typed in manually
            setWindowHash();
        }
    }

    // Highlight any lines specified by hash in either a direct page load or a history pop.
    $(document).ready(function() {
        processHash();
    });
    $(window).on('popstate', function() {
        removeAllHighlighting();
        processHash();
    });
});
<|endoftext|>


<commit_before>'use strict'

import Dialog from 'material/src/dialog.js'

import Component from 'material/src/component.js'
import Container from 'material/src/container.js'
import Button from 'material/src/button.js'
import Text from 'material/src/text.js'
import List from 'material/src/list.js'
import Item from 'material/src/item.js'
import event from 'material/src/element/event.js'

import countries from '../data/countries.json'

// controls

/**
 * [initTest description]
 * @return {[type]} [description]
 */
export default function(body) {
  var container = new Container({
    container: body,
    css: 'view-dialog'
  })

  var layout = [Component, 'simple-dialog', { display: 'flex', direction: 'vertical' },
    [Component, 'body', { display: 'flex', direction: 'vertical', flex: '1' },
      [Text, 'text', { text: 'Title', type: 'title' }],
      [Text, 'text', { text: 'This is a subheadubg 2', type: 'subheading2' }]
    ],
    [Component, 'action', { display: 'flex', direction: 'vertical', flex: '1' },
      [Button, 'cancel', { text: 'continue', flex: 'none' }],
      [Button, 'continue', { text: 'cancel', flex: 'none' }]
    ]
  ]

  var layout2 = [Component, 'list-dialog', { display: 'flex', direction: 'vertical' },
    [Component, 'head', { display: 'flex', direction: 'vertical', flex: '1' },
      [Text, 'text', { text: 'Choose a country', type: 'title' }]
    ],
    [List, 'list', { flex: '1' }],
    [Component, 'action', { display: 'flex', direction: 'vertical', flex: '1' },
      [Button, 'cancel', { text: 'cancel', flex: 'none' }],
      [Button, 'select', { text: 'select', flex: 'none' }]
    ]
  ]

  var dialog = new Dialog({
    class: 'simple-dialog',
    layout: layout
  }).insert(container)

  console.log('button continue', dialog.layout.get('continue'))

  dialog.layout.get('continue').on('click', function() {
    dialog.close()
  })

  dialog.layout.get('cancel').on('click', function() {
    dialog.close()
  })

  var dialog2 = new Dialog({
    class: 'simple-dialog',
    layout: layout2
  }).insert(container)

  dialog2.layout.get('accept').on('click', function() {
    dialog2.close()
  })

  dialog2.layout.get('decline').on('click', function() {
    dialog2.close()
  })

  var list = dialog2.layout.get('list')
  list.set('list', countries)

  new Button({
    primary: true,
    type: 'raised',
    label: 'show dialog'
  }).on('click', function() {
    dialog.show()
  }).insert(container)

  new Button({
    color: 'primary',
    type: 'raised',
    label: 'show dialog list'
  }).on('click', function() {
    dialog2.layout.get('accept').disable(true)
    dialog2.show()
  }).insert(container)
};<commit_msg>update dialog related to the layout update<commit_after>'use strict'

import Dialog from 'material/src/dialog.js'

import Component from 'material/src/component.js'
import Container from 'material/src/container.js'
import Button from 'material/src/button.js'
import Text from 'material/src/text.js'
import List from 'material/src/list.js'
import Item from 'material/src/item.js'
import event from 'material/src/element/event.js'

import countries from '../data/countries.json'

// controls

/**
 * [initTest description]
 * @return {[type]} [description]
 */
export default function(body) {
  var container = new Container({
    container: body,
    css: 'view-dialog'
  })

  var layout = [Component, 'simple-dialog', { display: 'flex', direction: 'vertical' },
    [Component, 'body', { display: 'flex', direction: 'vertical', flex: '1' },
      [Text, 'text', { text: 'Title', type: 'title' }],
      [Text, 'text', { text: 'This is a subheadubg 2', type: 'subheading2' }]
    ],
    [Component, 'action', { display: 'flex', direction: 'vertical', flex: '1' },
      [Button, 'cancel', { text: 'continue', flex: 'none' }],
      [Button, 'continue', { text: 'cancel', flex: 'none' }]
    ]
  ]

  var layout2 = [Component, 'list-dialog', { display: 'flex', direction: 'vertical' },
    [Component, 'head', { display: 'flex', direction: 'vertical', flex: '1' },
      [Text, 'text', { text: 'Choose a country', type: 'title' }]
    ],
    [List, 'list', { flex: '1' }],
    [Component, 'action', { display: 'flex', direction: 'vertical', flex: '1' },
      [Button, 'decline', { text: 'decline', flex: 'none' }],
      [Button, 'accept', { text: 'accept', flex: 'none' }]
    ]
  ]

  var dialog = new Dialog({
    class: 'simple-dialog',
    layout: layout
  }).insert(container)

  console.log('button continue', dialog.layout.get('continue'))

  dialog.layout.get('continue').on('click', function() {
    dialog.close()
  })

  dialog.layout.get('cancel').on('click', function() {
    dialog.close()
  })

  var dialog2 = new Dialog({
    class: 'simple-dialog',
    layout: layout2
  }).insert(container)

  dialog2.layout.get('accept').on('click', function() {
    dialog2.close()
  })

  dialog2.layout.get('decline').on('click', function() {
    dialog2.close()
  })

  var list = dialog2.layout.get('list')
  list.set('list', countries)

  new Button({
    primary: true,
    type: 'raised',
    label: 'show dialog'
  }).on('click', function() {
    dialog.show()
  }).insert(container)

  new Button({
    color: 'primary',
    type: 'raised',
    label: 'show dialog list'
  }).on('click', function() {
    dialog2.layout.get('accept').disable(true)
    dialog2.show()
  }).insert(container)
};<|endoftext|>


<commit_before>import m from 'mithril';
import _ from 'underscore';
import postgrest from 'mithril-postgrest';
import h from '../h';
import models from '../models';
import projectFilters from './project-filters-vm';

const idVM = h.idVM,
    currentUser = m.prop({}),
    createdVM = postgrest.filtersVM({ project_user_id: 'eq' });

const getUserCreatedProjects = (user_id, pageSize = 3) => {
    createdVM.project_user_id(user_id).order({ project_id: 'desc' });

    models.project.pageSize(pageSize);

    const lUserCreated = postgrest.loaderWithToken(models.project.getPageOptions(createdVM.parameters()));

    return lUserCreated.load();
};

const getPublicUserContributedProjects = (user_id, pageSize = 3) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });

    contextVM.user_id(user_id);

    models.contributor.pageSize(pageSize);

    const lUserContributed = postgrest.loaderWithToken(
        models.contributor.getPageOptions(contextVM.parameters()));

    return lUserContributed.load();
};

const getUserBalance = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });
    contextVM.user_id(user_id);

    const loader = postgrest.loaderWithToken(
        models.balance.getPageOptions(contextVM.parameters()));
    return loader.load();
};

const getUserBankAccount = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });

    contextVM.user_id(user_id);

    const lUserAccount = postgrest.loaderWithToken(
        models.bankAccount.getPageOptions(contextVM.parameters()));
    return lUserAccount.load();
};

const getUserProjectReminders = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq',
        without_notification: 'eq'
    });

    contextVM.user_id(user_id).without_notification(true);

    models.projectReminder;

    const lUserReminders = postgrest.loaderWithToken(
        models.projectReminder.getPageOptions(contextVM.parameters()));

    return lUserReminders.load();
};

const getMailMarketingLists = () => {
    models.userCreditCard.pageSize(false);

    const l = postgrest.loaderWithToken(
        models.mailMarketingList.getPageOptions({}));

    return l.load();
};

const getUserCreditCards = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });

    contextVM.user_id(user_id);

    models.userCreditCard.pageSize(false);

    const lUserCards = postgrest.loaderWithToken(
        models.userCreditCard.getPageOptions(contextVM.parameters()));

    return lUserCards.load();
};

const toggleDelivery = (projectId, contribution) => m.request({
    method: 'GET',
    config: h.setCsrfToken,
    url: `/projects/${projectId}/contributions/${contribution.contribution_id}/toggle_delivery`
});

const toggleAnonymous = (projectId, contribution) => m.request({
    method: 'GET',
    config: h.setCsrfToken,
    url: `/projects/${projectId}/contributions/${contribution.contribution_id}/toggle_anonymous`
});

const getUserContributedProjects = (user_id, pageSize = 3) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq',
        state: 'in'
    });

    contextVM.user_id(user_id).order({
        created_at: 'desc'
    }).state(['refunded', 'pending_refund', 'paid']);

    models.userContribution.pageSize(pageSize);

    const lUserContributed = postgrest.loaderWithToken(
        models.userContribution.getPageOptions(contextVM.parameters()));

    return lUserContributed.load();
};

const fetchUser = (user_id, handlePromise = true, customProp = currentUser) => {
    idVM.id(user_id);

    const lUser = postgrest.loaderWithToken(models.userDetail.getRowOptions(idVM.parameters()));

    return !handlePromise ? lUser.load() : lUser.load().then(_.compose(customProp, _.first));
};

const getCurrentUser = () => {
    fetchUser(h.getUserID());
    return currentUser;
};

const displayName = (user) => {
    let u = user || {name: 'no name'};
    return _.isEmpty(u.public_name) ? u.name : u.public_name;
};

const displayImage = (user) => {
    const defaultImg = 'https://catarse.me/assets/catarse_bootstrap/user.jpg';

    if (user) {
        return user.profile_img_thumbnail || defaultImg;
    }

    return defaultImg;
};

const displayCover = (user) => {
    if (user) {
        return user.profile_cover_image || displayImage(user);//
    }

    return displayImage(user);
};

const getUserRecommendedProjects = (contribution) => {
    const sample3 = _.partial(_.sample, _, 3),
        loaders = m.prop([]),
        collection = m.prop([]),
        { user_id } = h.getUser();

    const loader = () => _.reduce(loaders(), (memo, curr) => {
        const _memo = _.isFunction(memo) ? memo() : memo,
            _curr = _.isFunction(curr) ? curr() : curr;

        return _memo && _curr;
    }, true);

    const loadPopular = () => {
        const filters = projectFilters().filters;
        const popular = postgrest.loaderWithToken(
            models.project.getPageOptions(
                _.extend({}, { order: 'score.desc' }, filters.score.filter.parameters())
            )
        );

        loaders().push(popular);

        popular.load().then(_.compose(collection, sample3));
    };

    const pushProject = ({ project_id }) => {
        const project = postgrest.loaderWithToken(
            models.project.getPageOptions(
                postgrest.filtersVM({ project_id: 'eq' })
                    .project_id(project_id)
                    .parameters()
            )
        );

        loaders().push(project);
        project.load().then((data) => {
            collection().push(_.first(data));
        });
    };

    const projects = postgrest.loaderWithToken(
        models.recommendedProjects.getPageOptions(
            postgrest.filtersVM({ user_id: 'eq' })
                .user_id(user_id)
                .parameters()
        )
    );

    projects.load().then((recommended) => {
        if (recommended.length > 0) {
            _.map(recommended, pushProject);
        } else {
            loadPopular();
        }
    });

    return {
        loader,
        collection
    };
};

const userVM = {
    getUserCreatedProjects,
    getUserCreditCards,
    toggleDelivery,
    toggleAnonymous,
    getUserProjectReminders,
    getUserRecommendedProjects,
    getUserContributedProjects,
    getUserBalance,
    getUserBankAccount,
    getPublicUserContributedProjects,
    displayImage,
    displayCover,
    displayName,
    fetchUser,
    getCurrentUser,
    getMailMarketingLists
};

export default userVM;
<commit_msg>adjust getMailMarketingLists to handle with user context<commit_after>import m from 'mithril';
import _ from 'underscore';
import postgrest from 'mithril-postgrest';
import h from '../h';
import models from '../models';
import projectFilters from './project-filters-vm';

const idVM = h.idVM,
    currentUser = m.prop({}),
    createdVM = postgrest.filtersVM({ project_user_id: 'eq' });

const getUserCreatedProjects = (user_id, pageSize = 3) => {
    createdVM.project_user_id(user_id).order({ project_id: 'desc' });

    models.project.pageSize(pageSize);

    const lUserCreated = postgrest.loaderWithToken(models.project.getPageOptions(createdVM.parameters()));

    return lUserCreated.load();
};

const getPublicUserContributedProjects = (user_id, pageSize = 3) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });

    contextVM.user_id(user_id);

    models.contributor.pageSize(pageSize);

    const lUserContributed = postgrest.loaderWithToken(
        models.contributor.getPageOptions(contextVM.parameters()));

    return lUserContributed.load();
};

const getUserBalance = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });
    contextVM.user_id(user_id);

    const loader = postgrest.loaderWithToken(
        models.balance.getPageOptions(contextVM.parameters()));
    return loader.load();
};

const getUserBankAccount = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });

    contextVM.user_id(user_id);

    const lUserAccount = postgrest.loaderWithToken(
        models.bankAccount.getPageOptions(contextVM.parameters()));
    return lUserAccount.load();
};

const getUserProjectReminders = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq',
        without_notification: 'eq'
    });

    contextVM.user_id(user_id).without_notification(true);

    models.projectReminder;

    const lUserReminders = postgrest.loaderWithToken(
        models.projectReminder.getPageOptions(contextVM.parameters()));

    return lUserReminders.load();
};

const getMailMarketingLists = (user_id) => {
    const l = postgrest.loaderWithToken(
        models.mailMarketingList.getPageOptions({
            user_id: `in.${user_id},0`,
            order: 'id.asc' }));

    return l.load();
};

const getUserCreditCards = (user_id) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq'
    });

    contextVM.user_id(user_id);

    models.userCreditCard.pageSize(false);

    const lUserCards = postgrest.loaderWithToken(
        models.userCreditCard.getPageOptions(contextVM.parameters()));

    return lUserCards.load();
};

const toggleDelivery = (projectId, contribution) => m.request({
    method: 'GET',
    config: h.setCsrfToken,
    url: `/projects/${projectId}/contributions/${contribution.contribution_id}/toggle_delivery`
});

const toggleAnonymous = (projectId, contribution) => m.request({
    method: 'GET',
    config: h.setCsrfToken,
    url: `/projects/${projectId}/contributions/${contribution.contribution_id}/toggle_anonymous`
});

const getUserContributedProjects = (user_id, pageSize = 3) => {
    const contextVM = postgrest.filtersVM({
        user_id: 'eq',
        state: 'in'
    });

    contextVM.user_id(user_id).order({
        created_at: 'desc'
    }).state(['refunded', 'pending_refund', 'paid']);

    models.userContribution.pageSize(pageSize);

    const lUserContributed = postgrest.loaderWithToken(
        models.userContribution.getPageOptions(contextVM.parameters()));

    return lUserContributed.load();
};

const fetchUser = (user_id, handlePromise = true, customProp = currentUser) => {
    idVM.id(user_id);

    const lUser = postgrest.loaderWithToken(models.userDetail.getRowOptions(idVM.parameters()));

    return !handlePromise ? lUser.load() : lUser.load().then(_.compose(customProp, _.first));
};

const getCurrentUser = () => {
    fetchUser(h.getUserID());
    return currentUser;
};

const displayName = (user) => {
    let u = user || {name: 'no name'};
    return _.isEmpty(u.public_name) ? u.name : u.public_name;
};

const displayImage = (user) => {
    const defaultImg = 'https://catarse.me/assets/catarse_bootstrap/user.jpg';

    if (user) {
        return user.profile_img_thumbnail || defaultImg;
    }

    return defaultImg;
};

const displayCover = (user) => {
    if (user) {
        return user.profile_cover_image || displayImage(user);//
    }

    return displayImage(user);
};

const getUserRecommendedProjects = (contribution) => {
    const sample3 = _.partial(_.sample, _, 3),
        loaders = m.prop([]),
        collection = m.prop([]),
        { user_id } = h.getUser();

    const loader = () => _.reduce(loaders(), (memo, curr) => {
        const _memo = _.isFunction(memo) ? memo() : memo,
            _curr = _.isFunction(curr) ? curr() : curr;

        return _memo && _curr;
    }, true);

    const loadPopular = () => {
        const filters = projectFilters().filters;
        const popular = postgrest.loaderWithToken(
            models.project.getPageOptions(
                _.extend({}, { order: 'score.desc' }, filters.score.filter.parameters())
            )
        );

        loaders().push(popular);

        popular.load().then(_.compose(collection, sample3));
    };

    const pushProject = ({ project_id }) => {
        const project = postgrest.loaderWithToken(
            models.project.getPageOptions(
                postgrest.filtersVM({ project_id: 'eq' })
                    .project_id(project_id)
                    .parameters()
            )
        );

        loaders().push(project);
        project.load().then((data) => {
            collection().push(_.first(data));
        });
    };

    const projects = postgrest.loaderWithToken(
        models.recommendedProjects.getPageOptions(
            postgrest.filtersVM({ user_id: 'eq' })
                .user_id(user_id)
                .parameters()
        )
    );

    projects.load().then((recommended) => {
        if (recommended.length > 0) {
            _.map(recommended, pushProject);
        } else {
            loadPopular();
        }
    });

    return {
        loader,
        collection
    };
};

const userVM = {
    getUserCreatedProjects,
    getUserCreditCards,
    toggleDelivery,
    toggleAnonymous,
    getUserProjectReminders,
    getUserRecommendedProjects,
    getUserContributedProjects,
    getUserBalance,
    getUserBankAccount,
    getPublicUserContributedProjects,
    displayImage,
    displayCover,
    displayName,
    fetchUser,
    getCurrentUser,
    getMailMarketingLists
};

export default userVM;
<|endoftext|>


<commit_before>/**
 * Created by edgilmore on 1/15/2016.
 * Instructions: Convert the given number into a roman numeral. All roman numerals answers should be provided in upper-case.
 */
<commit_msg>added solution to roman numberals converter<commit_after>/**
 * Created by edgilmore on 1/15/2016.
 * Instructions: Convert the given number into a roman numeral. All roman numerals answers should be provided in upper-case.
 */
'use strict';
function convertToRoman(num) {
    var numbers = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
    var romanNumberals = ['M','CM','D','CD','C','XC','L','XL','X','IX','V','IV','I'];
    var convertedString ='';
    for(var i = 0; i <= numbers.length; i++ ){
        while(num >= numbers[i]){
            convertedString += romanNumberals[i];
            num -= numbers[i];
        }
    }
    return convertedString;
}

convertToRoman(16);<|endoftext|>


<commit_before>/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1

The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.

(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>

Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/

// TODO: proper backwards search - implement our own component?
//     : implement our own highlighter?
//     : frameset pages
//     : <ESC> should cancel search highlighting in 'incsearch' mode and jump
//       back to the presearch page location - can probably use the same
//       solution as marks
//     : 'linksearch' searches should highlight link matches only
//     : changing any search settings should also update the search state including highlighting
//     : incremental searches shouldn't permanently update search modifiers

// make sure you only create this object when the "liberator" object is ready
liberator.Search = function () //{{{
{
    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////// PRIVATE SECTION /////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    // FIXME:
    //var self = this;               // needed for callbacks since "this" is the "liberator" object in a callback
    var found = false;               // true if the last search was successful
    var backwards = false;           // currently searching backwards
    var searchString = "";           // current search string (without modifiers)
    var searchPattern = "";          // current search string (includes modifiers)
    var lastSearchPattern = "";      // the last searched pattern (includes modifiers)
    var lastSearchString = "";       // the last searched string (without modifiers)
    var lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
    var caseSensitive = false;       // search string is case sensitive
    var linksOnly = false;           // search is limited to link text only

    // Event handlers for search - closure is needed
    liberator.registerCallback("change", liberator.modes.SEARCH_FORWARD, function (command) { liberator.search.searchKeyPressed(command); });
    liberator.registerCallback("submit", liberator.modes.SEARCH_FORWARD, function (command) { liberator.search.searchSubmitted(command); });
    liberator.registerCallback("cancel", liberator.modes.SEARCH_FORWARD, function () { liberator.search.searchCanceled(); });
    // TODO: allow advanced modes in register/triggerCallback
    liberator.registerCallback("change", liberator.modes.SEARCH_BACKWARD, function (command) { liberator.search.searchKeyPressed(command); });
    liberator.registerCallback("submit", liberator.modes.SEARCH_BACKWARD, function (command) { liberator.search.searchSubmitted(command); });
    liberator.registerCallback("cancel", liberator.modes.SEARCH_BACKWARD, function () { liberator.search.searchCanceled(); });

    // set searchString, searchPattern, caseSensitive, linksOnly
    function processUserPattern(pattern)
    {
        //// strip off pattern terminator and offset
        //if (backwards)
        //    pattern = pattern.replace(/\?.*/, "");
        //else
        //    pattern = pattern.replace(/\/.*/, "");

        searchPattern = pattern;

        // links only search - \l wins if both modifiers specified
        if (/\\l/.test(pattern))
            linksOnly = true;
        else if (/\L/.test(pattern))
            linksOnly = false;
        else if (liberator.options["linksearch"])
            linksOnly = true;
        else
            linksOnly = false;

        // strip links-only modifiers
        pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // case sensitivity - \c wins if both modifiers specified
        if (/\c/.test(pattern))
            caseSensitive = false;
        else if (/\C/.test(pattern))
            caseSensitive = true;
        else if (liberator.options["ignorecase"] && liberator.options["smartcase"] && /[A-Z]/.test(pattern))
            caseSensitive = true;
        else if (liberator.options["ignorecase"])
            caseSensitive = false;
        else
            caseSensitive = true;

        // strip case-sensitive modifiers
        pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // remove any modifer escape \
        pattern = pattern.replace(/\\(\\[cClL])/g, "$1");

        searchString = pattern;
    }

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// OPTIONS /////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    liberator.options.add(["hlsearch", "hls"],
        "Highlight previous search pattern matches",
        "boolean", "false",
        {
            setter: function (value)
            {
                if (value)
                    liberator.search.highlight();
                else
                    liberator.search.clear();

                return value;
            }
        });

    liberator.options.add(["hlsearchstyle", "hlss"],
        "CSS specification of highlighted search items",
        "string", "color: black; background-color: yellow; padding: 0; display: inline;");

    liberator.options.add(["ignorecase", "ic"],
        "Ignore case in search patterns",
        "boolean", true);

    liberator.options.add(["incsearch", "is"],
        "Show where the search pattern matches as it is typed",
        "boolean", true);

    liberator.options.add(["linksearch", "lks"],
        "Limit the search to hyperlink text",
        "boolean", false);

    liberator.options.add(["smartcase", "scs"],
        "Override the 'ignorecase' option if the pattern contains uppercase characters",
        "boolean", true);

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// MAPPINGS ////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    var modes = liberator.config.browserModes || [liberator.modes.NORMAL];
    modes = modes.concat([liberator.modes.CARET]);

    liberator.mappings.add(modes,
        ["/"], "Search forward for a pattern",
        function () { liberator.search.openSearchDialog(liberator.modes.SEARCH_FORWARD); });

    liberator.mappings.add(modes,
        ["?"], "Search backwards for a pattern",
        function () { liberator.search.openSearchDialog(liberator.modes.SEARCH_BACKWARD); });

    liberator.mappings.add(modes,
        ["n"], "Find next",
        function () { liberator.search.findAgain(false); });

    liberator.mappings.add(modes,
        ["N"], "Find previous",
        function () { liberator.search.findAgain(true); });

    liberator.mappings.add(modes.concat([liberator.modes.CARET, liberator.modes.TEXTAREA]), ["*"],
        "Find word under cursor",
        function ()
        {
            liberator.search.searchSubmitted(liberator.buffer.getCurrentWord(), false);
            liberator.search.findAgain();
        });

    liberator.mappings.add(modes.concat([liberator.modes.CARET, liberator.modes.TEXTAREA]), ["#"],
        "Find word under cursor backwards",
        function ()
        {
            liberator.search.searchSubmitted(liberator.buffer.getCurrentWord(), true);
            liberator.search.findAgain();
        });

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// COMMANDS ////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    liberator.commands.add(["noh[lsearch]"],
        "Remove the search highlighting",
        function ()
        {
            liberator.search.clear();
        },
        { argCount: "0" });

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// PUBLIC SECTION //////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    return {

        // Called when the search dialog is asked for
        // If you omit "mode", it will default to forward searching
        openSearchDialog: function (mode)
        {
            if (mode == liberator.modes.SEARCH_BACKWARD)
            {
                liberator.commandline.open("?", "", liberator.modes.SEARCH_BACKWARD);
                backwards = true;
            }
            else
            {
                liberator.commandline.open("/", "", liberator.modes.SEARCH_FORWARD);
                backwards = false;
            }

            // TODO: focus the top of the currently visible screen
        },

        // Finds text in a page
        // TODO: backwards seems impossible i fear :(
        find: function (str, backwards)
        {
            var fastFind = getBrowser().fastFind;

            processUserPattern(str);

            fastFind.caseSensitive = caseSensitive;
            found = fastFind.find(searchString, linksOnly) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;

            if (!found)
                setTimeout(function () { liberator.echoerr("E486: Pattern not found: " + searchPattern); }, 0);

            return found;
        },

        // Called when the current search needs to be repeated
        findAgain: function (reverse)
        {
            // this hack is needed to make n/N work with the correct string, if
            // we typed /foo<esc> after the original search.  Since searchString is
            // readonly we have to call find() again to update it.
            if (getBrowser().fastFind.searchString != lastSearchString)
                this.find(lastSearchString, false);

            var up = reverse ? !lastSearchBackwards : lastSearchBackwards;
            var result = getBrowser().fastFind.findAgain(up, linksOnly);

            if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
            {
                liberator.echoerr("E486: Pattern not found: " + lastSearchPattern);
            }
            else if (result == Components.interfaces.nsITypeAheadFind.FIND_WRAPPED)
            {
                // hack needed, because wrapping causes a "scroll" event which clears
                // our command line
                setTimeout(function () {
                    if (up)
                        liberator.commandline.echo("search hit TOP, continuing at BOTTOM", liberator.commandline.HL_WARNING);
                    else
                        liberator.commandline.echo("search hit BOTTOM, continuing at TOP", liberator.commandline.HL_WARNING);
                }, 0);
            }
            else
            {
                liberator.echo((up ? "?" : "/") + lastSearchPattern, null, liberator.commandline.FORCE_SINGLELINE);

                if (liberator.options["hlsearch"])
                    this.highlight(lastSearchString);
            }
        },

        // Called when the user types a key in the search dialog. Triggers a find attempt if 'incsearch' is set
        searchKeyPressed: function (command)
        {
            if (liberator.options["incsearch"])
                this.find(command, backwards);
        },

        // Called when the enter key is pressed to trigger a search
        // use forcedBackward if you call this function directly
        searchSubmitted: function (command, forcedBackward)
        {
            if (typeof forcedBackward === "boolean")
                backwards = forcedBackward;

            // use the last pattern if none specified
            if (!command)
                command = lastSearchPattern;

            this.clear();
            this.find(command, backwards);

            lastSearchBackwards = backwards;
            //lastSearchPattern = command.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
            lastSearchPattern = command;
            lastSearchString = searchString;

            // TODO: move to find() when reverse incremental searching is kludged in
            // need to find again for reverse searching
            if (backwards)
                setTimeout(function () { liberator.search.findAgain(false); }, 0);

            if (liberator.options["hlsearch"])
                this.highlight(searchString);

            liberator.modes.reset();
        },

        // Called when the search is canceled - for example if someone presses
        // escape while typing a search
        searchCanceled: function ()
        {
            this.clear();
            // TODO: code to reposition the document to the place before search started
        },

        // FIXME: thunderbird incompatible
        // this is not dependent on the value of 'hlsearch'
        highlight: function (text)
        {
            if (liberator.config.name == "Muttator")
                return;

            // already highlighted?
            if (window.content.document.getElementsByClassName("__mozilla-findbar-search").length > 0)
                return;

            if (!text)
                text = lastSearchString;

            gFindBar._setCaseSensitivity(caseSensitive);
            gFindBar._highlightDoc("white", "black", text);

            // TODO: seems fast enough for now...just
            (function (win)
            {
                for (var i = 0; i < win.frames.length; i++)
                    arguments.callee(win.frames[i]);
                var spans = window.content.document.getElementsByClassName("__mozilla-findbar-search");
                for (var i = 0; i < spans.length; i++)
                    spans[i].setAttribute("style", liberator.options["hlsearchstyle"]);
            })(window.content);

            // recreate selection since _highlightDoc collapses the selection backwards
            getBrowser().fastFind.findAgain(false, linksOnly);

            // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"])
        },

        clear: function ()
        {
            gFindBar._highlightDoc();
            // need to manually collapse the selection if the document is not
            // highlighted
            getBrowser().fastFind.collapseSelection();
        }

    };
    //}}}
}; //}}}

// vim: set fdm=marker sw=4 ts=4 et:
<commit_msg>Don't jump to the next result when submitting a /search.<commit_after>/***** BEGIN LICENSE BLOCK ***** {{{
Version: MPL 1.1/GPL 2.0/LGPL 2.1

The contents of this file are subject to the Mozilla Public License Version
1.1 (the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS" basis,
WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
for the specific language governing rights and limitations under the
License.

(c) 2006-2008: Martin Stubenschrott <stubenschrott@gmx.net>

Alternatively, the contents of this file may be used under the terms of
either the GNU General Public License Version 2 or later (the "GPL"), or
the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
in which case the provisions of the GPL or the LGPL are applicable instead
of those above. If you wish to allow use of your version of this file only
under the terms of either the GPL or the LGPL, and not to allow others to
use your version of this file under the terms of the MPL, indicate your
decision by deleting the provisions above and replace them with the notice
and other provisions required by the GPL or the LGPL. If you do not delete
the provisions above, a recipient may use your version of this file under
the terms of any one of the MPL, the GPL or the LGPL.
}}} ***** END LICENSE BLOCK *****/

// TODO: proper backwards search - implement our own component?
//     : implement our own highlighter?
//     : frameset pages
//     : <ESC> should cancel search highlighting in 'incsearch' mode and jump
//       back to the presearch page location - can probably use the same
//       solution as marks
//     : 'linksearch' searches should highlight link matches only
//     : changing any search settings should also update the search state including highlighting
//     : incremental searches shouldn't permanently update search modifiers

// make sure you only create this object when the "liberator" object is ready
liberator.Search = function () //{{{
{
    ////////////////////////////////////////////////////////////////////////////////
    ////////////////////// PRIVATE SECTION /////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    // FIXME:
    //var self = this;               // needed for callbacks since "this" is the "liberator" object in a callback
    var found = false;               // true if the last search was successful
    var backwards = false;           // currently searching backwards
    var searchString = "";           // current search string (without modifiers)
    var searchPattern = "";          // current search string (includes modifiers)
    var lastSearchPattern = "";      // the last searched pattern (includes modifiers)
    var lastSearchString = "";       // the last searched string (without modifiers)
    var lastSearchBackwards = false; // like "backwards", but for the last search, so if you cancel a search with <esc> this is not set
    var caseSensitive = false;       // search string is case sensitive
    var linksOnly = false;           // search is limited to link text only

    // Event handlers for search - closure is needed
    liberator.registerCallback("change", liberator.modes.SEARCH_FORWARD, function (command) { liberator.search.searchKeyPressed(command); });
    liberator.registerCallback("submit", liberator.modes.SEARCH_FORWARD, function (command) { liberator.search.searchSubmitted(command); });
    liberator.registerCallback("cancel", liberator.modes.SEARCH_FORWARD, function () { liberator.search.searchCanceled(); });
    // TODO: allow advanced modes in register/triggerCallback
    liberator.registerCallback("change", liberator.modes.SEARCH_BACKWARD, function (command) { liberator.search.searchKeyPressed(command); });
    liberator.registerCallback("submit", liberator.modes.SEARCH_BACKWARD, function (command) { liberator.search.searchSubmitted(command); });
    liberator.registerCallback("cancel", liberator.modes.SEARCH_BACKWARD, function () { liberator.search.searchCanceled(); });

    // set searchString, searchPattern, caseSensitive, linksOnly
    function processUserPattern(pattern)
    {
        //// strip off pattern terminator and offset
        //if (backwards)
        //    pattern = pattern.replace(/\?.*/, "");
        //else
        //    pattern = pattern.replace(/\/.*/, "");

        searchPattern = pattern;

        // links only search - \l wins if both modifiers specified
        if (/\\l/.test(pattern))
            linksOnly = true;
        else if (/\L/.test(pattern))
            linksOnly = false;
        else if (liberator.options["linksearch"])
            linksOnly = true;
        else
            linksOnly = false;

        // strip links-only modifiers
        pattern = pattern.replace(/(\\)?\\[lL]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // case sensitivity - \c wins if both modifiers specified
        if (/\c/.test(pattern))
            caseSensitive = false;
        else if (/\C/.test(pattern))
            caseSensitive = true;
        else if (liberator.options["ignorecase"] && liberator.options["smartcase"] && /[A-Z]/.test(pattern))
            caseSensitive = true;
        else if (liberator.options["ignorecase"])
            caseSensitive = false;
        else
            caseSensitive = true;

        // strip case-sensitive modifiers
        pattern = pattern.replace(/(\\)?\\[cC]/g, function ($0, $1) { return $1 ? $0 : ""; });

        // remove any modifer escape \
        pattern = pattern.replace(/\\(\\[cClL])/g, "$1");

        searchString = pattern;
    }

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// OPTIONS /////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    liberator.options.add(["hlsearch", "hls"],
        "Highlight previous search pattern matches",
        "boolean", "false",
        {
            setter: function (value)
            {
                if (value)
                    liberator.search.highlight();
                else
                    liberator.search.clear();

                return value;
            }
        });

    liberator.options.add(["hlsearchstyle", "hlss"],
        "CSS specification of highlighted search items",
        "string", "color: black; background-color: yellow; padding: 0; display: inline;");

    liberator.options.add(["ignorecase", "ic"],
        "Ignore case in search patterns",
        "boolean", true);

    liberator.options.add(["incsearch", "is"],
        "Show where the search pattern matches as it is typed",
        "boolean", true);

    liberator.options.add(["linksearch", "lks"],
        "Limit the search to hyperlink text",
        "boolean", false);

    liberator.options.add(["smartcase", "scs"],
        "Override the 'ignorecase' option if the pattern contains uppercase characters",
        "boolean", true);

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// MAPPINGS ////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    var modes = liberator.config.browserModes || [liberator.modes.NORMAL];
    modes = modes.concat([liberator.modes.CARET]);

    liberator.mappings.add(modes,
        ["/"], "Search forward for a pattern",
        function () { liberator.search.openSearchDialog(liberator.modes.SEARCH_FORWARD); });

    liberator.mappings.add(modes,
        ["?"], "Search backwards for a pattern",
        function () { liberator.search.openSearchDialog(liberator.modes.SEARCH_BACKWARD); });

    liberator.mappings.add(modes,
        ["n"], "Find next",
        function () { liberator.search.findAgain(false); });

    liberator.mappings.add(modes,
        ["N"], "Find previous",
        function () { liberator.search.findAgain(true); });

    liberator.mappings.add(modes.concat([liberator.modes.CARET, liberator.modes.TEXTAREA]), ["*"],
        "Find word under cursor",
        function ()
        {
            liberator.search.searchSubmitted(liberator.buffer.getCurrentWord(), false);
            liberator.search.findAgain();
        });

    liberator.mappings.add(modes.concat([liberator.modes.CARET, liberator.modes.TEXTAREA]), ["#"],
        "Find word under cursor backwards",
        function ()
        {
            liberator.search.searchSubmitted(liberator.buffer.getCurrentWord(), true);
            liberator.search.findAgain();
        });

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// COMMANDS ////////////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    liberator.commands.add(["noh[lsearch]"],
        "Remove the search highlighting",
        function ()
        {
            liberator.search.clear();
        },
        { argCount: "0" });

    /////////////////////////////////////////////////////////////////////////////}}}
    ////////////////////// PUBLIC SECTION //////////////////////////////////////////
    /////////////////////////////////////////////////////////////////////////////{{{

    return {

        // Called when the search dialog is asked for
        // If you omit "mode", it will default to forward searching
        openSearchDialog: function (mode)
        {
            if (mode == liberator.modes.SEARCH_BACKWARD)
            {
                liberator.commandline.open("?", "", liberator.modes.SEARCH_BACKWARD);
                backwards = true;
            }
            else
            {
                liberator.commandline.open("/", "", liberator.modes.SEARCH_FORWARD);
                backwards = false;
            }

            // TODO: focus the top of the currently visible screen
        },

        // Finds text in a page
        // TODO: backwards seems impossible i fear :(
        find: function (str, backwards)
        {
            var fastFind = getBrowser().fastFind;

            processUserPattern(str);

            fastFind.caseSensitive = caseSensitive;
            found = fastFind.find(searchString, linksOnly) != Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND;

            if (!found)
                setTimeout(function () { liberator.echoerr("E486: Pattern not found: " + searchPattern); }, 0);

            return found;
        },

        // Called when the current search needs to be repeated
        findAgain: function (reverse)
        {
            // this hack is needed to make n/N work with the correct string, if
            // we typed /foo<esc> after the original search.  Since searchString is
            // readonly we have to call find() again to update it.
            if (getBrowser().fastFind.searchString != lastSearchString)
                this.find(lastSearchString, false);

            var up = reverse ? !lastSearchBackwards : lastSearchBackwards;
            var result = getBrowser().fastFind.findAgain(up, linksOnly);

            if (result == Components.interfaces.nsITypeAheadFind.FIND_NOTFOUND)
            {
                liberator.echoerr("E486: Pattern not found: " + lastSearchPattern);
            }
            else if (result == Components.interfaces.nsITypeAheadFind.FIND_WRAPPED)
            {
                // hack needed, because wrapping causes a "scroll" event which clears
                // our command line
                setTimeout(function () {
                    if (up)
                        liberator.commandline.echo("search hit TOP, continuing at BOTTOM", liberator.commandline.HL_WARNING);
                    else
                        liberator.commandline.echo("search hit BOTTOM, continuing at TOP", liberator.commandline.HL_WARNING);
                }, 0);
            }
            else
            {
                liberator.echo((up ? "?" : "/") + lastSearchPattern, null, liberator.commandline.FORCE_SINGLELINE);

                if (liberator.options["hlsearch"])
                    this.highlight(lastSearchString);
            }
        },

        // Called when the user types a key in the search dialog. Triggers a find attempt if 'incsearch' is set
        searchKeyPressed: function (command)
        {
            if (liberator.options["incsearch"])
                this.find(command, backwards);
        },

        // Called when the enter key is pressed to trigger a search
        // use forcedBackward if you call this function directly
        searchSubmitted: function (command, forcedBackward)
        {
            if (typeof forcedBackward === "boolean")
                backwards = forcedBackward;

            // use the last pattern if none specified
            if (!command)
                command = lastSearchPattern;

            this.clear();
            if (!liberator.options["incsearch"])
                this.find(command, backwards);

            lastSearchBackwards = backwards;
            //lastSearchPattern = command.replace(backwards ? /\?.*/ : /\/.*/, ""); // XXX
            lastSearchPattern = command;
            lastSearchString = searchString;

            // TODO: move to find() when reverse incremental searching is kludged in
            // need to find again for reverse searching
            if (backwards)
                setTimeout(function () { liberator.search.findAgain(false); }, 0);

            if (liberator.options["hlsearch"])
                this.highlight(searchString);

            liberator.modes.reset();
        },

        // Called when the search is canceled - for example if someone presses
        // escape while typing a search
        searchCanceled: function ()
        {
            this.clear();
            // TODO: code to reposition the document to the place before search started
        },

        // FIXME: thunderbird incompatible
        // this is not dependent on the value of 'hlsearch'
        highlight: function (text)
        {
            if (liberator.config.name == "Muttator")
                return;

            // already highlighted?
            if (window.content.document.getElementsByClassName("__mozilla-findbar-search").length > 0)
                return;

            if (!text)
                text = lastSearchString;

            gFindBar._setCaseSensitivity(caseSensitive);
            gFindBar._highlightDoc("white", "black", text);

            // TODO: seems fast enough for now...just
            (function (win)
            {
                for (var i = 0; i < win.frames.length; i++)
                    arguments.callee(win.frames[i]);
                var spans = window.content.document.getElementsByClassName("__mozilla-findbar-search");
                for (var i = 0; i < spans.length; i++)
                    spans[i].setAttribute("style", liberator.options["hlsearchstyle"]);
            })(window.content);

            // recreate selection since _highlightDoc collapses the selection backwards
            getBrowser().fastFind.findAgain(false, linksOnly);

            // TODO: remove highlighting from non-link matches (HTML - A/AREA with href attribute; XML - Xlink [type="simple"])
        },

        clear: function ()
        {
            gFindBar._highlightDoc();
            // need to manually collapse the selection if the document is not
            // highlighted
            getBrowser().fastFind.collapseSelection();
        }

    };
    //}}}
}; //}}}

// vim: set fdm=marker sw=4 ts=4 et:
<|endoftext|>