<commit_before>/*
Copyright 2017 New Vector Ltd

Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {Store} from 'flux/utils';
import dis from '../dispatcher';

const INITIAL_STATE = {
    orderedTags: null,
    orderedTagsAccountData: null,
    hasSynced: false,
    joinedGroupIds: null,
};

/**
 * A class for storing application state for ordering tags in the TagPanel.
 */
class TagOrderStore extends Store {
    constructor() {
        super(dis);

        // Initialise state
        this._state = INITIAL_STATE;
    }

    _setState(newState) {
        this._state = Object.assign(this._state, newState);
        this.__emitChange();
    }

    __onDispatch(payload) {
        switch (payload.action) {
            // Initialise state after initial sync
            case 'MatrixActions.sync': {
                if (!(payload.prevState === 'PREPARED' && payload.state === 'SYNCING')) {
                    break;
                }
                const tagOrderingEvent = payload.matrixClient.getAccountData('im.vector.web.tag_ordering');
                const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
                this._setState({
                    orderedTagsAccountData: tagOrderingEventContent.tags || null,
                    hasSynced: true,
                });
                this._updateOrderedTags();
                break;
            }
            // Get ordering from account data
            case 'MatrixActions.accountData': {
                if (payload.event_type !== 'im.vector.web.tag_ordering') break;
                this._setState({
                    orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
                });
                this._updateOrderedTags();
                break;
            }
            // Initialise the state such that if account data is unset, default to joined groups
            case 'GroupActions.fetchJoinedGroups.success': {
                this._setState({
                    joinedGroupIds: payload.result.groups.sort(), // Sort lexically
                    hasFetchedJoinedGroups: true,
                });
                this._updateOrderedTags();
                break;
            }
            // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag
            case 'order_tag': {
                if (!this._state.orderedTags ||
                    !payload.tag ||
                    !payload.targetTag ||
                    payload.tag === payload.targetTag
                ) return;

                const tags = this._state.orderedTags;

                let orderedTags = tags.filter((t) => t !== payload.tag);
                const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0);
                orderedTags = [
                    ...orderedTags.slice(0, newIndex),
                    payload.tag,
                    ...orderedTags.slice(newIndex),
                ];
                this._setState({orderedTags});
                break;
            }
        }
    }

    _updateOrderedTags() {
        this._setState({
            orderedTags:
                this._state.hasSynced &&
                this._state.hasFetchedJoinedGroups ?
                    this._mergeGroupsAndTags() : null,
        });
    }

    _mergeGroupsAndTags() {
        const groupIds = this._state.joinedGroupIds || [];
        const tags = this._state.orderedTagsAccountData || [];

        const tagsToKeep = tags.filter(
            (t) => t[0] !== '+' || groupIds.includes(t),
        );

        const groupIdsToAdd = groupIds.filter(
            (groupId) => !tags.includes(groupId),
        );

        return tagsToKeep.concat(groupIdsToAdd);
    }

    getOrderedTags() {
        return this._state.orderedTags;
    }
}

if (global.singletonTagOrderStore === undefined) {
    global.singletonTagOrderStore = new TagOrderStore();
}
export default global.singletonTagOrderStore;
<commit_msg>Copy state when initialisng, reset state when logging out<commit_after>/*
Copyright 2017 New Vector Ltd

Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {Store} from 'flux/utils';
import dis from '../dispatcher';

const INITIAL_STATE = {
    orderedTags: null,
    orderedTagsAccountData: null,
    hasSynced: false,
    joinedGroupIds: null,
};

/**
 * A class for storing application state for ordering tags in the TagPanel.
 */
class TagOrderStore extends Store {
    constructor() {
        super(dis);

        // Initialise state
        this._state = Object.assign({}, INITIAL_STATE);
    }

    _setState(newState) {
        this._state = Object.assign(this._state, newState);
        this.__emitChange();
    }

    __onDispatch(payload) {
        switch (payload.action) {
            // Initialise state after initial sync
            case 'MatrixActions.sync': {
                if (!(payload.prevState === 'PREPARED' && payload.state === 'SYNCING')) {
                    break;
                }
                const tagOrderingEvent = payload.matrixClient.getAccountData('im.vector.web.tag_ordering');
                const tagOrderingEventContent = tagOrderingEvent ? tagOrderingEvent.getContent() : {};
                this._setState({
                    orderedTagsAccountData: tagOrderingEventContent.tags || null,
                    hasSynced: true,
                });
                this._updateOrderedTags();
                break;
            }
            // Get ordering from account data
            case 'MatrixActions.accountData': {
                if (payload.event_type !== 'im.vector.web.tag_ordering') break;
                this._setState({
                    orderedTagsAccountData: payload.event_content ? payload.event_content.tags : null,
                });
                this._updateOrderedTags();
                break;
            }
            // Initialise the state such that if account data is unset, default to joined groups
            case 'GroupActions.fetchJoinedGroups.success': {
                this._setState({
                    joinedGroupIds: payload.result.groups.sort(), // Sort lexically
                    hasFetchedJoinedGroups: true,
                });
                this._updateOrderedTags();
                break;
            }
            // Puts payload.tag at payload.targetTag, placing the targetTag before or after the tag
            case 'order_tag': {
                if (!this._state.orderedTags ||
                    !payload.tag ||
                    !payload.targetTag ||
                    payload.tag === payload.targetTag
                ) return;

                const tags = this._state.orderedTags;

                let orderedTags = tags.filter((t) => t !== payload.tag);
                const newIndex = orderedTags.indexOf(payload.targetTag) + (payload.after ? 1 : 0);
                orderedTags = [
                    ...orderedTags.slice(0, newIndex),
                    payload.tag,
                    ...orderedTags.slice(newIndex),
                ];
                this._setState({orderedTags});
                break;
            }
            case 'on_logged_out': {
                // Reset state without pushing an update to the view, which generally assumes that
                // the matrix client isn't `null` and so causing a re-render will cause NPEs.
                this._state = Object.assign({}, INITIAL_STATE);
                break;
            }
        }
    }

    _updateOrderedTags() {
        this._setState({
            orderedTags:
                this._state.hasSynced &&
                this._state.hasFetchedJoinedGroups ?
                    this._mergeGroupsAndTags() : null,
        });
    }

    _mergeGroupsAndTags() {
        const groupIds = this._state.joinedGroupIds || [];
        const tags = this._state.orderedTagsAccountData || [];

        const tagsToKeep = tags.filter(
            (t) => t[0] !== '+' || groupIds.includes(t),
        );

        const groupIdsToAdd = groupIds.filter(
            (groupId) => !tags.includes(groupId),
        );

        return tagsToKeep.concat(groupIdsToAdd);
    }

    getOrderedTags() {
        return this._state.orderedTags;
    }
}

if (global.singletonTagOrderStore === undefined) {
    global.singletonTagOrderStore = new TagOrderStore();
}
export default global.singletonTagOrderStore;
<|endoftext|>


<commit_before>/*
Widget complements Autocomplete by enabling autocompletes to be used as
value holders. It looks very much like Autocomplete in its design. Thus, it
is recommended to read the source of Autocomplete first.

Widget behaves like the autocomplete in facebook profile page, which all
users should be able to use.

Behind the scenes, Widget maintains a normal hidden select which makes it
simple to play with on the server side like on the client side. If a value
is added and selected in the select element, then it is added to the deck,
and vice-versa.

It needs some elements, and established vocabulary:

- ".autocomplete-light-widget" element wraps all the HTML necessary for the
 widget,
- ".deck" contains the list of selected choice(s) as HTML,
- "input" should be the text input that has the Autocomplete,
- "select" a (optionnaly multiple) select
- ".remove" a (preferabely hidden) element that contains a value removal
 indicator, like an "X" sign or a trashcan icon, it is used to prefix every
 children of the deck
- ".choice-template" a (preferabely hidden) element that contains the template
 for choices which are added directly in the select, as they should be
 copied in the deck,

To avoid complexity, this script relies on extra HTML attributes, and
particularely one called 'data-value'. Learn more about data attributes:
http://dev.w3.org/html5/spec/global-attributes.html#embedding-custom-non-visible-data-with-the-data-attributes

When a choice is selected from the Autocomplete, its element is cloned
and appended to the deck - "deck" contains "choices". It is important that the
choice elements of the autocomplete all contain a data-value attribute.
The value of data-value is used to fill the selected options in the hidden
select field.

If choices may not all have a data-value attribute, then you can
override Widget.getValue() to implement your own logic.
*/

// Our class will live in the yourlabs global namespace.
if (window.yourlabs == undefined) window.yourlabs = {};

$.ajaxSettings.traditional = true

/*
Instanciate a Widget.
*/
yourlabs.Widget = function(widget) {
    // These attributes where described above.
    this.widget = widget;
    this.input = this.widget.find('input');
    this.select = this.widget.find('select');
    this.deck = this.widget.find('.deck');
    this.choiceTemplate = this.widget.find('.choice-template .choice');

    // The number of choices that the user may select with this widget. Set 0
    // for no limit. In the case of a foreign key you want to set it to 1.
    this.maximumValues = 0;

    // Clear input when choice made? 1 for yes, 0 for no
    this.clearInputOnSelectChoice = "1";
}

// When a choice is selected from the autocomplete of this widget,
// getValue() is called to add and select the option in the select.
yourlabs.Widget.prototype.getValue = function(choice) {
    return choice.attr('data-value');
};

// The widget is in charge of managing its Autocomplete.
yourlabs.Widget.prototype.initializeAutocomplete = function() {
    this.autocomplete = this.input.yourlabsAutocomplete()

    // Add a class to ease css selection of autocompletes for widgets
    this.autocomplete.box.addClass('autocomplete-light-widget');
};

// Bind Autocomplete.selectChoice signal to Widget.selectChoice()
yourlabs.Widget.prototype.bindSelectChoice = function() {
    this.input.bind('selectChoice', function(e, choice) {
        if (!choice.length)
            return // placeholder: create choice here

        var widget = $(this).parents('.autocomplete-light-widget'
            ).yourlabsWidget();

        widget.selectChoice(choice);
    });
};

// Called when a choice is selected from the Autocomplete.
yourlabs.Widget.prototype.selectChoice = function(choice) {
    // Get the value for this choice.
    var value = this.getValue(choice);

    if (!value) {
        if (window.console) console.log('yourlabs.Widget.getValue failed');
        return;
    }

    this.freeDeck();
    this.addToDeck(choice, value);
    this.addToSelect(choice, value);

    var index = $(':input:visible').index(this.input);
    this.resetDisplay();

    if (this.input.is(':visible')) {
        this.input.focus();
    } else {
        var next = $(':input:visible:eq('+ index +')');
        next.focus();
    }

    if (this.clearInputOnSelectChoice === "1")
        this.input.val('');
}

// Unselect a value if the maximum number of selected values has been
// reached.
yourlabs.Widget.prototype.freeDeck = function() {
    var slots = this.maximumValues - this.deck.children().length;

    if (this.maximumValues && slots < 1) {
        // We'll remove the first choice which is supposed to be the oldest
        var choice = $(this.deck.children()[0]);

        this.deselectChoice(choice);
    }
}

// Empty the search input and hide it if maximumValues has been reached.
yourlabs.Widget.prototype.resetDisplay = function() {
    var selected = this.select.find('option:selected').length;

    if (this.maximumValues && selected == this.maximumValues) {
        this.input.hide();
    } else {
        this.input.show();
    }

    this.deck.show();

    // Also fix the position if the autocomplete is shown.
    if (this.autocomplete.box.is(':visible')) this.autocomplete.fixPosition();
}

yourlabs.Widget.prototype.deckChoiceHtml = function(choice, value) {
    var deckChoice = choice.clone();

    this.addRemove(deckChoice);

    return deckChoice;
}

yourlabs.Widget.prototype.optionChoice = function(option) {
    var optionChoice = this.choiceTemplate.clone();

    var target = optionChoice.find('.append-option-html');

    if (target.length) {
        target.append(option.html());
    } else {
        optionChoice.html(option.html());
    }

    return optionChoice;
}

yourlabs.Widget.prototype.addRemove = function(choices) {
    var removeTemplate = this.widget.find('.remove:last')
        .clone().css('display', 'inline-block');

    var target = choices.find('.prepend-remove');

    if (target.length) {
        target.prepend(removeTemplate);
    } else {
        // Add the remove icon to each choice
        choices.prepend(removeTemplate);
    }
}

// Add a selected choice of a given value to the deck.
yourlabs.Widget.prototype.addToDeck = function(choice, value) {
    var existing_choice = this.deck.find('[data-value="'+value+'"]');

    // Avoid duplicating choices in the deck.
    if (!existing_choice.length) {
        var deckChoice = this.deckChoiceHtml(choice);

        // In case getValue() actually **created** the value, for example
        // with a post request.
        deckChoice.attr('data-value', value);

        this.deck.append(deckChoice);
    }
}

// Add a selected choice of a given value to the deck.
yourlabs.Widget.prototype.addToSelect = function(choice, value) {
    var option = this.select.find('option[value="'+value+'"]');

    if (! option.length) {
        this.select.append(
            '<option selected="selected" value="'+ value +'"></option>');
        option = this.select.find('option[value="'+value+'"]');
    }

    option.attr('selected', 'selected');

    this.select.trigger('change');
    this.updateAutocompleteExclude();
}

// Called when the user clicks .remove in a deck choice.
yourlabs.Widget.prototype.deselectChoice = function(choice) {
    var value = this.getValue(choice);

    this.select.find('option[value="'+value+'"]').remove();
    this.select.trigger('change');

    choice.remove();

    if (this.deck.children().length == 0) {
        this.deck.hide();
    }

    this.updateAutocompleteExclude();
    this.resetDisplay();
};

yourlabs.Widget.prototype.updateAutocompleteExclude = function() {
    var widget = this;
    var choices = this.deck.find(this.autocomplete.choiceSelector);

    this.autocomplete.data['exclude'] = $.map(choices, function(choice) {
        return widget.getValue($(choice));
    });
}

yourlabs.Widget.prototype.initialize = function() {
    this.initializeAutocomplete();

    // Working around firefox tempering form values after reload
    var widget = this;
    this.deck.find(this.autocomplete.choiceSelector).each(function() {
        var value = widget.getValue($(this));
        var option = widget.select.find('option[value="'+value+'"]');
        if (!option.attr('selected')) option.attr('selected', true);
    });

    var choices = this.deck.find(
        this.input.yourlabsAutocomplete().choiceSelector);

    this.addRemove(choices);
    this.resetDisplay();

    this.bindSelectChoice();
    this.clearBoth()
}

// Add an empty div with clear:both after the widget's container.
// This is meant to support django-responsive-admin templates.
yourlabs.Widget.prototype.clearBoth = function() {
    this.widget.parent().append('

');
}

// Destroy the widget. Takes a widget element because a cloned widget element
// will be dirty, ie. have wrong .input and .widget properties.
yourlabs.Widget.prototype.destroy = function(widget) {
    widget.find('input')
        .unbind('selectChoice')
        .yourlabsAutocomplete('destroy');
}

// Get or create or destroy a widget instance.
//
// On first call, yourlabsWidget() will instanciate a widget applying all
// passed overrides.
//
// On later calls, yourlabsWidget() will return the previously created widget
// instance, which is stored in widget.data('widget').
//
// Calling yourlabsWidget('destroy') will destroy the widget. Useful if the
// element was blindly cloned with .clone(true) for example.
$.fn.yourlabsWidget = function(overrides) {
    var overrides = overrides ? overrides : {};

    var widget = this.yourlabsRegistry('widget');

    if (overrides == 'destroy') {
        if (widget) {
            widget.destroy(this);
            this.removeData('widget');
        }
        return
    }

    if (widget == undefined) {
        // Instanciate the widget
        var widget = new yourlabs.Widget(this);

        // Extend the instance with data-widget-* overrides
        for (var key in this.data()) {
            if (!key) continue;
            if (key.substr(0, 6) != 'widget') continue;
            var newKey = key.replace('widget', '');
            var newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
            widget[newKey] = this.data(key);
        }

        // Allow javascript object overrides
        widget = $.extend(widget, overrides);

        $(this).yourlabsRegistry('widget', widget);

        // Setup for usage
        widget.initialize();

        // Widget is ready
        widget.widget.attr('data-widget-ready', 1);
        widget.widget.trigger('widget-ready');
    }

    return widget;
}

$(document).ready(function() {
    $('body').on('initialize', '.autocomplete-light-widget[data-widget-bootstrap=normal]', function() {
        /*
        Only setup widgets which have data-widget-bootstrap=normal, if you want to
        initialize some Widgets with custom code, then set
        data-widget-boostrap=yourbootstrap or something like that.
        */
        $(this).yourlabsWidget();
    });

    // Call Widget.deselectChoice when .remove is clicked
    $('body').on('click', '.autocomplete-light-widget .deck .remove', function() {
        var widget = $(this).parents('.autocomplete-light-widget'
            ).yourlabsWidget();

        var selector = widget.input.yourlabsAutocomplete().choiceSelector;
        var choice = $(this).parents(selector);

        widget.deselectChoice(choice);
    });

    // Solid initialization, usage:
    //
    //
    //      $(document).bind('yourlabsWidgetReady', function() {
    //          $('.your.autocomplete-light-widget').on('initialize', function() {
    //              $(this).yourlabsWidget({
    //                  yourCustomArgs: // ...
    //              })
    //          });
    //      });
    $(document).trigger('yourlabsWidgetReady');

    $('.autocomplete-light-widget:not([id*="__prefix__"])').each(function() {
        $(this).trigger('initialize');
    });

    $(document).bind('DOMNodeInserted', function(e) {
        /*
        Support values added directly in the select via js (ie. choices created in
        modal or popup).

        For this, we listen to DOMNodeInserted and intercept insert of <option> nodes.

        The reason for that is that change is not triggered when options are
        added like this:

            $('select#id-dependencies').append(
                '<option value="9999" selected="selected">blabla</option>')
        */
        if ($(e.target).is('option')) { // added an option ?
            var widget = $(e.target).parents('.autocomplete-light-widget');

            if (!widget.length) {
                return;
            }

            widget = widget.yourlabsWidget();
            var option = $(e.target);
            var value = option.attr('value');
            var choice = widget.deck.find('[data-value="'+value+'"]');

            if (!choice.length) {
                var deckChoice = widget.optionChoice(option);

                deckChoice.attr('data-value', value);

                widget.selectChoice(deckChoice);
            }
        } else { // added a widget ?
            var widget = $(e.target).find('.autocomplete-light-widget');

            if (!widget.length) {
                widget = $(e.target).is('.autocomplete-light-widget') ? $(e.target) : false;

                if (!widget) {
                    return;
                }
            }

            // Ignore inserted autocomplete box elements.
            if (widget.is('.yourlabs-autocomplete')) {
                return;
            }

            // Ensure that the newly added widget is clean, in case it was
            // cloned with data.
            widget.yourlabsWidget('destroy');
            widget.find('input').yourlabsAutocomplete('destroy');

            // added a widget: initialize the widget.
            widget.trigger('initialize');
        }
    });

    var ie = yourlabs.getInternetExplorerVersion();
    if (ie != -1 && ie < 9) {
        observe = [
            '.autocomplete-light-widget:not([data-yourlabs-skip])',
            '.autocomplete-light-widget option:not([data-yourlabs-skip])'
        ].join();
        $(observe).attr('data-yourlabs-skip', 1);

        function ieDOMNodeInserted() {
            // http://msdn.microsoft.com/en-us/library/ms536957
            $(observe).each(function() {
                $(document).trigger(jQuery.Event('DOMNodeInserted', {target: $(this)}));
                $(this).attr('data-yourlabs-skip', 1);
            });

            setTimeout(ieDOMNodeInserted, 500);
        }
        setTimeout(ieDOMNodeInserted, 500);
    }

});
<commit_msg>Bugfix: widget.js was re-initializing widgets<commit_after>/*
Widget complements Autocomplete by enabling autocompletes to be used as
value holders. It looks very much like Autocomplete in its design. Thus, it
is recommended to read the source of Autocomplete first.

Widget behaves like the autocomplete in facebook profile page, which all
users should be able to use.

Behind the scenes, Widget maintains a normal hidden select which makes it
simple to play with on the server side like on the client side. If a value
is added and selected in the select element, then it is added to the deck,
and vice-versa.

It needs some elements, and established vocabulary:

- ".autocomplete-light-widget" element wraps all the HTML necessary for the
 widget,
- ".deck" contains the list of selected choice(s) as HTML,
- "input" should be the text input that has the Autocomplete,
- "select" a (optionnaly multiple) select
- ".remove" a (preferabely hidden) element that contains a value removal
 indicator, like an "X" sign or a trashcan icon, it is used to prefix every
 children of the deck
- ".choice-template" a (preferabely hidden) element that contains the template
 for choices which are added directly in the select, as they should be
 copied in the deck,

To avoid complexity, this script relies on extra HTML attributes, and
particularely one called 'data-value'. Learn more about data attributes:
http://dev.w3.org/html5/spec/global-attributes.html#embedding-custom-non-visible-data-with-the-data-attributes

When a choice is selected from the Autocomplete, its element is cloned
and appended to the deck - "deck" contains "choices". It is important that the
choice elements of the autocomplete all contain a data-value attribute.
The value of data-value is used to fill the selected options in the hidden
select field.

If choices may not all have a data-value attribute, then you can
override Widget.getValue() to implement your own logic.
*/

// Our class will live in the yourlabs global namespace.
if (window.yourlabs == undefined) window.yourlabs = {};

$.ajaxSettings.traditional = true

/*
Instanciate a Widget.
*/
yourlabs.Widget = function(widget) {
    // These attributes where described above.
    this.widget = widget;
    this.input = this.widget.find('input');
    this.select = this.widget.find('select');
    this.deck = this.widget.find('.deck');
    this.choiceTemplate = this.widget.find('.choice-template .choice');

    // The number of choices that the user may select with this widget. Set 0
    // for no limit. In the case of a foreign key you want to set it to 1.
    this.maximumValues = 0;

    // Clear input when choice made? 1 for yes, 0 for no
    this.clearInputOnSelectChoice = "1";
}

// When a choice is selected from the autocomplete of this widget,
// getValue() is called to add and select the option in the select.
yourlabs.Widget.prototype.getValue = function(choice) {
    return choice.attr('data-value');
};

// The widget is in charge of managing its Autocomplete.
yourlabs.Widget.prototype.initializeAutocomplete = function() {
    this.autocomplete = this.input.yourlabsAutocomplete()

    // Add a class to ease css selection of autocompletes for widgets
    this.autocomplete.box.addClass('autocomplete-light-widget');
};

// Bind Autocomplete.selectChoice signal to Widget.selectChoice()
yourlabs.Widget.prototype.bindSelectChoice = function() {
    this.input.bind('selectChoice', function(e, choice) {
        if (!choice.length)
            return // placeholder: create choice here

        var widget = $(this).parents('.autocomplete-light-widget'
            ).yourlabsWidget();

        widget.selectChoice(choice);
    });
};

// Called when a choice is selected from the Autocomplete.
yourlabs.Widget.prototype.selectChoice = function(choice) {
    // Get the value for this choice.
    var value = this.getValue(choice);

    if (!value) {
        if (window.console) console.log('yourlabs.Widget.getValue failed');
        return;
    }

    this.freeDeck();
    this.addToDeck(choice, value);
    this.addToSelect(choice, value);

    var index = $(':input:visible').index(this.input);
    this.resetDisplay();

    if (this.input.is(':visible')) {
        this.input.focus();
    } else {
        var next = $(':input:visible:eq('+ index +')');
        next.focus();
    }

    if (this.clearInputOnSelectChoice === "1")
        this.input.val('');
}

// Unselect a value if the maximum number of selected values has been
// reached.
yourlabs.Widget.prototype.freeDeck = function() {
    var slots = this.maximumValues - this.deck.children().length;

    if (this.maximumValues && slots < 1) {
        // We'll remove the first choice which is supposed to be the oldest
        var choice = $(this.deck.children()[0]);

        this.deselectChoice(choice);
    }
}

// Empty the search input and hide it if maximumValues has been reached.
yourlabs.Widget.prototype.resetDisplay = function() {
    var selected = this.select.find('option:selected').length;

    if (this.maximumValues && selected == this.maximumValues) {
        this.input.hide();
    } else {
        this.input.show();
    }

    this.deck.show();

    // Also fix the position if the autocomplete is shown.
    if (this.autocomplete.box.is(':visible')) this.autocomplete.fixPosition();
}

yourlabs.Widget.prototype.deckChoiceHtml = function(choice, value) {
    var deckChoice = choice.clone();

    this.addRemove(deckChoice);

    return deckChoice;
}

yourlabs.Widget.prototype.optionChoice = function(option) {
    var optionChoice = this.choiceTemplate.clone();

    var target = optionChoice.find('.append-option-html');

    if (target.length) {
        target.append(option.html());
    } else {
        optionChoice.html(option.html());
    }

    return optionChoice;
}

yourlabs.Widget.prototype.addRemove = function(choices) {
    var removeTemplate = this.widget.find('.remove:last')
        .clone().css('display', 'inline-block');

    var target = choices.find('.prepend-remove');

    if (target.length) {
        target.prepend(removeTemplate);
    } else {
        // Add the remove icon to each choice
        choices.prepend(removeTemplate);
    }
}

// Add a selected choice of a given value to the deck.
yourlabs.Widget.prototype.addToDeck = function(choice, value) {
    var existing_choice = this.deck.find('[data-value="'+value+'"]');

    // Avoid duplicating choices in the deck.
    if (!existing_choice.length) {
        var deckChoice = this.deckChoiceHtml(choice);

        // In case getValue() actually **created** the value, for example
        // with a post request.
        deckChoice.attr('data-value', value);

        this.deck.append(deckChoice);
    }
}

// Add a selected choice of a given value to the deck.
yourlabs.Widget.prototype.addToSelect = function(choice, value) {
    var option = this.select.find('option[value="'+value+'"]');

    if (! option.length) {
        this.select.append(
            '<option selected="selected" value="'+ value +'"></option>');
        option = this.select.find('option[value="'+value+'"]');
    }

    option.attr('selected', 'selected');

    this.select.trigger('change');
    this.updateAutocompleteExclude();
}

// Called when the user clicks .remove in a deck choice.
yourlabs.Widget.prototype.deselectChoice = function(choice) {
    var value = this.getValue(choice);

    this.select.find('option[value="'+value+'"]').remove();
    this.select.trigger('change');

    choice.remove();

    if (this.deck.children().length == 0) {
        this.deck.hide();
    }

    this.updateAutocompleteExclude();
    this.resetDisplay();
};

yourlabs.Widget.prototype.updateAutocompleteExclude = function() {
    var widget = this;
    var choices = this.deck.find(this.autocomplete.choiceSelector);

    this.autocomplete.data['exclude'] = $.map(choices, function(choice) {
        return widget.getValue($(choice));
    });
}

yourlabs.Widget.prototype.initialize = function() {
    this.initializeAutocomplete();

    // Working around firefox tempering form values after reload
    var widget = this;
    this.deck.find(this.autocomplete.choiceSelector).each(function() {
        var value = widget.getValue($(this));
        var option = widget.select.find('option[value="'+value+'"]');
        if (!option.attr('selected')) option.attr('selected', true);
    });

    var choices = this.deck.find(
        this.input.yourlabsAutocomplete().choiceSelector);

    this.addRemove(choices);
    this.resetDisplay();

    this.bindSelectChoice();
    this.clearBoth()
}

// Add an empty div with clear:both after the widget's container.
// This is meant to support django-responsive-admin templates.
yourlabs.Widget.prototype.clearBoth = function() {
    this.widget.parent().append('

');
}

// Destroy the widget. Takes a widget element because a cloned widget element
// will be dirty, ie. have wrong .input and .widget properties.
yourlabs.Widget.prototype.destroy = function(widget) {
    widget.find('input')
        .unbind('selectChoice')
        .yourlabsAutocomplete('destroy');
}

// Get or create or destroy a widget instance.
//
// On first call, yourlabsWidget() will instanciate a widget applying all
// passed overrides.
//
// On later calls, yourlabsWidget() will return the previously created widget
// instance, which is stored in widget.data('widget').
//
// Calling yourlabsWidget('destroy') will destroy the widget. Useful if the
// element was blindly cloned with .clone(true) for example.
$.fn.yourlabsWidget = function(overrides) {
    var overrides = overrides ? overrides : {};

    var widget = this.yourlabsRegistry('widget');

    if (overrides == 'destroy') {
        if (widget) {
            widget.destroy(this);
            this.removeData('widget');
        }
        return
    }

    if (widget == undefined) {
        // Instanciate the widget
        var widget = new yourlabs.Widget(this);

        // Extend the instance with data-widget-* overrides
        for (var key in this.data()) {
            if (!key) continue;
            if (key.substr(0, 6) != 'widget') continue;
            var newKey = key.replace('widget', '');
            var newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1);
            widget[newKey] = this.data(key);
        }

        // Allow javascript object overrides
        widget = $.extend(widget, overrides);

        $(this).yourlabsRegistry('widget', widget);

        // Setup for usage
        widget.initialize();

        // Widget is ready
        widget.widget.attr('data-widget-ready', 1);
        widget.widget.trigger('widget-ready');
    }

    return widget;
}

$(document).ready(function() {
    $('body').on('initialize', '.autocomplete-light-widget[data-widget-bootstrap=normal]', function() {
        /*
        Only setup widgets which have data-widget-bootstrap=normal, if you want to
        initialize some Widgets with custom code, then set
        data-widget-boostrap=yourbootstrap or something like that.
        */
        $(this).yourlabsWidget();
    });

    // Call Widget.deselectChoice when .remove is clicked
    $('body').on('click', '.autocomplete-light-widget .deck .remove', function() {
        var widget = $(this).parents('.autocomplete-light-widget'
            ).yourlabsWidget();

        var selector = widget.input.yourlabsAutocomplete().choiceSelector;
        var choice = $(this).parents(selector);

        widget.deselectChoice(choice);
    });

    // Solid initialization, usage:
    //
    //
    //      $(document).bind('yourlabsWidgetReady', function() {
    //          $('.your.autocomplete-light-widget').on('initialize', function() {
    //              $(this).yourlabsWidget({
    //                  yourCustomArgs: // ...
    //              })
    //          });
    //      });
    $(document).trigger('yourlabsWidgetReady');

    $('.autocomplete-light-widget:not([id*="__prefix__"])').each(function() {
        $(this).trigger('initialize');
    });

    $(document).bind('DOMNodeInserted', function(e) {
        /*
        Support values added directly in the select via js (ie. choices created in
        modal or popup).

        For this, we listen to DOMNodeInserted and intercept insert of <option> nodes.

        The reason for that is that change is not triggered when options are
        added like this:

            $('select#id-dependencies').append(
                '<option value="9999" selected="selected">blabla</option>')
        */
        if ($(e.target).is('option')) { // added an option ?
            var widget = $(e.target).parents('.autocomplete-light-widget');

            if (!widget.length) {
                return;
            }

            widget = widget.yourlabsWidget();
            var option = $(e.target);
            var value = option.attr('value');
            var choice = widget.deck.find('[data-value="'+value+'"]');

            if (!choice.length) {
                var deckChoice = widget.optionChoice(option);

                deckChoice.attr('data-value', value);

                widget.selectChoice(deckChoice);
            }
        } else { // added a widget ?
            var notReady = '.autocomplete-light-widget:not([data-widget-ready])'
            var widget = $(e.target).find(notReady);

            if (!widget.length) {
                widget = $(e.target).is(notReady) ? $(e.target) : false;

                if (!widget) {
                    return;
                }
            }

            // Ignore inserted autocomplete box elements.
            if (widget.is('.yourlabs-autocomplete')) {
                return;
            }

            // Ensure that the newly added widget is clean, in case it was
            // cloned with data.
            widget.yourlabsWidget('destroy');
            widget.find('input').yourlabsAutocomplete('destroy');

            // added a widget: initialize the widget.
            widget.trigger('initialize');
        }
    });

    var ie = yourlabs.getInternetExplorerVersion();
    if (ie != -1 && ie < 9) {
        observe = [
            '.autocomplete-light-widget:not([data-yourlabs-skip])',
            '.autocomplete-light-widget option:not([data-yourlabs-skip])'
        ].join();
        $(observe).attr('data-yourlabs-skip', 1);

        function ieDOMNodeInserted() {
            // http://msdn.microsoft.com/en-us/library/ms536957
            $(observe).each(function() {
                $(document).trigger(jQuery.Event('DOMNodeInserted', {target: $(this)}));
                $(this).attr('data-yourlabs-skip', 1);
            });

            setTimeout(ieDOMNodeInserted, 500);
        }
        setTimeout(ieDOMNodeInserted, 500);
    }

});
<|endoftext|>


<commit_before>/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2014 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

var module = angular.module('axelor.ui');

MenuBarCtrl.$inject = ['$scope', '$element'];
function MenuBarCtrl($scope, $element) {

	this.isDivider = function(item) {
		return !item.title && !item.icon;
	};

	this.isSubMenu = function(item) {
		return item && item.items && item.items.length > 0;
	};

	$scope.isImage = function (menu) {
		return menu.icon && menu.icon.indexOf('fa-') !== 0;
	};

	$scope.isIcon = function (menu) {
		return menu.icon && menu.icon.indexOf('fa-') === 0;
	};

	$scope.canShowTitle = function(menu) {
		return menu.showTitle === null || menu.showTitle === undefined || menu.showTitle;
	};
}

module.directive('uiMenuBar', function() {

	return {
		replace: true,
		controller: MenuBarCtrl,
		scope: {
			menus: '=',
			handler: '='
		},
		link: function(scope, element, attrs, ctrl) {

			ctrl.handler = scope.handler;

			scope.onMenuClick = _.once(function onMenuClick(e) {
				element.find('.dropdown-toggle').dropdown();
				$(e.currentTarget).dropdown('toggle');
			});
		},

		template:
			"
" +
				"
" +
					"" +
						" " +
						" " +
						"{{menu.title}} " +
						"" +
					"" +
					"

" +
				"
" +
			"
"
	};
});

module.directive('uiMenu', function() {

	return {
		replace: true,
		require: '^uiMenuBar',
		scope: {
			menu: '=uiMenu'
		},
		link: function(scope, element, attrs, ctrl) {

		},
		template:
			"
" +
				"
" +
			"
"
	};
});

module.directive('uiMenuItem', ['$compile', 'ActionService', function($compile, ActionService) {

	return {
		replace: true,
		require: '^uiMenuBar',
		scope: {
			item: '=uiMenuItem'
		},
		link: function(scope, element, attrs, ctrl) {

			var item = scope.item;
			var handler = null;

			scope.field  = item;
			scope.isDivider = ctrl.isDivider(item);
			scope.isSubMenu = ctrl.isSubMenu(item);

			if (item.action) {
				handler = ActionService.handler(ctrl.handler, element, {
					action: item.action,
					prompt: item.prompt
				});
				element.addClass("action-item").attr("x-name", item.name);
			}

			scope.isRequired = function(){};
			scope.isValid = function(){};

			var attrs = {
				hidden: false,
				readonly: false
			};

			scope.attr = function(name, value) {
				attrs[name] = value;
			};

			scope.isReadonly = function(){
				if (attrs.readonly) return true;
				if (_.isFunction(item.active)) {
					return !item.active();
				}
				return false;
			};

			scope.isHidden = function(){
				return attrs.hidden;
			};

			var form = element.parents('.form-view:first');
			var formScope = form.data('$scope');

			if (formScope) {
				formScope.$watch('record', function(rec) {
					scope.record = rec;
				});
			}

			scope.onClick = function(e) {
				element.parents('.dropdown').dropdown('toggle');
				if (scope.isSubMenu) return;
				if (item.action) {
					return handler.onClick();
				}
				if (_.isFunction(item.click)) {
					return item.click(e);
				}
			};

			scope.cssClass = function() {
				if (scope.isDivider) {
					return 'divider';
				}
				if (scope.isSubMenu) {
					return 'dropdown-submenu';
				}
			};

			if (scope.isSubMenu) {
				$compile('

')(scope, function(cloned, scope) {
					element.append(cloned);
				});
			}
		},
		template:
			"
" +
				"{{item.title}}" +
				"{{item.title}}" +
			"
"
	};
}]);

module.directive('uiToolbarAdjust', function() {

	return function (scope, element, attrs) {

		var elemMenubar = null;
		var elemToolbar = null;
		var elemSiblings = null;

		var elemMenubarMobile = null;
		var elemToolbarMobile = null;

		var lastWidth = 0;

		function setup() {
			elemMenubar = element.children('.view-menubar');
			elemToolbar = element.children('.view-toolbar');
			elemSiblings = element.children(':not(.view-menubar,.view-toolbar)');

			elemMenubarMobile = element.children('.view-menubar-mobile').hide();
			elemToolbarMobile = element.children('.view-toolbar-mobile').hide();
		}

		function adjust() {

			if (elemMenubar === null || lastWidth === element.width()) {
				return;
			}

			var hasMenubar = !_.isEmpty(scope.menubar);
			var hasToolbar = !_.isEmpty(scope.toolbar);

			if (axelor.device.small) {
				elemToolbar.hide();
				elemMenubar.hide();
				elemMenubarMobile.hide();
				elemToolbarMobile.hide();
				if (hasMenubar) elemMenubarMobile.show();
				if (hasToolbar) elemToolbarMobile.show();
				return;
			}

			elemMenubarMobile.hide();
			elemToolbarMobile.hide();
			elemToolbar.show();
			elemMenubar.show();

			var total = elemToolbar.width() + elemMenubar.width();
			if (total === 0) {
				return;
			}

			lastWidth = element.width();
			var restWidth = 0;
			elemSiblings.each(function () {
				restWidth += $(this).width();
			});

			var width = lastWidth - restWidth;
			if (width <= total && hasMenubar) {
				total -= elemMenubar.width();
				elemMenubar.hide();
				elemMenubarMobile.show();
			}
			if (width <= total && hasToolbar) {
				elemToolbar.hide();
				elemToolbarMobile.show();
			}
		}

		element.on('adjustSize', adjust);
		setTimeout(setup, 100);
	};
});

}).call(this);
<commit_msg>Fix toolbar buttons adjustment issue<commit_after>/*
 * Axelor Business Solutions
 *
 * Copyright (C) 2005-2014 Axelor (<http://axelor.com>).
 *
 * This program is free software: you can redistribute it and/or  modify
 * it under the terms of the GNU Affero General Public License, version 3,
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
(function() {

var module = angular.module('axelor.ui');

MenuBarCtrl.$inject = ['$scope', '$element'];
function MenuBarCtrl($scope, $element) {

	this.isDivider = function(item) {
		return !item.title && !item.icon;
	};

	this.isSubMenu = function(item) {
		return item && item.items && item.items.length > 0;
	};

	$scope.isImage = function (menu) {
		return menu.icon && menu.icon.indexOf('fa-') !== 0;
	};

	$scope.isIcon = function (menu) {
		return menu.icon && menu.icon.indexOf('fa-') === 0;
	};

	$scope.canShowTitle = function(menu) {
		return menu.showTitle === null || menu.showTitle === undefined || menu.showTitle;
	};
}

module.directive('uiMenuBar', function() {

	return {
		replace: true,
		controller: MenuBarCtrl,
		scope: {
			menus: '=',
			handler: '='
		},
		link: function(scope, element, attrs, ctrl) {

			ctrl.handler = scope.handler;

			scope.onMenuClick = _.once(function onMenuClick(e) {
				element.find('.dropdown-toggle').dropdown();
				$(e.currentTarget).dropdown('toggle');
			});
		},

		template:
			"
" +
				"
" +
					"" +
						" " +
						" " +
						"{{menu.title}} " +
						"" +
					"" +
					"

" +
				"
" +
			"
"
	};
});

module.directive('uiMenu', function() {

	return {
		replace: true,
		require: '^uiMenuBar',
		scope: {
			menu: '=uiMenu'
		},
		link: function(scope, element, attrs, ctrl) {

		},
		template:
			"
" +
				"
" +
			"
"
	};
});

module.directive('uiMenuItem', ['$compile', 'ActionService', function($compile, ActionService) {

	return {
		replace: true,
		require: '^uiMenuBar',
		scope: {
			item: '=uiMenuItem'
		},
		link: function(scope, element, attrs, ctrl) {

			var item = scope.item;
			var handler = null;

			scope.field  = item;
			scope.isDivider = ctrl.isDivider(item);
			scope.isSubMenu = ctrl.isSubMenu(item);

			if (item.action) {
				handler = ActionService.handler(ctrl.handler, element, {
					action: item.action,
					prompt: item.prompt
				});
				element.addClass("action-item").attr("x-name", item.name);
			}

			scope.isRequired = function(){};
			scope.isValid = function(){};

			var attrs = {
				hidden: false,
				readonly: false
			};

			scope.attr = function(name, value) {
				attrs[name] = value;
			};

			scope.isReadonly = function(){
				if (attrs.readonly) return true;
				if (_.isFunction(item.active)) {
					return !item.active();
				}
				return false;
			};

			scope.isHidden = function(){
				return attrs.hidden;
			};

			var form = element.parents('.form-view:first');
			var formScope = form.data('$scope');

			if (formScope) {
				formScope.$watch('record', function(rec) {
					scope.record = rec;
				});
			}

			scope.onClick = function(e) {
				element.parents('.dropdown').dropdown('toggle');
				if (scope.isSubMenu) return;
				if (item.action) {
					return handler.onClick();
				}
				if (_.isFunction(item.click)) {
					return item.click(e);
				}
			};

			scope.cssClass = function() {
				if (scope.isDivider) {
					return 'divider';
				}
				if (scope.isSubMenu) {
					return 'dropdown-submenu';
				}
			};

			if (scope.isSubMenu) {
				$compile('

')(scope, function(cloned, scope) {
					element.append(cloned);
				});
			}
		},
		template:
			"
" +
				"{{item.title}}" +
				"{{item.title}}" +
			"
"
	};
}]);

module.directive('uiToolbarAdjust', function() {

	return function (scope, element, attrs) {

		var elemMenubar = null;
		var elemToolbar = null;
		var elemSiblings = null;

		var elemMenubarMobile = null;
		var elemToolbarMobile = null;

		var lastWidth = 0;

		function setup() {
			elemMenubar = element.children('.view-menubar');
			elemToolbar = element.children('.view-toolbar');
			elemSiblings = element.children(':not(.view-menubar,.view-toolbar)');

			elemMenubarMobile = element.children('.view-menubar-mobile').hide();
			elemToolbarMobile = element.children('.view-toolbar-mobile').hide();

			element.on('adjustSize', adjust);
		}

		function adjust() {
			if (elemMenubar === null || lastWidth === element.width()) {
				return;
			}

			var hasMenubar = !_.isEmpty(scope.menubar);
			var hasToolbar = !_.isEmpty(scope.toolbar);

			if (axelor.device.small) {
				elemToolbar.hide();
				elemMenubar.hide();
				elemMenubarMobile.hide();
				elemToolbarMobile.hide();
				if (hasMenubar) elemMenubarMobile.show();
				if (hasToolbar) elemToolbarMobile.show();
				return;
			}

			elemMenubarMobile.hide();
			elemToolbarMobile.hide();
			elemToolbar.show();
			elemMenubar.show();

			var total = elemToolbar.width() + elemMenubar.width();
			if (total === 0) {
				return;
			}

			lastWidth = element.width();
			var restWidth = 0;
			elemSiblings.each(function () {
				restWidth += $(this).width();
			});

			var width = lastWidth - restWidth;
			if (width <= total && hasMenubar) {
				total -= elemMenubar.width();
				elemMenubar.hide();
				elemMenubarMobile.show();
			}
			if (width <= total && hasToolbar) {
				elemToolbar.hide();
				elemToolbarMobile.show();
			}
		}

		scope.waitForActions(setup, 100);
	};
});

}).call(this);
<|endoftext|>


<commit_before>//var autoTester = Script.require("https://raw.githubusercontent.com/highfidelity/hifi_tests/master/tests/utils/autoTester.js" );
var autoTester = Script.require("../../../../utils/autoTester.js" );

autoTester.perform("effect - haze", Script.resolvePath("."), function() {
    var spectatorCameraConfig = autoTester.setupTest();

    var TESTS_URL = "https://github.com/NissimHadar/hifi_tests/blob/addRecursionToAutotester/";
    var SUFFIX = "?raw=true";

    var pos =  MyAvatar.position;

    var terrain = Entities.addEntity({
        type: 'Box',
        name: 'Terrain',
        shape: 'Cube',
        dimensions: { x: 1000.0, y: - 2.0, z: 1000.0 },
        position: { x: pos.x, y: pos.y - 3.0, z: pos.z },
        color: { "blue": 200, "green": 200, "red": 200
        }
    });

    // A far away "tower"
    var base = Entities.addEntity({
        type: 'Box',
        name: 'Tower base',
        shape: 'Cube',
        dimensions: { x: 10.0, y: 10, z: 10.0 },
        position: { x: pos.x, y: pos.y + 1.0, z: pos.z - 1000.0},
        color: { "blue": 200, "green": 200, "red": 0
        }
    });
    var tower = Entities.addEntity({
        type: 'Box',
        name: 'Tower',
        shape: 'Cube',
        dimensions: { x: 3.0, y: 300, z: 3.0 },
        position: { x: pos.x, y: pos.y + 150, z: pos.z - 1000.0},
        color: { "blue": 0, "green": 200, "red": 200
        }
    });

    var SKY_URL = Script.resolvePath(TESTS_URL + 'assets/skymaps/Sky_Day-Sun-Mid-photo.ktx' + SUFFIX);
    var sky = Entities.addEntity({
        type: "Zone",
        name: "Sky",

        position: {x: MyAvatar.position.x, y: MyAvatar.position.y - 2.0, z: MyAvatar.position.z - 25.0},
        rotation: MyAvatar.orientation,
        dimensions: { x: 10000.0, y: 600.0, z: 10000.0 },

        keyLightMode: "enabled",
        keyLight:{
            color: {"red":255, "green":255, "blue":255},
            direction: {
                "x":  0.037007175385951996,
                "y": -0.7071067690849304,
                "z": -0.7061376571655273
            },
            intensity: 0.8
        },

        skyboxMode: "enabled",
        skybox: {
            color: {"red":255,"green":255,"blue":255},
            url: SKY_URL
        },

        ambientLightMode: "enabled",
        ambientLight: {
            ambientURL: SKY_URL
        }
    });

	spectatorCameraConfig.position = {x: MyAvatar.position.x, y: MyAvatar.position.y + 0.6, z: MyAvatar.position.z};

    // Look down X axis
    MyAvatar.bodyYaw = 90.0;
    MyAvatar.headYaw = 90.0;
    spectatorCameraConfig.orientation = { x: 0.0, y: 0.7071, z: 0.0, w: 0.7071 };

    autoTester.addStepSnapshot("Initial state");

    autoTester.addStepSnapshot("Enable haze, range at 15000 - sky is not visible, tower is visible",
        function () {
            var newProperty = {
                hazeMode: "enabled",
                haze: {
                    hazeRange: 15000.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set range to 500 - tower no longer visible",
        function () {
            var newProperty = {
                haze: {
                    hazeRange: 500.0
                }
            };
            Entities.editEntity(sky, newProperty);

        }
    );

    autoTester.addStepSnapshot("Turn on altitude effect - top of tower is visible",
        function () {
            var newProperty = {
                haze: {
                    hazeAltitudeEffect: 1
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set ceiling to 500 - tower no longer visible",
        function () {
            var newProperty = {
                haze: {
                    hazeCeiling: 500.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set base to -200 - top of tower is visible",
        function () {
            var newProperty = {
                haze: {
                    hazeBaseRef: -200.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set base to 0 and haze colour to sandy - haze looks like a sandstorm",
        function () {
            var newProperty = {
                haze: {
                    hazeBaseRef: 0.0,
                    hazeColor:{"red":153,"green":107,"blue":47}
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set background blend to 0.5 - tower and sky are partially visible",
        function () {
            var newProperty = {
                haze: {
                    hazeBackgroundBlend: 0.5
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set background blend to 1 - sky becomes clear",
        function () {
            var newProperty = {
                haze: {
                    hazeBackgroundBlend: 1.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set background blend back to 0 - cannot see tower nor sky",
        function () {
            var newProperty = {
                haze: {
                    hazeBackgroundBlend: 0.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Test glare effect (sun is 15 degrees elevation, 095 azimuth) - large burgundy sun glare effect can be seen",
        function () {
            var newProperty = {
                keyLight: {
                    direction:{"x":0.9622501869, "y":-0.2588190451, "z":-0.08418598283}
                },
                haze: {
                    hazeCeiling: 5000.0,
                    hazeEnableGlare: 1,
                    hazeGlareColor:{"red":176,"green":25,"blue":68},
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set glare angle to 5 degrees - sun glare angle becomes smaller",
        function () {
            var newProperty = {
                haze: {
                    hazeGlareAngle: 5
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Clean up",
        function () {
            Entities.deleteEntity(terrain);
            Entities.deleteEntity(sky);
        }
    );

    var result = autoTester.runTest();
});
<commit_msg>WIP - haze test.<commit_after>var autoTester = Script.require("https://raw.githubusercontent.com/NissimHadar/hifi_tests/addRecursionToAutotester/tests/utils/autoTester.js" );

autoTester.perform("effect - haze", Script.resolvePath("."), function() {
    var spectatorCameraConfig = autoTester.setupTest();

    var TESTS_URL = "https://github.com/NissimHadar/hifi_tests/blob/addRecursionToAutotester/";
    var SUFFIX = "?raw=true";

    var pos =  MyAvatar.position;

    var terrain = Entities.addEntity({
        type: 'Box',
        name: 'Terrain',
        shape: 'Cube',
        dimensions: { x: 1000.0, y: 0.2, z: 1000.0 },
        position: { x: pos.x, y: pos.y - 3.0, z: pos.z },
        color: { "blue": 200, "green": 200, "red": 200
        }
    });

    // A far away "tower"
    var base = Entities.addEntity({
        type: 'Box',
        name: 'Tower base',
        shape: 'Cube',
        dimensions: { x: 10.0, y: 10, z: 10.0 },
        position: { x: pos.x, y: pos.y + 1.0, z: pos.z - 1000.0},
        color: { "blue": 200, "green": 200, "red": 0
        }
    });
    var tower = Entities.addEntity({
        type: 'Box',
        name: 'Tower',
        shape: 'Cube',
        dimensions: { x: 3.0, y: 300, z: 3.0 },
        position: { x: pos.x, y: pos.y + 150, z: pos.z - 1000.0},
        color: { "blue": 0, "green": 200, "red": 200
        }
    });

    var SKY_URL = Script.resolvePath(TESTS_URL + 'assets/skymaps/Sky_Day-Sun-Mid-photo.ktx' + SUFFIX);
    var sky = Entities.addEntity({
        type: "Zone",
        name: "Sky",

        position: {x: MyAvatar.position.x, y: MyAvatar.position.y - 2.0, z: MyAvatar.position.z - 25.0},
        rotation: MyAvatar.orientation,
        dimensions: { x: 10000.0, y: 600.0, z: 10000.0 },

        keyLightMode: "enabled",
        keyLight:{
            color: {"red":255, "green":255, "blue":255},
            direction: {
                "x":  0.037007175385951996,
                "y": -0.7071067690849304,
                "z": -0.7061376571655273
            },
            intensity: 0.8
        },

        skyboxMode: "enabled",
        skybox: {
            color: {"red":255,"green":255,"blue":255},
            url: SKY_URL
        },

        ambientLightMode: "enabled",
        ambientLight: {
            ambientURL: SKY_URL
        }
    });

	spectatorCameraConfig.position = {x: MyAvatar.position.x, y: MyAvatar.position.y + 0.6, z: MyAvatar.position.z};

    // Look down X axis
    MyAvatar.bodyYaw = 90.0;
    MyAvatar.headYaw = 90.0;
    spectatorCameraConfig.orientation = { x: 0.0, y: 0.7071, z: 0.0, w: 0.7071 };

    autoTester.addStepSnapshot("Initial state");

    autoTester.addStepSnapshot("Enable haze, range at 15000 - sky is not visible, tower is visible",
        function () {
            var newProperty = {
                hazeMode: "enabled",
                haze: {
                    hazeRange: 15000.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set range to 500 - tower no longer visible",
        function () {
            var newProperty = {
                haze: {
                    hazeRange: 500.0
                }
            };
            Entities.editEntity(sky, newProperty);

        }
    );

    autoTester.addStepSnapshot("Turn on altitude effect - top of tower is visible",
        function () {
            var newProperty = {
                haze: {
                    hazeAltitudeEffect: 1
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set ceiling to 500 - tower no longer visible",
        function () {
            var newProperty = {
                haze: {
                    hazeCeiling: 500.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set base to -200 - top of tower is visible",
        function () {
            var newProperty = {
                haze: {
                    hazeBaseRef: -200.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set base to 0 and haze colour to sandy - haze looks like a sandstorm",
        function () {
            var newProperty = {
                haze: {
                    hazeBaseRef: 0.0,
                    hazeColor:{"red":153,"green":107,"blue":47}
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set background blend to 0.5 - tower and sky are partially visible",
        function () {
            var newProperty = {
                haze: {
                    hazeBackgroundBlend: 0.5
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set background blend to 1 - sky becomes clear",
        function () {
            var newProperty = {
                haze: {
                    hazeBackgroundBlend: 1.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set background blend back to 0 - cannot see tower nor sky",
        function () {
            var newProperty = {
                haze: {
                    hazeBackgroundBlend: 0.0
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Test glare effect (sun is 15 degrees elevation, 095 azimuth) - large burgundy sun glare effect can be seen",
        function () {
            var newProperty = {
                keyLight: {
                    direction:{"x":0.9622501869, "y":-0.2588190451, "z":-0.08418598283}
                },
                haze: {
                    hazeCeiling: 5000.0,
                    hazeEnableGlare: 1,
                    hazeGlareColor:{"red":176,"green":25,"blue":68},
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Set glare angle to 5 degrees - sun glare angle becomes smaller",
        function () {
            var newProperty = {
                haze: {
                    hazeGlareAngle: 5
                }
            };
            Entities.editEntity(sky, newProperty);
        }
    );

    autoTester.addStepSnapshot("Clean up",
        function () {
            Entities.deleteEntity(terrain);
            Entities.deleteEntity(sky);
        }
    );

    var result = autoTester.runTest(testType);
});
<|endoftext|>


<commit_before>
// Go to root using the optional operationType parameter
// @param operationType PageStackAction.Immediate or Animated, Animated is default)
function goToRoot(operationType)
{
    if (operationType !== PageStackAction.Immediate &&
            operationType !== PageStackAction.Animated)
        operationType = PageStackAction.Animated;

    // find the first page
    var firstPage = pageStack.previousPage();
    if (!firstPage)
        return;
    while (pageStack.previousPage(firstPage)) {
        firstPage = pageStack.previousPage(firstPage);
    }

    // pop to first page
    pageStack.pop(firstPage, operationType);
}

function goToFolder(folder, fromFolder)
{
    // if only moving up in hierarchy, then just pop
    if (fromFolder) {
        var fromFolderStart = fromFolder.substring(0, folder.length); // hack to do startsWith
        if (fromFolderStart === folder) {
            var from = fromFolder.split("/").length;
            var to = folder.split("/").length;
            for (var i = 0; i < from-to; ++i) {
                // animate the last pop
                var action = (i < from-to-1) ? PageStackAction.Immediate
                                             : PageStackAction.Animated;
                pageStack.pop(pageStack.previousPage(), action);
            }
            return;
        }
    }

    goToRoot(PageStackAction.Immediate);

    // go down the folders one by one
    var dirs = folder.split("/");
    var path = "";
    for (var i = 1; i < dirs.length; ++i) {
        path += "/"+dirs[i];
        // animate the last push
        var action = (i < dirs.length-1) ? PageStackAction.Immediate : PageStackAction.Animated;
        pageStack.push(Qt.resolvedUrl("DirectoryPage.qml"), { dir: path }, action);
    }
}

// Goes to Home folder - requires document path from StandardPaths to resolve Home
function goToHome(documentPath, fromFolder)
{
    var lastPos = documentPath.lastIndexOf("/");
    if (lastPos < 0)
        return;

    var homePath = documentPath.substring(0, lastPos);
    goToFolder(homePath, fromFolder);
}

function formatPathForTitle(path)
{
    if (path === "/")
        return "File Browser: /";

    var i = path.lastIndexOf("/");
    if (i < -1)
        return path;

    return path.substring(i+1)+"/";
}

function formatPathForCover(path)
{
    if (path === "/")
        return "";

    var i = path.lastIndexOf("/");
    if (i < -1)
        return path;

    return path.substring(i+1);
}
<commit_msg>Make goToHome() pop only once to get better animation<commit_after>
// Go to root using the optional operationType parameter
// @param operationType PageStackAction.Immediate or Animated, Animated is default)
function goToRoot(operationType)
{
    if (operationType !== PageStackAction.Immediate &&
            operationType !== PageStackAction.Animated)
        operationType = PageStackAction.Animated;

    // find the first page
    var firstPage = pageStack.previousPage();
    if (!firstPage)
        return;
    while (pageStack.previousPage(firstPage)) {
        firstPage = pageStack.previousPage(firstPage);
    }

    // pop to first page
    pageStack.pop(firstPage, operationType);
}

function startsWith(s1, s2)
{
    if (!s1 || !s2)
        return false;

    var start = s1.substring(0, s2.length);
    return start === s2;
}

function goToFolder(folder, fromFolder)
{
    if (folder === fromFolder)
        return;

    // if only moving up in hierarchy, then just pop
    if (fromFolder && startsWith(fromFolder, folder)) {

        // get previous pages until folder page
        var fromCount = fromFolder.split("/").length;
        var toCount = folder.split("/").length;
        var page = pageStack.currentPage;
        for (var i = 0; i < fromCount-toCount; ++i) {
            page = pageStack.previousPage(page);
        }

        pageStack.pop(page, PageStackAction.Animated);
        return;
    }

    goToRoot(PageStackAction.Immediate);

    // open the folders one by one
    var dirs = folder.split("/");
    var path = "";
    for (var i = 1; i < dirs.length; ++i) {
        path += "/"+dirs[i];
        // animate the last push
        var action = (i < dirs.length-1) ? PageStackAction.Immediate : PageStackAction.Animated;
        pageStack.push(Qt.resolvedUrl("DirectoryPage.qml"), { dir: path }, action);
    }
}

// Goes to Home folder - requires document path from StandardPaths to resolve Home
function goToHome(documentPath, fromFolder)
{
    var lastPos = documentPath.lastIndexOf("/");
    if (lastPos < 0)
        return;

    var homePath = documentPath.substring(0, lastPos);
    goToFolder(homePath, fromFolder);
}

function formatPathForTitle(path)
{
    if (path === "/")
        return "File Browser: /";

    var i = path.lastIndexOf("/");
    if (i < -1)
        return path;

    return path.substring(i+1)+"/";
}

function formatPathForCover(path)
{
    if (path === "/")
        return "";

    var i = path.lastIndexOf("/");
    if (i < -1)
        return path;

    return path.substring(i+1);
}
<|endoftext|>


<commit_before>$('document').ready(function() {
  var thermostat = new Thermostat;

  update(getColour());

  function checked() {
    return $('#power-saving').is(':checked');
  };

  function getColour() {
    if (thermostat.checkEfficiency() === 'good') return 'green';
    if (thermostat.checkEfficiency() === 'bad') return 'red';
    if (thermostat.checkEfficiency() === 'medium') return 'yellow';
  }

  function update(colour) {
    $('body').css('background-color', colour)
    $('#temp-display').text(thermostat.temp);
  };

  $('#up-button').click(function() {
    thermostat.setMode(checked());
    thermostat.increaseTemp();
    update(getColour());
  });

  $('#down-button').click(function() {
    thermostat.decreaseTemp();
    update(getColour());
  });

  $('#reset-button').click(function() {
    thermostat.resetTemp();
    update(getColour());
  )};

  $('#power-saving').click(function() {
    thermostat.resetTemp();
    update(getColour());
  });

});

<commit_msg>added gradient<commit_after>$('document').ready(function() {
  var thermostat = new Thermostat;

  update(getColour());

  function checked() {
    return $('#power-saving').is(':checked');
  };

  // function getColour() {
  //   if (thermostat.checkEfficiency() === 'good') return 'green';
  //   if (thermostat.checkEfficiency() === 'bad') return 'red';
  //   if (thermostat.checkEfficiency() === 'medium') return 'yellow';
  // }

  function getColour() {
    inc2 = 255;
    inc = 30 * (thermostat.temp -10);
    inc2 -= (30 * (thermostat.temp - 25));
    if (thermostat.temp < 25) return "rgba(" + inc + ", 255, 0, 1)";
    return "rgba(255, " + inc2 + ", 0, 1)";
  }

  function update(colour) {
    $('body').css('background-color', colour)
    $('#temp-display').text(thermostat.temp);
  };

  $('#up-button').click(function() {
    thermostat.setMode(checked());
    thermostat.increaseTemp();
    update(getColour());
  });

  $('#down-button').click(function() {
    thermostat.decreaseTemp();
    update(getColour());
  });

  $('#reset-button').click(function() {
    thermostat.resetTemp();
    update(getColour());
  )};

  $('#power-saving').click(function() {
    thermostat.resetTemp();
    update(getColour());
  });

});

<|endoftext|>


<commit_before>const browserify = require('browserify')
const path = require('path')
const fs = require('fs')
var renderify = require('electron-renderify')

const outFile = path.resolve(__dirname, '../dist/bundle.js')

/* avoid adding modules to this list, require() them from the correct place instead */
const legacyModules = [
  'dist/localization.build.js',
  'js/default.js',
  'js/windowsCaptionButtons.js',
  'js/util/database.js',
  'js/util/defaultKeyMap.js',
  'js/util/settings.js',
  'js/util/searchEngine.js',
  'js/menuBarVisibility.js',
  'js/tabState.js',
  'js/util/urlParser.js',
  'js/filteringRenderer.js',
  'js/webviews.js',
  'js/phishingWarning.js',
  'js/webviewMenu.js',
  'js/bookmarksHistory/bookmarksHistory.js',
  'js/api-wrapper.js',
  'js/searchbar/searchbar.js',
  'js/searchbar/searchbar-plugins.js',
  'js/searchbar/searchbar-autocomplete.js',
  'js/searchbar/placesPlugin.js',
  'js/searchbar/instantAnswerPlugin.js',
  'js/searchbar/openTabsPlugin.js',
  'js/searchbar/bangsPlugin.js',
  'js/searchbar/customBangs.js',
  'js/searchbar/searchSuggestionsPlugin.js',
  'js/searchbar/placeSuggestionsPlugin.js',
  'js/searchbar/hostsSuggestionsPlugin.js',
  'js/readerview.js',
  'js/navbar/tabActivity.js',
  'js/navbar/tabColor.js',
  'js/navbar/tabBar.js',
  'js/taskOverlay/taskOverlay.js',
  'js/taskOverlay/taskOverlayBuilder.js',
  'js/navbar/addTabButton.js',
  'js/navbar/goBackButton.js',
  'js/navbar/menuButton.js',
  'js/keybindings.js',
  'js/pdfViewer.js',
  'js/findinpage.js',
  'js/userscripts.js',
  'js/sessionRestore.js',
  'js/tabRestore.js',
  'js/focusMode.js',
  'js/util/theme.js',
  'js/webviewGestures.js'
]

const intermediateOutput = path.resolve(__dirname, '../dist/build.js')

/* concatenate legacy modules */
let output = ''
legacyModules.forEach(function (script) {
  output += fs.readFileSync(path.resolve(__dirname, '../', script)) + ';\n'
})

fs.writeFileSync(intermediateOutput, output, 'utf-8')

let instance = browserify(intermediateOutput, {
  ignoreMissing: true,
  node: true,
  detectGlobals: false
})

instance.transform(renderify)
let stream = fs.createWriteStream(outFile, {encoding: 'utf-8'})
instance.bundle().pipe(stream)
<commit_msg>cleanup<commit_after>const browserify = require('browserify')
const renderify = require('electron-renderify')
const path = require('path')
const fs = require('fs')

const intermediateOutput = path.resolve(__dirname, '../dist/build.js')
const outFile = path.resolve(__dirname, '../dist/bundle.js')

/* avoid adding modules to this list, require() them from the correct place instead */
const legacyModules = [
  'dist/localization.build.js',
  'js/default.js',
  'js/windowsCaptionButtons.js',
  'js/util/database.js',
  'js/util/defaultKeyMap.js',
  'js/util/settings.js',
  'js/util/searchEngine.js',
  'js/menuBarVisibility.js',
  'js/tabState.js',
  'js/util/urlParser.js',
  'js/filteringRenderer.js',
  'js/webviews.js',
  'js/phishingWarning.js',
  'js/webviewMenu.js',
  'js/bookmarksHistory/bookmarksHistory.js',
  'js/api-wrapper.js',
  'js/searchbar/searchbar.js',
  'js/searchbar/searchbar-plugins.js',
  'js/searchbar/searchbar-autocomplete.js',
  'js/searchbar/placesPlugin.js',
  'js/searchbar/instantAnswerPlugin.js',
  'js/searchbar/openTabsPlugin.js',
  'js/searchbar/bangsPlugin.js',
  'js/searchbar/customBangs.js',
  'js/searchbar/searchSuggestionsPlugin.js',
  'js/searchbar/placeSuggestionsPlugin.js',
  'js/searchbar/hostsSuggestionsPlugin.js',
  'js/readerview.js',
  'js/navbar/tabActivity.js',
  'js/navbar/tabColor.js',
  'js/navbar/tabBar.js',
  'js/taskOverlay/taskOverlay.js',
  'js/taskOverlay/taskOverlayBuilder.js',
  'js/navbar/addTabButton.js',
  'js/navbar/goBackButton.js',
  'js/navbar/menuButton.js',
  'js/keybindings.js',
  'js/pdfViewer.js',
  'js/findinpage.js',
  'js/userscripts.js',
  'js/sessionRestore.js',
  'js/tabRestore.js',
  'js/focusMode.js',
  'js/util/theme.js',
  'js/webviewGestures.js'
]

/* concatenate legacy modules */
let output = ''
legacyModules.forEach(function (script) {
  output += fs.readFileSync(path.resolve(__dirname, '../', script)) + ';\n'
})

fs.writeFileSync(intermediateOutput, output, 'utf-8')

let instance = browserify(intermediateOutput, {
  ignoreMissing: true,
  node: true,
  detectGlobals: false
})

instance.transform(renderify)
let stream = fs.createWriteStream(outFile, {encoding: 'utf-8'})
instance.bundle().pipe(stream)
<|endoftext|>


<commit_before>import angular from 'angular';
import findIndex from 'lodash.findindex';
import matches from 'lodash.matches';
import isEmpty from 'lodash.isempty';

import ngModule from '../module';
import {uuid} from '../../core/utils';
import './provider';

class AvDropdownController {

  constructor($element, $attrs, avDropdownConfig, $scope, $timeout, $parse) {

    this.av = {$element, $attrs, avDropdownConfig, $scope, $timeout, $parse};

    this.options = {};
    this.match = null;
    this.ngModel = null;
    this.locals = {};

  }

  init(context) {
    this.ngModel = context.ngModel;

    this.multiple = angular.isDefined(this.av.$attrs.multiple);

    this.options = angular.extend({}, this.av.avDropdownConfig.DEFAULTS, this.av.$scope.$eval(this.av.$attrs.options));

    if (this.isRemoteMultiple()) {
      this.options.multiple = angular.isDefined(this.av.$attrs.multiple);
    }

    if (this.options.query) {

      // Map AvSelectResource into Select2 options
      this.resource = this.options.query;

      // Function used to query results for the search term.
      this.options.query = options => {
        this.query(options);
      };
      // Function used to get the id from the choice object or a string representing the key under which the id is stored.
      this.options.id = this.resource.getId;
      this.options.initSelection = this.resource.initSelection;

    }

    // if element is type input, initSelection is required to use val
    if (!this.av.$element.is('select') && !this.options.initSelection) {
      this.options.initSelection = this.initSelection;
    }

  }

  isRemoteMultiple() {
    return angular.isDefined(this.av.$attrs.multiple) && this.av.$element.get(0).tagName.toLowerCase() === 'input';
  }

  initSelection(element, callback) {
    callback();
  }

  setRemoteViewValue(e) {

    let values = this.ngModel.$viewValue;

    if (!angular.isArray(values) || !angular.isObject(values)) {
      values = [];
    }

    if (e.added) {
      // Adding to collection
      values.push(e.added);
    } else {
      // Removing from collection
      const index = findIndex(values, value => matches(e.removed)(value));
      values.splice(index, 1);
    }

    this.ngModel.$setViewValue(values);

  }

  setViewValue(e) {
    this.ngModel.$setViewValue(e.added);
  }

  hashKey(obj, nextUidFn) {
    let key = obj && obj.$$hashKey;

    if (key) {
      if (typeof key === 'function') {
        key = obj.$$hashKey();
      }
      return key;
    }

    const objType = typeof obj;
    if (objType === 'function' || (objType === 'object' && obj !== null)) {
      key = obj.$$hashKey = objType + ':' + (nextUidFn || uuid)();
    } else {
      key = objType + ':' + obj;
    }

    return key;
  }

  getTrackByValueFn(value, locals) {

    if (this.trackBy) {
      return this.trackByFn(this.av.$scope, locals);
    }

    return this.hashKey(value);

  }

  getSelected(model) {

    const self = this;

    if (this.options.query) {
      return 0;
    }

    if (!this.collection) {
      // If we're not using ng-options, the model value is just the raw value of the option,
      // rather than an index, so return it as is.
      return model;
    }

    const optionValues = this.valuesFn(self.av.$scope) || [];
    const optionValuesKeys = this.getOptionValuesKeys(optionValues);

    const index = findIndex(this.collection, item => {
      return angular.equals(model, item) || angular.isObject(item) && this.valueName && angular.equals(model, item[this.valueName]);
    });

    const key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
    const value = optionValues[key];
    const locals = self.getLocals(value, key);
    const viewValue = self.viewValueFn(self.av.$scope, locals);
    const selectValue = self.getTrackByValueFn(viewValue, locals);

    return selectValue;

  }

  getMultiSelected(viewValues) {

    const self = this;

    const values = [];

    if (!viewValues) {
      return values;
    }

    if (this.av.$element.get(0).tagName.toLowerCase() !== 'input') {

      viewValues.forEach(viewValue => {

        const selected = self.getSelected(viewValue);

        values.push(selected);

      });

    }

    return values;

  }

  // Wrapper around the query function for Select2.  When the promise resolves
  // the callback
  query(options) {

    this.resource.onQuery(options).then(response => {

      // Callback function that should be called with the result object. The result object:
      //
      // result.results (object) - Array of result objects. The default renderers
      //    expect objects with id and text keys. The id property is required,
      //    even if custom renderers are used. The object may also contain a children
      //    key if hierarchical data is displayed. The object may also contain a disabled
      //    boolean property indicating whether this result can be selected.
      //
      // result.more (boolean) - true if more results are available for the current
      //    search term.
      //
      // results.context (object) - A user-defined object that should be made available
      //    as the context parameter to the query function on subsequent queries to load
      //    more result pages for the same search term. See the description of
      //    options.context parameter.
      options.callback({more: response.more, results: response.results});
    });
  }

  setValue() {

    const self = this;

    const viewValue = this.ngModel.$viewValue;

    let selected = null;
    if (this.multiple) {
      selected = this.getMultiSelected(viewValue);
    } else {
      selected = this.getSelected(viewValue);
    }

    // null === '' for Select2
    selected = (selected === null || selected === 'undefined') ? '' : selected;

    if (this.options.query) {
      this.av.$timeout(() => self.av.$element.select2('data', viewValue));
    } else {
      this.av.$timeout(() => self.av.$element.select2('val', selected));
    }
  }

  setValues() {

    let viewValue = this.ngModel.$viewValue;

    if (!angular.isArray(viewValue)) {
      viewValue = [];
    }

    if (!isEmpty(viewValue) && angular.isObject(viewValue[0])) {
      viewValue = this.getMultiSelected(viewValue);
    }

    this.av.$timeout(() => this.av.$element.select2('val', viewValue));

  }

  ngOptions() {

    this.match = this.av.$attrs.ngOptions.match(this.av.avDropdownConfig.NG_OPTIONS_REGEXP);
    if (!this.match) {
      throw new Error('Invalid ngOptions for avDropdown.  @See https://docs.angularjs.org/api/ng/directive/ngOptions');
    }

    // NG_OPTIONS_REGEXP regex will parse into arrays like below:

    // 1: value expression (valueFn)
    // 2: label expression (displayFn)
    // 3: group by expression (groupByFn)
    // 4: disable when expression (disableWhenFn)
    // 5: array item variable name
    // 6: object item key variable name
    // 7: object item value variable name
    // 8: collection expression
    // 9: track by expression

    // The variable name for the value of the item in the collection
    this.valueName = this.match[5] || this.match[7];

    // An expression that generates the viewValue for an option if there is no label expression
    this.valueFn = this.av.$parse(this.match[2] ? this.match[1] : this.valueName);

    // The variable name for the key of the item in the collection
    this.keyName = this.match[6];

    // An expression that generates the viewValue for an option if there is a label expression
    this.selectAs = / as /.test(this.match[0]) && this.match[1];

    // An expression that generates the viewValue for an option if there is a label expression
    // An expression that is used to track the id of each object in the options collection
    this.trackBy = this.match[9];
    this.selectAsFn = this.selectAs && this.av.$parse(this.selectAs);
    this.viewValueFn = this.selectAsFn || this.valueFn;
    this.trackByFn = this.trackBy && this.av.$parse(this.trackBy);

    this.displayFn = this.av.$parse(this.match[2] || this.match[1]);
    this.groupByFn = this.av.$parse(this.match[3] || '');
    this.disableWhenFn = this.av.$parse(this.match[4] || '');
    this.valuesFn = this.av.$parse(this.match[8]);
    this.collection = this.valuesFn(this.av.$scope);

    this.av.$scope.$watchCollection(this.collection, (newVal, oldVal) => {

      if (angular.equals(newVal, oldVal)) {
        return;
      }

      self.setValue();

    }, true);

  }

  getLocals(value, key) {

    const locals = {};

    if (this.keyName) {
      locals[this.keyName] = key;
      locals[this.valueName] = value;
    } else {
      locals[this.valueName] = value;
    }

    return locals;

  }

  getOptionValuesKeys(optionValues) {

    let optionValuesKeys;

    if (!this.keyName && Array.isArray(optionValues)) {
      optionValuesKeys = optionValues;
    } else {
      // if object, extract keys, in enumeration order, unsorted
      optionValuesKeys = [];
      for (const itemKey in optionValues) {
        if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
          optionValuesKeys.push(itemKey);
        }
      }
    }
    return optionValuesKeys;
  }

}

ngModule.controller('AvDropdownController', AvDropdownController);

export default AvDropdownController;
<commit_msg>issue #286 found another issue with query option not representing model.<commit_after>import angular from 'angular';
import findIndex from 'lodash.findindex';
import matches from 'lodash.matches';
import isEmpty from 'lodash.isempty';

import ngModule from '../module';
import {uuid} from '../../core/utils';
import './provider';

class AvDropdownController {

  constructor($element, $attrs, avDropdownConfig, $scope, $timeout, $parse) {

    this.av = {$element, $attrs, avDropdownConfig, $scope, $timeout, $parse};

    this.options = {};
    this.match = null;
    this.ngModel = null;
    this.locals = {};

  }

  init(context) {
    this.ngModel = context.ngModel;

    this.multiple = angular.isDefined(this.av.$attrs.multiple);

    this.options = angular.extend({}, this.av.avDropdownConfig.DEFAULTS, this.av.$scope.$eval(this.av.$attrs.options));

    if (this.isRemoteMultiple()) {
      this.options.multiple = angular.isDefined(this.av.$attrs.multiple);
    }

    if (this.options.query) {

      // Map AvSelectResource into Select2 options
      this.resource = this.options.query;

      // Function used to query results for the search term.
      this.options.query = options => {
        this.query(options);
      };
      // Function used to get the id from the choice object or a string representing the key under which the id is stored.
      this.options.id = this.resource.getId;
      this.options.initSelection = this.resource.initSelection;

    }

    // if element is type input, initSelection is required to use val
    if (!this.av.$element.is('select') && !this.options.initSelection) {
      this.options.initSelection = this.initSelection;
    }

  }

  isRemoteMultiple() {
    return angular.isDefined(this.av.$attrs.multiple) && this.av.$element.get(0).tagName.toLowerCase() === 'input';
  }

  initSelection(element, callback) {
    callback();
  }

  setRemoteViewValue(e) {

    let values = this.ngModel.$viewValue;

    if (!angular.isArray(values) || !angular.isObject(values)) {
      values = [];
    }

    if (e.added) {
      // Adding to collection
      values.push(e.added);
    } else {
      // Removing from collection
      const index = findIndex(values, value => matches(e.removed)(value));
      values.splice(index, 1);
    }

    this.ngModel.$setViewValue(values);

  }

  setViewValue(e) {
    this.ngModel.$setViewValue(e.added);
  }

  hashKey(obj, nextUidFn) {
    let key = obj && obj.$$hashKey;

    if (key) {
      if (typeof key === 'function') {
        key = obj.$$hashKey();
      }
      return key;
    }

    const objType = typeof obj;
    if (objType === 'function' || (objType === 'object' && obj !== null)) {
      key = obj.$$hashKey = objType + ':' + (nextUidFn || uuid)();
    } else {
      key = objType + ':' + obj;
    }

    return key;
  }

  getTrackByValueFn(value, locals) {

    if (this.trackBy) {
      return this.trackByFn(this.av.$scope, locals);
    }

    return this.hashKey(value);

  }

  getSelected(model) {

    const self = this;

    if (this.options.query) {
      return 0;
    }

    if (!this.collection) {
      // If we're not using ng-options, the model value is just the raw value of the option,
      // rather than an index, so return it as is.
      return model;
    }

    const optionValues = this.valuesFn(self.av.$scope) || [];
    const optionValuesKeys = this.getOptionValuesKeys(optionValues);

    const index = findIndex(this.collection, item => {
      return angular.equals(model, item) || angular.isObject(item) && this.valueName && angular.equals(model, item[this.valueName]);
    });

    const key = (optionValues === optionValuesKeys) ? index : optionValuesKeys[index];
    const value = optionValues[key];
    const locals = self.getLocals(value, key);
    const viewValue = self.viewValueFn(self.av.$scope, locals);
    const selectValue = self.getTrackByValueFn(viewValue, locals);

    return selectValue;

  }

  getMultiSelected(viewValues) {

    const self = this;

    const values = [];

    if (!viewValues) {
      return values;
    }

    if (this.av.$element.get(0).tagName.toLowerCase() !== 'input') {

      viewValues.forEach(viewValue => {

        const selected = self.getSelected(viewValue);

        values.push(selected);

      });

    }

    return values;

  }

  // Wrapper around the query function for Select2.  When the promise resolves
  // the callback
  query(options) {

    this.resource.onQuery(options).then(response => {

      // Callback function that should be called with the result object. The result object:
      //
      // result.results (object) - Array of result objects. The default renderers
      //    expect objects with id and text keys. The id property is required,
      //    even if custom renderers are used. The object may also contain a children
      //    key if hierarchical data is displayed. The object may also contain a disabled
      //    boolean property indicating whether this result can be selected.
      //
      // result.more (boolean) - true if more results are available for the current
      //    search term.
      //
      // results.context (object) - A user-defined object that should be made available
      //    as the context parameter to the query function on subsequent queries to load
      //    more result pages for the same search term. See the description of
      //    options.context parameter.
      options.callback({more: response.more, results: response.results});
    });
  }

  setValue() {
    const self = this;
    if (this.options.query) {
      const modelValue = this.ngModel.$modelValue;
      this.av.$timeout(() => self.av.$element.select2('data', modelValue));
    } else {
      const viewValue = this.ngModel.$viewValue;

      let selected = null;
      if (this.multiple) {
        selected = this.getMultiSelected(viewValue);
      } else {
        selected = this.getSelected(viewValue);
      }

      // null === '' for Select2
      selected = (selected === null || selected === 'undefined') ? '' : selected;

      this.av.$timeout(() => self.av.$element.select2('val', selected));
    }
  }

  setValues() {
    if (this.options.query) {
      const modelValue = this.ngModel.$modelValue;
      this.av.$timeout(() => self.av.$element.select2('data', modelValue));
    } else {
      let viewValue = this.ngModel.$viewValue;

      if (!angular.isArray(viewValue)) {
        viewValue = [];
      }

      if (!isEmpty(viewValue) && angular.isObject(viewValue[0])) {
        viewValue = this.getMultiSelected(viewValue);
      }

      this.av.$timeout(() => this.av.$element.select2('val', viewValue));
    }
  }

  ngOptions() {

    this.match = this.av.$attrs.ngOptions.match(this.av.avDropdownConfig.NG_OPTIONS_REGEXP);
    if (!this.match) {
      throw new Error('Invalid ngOptions for avDropdown.  @See https://docs.angularjs.org/api/ng/directive/ngOptions');
    }

    // NG_OPTIONS_REGEXP regex will parse into arrays like below:

    // 1: value expression (valueFn)
    // 2: label expression (displayFn)
    // 3: group by expression (groupByFn)
    // 4: disable when expression (disableWhenFn)
    // 5: array item variable name
    // 6: object item key variable name
    // 7: object item value variable name
    // 8: collection expression
    // 9: track by expression

    // The variable name for the value of the item in the collection
    this.valueName = this.match[5] || this.match[7];

    // An expression that generates the viewValue for an option if there is no label expression
    this.valueFn = this.av.$parse(this.match[2] ? this.match[1] : this.valueName);

    // The variable name for the key of the item in the collection
    this.keyName = this.match[6];

    // An expression that generates the viewValue for an option if there is a label expression
    this.selectAs = / as /.test(this.match[0]) && this.match[1];

    // An expression that generates the viewValue for an option if there is a label expression
    // An expression that is used to track the id of each object in the options collection
    this.trackBy = this.match[9];
    this.selectAsFn = this.selectAs && this.av.$parse(this.selectAs);
    this.viewValueFn = this.selectAsFn || this.valueFn;
    this.trackByFn = this.trackBy && this.av.$parse(this.trackBy);

    this.displayFn = this.av.$parse(this.match[2] || this.match[1]);
    this.groupByFn = this.av.$parse(this.match[3] || '');
    this.disableWhenFn = this.av.$parse(this.match[4] || '');
    this.valuesFn = this.av.$parse(this.match[8]);
    this.collection = this.valuesFn(this.av.$scope);

    this.av.$scope.$watchCollection(this.collection, (newVal, oldVal) => {

      if (angular.equals(newVal, oldVal)) {
        return;
      }

      self.setValue();

    }, true);

  }

  getLocals(value, key) {

    const locals = {};

    if (this.keyName) {
      locals[this.keyName] = key;
      locals[this.valueName] = value;
    } else {
      locals[this.valueName] = value;
    }

    return locals;

  }

  getOptionValuesKeys(optionValues) {

    let optionValuesKeys;

    if (!this.keyName && Array.isArray(optionValues)) {
      optionValuesKeys = optionValues;
    } else {
      // if object, extract keys, in enumeration order, unsorted
      optionValuesKeys = [];
      for (const itemKey in optionValues) {
        if (optionValues.hasOwnProperty(itemKey) && itemKey.charAt(0) !== '$') {
          optionValuesKeys.push(itemKey);
        }
      }
    }
    return optionValuesKeys;
  }

}

ngModule.controller('AvDropdownController', AvDropdownController);

export default AvDropdownController;
<|endoftext|>