Changeset - 190cb30841de
[Not reviewed]
default
0 4 0
Mads Kiilerich - 9 years ago 2016-07-28 16:34:49
madski@unity3d.com
branches: fix performance of branch selectors with many branches - only show the first 200 results

The way we use select2, it will cause browser performance problems when a
select list contains thousands of entries. The primary bottleneck is the DOM
creation, secondarily for the query to filter through the entries and decide
what to show. We thus primarily have to limit how many entries we put in the
drop-down, secondarily limit the iteration over data.

One tricky case is where the user specifies a short but full branch name (like
'trunk') but many other branches contains the same string (not necessarily at
the beginning, like 'for-trunk-next-week') which come before the perfect match
in the branch list. It is thus not a solution to just stop searching when a
fixed amount of matches have been found.

Instead, we limit the amount of ordinary query matches, but always show all
prefix matches. We thus always have to iterate through all entries, but we
start using the (presumably) cheaper prefix search when the limit has been
reached.

There is no filtering initially when there is no query term, so that case has
to be handled specially.

Upstream select2 is now at 4.x. Upgrading is not trivial, and getting this
fixed properly upstream is not a short term solution. Instead, we customize our
copy. The benefit from this patch is bigger than the overhead of "maintaining"
it locally.
4 files changed with 25 insertions and 4 deletions:
0 comments (0 inline, 0 general)
kallithea/public/js/select2/select2.js
Show inline comments
 
@@ -716,842 +716,861 @@ the specific language governing permissi
 
            this.container.attr("id", this.containerId);
 

	
 
            this.container.attr("title", opts.element.attr("title"));
 

	
 
            this.body = $(document.body);
 

	
 
            syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
 

	
 
            this.container.attr("style", opts.element.attr("style"));
 
            this.container.css(evaluate(opts.containerCss, this.opts.element));
 
            this.container.addClass(evaluate(opts.containerCssClass, this.opts.element));
 

	
 
            this.elementTabIndex = this.opts.element.attr("tabindex");
 

	
 
            // swap container for the element
 
            this.opts.element
 
                .data("select2", this)
 
                .attr("tabindex", "-1")
 
                .before(this.container)
 
                .on("click.select2", killEvent); // do not leak click events
 

	
 
            this.container.data("select2", this);
 

	
 
            this.dropdown = this.container.find(".select2-drop");
 

	
 
            syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
 

	
 
            this.dropdown.addClass(evaluate(opts.dropdownCssClass, this.opts.element));
 
            this.dropdown.data("select2", this);
 
            this.dropdown.on("click", killEvent);
 

	
 
            this.results = results = this.container.find(resultsSelector);
 
            this.search = search = this.container.find("input.select2-input");
 

	
 
            this.queryCount = 0;
 
            this.resultsPage = 0;
 
            this.context = null;
 

	
 
            // initialize the container
 
            this.initContainer();
 

	
 
            this.container.on("click", killEvent);
 

	
 
            installFilteredMouseMove(this.results);
 

	
 
            this.dropdown.on("mousemove-filtered", resultsSelector, this.bind(this.highlightUnderEvent));
 
            this.dropdown.on("touchstart touchmove touchend", resultsSelector, this.bind(function (event) {
 
                this._touchEvent = true;
 
                this.highlightUnderEvent(event);
 
            }));
 
            this.dropdown.on("touchmove", resultsSelector, this.bind(this.touchMoved));
 
            this.dropdown.on("touchstart touchend", resultsSelector, this.bind(this.clearTouchMoved));
 

	
 
            // Waiting for a click event on touch devices to select option and hide dropdown
 
            // otherwise click will be triggered on an underlying element
 
            this.dropdown.on('click', this.bind(function (event) {
 
                if (this._touchEvent) {
 
                    this._touchEvent = false;
 
                    this.selectHighlighted();
 
                }
 
            }));
 

	
 
            installDebouncedScroll(80, this.results);
 
            this.dropdown.on("scroll-debounced", resultsSelector, this.bind(this.loadMoreIfNeeded));
 

	
 
            // do not propagate change event from the search field out of the component
 
            $(this.container).on("change", ".select2-input", function(e) {e.stopPropagation();});
 
            $(this.dropdown).on("change", ".select2-input", function(e) {e.stopPropagation();});
 

	
 
            // if jquery.mousewheel plugin is installed we can prevent out-of-bounds scrolling of results via mousewheel
 
            if ($.fn.mousewheel) {
 
                results.mousewheel(function (e, delta, deltaX, deltaY) {
 
                    var top = results.scrollTop();
 
                    if (deltaY > 0 && top - deltaY <= 0) {
 
                        results.scrollTop(0);
 
                        killEvent(e);
 
                    } else if (deltaY < 0 && results.get(0).scrollHeight - results.scrollTop() + deltaY <= results.height()) {
 
                        results.scrollTop(results.get(0).scrollHeight - results.height());
 
                        killEvent(e);
 
                    }
 
                });
 
            }
 

	
 
            installKeyUpChangeEvent(search);
 
            search.on("keyup-change input paste", this.bind(this.updateResults));
 
            search.on("focus", function () { search.addClass("select2-focused"); });
 
            search.on("blur", function () { search.removeClass("select2-focused");});
 

	
 
            this.dropdown.on("mouseup", resultsSelector, this.bind(function (e) {
 
                if ($(e.target).closest(".select2-result-selectable").length > 0) {
 
                    this.highlightUnderEvent(e);
 
                    this.selectHighlighted(e);
 
                }
 
            }));
 

	
 
            // trap all mouse events from leaving the dropdown. sometimes there may be a modal that is listening
 
            // for mouse events outside of itself so it can close itself. since the dropdown is now outside the select2's
 
            // dom it will trigger the popup close, which is not what we want
 
            // focusin can cause focus wars between modals and select2 since the dropdown is outside the modal.
 
            this.dropdown.on("click mouseup mousedown touchstart touchend focusin", function (e) { e.stopPropagation(); });
 

	
 
            this.lastSearchTerm = undefined;
 

	
 
            if ($.isFunction(this.opts.initSelection)) {
 
                // initialize selection based on the current value of the source element
 
                this.initSelection();
 

	
 
                // if the user has provided a function that can set selection based on the value of the source element
 
                // we monitor the change event on the element and trigger it, allowing for two way synchronization
 
                this.monitorSource();
 
            }
 

	
 
            if (opts.maximumInputLength !== null) {
 
                this.search.attr("maxlength", opts.maximumInputLength);
 
            }
 

	
 
            var disabled = opts.element.prop("disabled");
 
            if (disabled === undefined) disabled = false;
 
            this.enable(!disabled);
 

	
 
            var readonly = opts.element.prop("readonly");
 
            if (readonly === undefined) readonly = false;
 
            this.readonly(readonly);
 

	
 
            // Calculate size of scrollbar
 
            scrollBarDimensions = scrollBarDimensions || measureScrollbar();
 

	
 
            this.autofocus = opts.element.prop("autofocus");
 
            opts.element.prop("autofocus", false);
 
            if (this.autofocus) this.focus();
 

	
 
            this.search.attr("placeholder", opts.searchInputPlaceholder);
 
        },
 

	
 
        // abstract
 
        destroy: function () {
 
            var element=this.opts.element, select2 = element.data("select2"), self = this;
 

	
 
            this.close();
 

	
 
            if (element.length && element[0].detachEvent && self._sync) {
 
                element.each(function () {
 
                    if (self._sync) {
 
                        this.detachEvent("onpropertychange", self._sync);
 
                    }
 
                });
 
            }
 
            if (this.propertyObserver) {
 
                this.propertyObserver.disconnect();
 
                this.propertyObserver = null;
 
            }
 
            this._sync = null;
 

	
 
            if (select2 !== undefined) {
 
                select2.container.remove();
 
                select2.liveRegion.remove();
 
                select2.dropdown.remove();
 
                element.removeData("select2")
 
                    .off(".select2");
 
                if (!element.is("input[type='hidden']")) {
 
                    element
 
                        .show()
 
                        .prop("autofocus", this.autofocus || false);
 
                    if (this.elementTabIndex) {
 
                        element.attr({tabindex: this.elementTabIndex});
 
                    } else {
 
                        element.removeAttr("tabindex");
 
                    }
 
                    element.show();
 
                } else {
 
                    element.css("display", "");
 
                }
 
            }
 

	
 
            cleanupJQueryElements.call(this,
 
                "container",
 
                "liveRegion",
 
                "dropdown",
 
                "results",
 
                "search"
 
            );
 
        },
 

	
 
        // abstract
 
        optionToData: function(element) {
 
            if (element.is("option")) {
 
                return {
 
                    id:element.prop("value"),
 
                    text:element.text(),
 
                    element: element.get(),
 
                    css: element.attr("class"),
 
                    disabled: element.prop("disabled"),
 
                    locked: equal(element.attr("locked"), "locked") || equal(element.data("locked"), true)
 
                };
 
            } else if (element.is("optgroup")) {
 
                return {
 
                    text:element.attr("label"),
 
                    children:[],
 
                    element: element.get(),
 
                    css: element.attr("class")
 
                };
 
            }
 
        },
 

	
 
        // abstract
 
        prepareOpts: function (opts) {
 
            var element, select, idKey, ajaxUrl, self = this;
 

	
 
            element = opts.element;
 

	
 
            if (element.get(0).tagName.toLowerCase() === "select") {
 
                this.select = select = opts.element;
 
            }
 

	
 
            if (select) {
 
                // these options are not allowed when attached to a select because they are picked up off the element itself
 
                $.each(["id", "multiple", "ajax", "query", "createSearchChoice", "initSelection", "data", "tags"], function () {
 
                    if (this in opts) {
 
                        throw new Error("Option '" + this + "' is not allowed for Select2 when attached to a <select> element.");
 
                    }
 
                });
 
            }
 

	
 
            opts.debug = opts.debug || $.fn.select2.defaults.debug;
 

	
 
            // Warnings for options renamed/removed in Select2 4.0.0
 
            // Only when it's enabled through debug mode
 
            if (opts.debug && console && console.warn) {
 
                // id was removed
 
                if (opts.id != null) {
 
                    console.warn(
 
                        'Select2: The `id` option has been removed in Select2 4.0.0, ' +
 
                        'consider renaming your `id` property or mapping the property before your data makes it to Select2. ' +
 
                        'You can read more at https://select2.github.io/announcements-4.0.html#changed-id'
 
                    );
 
                }
 

	
 
                // text was removed
 
                if (opts.text != null) {
 
                    console.warn(
 
                        'Select2: The `text` option has been removed in Select2 4.0.0, ' +
 
                        'consider renaming your `text` property or mapping the property before your data makes it to Select2. ' +
 
                        'You can read more at https://select2.github.io/announcements-4.0.html#changed-id'
 
                    );
 
                }
 

	
 
                // sortResults was renamed to results
 
                if (opts.sortResults != null) {
 
                    console.warn(
 
                        'Select2: the `sortResults` option has been renamed to `sorter` in Select2 4.0.0. '
 
                    );
 
                }
 

	
 
                // selectOnBlur was renamed to selectOnClose
 
                if (opts.selectOnBlur != null) {
 
                    console.warn(
 
                        'Select2: The `selectOnBlur` option has been renamed to `selectOnClose` in Select2 4.0.0.'
 
                    );
 
                }
 

	
 
                // ajax.results was renamed to ajax.processResults
 
                if (opts.ajax != null && opts.ajax.results != null) {
 
                    console.warn(
 
                        'Select2: The `ajax.results` option has been renamed to `ajax.processResults` in Select2 4.0.0.'
 
                    );
 
                }
 

	
 
                // format* options were renamed to language.*
 
                if (opts.formatNoResults != null) {
 
                    console.warn(
 
                        'Select2: The `formatNoResults` option has been renamed to `language.noResults` in Select2 4.0.0.'
 
                    );
 
                }
 
                if (opts.formatSearching != null) {
 
                    console.warn(
 
                        'Select2: The `formatSearching` option has been renamed to `language.searching` in Select2 4.0.0.'
 
                    );
 
                }
 
                if (opts.formatInputTooShort != null) {
 
                    console.warn(
 
                        'Select2: The `formatInputTooShort` option has been renamed to `language.inputTooShort` in Select2 4.0.0.'
 
                    );
 
                }
 
                if (opts.formatInputTooLong != null) {
 
                    console.warn(
 
                        'Select2: The `formatInputTooLong` option has been renamed to `language.inputTooLong` in Select2 4.0.0.'
 
                    );
 
                }
 
                if (opts.formatLoading != null) {
 
                    console.warn(
 
                        'Select2: The `formatLoading` option has been renamed to `language.loadingMore` in Select2 4.0.0.'
 
                    );
 
                }
 
                if (opts.formatSelectionTooBig != null) {
 
                    console.warn(
 
                        'Select2: The `formatSelectionTooBig` option has been renamed to `language.maximumSelected` in Select2 4.0.0.'
 
                    );
 
                }
 

	
 
                if (opts.element.data('select2Tags')) {
 
                    console.warn(
 
                        'Select2: The `data-select2-tags` attribute has been renamed to `data-tags` in Select2 4.0.0.'
 
                    );
 
                }
 
            }
 

	
 
            // Aliasing options renamed in Select2 4.0.0
 

	
 
            // data-select2-tags -> data-tags
 
            if (opts.element.data('tags') != null) {
 
                var elemTags = opts.element.data('tags');
 

	
 
                // data-tags should actually be a boolean
 
                if (!$.isArray(elemTags)) {
 
                    elemTags = [];
 
                }
 

	
 
                opts.element.data('select2Tags', elemTags);
 
            }
 

	
 
            // sortResults -> sorter
 
            if (opts.sorter != null) {
 
                opts.sortResults = opts.sorter;
 
            }
 

	
 
            // selectOnBlur -> selectOnClose
 
            if (opts.selectOnClose != null) {
 
                opts.selectOnBlur = opts.selectOnClose;
 
            }
 

	
 
            // ajax.results -> ajax.processResults
 
            if (opts.ajax != null) {
 
                if ($.isFunction(opts.ajax.processResults)) {
 
                    opts.ajax.results = opts.ajax.processResults;
 
                }
 
            }
 

	
 
            // Formatters/language options
 
            if (opts.language != null) {
 
                var lang = opts.language;
 

	
 
                // formatNoMatches -> language.noMatches
 
                if ($.isFunction(lang.noMatches)) {
 
                    opts.formatNoMatches = lang.noMatches;
 
                }
 

	
 
                // formatSearching -> language.searching
 
                if ($.isFunction(lang.searching)) {
 
                    opts.formatSearching = lang.searching;
 
                }
 

	
 
                // formatInputTooShort -> language.inputTooShort
 
                if ($.isFunction(lang.inputTooShort)) {
 
                    opts.formatInputTooShort = lang.inputTooShort;
 
                }
 

	
 
                // formatInputTooLong -> language.inputTooLong
 
                if ($.isFunction(lang.inputTooLong)) {
 
                    opts.formatInputTooLong = lang.inputTooLong;
 
                }
 

	
 
                // formatLoading -> language.loadingMore
 
                if ($.isFunction(lang.loadingMore)) {
 
                    opts.formatLoading = lang.loadingMore;
 
                }
 

	
 
                // formatSelectionTooBig -> language.maximumSelected
 
                if ($.isFunction(lang.maximumSelected)) {
 
                    opts.formatSelectionTooBig = lang.maximumSelected;
 
                }
 
            }
 

	
 
            opts = $.extend({}, {
 
                populateResults: function(container, results, query) {
 
                    var populate, id=this.opts.id, liveRegion=this.liveRegion;
 

	
 
                    populate=function(results, container, depth) {
 

	
 
                        var i, l, result, selectable, disabled, compound, node, label, innerContainer, formatted;
 

	
 
                        results = opts.sortResults(results, container, query);
 

	
 
                        // collect the created nodes for bulk append
 
                        var nodes = [];
 
                        for (i = 0, l = results.length; i < l; i = i + 1) {
 

	
 
                        // Kallithea customization: maxResults
 
                        l = results.length;
 
                        if (query.term.length == 0 && l > opts.maxResults) {
 
                            l = opts.maxResults;
 
                        }
 
                        for (i = 0; i < l; i = i + 1) {
 

	
 
                            result=results[i];
 

	
 
                            disabled = (result.disabled === true);
 
                            selectable = (!disabled) && (id(result) !== undefined);
 

	
 
                            compound=result.children && result.children.length > 0;
 

	
 
                            node=$("<li></li>");
 
                            node.addClass("select2-results-dept-"+depth);
 
                            node.addClass("select2-result");
 
                            node.addClass(selectable ? "select2-result-selectable" : "select2-result-unselectable");
 
                            if (disabled) { node.addClass("select2-disabled"); }
 
                            if (compound) { node.addClass("select2-result-with-children"); }
 
                            node.addClass(self.opts.formatResultCssClass(result));
 
                            node.attr("role", "presentation");
 

	
 
                            label=$(document.createElement("div"));
 
                            label.addClass("select2-result-label");
 
                            label.attr("id", "select2-result-label-" + nextUid());
 
                            label.attr("role", "option");
 

	
 
                            formatted=opts.formatResult(result, label, query, self.opts.escapeMarkup);
 
                            if (formatted!==undefined) {
 
                                label.html(formatted);
 
                                node.append(label);
 
                            }
 

	
 

	
 
                            if (compound) {
 
                                innerContainer=$("<ul></ul>");
 
                                innerContainer.addClass("select2-result-sub");
 
                                populate(result.children, innerContainer, depth+1);
 
                                node.append(innerContainer);
 
                            }
 

	
 
                            node.data("select2-data", result);
 
                            nodes.push(node[0]);
 
                        }
 

	
 
                        if (results.length >= opts.maxResults) {
 
                            nodes.push($('<li class="select2-no-results"><div class="select2-result-label">Too many matches found</div></li>'));
 
                        }
 

	
 
                        // bulk append the created nodes
 
                        container.append(nodes);
 
                        liveRegion.text(opts.formatMatches(results.length));
 
                    };
 

	
 
                    populate(results, container, 0);
 
                }
 
            }, $.fn.select2.defaults, opts);
 

	
 
            if (typeof(opts.id) !== "function") {
 
                idKey = opts.id;
 
                opts.id = function (e) { return e[idKey]; };
 
            }
 

	
 
            if ($.isArray(opts.element.data("select2Tags"))) {
 
                if ("tags" in opts) {
 
                    throw "tags specified as both an attribute 'data-select2-tags' and in options of Select2 " + opts.element.attr("id");
 
                }
 
                opts.tags=opts.element.data("select2Tags");
 
            }
 

	
 
            if (select) {
 
                opts.query = this.bind(function (query) {
 
                    // Kallithea customization: maxResults
 
                    var data = { results: [], more: false },
 
                        term = query.term,
 
                        children, placeholderOption, process;
 
                        children, placeholderOption, process,
 
                        maxResults = opts.maxResults || -1,
 
                        termLower = term.toLowerCase();
 

	
 
                    process=function(element, collection) {
 
                        var group;
 
                        if (element.is("option")) {
 
                          if (collection.length < maxResults) {
 
                            if (query.matcher(term, element.text(), element)) {
 
                                collection.push(self.optionToData(element));
 
                            }
 
                          } else {
 
                            if (element.text().toLowerCase().indexOf(termLower) == 0) {
 
                                collection.push(self.optionToData(element));
 
                            }
 
                          }
 
                        } else if (element.is("optgroup")) {
 
                            group=self.optionToData(element);
 
                            element.children().each2(function(i, elm) { process(elm, group.children); });
 
                            if (group.children.length>0) {
 
                                collection.push(group);
 
                            }
 
                        }
 
                    };
 

	
 
                    children=element.children();
 

	
 
                    // ignore the placeholder option if there is one
 
                    if (this.getPlaceholder() !== undefined && children.length > 0) {
 
                        placeholderOption = this.getPlaceholderOption();
 
                        if (placeholderOption) {
 
                            children=children.not(placeholderOption);
 
                        }
 
                    }
 

	
 
                    children.each2(function(i, elm) { process(elm, data.results); });
 

	
 
                    query.callback(data);
 
                });
 
                // this is needed because inside val() we construct choices from options and their id is hardcoded
 
                opts.id=function(e) { return e.id; };
 
            } else {
 
                if (!("query" in opts)) {
 
                    if ("ajax" in opts) {
 
                        ajaxUrl = opts.element.data("ajax-url");
 
                        if (ajaxUrl && ajaxUrl.length > 0) {
 
                            opts.ajax.url = ajaxUrl;
 
                        }
 
                        opts.query = ajax.call(opts.element, opts.ajax);
 
                    } else if ("data" in opts) {
 
                        opts.query = local(opts.data);
 
                    } else if ("tags" in opts) {
 
                        opts.query = tags(opts.tags);
 
                        if (opts.createSearchChoice === undefined) {
 
                            opts.createSearchChoice = function (term) { return {id: $.trim(term), text: $.trim(term)}; };
 
                        }
 
                        if (opts.initSelection === undefined) {
 
                            opts.initSelection = function (element, callback) {
 
                                var data = [];
 
                                $(splitVal(element.val(), opts.separator, opts.transformVal)).each(function () {
 
                                    var obj = { id: this, text: this },
 
                                        tags = opts.tags;
 
                                    if ($.isFunction(tags)) tags=tags();
 
                                    $(tags).each(function() { if (equal(this.id, obj.id)) { obj = this; return false; } });
 
                                    data.push(obj);
 
                                });
 

	
 
                                callback(data);
 
                            };
 
                        }
 
                    }
 
                }
 
            }
 
            if (typeof(opts.query) !== "function") {
 
                throw "query function not defined for Select2 " + opts.element.attr("id");
 
            }
 

	
 
            if (opts.createSearchChoicePosition === 'top') {
 
                opts.createSearchChoicePosition = function(list, item) { list.unshift(item); };
 
            }
 
            else if (opts.createSearchChoicePosition === 'bottom') {
 
                opts.createSearchChoicePosition = function(list, item) { list.push(item); };
 
            }
 
            else if (typeof(opts.createSearchChoicePosition) !== "function")  {
 
                throw "invalid createSearchChoicePosition option must be 'top', 'bottom' or a custom function";
 
            }
 

	
 
            return opts;
 
        },
 

	
 
        /**
 
         * Monitor the original element for changes and update select2 accordingly
 
         */
 
        // abstract
 
        monitorSource: function () {
 
            var el = this.opts.element, observer, self = this;
 

	
 
            el.on("change.select2", this.bind(function (e) {
 
                if (this.opts.element.data("select2-change-triggered") !== true) {
 
                    this.initSelection();
 
                }
 
            }));
 

	
 
            this._sync = this.bind(function () {
 

	
 
                // sync enabled state
 
                var disabled = el.prop("disabled");
 
                if (disabled === undefined) disabled = false;
 
                this.enable(!disabled);
 

	
 
                var readonly = el.prop("readonly");
 
                if (readonly === undefined) readonly = false;
 
                this.readonly(readonly);
 

	
 
                if (this.container) {
 
                    syncCssClasses(this.container, this.opts.element, this.opts.adaptContainerCssClass);
 
                    this.container.addClass(evaluate(this.opts.containerCssClass, this.opts.element));
 
                }
 

	
 
                if (this.dropdown) {
 
                    syncCssClasses(this.dropdown, this.opts.element, this.opts.adaptDropdownCssClass);
 
                    this.dropdown.addClass(evaluate(this.opts.dropdownCssClass, this.opts.element));
 
                }
 

	
 
            });
 

	
 
            // IE8-10 (IE9/10 won't fire propertyChange via attachEventListener)
 
            if (el.length && el[0].attachEvent) {
 
                el.each(function() {
 
                    this.attachEvent("onpropertychange", self._sync);
 
                });
 
            }
 

	
 
            // safari, chrome, firefox, IE11
 
            observer = window.MutationObserver || window.WebKitMutationObserver|| window.MozMutationObserver;
 
            if (observer !== undefined) {
 
                if (this.propertyObserver) { delete this.propertyObserver; this.propertyObserver = null; }
 
                this.propertyObserver = new observer(function (mutations) {
 
                    $.each(mutations, self._sync);
 
                });
 
                this.propertyObserver.observe(el.get(0), { attributes:true, subtree:false });
 
            }
 
        },
 

	
 
        // abstract
 
        triggerSelect: function(data) {
 
            var evt = $.Event("select2-selecting", { val: this.id(data), object: data, choice: data });
 
            this.opts.element.trigger(evt);
 
            return !evt.isDefaultPrevented();
 
        },
 

	
 
        /**
 
         * Triggers the change event on the source element
 
         */
 
        // abstract
 
        triggerChange: function (details) {
 

	
 
            details = details || {};
 
            details= $.extend({}, details, { type: "change", val: this.val() });
 
            // prevents recursive triggering
 
            this.opts.element.data("select2-change-triggered", true);
 
            this.opts.element.trigger(details);
 
            this.opts.element.data("select2-change-triggered", false);
 

	
 
            // some validation frameworks ignore the change event and listen instead to keyup, click for selects
 
            // so here we trigger the click event manually
 
            this.opts.element.click();
 

	
 
            // ValidationEngine ignores the change event and listens instead to blur
 
            // so here we trigger the blur event manually if so desired
 
            if (this.opts.blurOnChange)
 
                this.opts.element.blur();
 
        },
 

	
 
        //abstract
 
        isInterfaceEnabled: function()
 
        {
 
            return this.enabledInterface === true;
 
        },
 

	
 
        // abstract
 
        enableInterface: function() {
 
            var enabled = this._enabled && !this._readonly,
 
                disabled = !enabled;
 

	
 
            if (enabled === this.enabledInterface) return false;
 

	
 
            this.container.toggleClass("select2-container-disabled", disabled);
 
            this.close();
 
            this.enabledInterface = enabled;
 

	
 
            return true;
 
        },
 

	
 
        // abstract
 
        enable: function(enabled) {
 
            if (enabled === undefined) enabled = true;
 
            if (this._enabled === enabled) return;
 
            this._enabled = enabled;
 

	
 
            this.opts.element.prop("disabled", !enabled);
 
            this.enableInterface();
 
        },
 

	
 
        // abstract
 
        disable: function() {
 
            this.enable(false);
 
        },
 

	
 
        // abstract
 
        readonly: function(enabled) {
 
            if (enabled === undefined) enabled = false;
 
            if (this._readonly === enabled) return;
 
            this._readonly = enabled;
 

	
 
            this.opts.element.prop("readonly", enabled);
 
            this.enableInterface();
 
        },
 

	
 
        // abstract
 
        opened: function () {
 
            return (this.container) ? this.container.hasClass("select2-dropdown-open") : false;
 
        },
 

	
 
        // abstract
 
        positionDropdown: function() {
 
            var $dropdown = this.dropdown,
 
                container = this.container,
 
                offset = container.offset(),
 
                height = container.outerHeight(false),
 
                width = container.outerWidth(false),
 
                dropHeight = $dropdown.outerHeight(false),
 
                $window = $(window),
 
                windowWidth = $window.width(),
 
                windowHeight = $window.height(),
 
                viewPortRight = $window.scrollLeft() + windowWidth,
 
                viewportBottom = $window.scrollTop() + windowHeight,
 
                dropTop = offset.top + height,
 
                dropLeft = offset.left,
 
                enoughRoomBelow = dropTop + dropHeight <= viewportBottom,
 
                enoughRoomAbove = (offset.top - dropHeight) >= $window.scrollTop(),
 
                dropWidth = $dropdown.outerWidth(false),
 
                enoughRoomOnRight = function() {
 
                    return dropLeft + dropWidth <= viewPortRight;
 
                },
 
                enoughRoomOnLeft = function() {
 
                    return offset.left + viewPortRight + container.outerWidth(false)  > dropWidth;
 
                },
 
                aboveNow = $dropdown.hasClass("select2-drop-above"),
 
                bodyOffset,
 
                above,
 
                changeDirection,
 
                css,
 
                resultsListNode;
 

	
 
            // always prefer the current above/below alignment, unless there is not enough room
 
            if (aboveNow) {
 
                above = true;
 
                if (!enoughRoomAbove && enoughRoomBelow) {
 
                    changeDirection = true;
 
                    above = false;
 
                }
 
            } else {
 
                above = false;
 
                if (!enoughRoomBelow && enoughRoomAbove) {
 
                    changeDirection = true;
 
                    above = true;
 
                }
 
            }
 

	
 
            //if we are changing direction we need to get positions when dropdown is hidden;
 
            if (changeDirection) {
 
                $dropdown.hide();
 
                offset = this.container.offset();
 
                height = this.container.outerHeight(false);
 
                width = this.container.outerWidth(false);
 
                dropHeight = $dropdown.outerHeight(false);
 
                viewPortRight = $window.scrollLeft() + windowWidth;
 
                viewportBottom = $window.scrollTop() + windowHeight;
 
                dropTop = offset.top + height;
 
                dropLeft = offset.left;
 
                dropWidth = $dropdown.outerWidth(false);
 
                $dropdown.show();
 

	
 
                // fix so the cursor does not move to the left within the search-textbox in IE
 
                this.focusSearch();
 
            }
 

	
 
            if (this.opts.dropdownAutoWidth) {
 
                resultsListNode = $('.select2-results', $dropdown)[0];
 
                $dropdown.addClass('select2-drop-auto-width');
 
                $dropdown.css('width', '');
 
                // Add scrollbar width to dropdown if vertical scrollbar is present
 
                dropWidth = $dropdown.outerWidth(false) + (resultsListNode.scrollHeight === resultsListNode.clientHeight ? 0 : scrollBarDimensions.width);
 
                dropWidth > width ? width = dropWidth : dropWidth = width;
 
                dropHeight = $dropdown.outerHeight(false);
 
            }
 
            else {
 
                this.container.removeClass('select2-drop-auto-width');
 
            }
 

	
 
            //console.log("below/ droptop:", dropTop, "dropHeight", dropHeight, "sum", (dropTop+dropHeight)+" viewport bottom", viewportBottom, "enough?", enoughRoomBelow);
 
            //console.log("above/ offset.top", offset.top, "dropHeight", dropHeight, "top", (offset.top-dropHeight), "scrollTop", this.body.scrollTop(), "enough?", enoughRoomAbove);
 

	
 
            // fix positioning when body has an offset and is not position: static
 
            if (this.body.css('position') !== 'static') {
 
                bodyOffset = this.body.offset();
 
                dropTop -= bodyOffset.top;
 
                dropLeft -= bodyOffset.left;
 
            }
 

	
 
            if (!enoughRoomOnRight() && enoughRoomOnLeft()) {
 
                dropLeft = offset.left + this.container.outerWidth(false) - dropWidth;
 
            }
 

	
 
            css =  {
 
                left: dropLeft,
 
                width: width
 
            };
 

	
 
            if (above) {
 
                this.container.addClass("select2-drop-above");
 
                $dropdown.addClass("select2-drop-above");
 
                dropHeight = $dropdown.outerHeight(false);
 
                css.top = offset.top - dropHeight;
 
                css.bottom = 'auto';
 
            }
 
            else {
 
                css.top = dropTop;
 
                css.bottom = 'auto';
 
                this.container.removeClass("select2-drop-above");
 
                $dropdown.removeClass("select2-drop-above");
 
            }
 
            css = $.extend(css, evaluate(this.opts.dropdownCss, this.opts.element));
 

	
 
            $dropdown.css(css);
 
        },
 

	
 
        // abstract
 
        shouldOpen: function() {
 
            var event;
 

	
 
            if (this.opened()) return false;
 

	
 
            if (this._enabled === false || this._readonly === true) return false;
 

	
 
            event = $.Event("select2-opening");
 
            this.opts.element.trigger(event);
 
            return !event.isDefaultPrevented();
 
        },
 

	
 
        // abstract
 
        clearDropdownAlignmentPreference: function() {
 
            // clear the classes used to figure out the preference of where the dropdown should be opened
 
            this.container.removeClass("select2-drop-above");
 
            this.dropdown.removeClass("select2-drop-above");
 
        },
 

	
 
        /**
 
         * Opens the dropdown
 
         *
 
         * @return {Boolean} whether or not dropdown was opened. This method will return false if, for example,
 
         * the dropdown is already open, or if the 'open' event listener on the element called preventDefault().
 
         */
 
        // abstract
 
        open: function () {
 

	
 
            if (!this.shouldOpen()) return false;
 

	
 
            this.opening();
 

	
 
            // Only bind the document mousemove when the dropdown is visible
 
            $document.on("mousemove.select2Event", function (e) {
 
                lastMousePosition.x = e.pageX;
 
                lastMousePosition.y = e.pageY;
 
            });
 

	
 
            return true;
 
        },
 

	
 
        /**
 
         * Performs the opening of the dropdown
 
         */
 
        // abstract
 
        opening: function() {
 
            var cid = this.containerEventName,
 
                scroll = "scroll." + cid,
 
                resize = "resize."+cid,
 
                orient = "orientationchange."+cid,
 
                mask;
 

	
 
            this.container.addClass("select2-dropdown-open").addClass("select2-container-active");
 

	
 
            this.clearDropdownAlignmentPreference();
 

	
 
            if(this.dropdown[0] !== this.body.children().last()[0]) {
 
                this.dropdown.detach().appendTo(this.body);
 
            }
 

	
 
            // create the dropdown mask if doesn't already exist
kallithea/templates/changelog/changelog.html
Show inline comments
 
## -*- coding: utf-8 -*-
 

	
 
<%inherit file="/base/base.html"/>
 

	
 
<%block name="title">
 
    ${_('%s Changelog') % c.repo_name}
 
    %if c.changelog_for_path:
 
      /${c.changelog_for_path}
 
    %endif
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    <% size = c.size if c.size <= c.total_cs else c.total_cs %>
 
    ${_('Changelog')}
 
    %if c.changelog_for_path:
 
     - /${c.changelog_for_path}
 
    %endif
 
    %if c.revision:
 
    @ ${h.short_id(c.first_revision.raw_id)}
 
    %endif
 
    - ${ungettext('showing %d out of %d revision', 'showing %d out of %d revisions', size) % (size, c.total_cs)}
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%def name="main()">
 
${self.repo_context_bar('changelog', c.first_revision.raw_id if c.first_revision else None)}
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 
    <div class="table">
 
        % if c.pagination:
 
            <div>
 
                <div style="overflow:auto; ${'display:none' if c.changelog_for_path else ''}">
 
                    <div class="container_header">
 
                        <div style="float:left; margin-left:20px;">
 
                          ${h.form(h.url.current(),method='get',style="display:inline")}
 
                            ${h.submit('set',_('Show'),class_="btn btn-small")}
 
                            ${h.text('size',size=3,value=c.size)}
 
                            ${_('revisions')}
 
                            %if c.branch_name:
 
                            ${h.hidden('branch', c.branch_name)}
 
                            %endif
 
                          ${h.end_form()}
 
                          <a href="#" class="btn btn-small" id="rev_range_clear" style="display:none">${_('Clear selection')}</a>
 
                        </div>
 
                        <div style="float: right; margin: 0px 0px 0px 4px">
 
                            <a href="#" class="btn btn-small" id="rev_range_container" style="display:none"></a>
 
                            %if c.revision:
 
                                <a class="btn btn-small" href="${h.url('changelog_home', repo_name=c.repo_name)}">
 
                                    ${_('Go to tip of repository')}
 
                                </a>
 
                            %endif
 
                            %if c.db_repo.fork:
 
                                <a id="compare_fork"
 
                                   title="${_('Compare fork with %s' % c.db_repo.fork.repo_name)}"
 
                                   href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1],other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}"
 
                                   class="btn btn-small"><i class="icon-git-compare"></i> ${_('Compare fork with parent repository (%s)' % c.db_repo.fork.repo_name)}</a>
 
                            %endif
 
                            ## text and href of open_new_pr is controlled from javascript
 
                            <a id="open_new_pr" class="btn btn-small"></a>
 
                            ${_("Branch filter:")} ${h.select('branch_filter',c.branch_name,c.branch_filters)}
 
                        </div>
 
                    </div>
 
                </div>
 

	
 
                <div id="changelog" style="clear:both">
 

	
 
                <div id="graph_nodes">
 
                    <canvas id="graph_canvas" style="width:0"></canvas>
 
                </div>
 
                <div id="graph_content" style="${'margin: 0px' if c.changelog_for_path else ''}">
 

	
 
                <table id="changesets">
 
                <tbody>
 
                %for cnt,cs in enumerate(c.pagination):
 
                    <tr id="chg_${cnt+1}" class="container ${'mergerow' if len(cs.parents) > 1 else ''}">
 
                        <td class="checkbox">
 
                            %if c.changelog_for_path:
 
                                ${h.checkbox(cs.raw_id,class_="changeset_range", disabled="disabled")}
 
                            %else:
 
                                ${h.checkbox(cs.raw_id,class_="changeset_range")}
 
                            %endif
 
                        <td class="status">
 
                          %if c.statuses.get(cs.raw_id):
 
                            <div class="changeset-status-ico">
 
                            %if c.statuses.get(cs.raw_id)[2]:
 
                              <a class="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username, c.statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
 
                                <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
 
                              </a>
 
                            %else:
 
                              <a class="tooltip" title="${_('Changeset status: %s by %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username)}"
 
                                 href="${c.comments[cs.raw_id][0].url()}">
 
                                  <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
 
                              </a>
 
                            %endif
 
                            </div>
 
                          %endif
 
                        </td>
 
                        <td class="author">
 
                            ${h.gravatar(h.email_or_none(cs.author), size=16)}
 
                            <span title="${cs.author}" class="user">${h.shorter(h.person(cs.author),22)}</span>
 
                        </td>
 
                        <td class="hash" style="width:${len(h.show_id(cs))*6.5}px">
 
                            ${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id), class_='changeset_hash')}
 
                        </td>
 
                        <td class="date">
 
                            <div class="date tooltip" title="${h.fmt_date(cs.date)}">${h.age(cs.date,True)}</div>
 
                        </td>
 
                        <td class="expand_commit" commit_id="${cs.raw_id}" title="${_('Expand commit message')}">
 
                            <i class="icon-align-left" style="color:#999"></i>
 
                        </td>
 
                        <td class="mid">
 
                            <div class="log-container">
 
                                <div class="message" id="C-${cs.raw_id}">${h.urlify_commit(cs.message, c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
 
                                <div class="extra-container">
 
                                    %if c.comments.get(cs.raw_id):
 
                                        <div class="comments-container">
 
                                            <div class="comments-cnt" title="${_('Changeset has comments')}">
 
                                                <a href="${c.comments[cs.raw_id][0].url()}">
 
                                                    ${len(c.comments[cs.raw_id])}
 
                                                    <i class="icon-comment-discussion"></i>
 
                                                </a>
 
                                            </div>
 
                                        </div>
 
                                    %endif
 
                                    %if cs.bumped:
 
                                        <div class="bumpedtag" title="Bumped">
 
                                            Bumped
 
                                        </div>
 
                                    %endif
 
                                    %if cs.divergent:
 
                                        <div class="divergenttag" title="Divergent">
 
                                            Divergent
 
                                        </div>
 
                                    %endif
 
                                    %if cs.extinct:
 
                                        <div class="extincttag" title="Extinct">
 
                                            Extinct
 
                                        </div>
 
                                    %endif
 
                                    %if cs.unstable:
 
                                        <div class="unstabletag" title="Unstable">
 
                                            Unstable
 
                                        </div>
 
                                    %endif
 
                                    %if cs.phase:
 
                                        <div class="phasetag" title="Phase">
 
                                            ${cs.phase}
 
                                        </div>
 
                                    %endif
 
                                    %if h.is_hg(c.db_repo_scm_instance):
 
                                        %for book in cs.bookmarks:
 
                                            <div class="booktag" title="${_('Bookmark %s') % book}">
 
                                                ${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
 
                                            </div>
 
                                        %endfor
 
                                    %endif
 
                                    %for tag in cs.tags:
 
                                        <div class="tagtag" title="${_('Tag %s') % tag}">
 
                                            ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
 
                                        </div>
 
                                    %endfor
 
                                    %if (not c.branch_name) and cs.branch:
 
                                        <div class="branchtag" title="${_('Branch %s' % cs.branch)}">
 
                                            ${h.link_to(cs.branch,h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch))}
 
                                        </div>
 
                                    %endif
 
                                </div>
 
                            </div>
 
                        </td>
 
                    </tr>
 
                %endfor
 
                </tbody>
 
                </table>
 

	
 
                <input type="checkbox" id="singlerange" style="display:none"/>
 

	
 
                </div>
 

	
 
                <div class="pagination-wh pagination-left">
 
                    ${c.pagination.pager('$link_previous ~2~ $link_next')}
 
                </div>
 
            </div>
 
        </div>
 

	
 
        <script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
 
        <script type="text/javascript">
 
            $(document).ready(function(){
 
                var $checkboxes = $('.changeset_range');
 

	
 
                pyroutes.register('changeset_home', "${h.url('changeset_home', repo_name='%(repo_name)s', revision='%(revision)s')}", ['repo_name', 'revision']);
 

	
 
                var checkbox_checker = function(e) {
 
                    var $checked_checkboxes = $checkboxes.filter(':checked');
 
                    var $singlerange = $('#singlerange');
 

	
 
                    $('#rev_range_container').hide();
 
                    $checkboxes.show();
 
                    $singlerange.show();
 

	
 
                    if ($checked_checkboxes.length > 0) {
 
                        $checked_checkboxes.first().parent('td').append($singlerange);
 
                        var singlerange = $singlerange.prop('checked');
 
                        var rev_end = $checked_checkboxes.first().prop('name');
 
                        if ($checked_checkboxes.length > 1 || singlerange) {
 
                            var rev_start = $checked_checkboxes.last().prop('name');
 
                            $('#rev_range_container').prop('href',
 
                                pyroutes.url('changeset_home', {'repo_name': '${c.repo_name}',
 
                                                                'revision': rev_start + '...' + rev_end}));
 
                            $('#rev_range_container').html(
 
                                 _TM['Show Selected Changesets {0} &rarr; {1}'].format(rev_start.substr(0, 12), rev_end.substr(0, 12)));
 
                            $('#rev_range_container').show();
 
                            $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
 
                                                                        {'repo_name': '${c.repo_name}',
 
                                                                         'rev_start': rev_start,
 
                                                                         'rev_end': rev_end}));
 
                            $('#open_new_pr').html(_TM['Open New Pull Request for {0} &rarr; {1}'].format(rev_start.substr(0, 12), rev_end.substr(0, 12)));
 
                        } else {
 
                            $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
 
                                                                        {'repo_name': '${c.repo_name}',
 
                                                                         'rev_end': rev_end}));
 
                            $('#open_new_pr').html(_TM['Open New Pull Request from {0}'].format(rev_end.substr(0, 12)));
 
                        }
 
                        $('#rev_range_clear').show();
 
                        $('#compare_fork').hide();
 

	
 
                        var disabled = true;
 
                        $checkboxes.each(function(){
 
                            var $this = $(this);
 
                            if (disabled) {
 
                                if ($this.prop('checked')) {
 
                                    $this.closest('tr').removeClass('out-of-range');
 
                                    disabled = singlerange;
 
                                } else {
 
                                    $this.closest('tr').addClass('out-of-range');
 
                                }
 
                            } else {
 
                                $this.closest('tr').removeClass('out-of-range');
 
                                disabled = $this.prop('checked');
 
                            }
 
                        });
 

	
 
                        if ($checked_checkboxes.length + (singlerange ? 1 : 0) >= 2) {
 
                            $checkboxes.hide();
 
                            $checked_checkboxes.show();
 
                            if (!singlerange)
 
                                $singlerange.hide();
 
                        }
 
                    } else {
 
                        $('#singlerange').hide().prop('checked', false);
 
                        $('#rev_range_clear').hide();
 
                        %if c.revision:
 
                            $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
 
                                                                        {'repo_name': '${c.repo_name}',
 
                                                                         'rev_end':'${c.first_revision.raw_id}'}));
 
                            $('#open_new_pr').html(_TM['Open New Pull Request from {0}'].format('${c.revision}'));
 
                        %else:
 
                            $('#open_new_pr').prop('href', pyroutes.url('pullrequest_home',
 
                                                                        {'repo_name': '${c.repo_name}',
 
                                                                        'branch':'${c.first_revision.branch}'}));
 
                            $('#open_new_pr').html(_TM['Open New Pull Request from {0}'].format('${c.first_revision.branch}'));
 
                        %endif
 
                        $('#compare_fork').show();
 
                        $checkboxes.closest('tr').removeClass('out-of-range');
 
                    }
 
                };
 
                checkbox_checker();
 
                $checkboxes.click(function() {
 
                    checkbox_checker();
 
                    r.render(jsdata,100);
 
                });
 
                $('#singlerange').click(checkbox_checker);
 

	
 
                $('#rev_range_clear').click(function(e){
 
                    $checkboxes.prop('checked', false);
 
                    checkbox_checker();
 
                    r.render(jsdata,100);
 
                });
 

	
 
                var $msgs = $('.message');
 
                // get first element height
 
                var el = $('#graph_content .container')[0];
 
                var row_h = el.clientHeight;
 
                $msgs.each(function() {
 
                    var m = this;
 

	
 
                    var h = m.clientHeight;
 
                    if(h > row_h){
 
                        var offset = row_h - (h+12);
 
                        $(m.nextElementSibling).css('display', 'block');
 
                        $(m.nextElementSibling).css('margin-top', offset+'px');
 
                    }
 
                });
 

	
 
                $('.expand_commit').on('click',function(e){
 
                    var cid = $(this).attr('commit_id');
 
                    $('#C-'+cid).toggleClass('expanded');
 

	
 
                    //redraw the graph, r and jsdata are bound outside function
 
                    r.render(jsdata,100);
 
                });
 

	
 
                // change branch filter
 
                $("#branch_filter").select2({
 
                    dropdownAutoWidth: true,
 
                    minimumInputLength: 1,
 
                    maxResults: 50,
 
                    sortResults: branchSort
 
                    });
 

	
 
                $("#branch_filter").change(function(e){
 
                    var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
 
                    if(selected_branch != ''){
 
                        window.location = pyroutes.url('changelog_home', {'repo_name': '${c.repo_name}',
 
                                                                          'branch': selected_branch});
 
                    }else{
 
                        window.location = pyroutes.url('changelog_home', {'repo_name': '${c.repo_name}'});
 
                    }
 
                    $("#changelog").hide();
 
                });
 

	
 
                var jsdata = ${c.jsdata|n};
 
                var r = new BranchRenderer('graph_canvas', 'graph_content', 'chg_');
 
                r.render(jsdata,100);
 
            });
 

	
 
        </script>
 
        %else:
 
            ${_('There are no changes yet')}
 
        %endif
 
    </div>
 
</div>
 
</%def>
kallithea/templates/files/files.html
Show inline comments
 
<%inherit file="/base/base.html"/>
 

	
 
<%block name="title">
 
    ${_('%s Files') % c.repo_name}
 
    %if hasattr(c,'file'):
 
        &middot; ${h.safe_unicode(c.file.path) or '/'}
 
    %endif
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${_('Files')}
 
    %if c.file:
 
        @ ${h.show_id(c.changeset)}
 
    %endif
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%def name="main()">
 
${self.repo_context_bar('files', c.revision)}
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
        <ul class="links">
 
            <li style="color:white">
 
              ${_("Branch filter:")} ${h.select('branch_selector',c.changeset.raw_id,c.revision_options)}
 
            </li>
 
        </ul>
 
    </div>
 
    <div class="table">
 
        <div id="files_data">
 
            <%include file='files_ypjax.html'/>
 
        </div>
 
    </div>
 
</div>
 

	
 
<script type="text/javascript">
 
var CACHE = {};
 
var CACHE_EXPIRE = 5*60*1000; //cache for 5*60s
 
//used to construct links from the search list
 
var url_base = '${h.url("files_home",repo_name=c.repo_name,revision='__REV__',f_path='__FPATH__')}';
 
//send the nodelist request to this url
 
var node_list_url = '${h.url("files_nodelist_home",repo_name=c.repo_name,revision='__REV__',f_path='__FPATH__')}';
 
// send the node history requst to this url
 
var node_history_url = '${h.url("files_history_home",repo_name=c.repo_name,revision='__REV__',f_path='__FPATH__')}';
 

	
 
## new pyroutes URLs
 
pyroutes.register('files_nodelist_home', "${h.url('files_nodelist_home', repo_name=c.repo_name,revision='%(revision)s',f_path='%(f_path)s')}", ['revision', 'f_path']);
 
pyroutes.register('files_history_home', "${h.url('files_history_home', repo_name=c.repo_name,revision='%(revision)s',f_path='%(f_path)s')}", ['revision', 'f_path']);
 
pyroutes.register('files_authors_home', "${h.url('files_authors_home', repo_name=c.repo_name,revision='%(revision)s',f_path='%(f_path)s')}", ['revision', 'f_path']);
 

	
 
var ypjax_links = function(){
 
    $('.ypjax-link').click(function(e){
 

	
 
        //don't do ypjax on middle click
 
        if(e.which == 2 || !History.enabled){
 
            return true;
 
        }
 

	
 
        var el = e.currentTarget;
 
        var url = el.href;
 

	
 
        var _base_url = '${h.url("files_home",repo_name=c.repo_name,revision='',f_path='')}';
 
        _base_url = _base_url.replace('//','/');
 

	
 
        //extract rev and the f_path from url.
 
        parts = url.split(_base_url);
 
        if(parts.length != 2){
 
            return false;
 
        }
 

	
 
        var parts2 = parts[1].split('/');
 
        var rev = parts2.shift(); // pop the first element which is the revision
 
        var f_path = parts2.join('/');
 

	
 
        //page title - make this consistent with title mako block above
 
        var title = "${_('%s Files') % c.repo_name}" + " \u00B7 " + (f_path || '/') + " \u00B7 " + "${c.site_name}";
 

	
 
        var _node_list_url = node_list_url.replace('__REV__',rev).replace('__FPATH__', f_path);
 
        var _url_base = url_base.replace('__REV__',rev);
 

	
 
        // Change our States and save some data for handling events
 
        var data = {url:url,title:title, url_base:_url_base,
 
                    node_list_url:_node_list_url, rev:rev, f_path:f_path};
 
        History.pushState(data, title, url);
 

	
 
        //now we're sure that we can do ypjax things
 
        e.preventDefault();
 
        return false;
 
    });
 
}
 

	
 
// callbacks needed to process the ypjax filebrowser
 
var callbacks = function(State){
 
    ypjax_links();
 
    tooltip_activate();
 

	
 
    if(State !== undefined){
 
        //initially loaded stuff
 
        var _f_path = State.data.f_path;
 
        var _rev = State.data.rev;
 

	
 
        fileBrowserListeners(State.url, State.data.node_list_url, State.data.url_base);
 
        // Inform Google Analytics of the change
 
        if ( typeof window.pageTracker !== 'undefined' ) {
 
            window.pageTracker._trackPageview(State.url);
 
        }
 
    }
 

	
 
    function highlight_lines(lines){
 
        for(pos in lines){
 
          $('#L'+lines[pos]).css('background-color','#FFFFBE');
 
        }
 
    }
 
    page_highlights = location.href.substring(location.href.indexOf('#')+1).split('L');
 
    if (page_highlights.length == 2){
 
       highlight_ranges  = page_highlights[1].split(",");
 

	
 
       var h_lines = [];
 
       for (pos in highlight_ranges){
 
            var _range = highlight_ranges[pos].split('-');
 
            if(_range.length == 2){
 
                var start = parseInt(_range[0]);
 
                var end = parseInt(_range[1]);
 
                if (start < end){
 
                    for(var i=start;i<=end;i++){
 
                        h_lines.push(i);
 
                    }
 
                }
 
            }
 
            else{
 
                h_lines.push(parseInt(highlight_ranges[pos]));
 
            }
 
      }
 
      highlight_lines(h_lines);
 
      $('#L'+h_lines[0]).each(function(){
 
          this.scrollIntoView();
 
      });
 
    }
 

	
 
    // select code link event
 
    $('#hlcode').mouseup(getSelectionLink);
 

	
 
    // history select field
 
    var cache = {};
 
    $("#diff1").select2({
 
        placeholder: _TM['Select changeset'],
 
        dropdownAutoWidth: true,
 
        query: function(query){
 
          var key = 'cache';
 
          var cached = cache[key] ;
 
          if(cached) {
 
            var data = {results: []};
 
            //filter results
 
            $.each(cached.results, function(){
 
                var section = this.text;
 
                var children = [];
 
                $.each(this.children, function(){
 
                    if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
 
                        children.push({'id': this.id, 'text': this.text});
 
                    }
 
                });
 
                data.results.push({'text': section, 'children': children});
 
            });
 
            query.callback(data);
 
          }else{
 
              $.ajax({
 
                url: pyroutes.url('files_history_home', {'revision': _rev, 'f_path': _f_path}),
 
                data: {},
 
                dataType: 'json',
 
                type: 'GET',
 
                success: function(data) {
 
                  cache[key] = data;
 
                  query.callback({results: data.results});
 
                }
 
              });
 
          }
 
        }
 
    });
 
    $('#show_authors').on('click', function(){
 
        $.ajax({
 
            url: pyroutes.url('files_authors_home', {'revision': _rev, 'f_path': _f_path}),
 
            success: function(data) {
 
                $('#file_authors').html(data);
 
                $('#file_authors').show();
 
                tooltip_activate();
 
            }
 
        });
 
    });
 
}
 

	
 
$(document).ready(function(){
 
    ypjax_links();
 
    var $files_data = $('#files_data');
 
    //Bind to StateChange Event
 
    History.Adapter.bind(window,'statechange',function(){
 
        var State = History.getState();
 
        cache_key = State.url;
 
        //check if we have this request in cache maybe ?
 
        var _cache_obj = CACHE[cache_key];
 
        var _cur_time = new Date().getTime();
 
        // get from cache if it's there and not yet expired !
 
        if(_cache_obj !== undefined && _cache_obj[0] > _cur_time){
 
            $files_data.html(_cache_obj[1]);
 
            $files_data.css('opacity','1.0');
 
            //callbacks after ypjax call
 
            callbacks(State);
 
        }
 
        else{
 
            asynchtml(State.url, $files_data, function(){
 
                    callbacks(State);
 
                    var expire_on = new Date().getTime() + CACHE_EXPIRE;
 
                    CACHE[cache_key] = [expire_on, $files_data.html()];
 
                });
 
        }
 
    });
 

	
 
    // init the search filter
 
    var _State = {
 
       url: "${h.url.current()}",
 
       data: {
 
         node_list_url: node_list_url.replace('__REV__',"${c.changeset.raw_id}").replace('__FPATH__', "${h.safe_unicode(c.file.path)}"),
 
         url_base: url_base.replace('__REV__',"${c.changeset.raw_id}"),
 
         rev:"${c.changeset.raw_id}",
 
         f_path: "${h.safe_unicode(c.file.path)}"
 
       }
 
    }
 
    fileBrowserListeners(_State.url, _State.data.node_list_url, _State.data.url_base);
 

	
 
    // change branch filter
 
    $("#branch_selector").select2({
 
        dropdownAutoWidth: true,
 
        minimumInputLength: 1,
 
        maxResults: 50,
 
        sortResults: branchSort
 
        });
 

	
 
    $("#branch_selector").change(function(e){
 
        var selected = e.currentTarget.options[e.currentTarget.selectedIndex].value;
 
        if(selected && selected != "${c.changeset.raw_id}"){
 
            window.location = pyroutes.url('files_home', {'repo_name': "${h.safe_unicode(c.repo_name)}", 'revision': selected, 'f_path': "${h.safe_unicode(c.file.path)}"});
 
            $("#body.browserblock").hide();
 
        } else {
 
            $("#branch_selector").val("${c.changeset.raw_id}");
 
        }
 
    });
 

	
 
});
 

	
 
</script>
 

	
 
</%def>
kallithea/templates/pullrequests/pullrequest.html
Show inline comments
 
<%inherit file="/base/base.html"/>
 

	
 
<%block name="title">
 
    ${c.repo_name} ${_('New Pull Request')}
 
</%block>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${_('New Pull Request')}
 
</%def>
 

	
 
<%block name="header_menu">
 
    ${self.menu('repositories')}
 
</%block>
 

	
 
<%def name="main()">
 
${self.repo_context_bar('showpullrequest')}
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 

	
 
    ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
 
    <div class="form">
 
        <!-- fields -->
 

	
 
        <div class="fields" style="float:left;width:50%;padding-right:30px;">
 

	
 
             <div class="field">
 
                <div class="label">
 
                    <label for="pullrequest_title">${_('Title')}:</label>
 
                </div>
 
                <div class="input">
 
                    ${h.text('pullrequest_title',class_="large",placeholder=_('Summarize the changes - or leave empty'))}
 
                </div>
 
             </div>
 

	
 
            <div class="field">
 
                <div class="label label-textarea">
 
                    <label for="pullrequest_desc">${_('Description')}:</label>
 
                </div>
 
                <div class="textarea text-area editor">
 
                    ${h.textarea('pullrequest_desc',size=30,placeholder=_('Write a short description on this pull request'))}
 
                </div>
 
            </div>
 

	
 
            <div class="field">
 
                <div class="label label-textarea">
 
                    <label for="pullrequest_desc">${_('Changeset flow')}:</label>
 
                </div>
 
                <div class="input">
 
                    ##ORG
 
                    <div>
 
                        <div>
 
                            <div style="padding:5px 3px 3px 3px;">
 
                            <b>${_('Origin repository')}:</b> <span id="org_repo_desc">${c.db_repo.description.split('\n')[0]}</span>
 
                            </div>
 
                            <div>
 
                            ${h.select('org_repo','',c.cs_repos,class_='refs')}:${h.select('org_ref',c.default_cs_ref,c.cs_refs,class_='refs')}
 
                            </div>
 
                            <div style="padding:5px 3px 3px 3px;">
 
                            <b>${_('Revision')}:</b> <span id="org_rev_span">-</span>
 
                            </div>
 
                        </div>
 
                    </div>
 

	
 
                    ##OTHER, most Probably the PARENT OF THIS FORK
 
                    <div style="border-top: 1px solid #EEE; margin: 5px 0px 0px 0px">
 
                        <div>
 
                            ## filled with JS
 
                            <div style="padding:5px 3px 3px 3px;">
 
                            <b>${_('Destination repository')}:</b> <span id="other_repo_desc">${c.a_repo.description.split('\n')[0]}</span>
 
                            </div>
 
                            <div>
 
                            ${h.select('other_repo',c.a_repo.repo_name,c.a_repos,class_='refs')}:${h.select('other_ref',c.default_a_ref,c.a_refs,class_='refs')}
 
                            </div>
 
                            <div style="padding:5px 3px 3px 3px;">
 
                            <b>${_('Revision')}:</b> <span id="other_rev_span">-</span>
 
                            </div>
 
                        </div>
 
                    </div>
 
                    <div style="clear:both"></div>
 
                </div>
 
            </div>
 

	
 
            <div class="field">
 
                <div class="buttons">
 
                    ${h.submit('save',_('Create Pull Request'),class_="btn")}
 
                    ${h.reset('reset',_('Reset'),class_="btn")}
 
               </div>
 
            </div>
 

	
 
        </div>
 

	
 
        <div style="clear:both;padding: 0 0 30px 0;"></div>
 

	
 
        <h4>${_('Changesets')}</h4>
 
        <div style="float:left;padding:0px 30px 30px 30px">
 
           ## overview pulled by ajax
 
           <div style="float:left" id="pull_request_overview"></div>
 
        </div>
 
        <div style="clear:both;"></div>
 

	
 
    </div>
 

	
 
    ${h.end_form()}
 

	
 
</div>
 

	
 
<script type="text/javascript" src="${h.url('/js/graph.js', ver=c.kallithea_version)}"></script>
 
<script type="text/javascript">
 
  pyroutes.register('pullrequest_repo_info', "${url('pullrequest_repo_info',repo_name='%(repo_name)s')}", ['repo_name']);
 

	
 
  var pendingajax = undefined;
 
  var otherrepoChanged = function(){
 
      var $other_ref = $('#other_ref');
 
      $other_ref.prop('disabled', true);
 
      var repo_name = $('#other_repo').val();
 
      if (pendingajax) {
 
          pendingajax.abort();
 
          pendingajax = undefined;
 
      }
 
      pendingajax = ajaxGET(pyroutes.url('pullrequest_repo_info', {"repo_name": repo_name}),
 
          function(data){
 
              pendingajax = undefined;
 
              $('#other_repo_desc').html(data.description);
 

	
 
              // replace options of other_ref with the ones for the current other_repo
 
              $other_ref.empty();
 
              for(var i = 0; i < data.refs.length; i++)
 
              {
 
                var $optgroup = $('<optgroup/>').prop('label', data.refs[i][1]);
 
                var options = data.refs[i][0];
 
                var length = options.length;
 
                for(var j = 0; j < length; j++)
 
                {
 
                  $optgroup.append($('<'+'option/>').text(options[j][1]).val(options[j][0]));
 
                }
 
                $other_ref.append($optgroup);
 
              }
 
              $other_ref.val(data.selected_ref);
 

	
 
              // re-populate the select2 thingy
 
              $("#other_ref").select2({
 
                  dropdownAutoWidth: true
 
              });
 

	
 
              $other_ref.prop('disabled', false);
 
              loadPreview();
 
          });
 
  };
 

	
 
  var loadPreview = function(){
 
      //url template
 
      var url = "${h.url('compare_url',
 
                         repo_name='__other_repo__',
 
                         org_ref_type='rev',
 
                         org_ref_name='__other_ref_name__',
 
                         other_repo='__org_repo__',
 
                         other_ref_type='rev',
 
                         other_ref_name='__org_ref_name__',
 
                         as_form=True,
 
                         merge=True,
 
                         )}";
 
      var org_repo = $('#pull_request_form #org_repo').val();
 
      var org_ref = $('#pull_request_form #org_ref').val().split(':');
 
      ## TODO: make nice link like link_to_ref() do
 
      $('#org_rev_span').html(org_ref[2].substr(0,12));
 

	
 
      var other_repo = $('#pull_request_form #other_repo').val();
 
      var other_ref = $('#pull_request_form #other_ref').val().split(':');
 
      $('#other_rev_span').html(other_ref[2].substr(0,12));
 

	
 
      var rev_data = {
 
          '__org_repo__': org_repo,
 
          '__org_ref_name__': org_ref[2],
 
          '__other_repo__': other_repo,
 
          '__other_ref_name__': other_ref[2]
 
      }; // gather the org/other ref and repo here
 

	
 
      for (k in rev_data){
 
          url = url.replace(k,rev_data[k]);
 
      }
 

	
 
      if (pendingajax) {
 
          pendingajax.abort();
 
          pendingajax = undefined;
 
      }
 
      pendingajax = asynchtml(url, $('#pull_request_overview'), function(o){
 
          pendingajax = undefined;
 
          var jsdata = eval('('+$('#jsdata').html()+')'); // TODO: just get json
 
          var r = new BranchRenderer('graph_canvas', 'graph_content_pr', 'chg_');
 
          r.render(jsdata,100);
 
      });
 
  }
 

	
 
  $(document).ready(function(){
 
      $("#org_repo").select2({
 
          dropdownAutoWidth: true
 
      });
 
      ## (org_repo can't change)
 

	
 
      $("#org_ref").select2({
 
          dropdownAutoWidth: true,
 
          maxResults: 50,
 
          sortResults: branchSort
 
      });
 
      $("#org_ref").on("change", function(e){
 
          loadPreview();
 
      });
 

	
 
      $("#other_repo").select2({
 
          dropdownAutoWidth: true
 
      });
 
      $("#other_repo").on("change", function(e){
 
          otherrepoChanged();
 
      });
 

	
 
      $("#other_ref").select2({
 
          dropdownAutoWidth: true,
 
          maxResults: 50,
 
          sortResults: branchSort
 
      });
 
      $("#other_ref").on("change", function(e){
 
          loadPreview();
 
      });
 

	
 
      //lazy load overview after 0.5s
 
      setTimeout(loadPreview, 500);
 
  });
 

	
 
</script>
 

	
 
</%def>
0 comments (0 inline, 0 general)