<commit_before>"use strict";

var markersLayer = L.markerClusterGroup({
        spiderfyOnMaxZoom: true,
        showCoverageOnHover: false,
        disableClusteringAtZoom: 12
    });
// Coordinates for Heatmap
var markerCoordinates = [];
var heatLayer = null;
var userLocation = null;
// Localisation initiation for datepickers
moment.locale('fi');

$(document).ready(function() {
    var params = {};
    var start_date = $("#datepicker-start").data("DateTimePicker").date().toISOString();
    params["start_date"] = start_date;
    var end_date = $("#datepicker-end").data("DateTimePicker").date().toISOString();
    params["end_date"] = end_date;
    params["status"] = "open";
    getData(params, true);
});

function clearMarkers() {
    // Coordinates for Heatmap
    if (markerCoordinates.length > 0) {
        markerCoordinates.length = 0;
    }

    if (heatLayer) {
        map.removeLayer(heatLayer);
    }

    markersLayer.clearLayers();
}

function getData(params, markersVisible, heatmapVisible, onSuccess) {
    $.getJSON("/api/v1/requests.json/?extensions=true", params, function (data) {

        clearMarkers();

        $.each(data, function (key, feedback) {

            // specify popup options
            var customOptions =
                {
                    'maxWidth': '300',
                    'className' : 'custom'
                }

            // Generate popup-window content
            var popupContent = "";

            popupContent += "

" +
                "

" +
                "

" +
                "

" +
                "";

            // Initiate marker of feedback
            var marker = L.marker([feedback.lat, feedback.long]).bindPopup(popupContent, customOptions).addTo(markersLayer);
            marker.feedback = feedback;
            markerCoordinates.push([feedback.lat, feedback.long]);
            // Assign colour to marker based on status
            if (feedback.status === "open") {
                marker.setIcon(feedback_icon_open);
            }
            else {
                marker.setIcon(feedback_icon_closed);
            }

            // On click, fill the popup with feedback details
            marker.on('click', function(e) {
                // Truncate feedback details so that they fit the popup window
                var title_len = 50;
                var title = e.target.feedback.extended_attributes.title;
                if (title.length > title_len) {
                    title = title.substring(0, title_len) + "...";
                }

                $('#feedback_title').text(title);
                $('.feedback_list_vote_badge').text(e.target.feedback.vote_counter);
                $('.feedback_list_vote_icon').attr("id", e.target.feedback.service_request_id);
                $('#feedback_service_name').text("Aihe: " + e.target.feedback.service_name);
                var datetime = moment(e.target.feedback.requested_datetime).fromNow();
                $('#feedback_requested_datetime').text("Lisätty: " + datetime);
                var desc_len = 135;
                var desc = e.target.feedback.description;
                if (desc.length > desc_len) {
                    desc = desc.substring(0, desc_len)  + "...";
                }
                $('#feedback_description').text(desc);
                var feedback_url = "/feedbacks/" + e.target.feedback.id;
                $('#feedback_details').text("Lisää");
                $('#feedback_details').attr("href", feedback_url);
                $('#feedback_info').css("visibility", "visible");
            });
        });
    }).always(function() {
        if (onSuccess) {
            onSuccess();
        }
    }).done(function() {
        if (markersVisible) {
            showMarkers(markersVisible);
        }

        if (heatmapVisible) {
            showHeatmap(heatmapVisible);
        }
    });
}

var center_icon = L.MakiMarkers.icon({icon: "circle", color: "#0072C6", size: "l"});
var new_feedback_icon = L.MakiMarkers.icon({icon: "circle", color: "#FFC61E", size: "l"});
var feedback_icon = L.MakiMarkers.icon({icon: "circle", color: "#0072C6", size: "m"});
var feedback_icon_open = L.MakiMarkers.icon({icon: "circle", color: "#D4251C", size: "m"});
var feedback_icon_closed = L.MakiMarkers.icon({icon: "circle", color: "#16A427", size: "m"});

var HelsinkiCoord = {lat: 60.17067, lng: 24.94152};
// Bounds from Helsinki's Servicemap code (https://github.com/City-of-Helsinki/servicemap/)
var bounds = L.bounds(L.point(-548576, 6291456), L.point(1548576, 8388608));

var crs = function() {
    var bounds, crsName, crsOpts, originNw, projDef;
    crsName = 'EPSG:3067';
    projDef = '+proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs';
    bounds = L.bounds(L.point(-548576, 6291456), L.point(1548576, 8388608));
    originNw = [bounds.min.x, bounds.max.y];
    crsOpts = {
        resolutions: [8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.25, 0.125],
        bounds: bounds,
        transformation: new L.Transformation(1, -originNw[0], -1, originNw[1])
    };
    return new L.Proj.CRS(crsName, projDef, crsOpts);
}

var map = L.map('map', {
    crs: crs(),
    zoomControl: false,
    maxZoom: 15
}).setView([HelsinkiCoord.lat, HelsinkiCoord.lng], 11);

// Automatically fetch user location and center
getUserLocation();

L.tileLayer("http://geoserver.hel.fi/mapproxy/wmts/osm-sm/etrs_tm35fin/{z}/{x}/{y}.png", {
    attribution: 'Map data © OpenStreetMap contributors, © CartoDB',
    maxZoom: 18,
    continuousWorld: true,
    tms: false
}).addTo(map);

map.addControl(L.control.zoom({position: 'topright'}));

var legend = L.control({position: "bottomright"});

legend.onAdd = function (map) {
    var div = L.DomUtil.create("div", "map-legend");
    div.innerHTML += "" + "Avoin palaute
";
    div.innerHTML += "" + "Suljettu palaute
";
    div.innerHTML += "" + "Sijaintisi
";
    div.innerHTML += "" + "Uusi palaute
";
    return div;
};

legend.addTo(map);

function getUserLocation(e) {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function (position) {
            var newLocation = L.latLng(position.coords.latitude, position.coords.longitude);
            if(userLocation) {
                userLocation.setLatLng(newLocation);
            }
            else {
                userLocation = new L.marker(newLocation, {icon: center_icon}).addTo(map);
            }
            map.panTo(newLocation);
        }.bind(this));
        /*,
        function(error) {
            if (error.code == error.PERMISSION_DENIED)
            {
                var newLocation = L.latLng(HelsinkiCoord.lat, HelsinkiCoord.lng);
                userLocation = new L.marker(newLocation, {icon: center_icon}).addTo(map);
                map.panTo(newLocation);
            }
        });*/
    }
    else {
        console.error("Geolocation is not supported by this browser.");
    }
}

function showMarkers(show) {
    if (show) {
        map.addLayer(markersLayer);
    }
    else
    {
        if (map.hasLayer(markersLayer)) {
            map.removeLayer(markersLayer);
        }
    }
}

function showHeatmap(show) {
    console.log("*-----------------------------------------------*");
    console.log("showHeatmap: " + show);

    if (heatLayer) {
        console.log("showHeatmap: removeLayer");
        map.removeLayer(heatLayer);
    }

    if (show) {
        console.log("showHeatmap: add heatLayer");
        console.log(markerCoordinates);
        heatLayer = L.heatLayer(markerCoordinates, {minOpacity: 0.4, maxZoom: 18}).addTo(map);
    }
}

function onToggleMenu() {
    $("#sidebar-wrapper").toggleClass("toggled");
    $("#toggleButtonContainer").toggleClass("toggled");
    $("#toggleButtonIcon").toggleClass("glyphicon-chevron-left glyphicon-chevron-right");
}<commit_msg>#127: fixed a calendar bug in getData function in map.js<commit_after>"use strict";

var markersLayer = L.markerClusterGroup({
        spiderfyOnMaxZoom: true,
        showCoverageOnHover: false,
        disableClusteringAtZoom: 12
    });
// Coordinates for Heatmap
var markerCoordinates = [];
var heatLayer = null;
var userLocation = null;
// Localisation initiation for datepickers
moment.locale('fi');

$(document).ready(function() {
    var params = {};
    var start_date = moment().subtract(12, 'months').toISOString();
    params["start_date"] = start_date;
    var end_date = moment().toISOString();
    params["end_date"] = end_date;
    params["status"] = "open";
    getData(params, true);
});

function clearMarkers() {
    // Coordinates for Heatmap
    if (markerCoordinates.length > 0) {
        markerCoordinates.length = 0;
    }

    if (heatLayer) {
        map.removeLayer(heatLayer);
    }

    markersLayer.clearLayers();
}

function getData(params, markersVisible, heatmapVisible, onSuccess) {
    $.getJSON("/api/v1/requests.json/?extensions=true", params, function (data) {

        clearMarkers();

        $.each(data, function (key, feedback) {

            // specify popup options
            var customOptions =
                {
                    'maxWidth': '300',
                    'className' : 'custom'
                }

            // Generate popup-window content
            var popupContent = "";

            popupContent += "

" +
                "

" +
                "

" +
                "

" +
                "";

            // Initiate marker of feedback
            var marker = L.marker([feedback.lat, feedback.long]).bindPopup(popupContent, customOptions).addTo(markersLayer);
            marker.feedback = feedback;
            markerCoordinates.push([feedback.lat, feedback.long]);
            // Assign colour to marker based on status
            if (feedback.status === "open") {
                marker.setIcon(feedback_icon_open);
            }
            else {
                marker.setIcon(feedback_icon_closed);
            }

            // On click, fill the popup with feedback details
            marker.on('click', function(e) {
                // Truncate feedback details so that they fit the popup window
                var title_len = 50;
                var title = e.target.feedback.extended_attributes.title;
                if (title.length > title_len) {
                    title = title.substring(0, title_len) + "...";
                }

                $('#feedback_title').text(title);
                $('.feedback_list_vote_badge').text(e.target.feedback.vote_counter);
                $('.feedback_list_vote_icon').attr("id", e.target.feedback.service_request_id);
                $('#feedback_service_name').text("Aihe: " + e.target.feedback.service_name);
                var datetime = moment(e.target.feedback.requested_datetime).fromNow();
                $('#feedback_requested_datetime').text("Lisätty: " + datetime);
                var desc_len = 135;
                var desc = e.target.feedback.description;
                if (desc.length > desc_len) {
                    desc = desc.substring(0, desc_len)  + "...";
                }
                $('#feedback_description').text(desc);
                var feedback_url = "/feedbacks/" + e.target.feedback.id;
                $('#feedback_details').text("Lisää");
                $('#feedback_details').attr("href", feedback_url);
                $('#feedback_info').css("visibility", "visible");
            });
        });
    }).always(function() {
        if (onSuccess) {
            onSuccess();
        }
    }).done(function() {
        if (markersVisible) {
            showMarkers(markersVisible);
        }

        if (heatmapVisible) {
            showHeatmap(heatmapVisible);
        }
    });
}

var center_icon = L.MakiMarkers.icon({icon: "circle", color: "#0072C6", size: "l"});
var new_feedback_icon = L.MakiMarkers.icon({icon: "circle", color: "#FFC61E", size: "l"});
var feedback_icon = L.MakiMarkers.icon({icon: "circle", color: "#0072C6", size: "m"});
var feedback_icon_open = L.MakiMarkers.icon({icon: "circle", color: "#D4251C", size: "m"});
var feedback_icon_closed = L.MakiMarkers.icon({icon: "circle", color: "#16A427", size: "m"});

var HelsinkiCoord = {lat: 60.17067, lng: 24.94152};
// Bounds from Helsinki's Servicemap code (https://github.com/City-of-Helsinki/servicemap/)
var bounds = L.bounds(L.point(-548576, 6291456), L.point(1548576, 8388608));

var crs = function() {
    var bounds, crsName, crsOpts, originNw, projDef;
    crsName = 'EPSG:3067';
    projDef = '+proj=utm +zone=35 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs';
    bounds = L.bounds(L.point(-548576, 6291456), L.point(1548576, 8388608));
    originNw = [bounds.min.x, bounds.max.y];
    crsOpts = {
        resolutions: [8192, 4096, 2048, 1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1, 0.5, 0.25, 0.125],
        bounds: bounds,
        transformation: new L.Transformation(1, -originNw[0], -1, originNw[1])
    };
    return new L.Proj.CRS(crsName, projDef, crsOpts);
}

var map = L.map('map', {
    crs: crs(),
    zoomControl: false,
    maxZoom: 15
}).setView([HelsinkiCoord.lat, HelsinkiCoord.lng], 11);

// Automatically fetch user location and center
getUserLocation();

L.tileLayer("http://geoserver.hel.fi/mapproxy/wmts/osm-sm/etrs_tm35fin/{z}/{x}/{y}.png", {
    attribution: 'Map data © OpenStreetMap contributors, © CartoDB',
    maxZoom: 18,
    continuousWorld: true,
    tms: false
}).addTo(map);

map.addControl(L.control.zoom({position: 'topright'}));

var legend = L.control({position: "bottomright"});

legend.onAdd = function (map) {
    var div = L.DomUtil.create("div", "map-legend");
    div.innerHTML += "" + "Avoin palaute
";
    div.innerHTML += "" + "Suljettu palaute
";
    div.innerHTML += "" + "Sijaintisi
";
    div.innerHTML += "" + "Uusi palaute
";
    return div;
};

legend.addTo(map);

function getUserLocation(e) {
    if (navigator.geolocation) {
        navigator.geolocation.getCurrentPosition(function (position) {
            var newLocation = L.latLng(position.coords.latitude, position.coords.longitude);
            if(userLocation) {
                userLocation.setLatLng(newLocation);
            }
            else {
                userLocation = new L.marker(newLocation, {icon: center_icon}).addTo(map);
            }
            map.panTo(newLocation);
        }.bind(this));
        /*,
        function(error) {
            if (error.code == error.PERMISSION_DENIED)
            {
                var newLocation = L.latLng(HelsinkiCoord.lat, HelsinkiCoord.lng);
                userLocation = new L.marker(newLocation, {icon: center_icon}).addTo(map);
                map.panTo(newLocation);
            }
        });*/
    }
    else {
        console.error("Geolocation is not supported by this browser.");
    }
}

function showMarkers(show) {
    if (show) {
        map.addLayer(markersLayer);
    }
    else
    {
        if (map.hasLayer(markersLayer)) {
            map.removeLayer(markersLayer);
        }
    }
}

function showHeatmap(show) {
    if (heatLayer) {
        map.removeLayer(heatLayer);
    }

    if (show) {
        heatLayer = L.heatLayer(markerCoordinates, {minOpacity: 0.4, maxZoom: 18}).addTo(map);
    }
}

function onToggleMenu() {
    $("#sidebar-wrapper").toggleClass("toggled");
    $("#toggleButtonContainer").toggleClass("toggled");
    $("#toggleButtonIcon").toggleClass("glyphicon-chevron-left glyphicon-chevron-right");
}<|endoftext|>


<commit_before>(function()
{
    'use strict';

    angular
        .module('lumx.dropdown')
        .directive('lxDropdown', lxDropdown)
        .directive('lxDropdownToggle', lxDropdownToggle)
        .directive('lxDropdownMenu', lxDropdownMenu)
        .directive('lxDropdownFilter', lxDropdownFilter);

    lxDropdown.$inject = ['$document'];

    function lxDropdown($document)
    {
        return {
            restrict: 'E',
            templateUrl: 'dropdown.html',
            scope:
            {
                escapeClose: '=?lxEscapeClose',
                overToggle: '=?lxOverToggle',
                position: '@?lxPosition',
                width: '@?lxWidth'
            },
            link: link,
            controller: LxDropdownController,
            controllerAs: 'lxDropdown',
            bindToController: true,
            transclude: true
        };

        function link(scope, element, attrs, ctrl)
        {
            var backwardOneWay = ['position', 'width'];
            var backwardTwoWay = ['escapeClose', 'overToggle'];

            angular.forEach(backwardOneWay, function(attribute)
            {
                if (angular.isDefined(attrs[attribute]))
                {
                    attrs.$observe(attribute, function(newValue)
                    {
                        scope.lxTextField[attribute] = newValue;
                    });
                }
            });

            angular.forEach(backwardTwoWay, function(attribute)
            {
                if (angular.isDefined(attrs[attribute]))
                {
                    scope.$watch(function()
                    {
                        return scope.$parent.$eval(attrs[attribute]);
                    }, function(newValue)
                    {
                        scope.lxTextField[attribute] = newValue;
                    });
                }
            });

            $document.bind('click', closeDropdownMenu);

            scope.$on('$destroy', function()
            {
                ctrl.closeDropdownMenu();
                $document.unbind('click', closeDropdownMenu);
            });

            function closeDropdownMenu()
            {
                scope.$apply(function()
                {
                    ctrl.closeDropdownMenu();
                });
            }
        }
    }

    LxDropdownController.$inject = ['$element', '$scope', '$timeout', '$window', 'LxEventSchedulerService'];

    function LxDropdownController($element, $scope, $timeout, $window, LxEventSchedulerService)
    {
        var lxDropdown = this;
        var dropdownMenu;
        var dropdownToggle;
        var idEventScheduler;

        lxDropdown.closeDropdownMenu = closeDropdownMenu;
        lxDropdown.registerDropdownMenu = registerDropdownMenu;
        lxDropdown.registerDropdownToggle = registerDropdownToggle;
        lxDropdown.toggle = toggle;

        lxDropdown.escapeClose = angular.isDefined(lxDropdown.escapeClose) ? lxDropdown.escapeClose : true;
        lxDropdown.isOpen = false;
        lxDropdown.overToggle = angular.isDefined(lxDropdown.overToggle) ? lxDropdown.overToggle : false;
        lxDropdown.position = angular.isDefined(lxDropdown.position) ? lxDropdown.position : 'left';

        ////////////

        function closeDropdownMenu()
        {
            dropdownMenu.velocity(
            {
                width: 0,
                height: 0,
            },
            {
                duration: 200,
                easing: 'easeOutQuint',
                complete: function()
                {
                    dropdownMenu.removeAttr('style');

                    $scope.$apply(function()
                    {
                        lxDropdown.isOpen = false;
                        $element.removeClass('dropdown--is-open');

                        if (lxDropdown.escapeClose)
                        {
                            LxEventSchedulerService.unregister(idEventScheduler);
                            idEventScheduler = undefined;
                        }
                    });
                }
            });
        }

        function openDropdownMenu()
        {
            lxDropdown.isOpen = true;
            $element.addClass('dropdown--is-open');

            if (lxDropdown.escapeClose)
            {
                idEventScheduler = LxEventSchedulerService.register('keyup', onKeyUp);
            }

            $timeout(function()
            {
                var enoughtHeight = true;
                var availableHeight = setDropdownMenuCss();
                var width = dropdownMenu.outerWidth();
                var height = dropdownMenu.outerHeight();

                if (availableHeight < height)
                {
                    enoughtHeight = false;
                    height = availableHeight;
                }

                dropdownMenu.css(
                {
                    width: 0,
                    height: 0,
                    opacity: 1
                });

                dropdownMenu.find('.dropdown-menu__content').css(
                {
                    width: width,
                    height: height
                });

                dropdownMenu.velocity(
                {
                    width: width
                },
                {
                    duration: 200,
                    easing: 'easeOutQuint',
                    queue: false
                });

                dropdownMenu.velocity(
                {
                    height: height
                },
                {
                    duration: 500,
                    easing: 'easeOutQuint',
                    queue: false,
                    complete: function()
                    {
                        if (enoughtHeight)
                        {
                            dropdownMenu.css(
                            {
                                height: 'auto'
                            });
                        }

                        if (angular.isUndefined(lxDropdown.width))
                        {
                            dropdownMenu.css(
                            {
                                width: 'auto'
                            });
                        }

                        dropdownMenu.find('.dropdown-menu__content').removeAttr('style');
                    }
                });
            });
        }

        function onKeyUp(_event)
        {
            if (_event.keyCode == 27)
            {
                closeDropdownMenu();
            }

            _event.stopPropagation();
        }

        function registerDropdownMenu(_dropdownMenu)
        {
            dropdownMenu = _dropdownMenu;
        }

        function registerDropdownToggle(_dropdownToggle)
        {
            dropdownToggle = _dropdownToggle;
        }

        function setDropdownMenuCss()
        {
            var windowHeight = $window.innerHeight;
            var dropdownToggleHeight = dropdownToggle.outerHeight();
            var dropdownToggleTop = dropdownToggle.offset().top - angular.element($window).scrollTop();
            var dropdownMenuTopAvailable;
            var dropdownMenuBottomAvailable;

            if (lxDropdown.overToggle)
            {
                dropdownMenuTopAvailable = dropdownToggleTop + dropdownToggleHeight;
                dropdownMenuBottomAvailable = windowHeight - dropdownToggleTop;
            }
            else
            {
                dropdownMenuTopAvailable = dropdownToggleTop;
                dropdownMenuBottomAvailable = windowHeight - (dropdownToggleTop + dropdownToggleHeight);
            }

            dropdownMenu.css(
            {
                right: lxDropdown.position === 'right' ? 0 : 'auto',
                left: lxDropdown.position === 'right' ? 'auto' : 0,
                width: angular.isDefined(lxDropdown.width) ? lxDropdown.width : 'auto'
            });

            if (dropdownMenuTopAvailable > dropdownMenuBottomAvailable)
            {
                dropdownMenu.css(
                {
                    bottom: lxDropdown.overToggle ? 0 : dropdownToggleHeight
                });

                return dropdownMenuTopAvailable;
            }
            else
            {
                if (lxDropdown.overToggle)
                {
                    dropdownMenu.css(
                    {
                        top: 0
                    });
                }

                return dropdownMenuBottomAvailable;
            }
        }

        function toggle()
        {
            if (!lxDropdown.isOpen)
            {
                openDropdownMenu();
            }
            else
            {
                closeDropdownMenu();
            }
        }
    }

    function lxDropdownToggle()
    {
        return {
            restrict: 'AE',
            templateUrl: 'dropdown.html',
            require: '^lxDropdown',
            scope: true,
            link: link,
            replace: true,
            transclude: true
        };

        function link(scope, element, attrs, ctrl)
        {
            ctrl.registerDropdownToggle(element);

            element.bind('click', function(_event)
            {
                _event.stopPropagation();

                angular.element('.dropdown').each(function(index, dropdownElem)
                {
                    if (angular.isDefined(angular.element(dropdownElem).scope().lxDropdown) && angular.element(dropdownElem).scope().lxDropdown.isOpen)
                    {
                        angular.element(dropdownElem).scope().lxDropdown.closeDropdownMenu();
                    }
                });

                scope.$apply(function()
                {
                    ctrl.toggle();
                });
            });
        }
    }

    function lxDropdownMenu()
    {
        return {
            restrict: 'E',
            templateUrl: 'dropdown-menu.html',
            require: ['lxDropdownMenu', '^lxDropdown'],
            scope: true,
            link: link,
            controller: LxDropdownMenuController,
            controllerAs: 'lxDropdownMenu',
            bindToController: true,
            replace: true,
            transclude: true
        };

        function link(scope, element, attrs, ctrls)
        {
            ctrls[1].registerDropdownMenu(element);
            ctrls[0].setParentController(ctrls[1]);
        }
    }

    function LxDropdownMenuController()
    {
        var lxDropdownMenu = this;

        lxDropdownMenu.setParentController = setParentController;

        ////////////

        function setParentController(_parentCtrl)
        {
            lxDropdownMenu.parentCtrl = _parentCtrl;
        }
    }

    lxDropdownFilter.$inject = ['$timeout'];

    function lxDropdownFilter($timeout)
    {
        return {
            restrict: 'A',
            link: link
        };

        function link(scope, element)
        {
            element.bind('click', function(_event)
            {
                _event.stopPropagation();
            });

            $timeout(function()
            {
                element.find('input').focus();
            }, 200);
        }
    }
})();<commit_msg>fix dropdown: fix backward compatibility<commit_after>(function()
{
    'use strict';

    angular
        .module('lumx.dropdown')
        .directive('lxDropdown', lxDropdown)
        .directive('lxDropdownToggle', lxDropdownToggle)
        .directive('lxDropdownMenu', lxDropdownMenu)
        .directive('lxDropdownFilter', lxDropdownFilter);

    lxDropdown.$inject = ['$document'];

    function lxDropdown($document)
    {
        return {
            restrict: 'E',
            templateUrl: 'dropdown.html',
            scope:
            {
                escapeClose: '=?lxEscapeClose',
                overToggle: '=?lxOverToggle',
                position: '@?lxPosition',
                width: '@?lxWidth'
            },
            link: link,
            controller: LxDropdownController,
            controllerAs: 'lxDropdown',
            bindToController: true,
            transclude: true
        };

        function link(scope, element, attrs, ctrl)
        {
            var backwardOneWay = ['position', 'width'];
            var backwardTwoWay = ['escapeClose', 'overToggle'];

            angular.forEach(backwardOneWay, function(attribute)
            {
                if (angular.isDefined(attrs[attribute]))
                {
                    attrs.$observe(attribute, function(newValue)
                    {
                        scope.lxDropdown[attribute] = newValue;
                    });
                }
            });

            angular.forEach(backwardTwoWay, function(attribute)
            {
                if (angular.isDefined(attrs[attribute]))
                {
                    scope.$watch(function()
                    {
                        return scope.$parent.$eval(attrs[attribute]);
                    }, function(newValue)
                    {
                        scope.lxDropdown[attribute] = newValue;
                    });
                }
            });

            $document.bind('click', closeDropdownMenu);

            scope.$on('$destroy', function()
            {
                ctrl.closeDropdownMenu();
                $document.unbind('click', closeDropdownMenu);
            });

            function closeDropdownMenu()
            {
                scope.$apply(function()
                {
                    ctrl.closeDropdownMenu();
                });
            }
        }
    }

    LxDropdownController.$inject = ['$element', '$scope', '$timeout', '$window', 'LxEventSchedulerService'];

    function LxDropdownController($element, $scope, $timeout, $window, LxEventSchedulerService)
    {
        var lxDropdown = this;
        var dropdownMenu;
        var dropdownToggle;
        var idEventScheduler;

        lxDropdown.closeDropdownMenu = closeDropdownMenu;
        lxDropdown.registerDropdownMenu = registerDropdownMenu;
        lxDropdown.registerDropdownToggle = registerDropdownToggle;
        lxDropdown.toggle = toggle;

        lxDropdown.escapeClose = angular.isDefined(lxDropdown.escapeClose) ? lxDropdown.escapeClose : true;
        lxDropdown.isOpen = false;
        lxDropdown.overToggle = angular.isDefined(lxDropdown.overToggle) ? lxDropdown.overToggle : false;
        lxDropdown.position = angular.isDefined(lxDropdown.position) ? lxDropdown.position : 'left';

        ////////////

        function closeDropdownMenu()
        {
            dropdownMenu.velocity(
            {
                width: 0,
                height: 0,
            },
            {
                duration: 200,
                easing: 'easeOutQuint',
                complete: function()
                {
                    dropdownMenu.removeAttr('style');

                    $scope.$apply(function()
                    {
                        lxDropdown.isOpen = false;
                        $element.removeClass('dropdown--is-open');

                        if (lxDropdown.escapeClose)
                        {
                            LxEventSchedulerService.unregister(idEventScheduler);
                            idEventScheduler = undefined;
                        }
                    });
                }
            });
        }

        function openDropdownMenu()
        {
            lxDropdown.isOpen = true;
            $element.addClass('dropdown--is-open');

            if (lxDropdown.escapeClose)
            {
                idEventScheduler = LxEventSchedulerService.register('keyup', onKeyUp);
            }

            $timeout(function()
            {
                var enoughtHeight = true;
                var availableHeight = setDropdownMenuCss();
                var width = dropdownMenu.outerWidth();
                var height = dropdownMenu.outerHeight();

                if (availableHeight < height)
                {
                    enoughtHeight = false;
                    height = availableHeight;
                }

                dropdownMenu.css(
                {
                    width: 0,
                    height: 0,
                    opacity: 1
                });

                dropdownMenu.find('.dropdown-menu__content').css(
                {
                    width: width,
                    height: height
                });

                dropdownMenu.velocity(
                {
                    width: width
                },
                {
                    duration: 200,
                    easing: 'easeOutQuint',
                    queue: false
                });

                dropdownMenu.velocity(
                {
                    height: height
                },
                {
                    duration: 500,
                    easing: 'easeOutQuint',
                    queue: false,
                    complete: function()
                    {
                        if (enoughtHeight)
                        {
                            dropdownMenu.css(
                            {
                                height: 'auto'
                            });
                        }

                        if (angular.isUndefined(lxDropdown.width))
                        {
                            dropdownMenu.css(
                            {
                                width: 'auto'
                            });
                        }

                        dropdownMenu.find('.dropdown-menu__content').removeAttr('style');
                    }
                });
            });
        }

        function onKeyUp(_event)
        {
            if (_event.keyCode == 27)
            {
                closeDropdownMenu();
            }

            _event.stopPropagation();
        }

        function registerDropdownMenu(_dropdownMenu)
        {
            dropdownMenu = _dropdownMenu;
        }

        function registerDropdownToggle(_dropdownToggle)
        {
            dropdownToggle = _dropdownToggle;
        }

        function setDropdownMenuCss()
        {
            var windowHeight = $window.innerHeight;
            var dropdownToggleHeight = dropdownToggle.outerHeight();
            var dropdownToggleTop = dropdownToggle.offset().top - angular.element($window).scrollTop();
            var dropdownMenuTopAvailable;
            var dropdownMenuBottomAvailable;

            if (lxDropdown.overToggle)
            {
                dropdownMenuTopAvailable = dropdownToggleTop + dropdownToggleHeight;
                dropdownMenuBottomAvailable = windowHeight - dropdownToggleTop;
            }
            else
            {
                dropdownMenuTopAvailable = dropdownToggleTop;
                dropdownMenuBottomAvailable = windowHeight - (dropdownToggleTop + dropdownToggleHeight);
            }

            dropdownMenu.css(
            {
                right: lxDropdown.position === 'right' ? 0 : 'auto',
                left: lxDropdown.position === 'right' ? 'auto' : 0,
                width: angular.isDefined(lxDropdown.width) ? lxDropdown.width : 'auto'
            });

            if (dropdownMenuTopAvailable > dropdownMenuBottomAvailable)
            {
                dropdownMenu.css(
                {
                    bottom: lxDropdown.overToggle ? 0 : dropdownToggleHeight
                });

                return dropdownMenuTopAvailable;
            }
            else
            {
                if (lxDropdown.overToggle)
                {
                    dropdownMenu.css(
                    {
                        top: 0
                    });
                }

                return dropdownMenuBottomAvailable;
            }
        }

        function toggle()
        {
            if (!lxDropdown.isOpen)
            {
                openDropdownMenu();
            }
            else
            {
                closeDropdownMenu();
            }
        }
    }

    function lxDropdownToggle()
    {
        return {
            restrict: 'AE',
            templateUrl: 'dropdown.html',
            require: '^lxDropdown',
            scope: true,
            link: link,
            replace: true,
            transclude: true
        };

        function link(scope, element, attrs, ctrl)
        {
            ctrl.registerDropdownToggle(element);

            element.bind('click', function(_event)
            {
                _event.stopPropagation();

                angular.element('.dropdown').each(function(index, dropdownElem)
                {
                    if (angular.isDefined(angular.element(dropdownElem).scope().lxDropdown) && angular.element(dropdownElem).scope().lxDropdown.isOpen)
                    {
                        angular.element(dropdownElem).scope().lxDropdown.closeDropdownMenu();
                    }
                });

                scope.$apply(function()
                {
                    ctrl.toggle();
                });
            });
        }
    }

    function lxDropdownMenu()
    {
        return {
            restrict: 'E',
            templateUrl: 'dropdown-menu.html',
            require: ['lxDropdownMenu', '^lxDropdown'],
            scope: true,
            link: link,
            controller: LxDropdownMenuController,
            controllerAs: 'lxDropdownMenu',
            bindToController: true,
            replace: true,
            transclude: true
        };

        function link(scope, element, attrs, ctrls)
        {
            ctrls[1].registerDropdownMenu(element);
            ctrls[0].setParentController(ctrls[1]);
        }
    }

    function LxDropdownMenuController()
    {
        var lxDropdownMenu = this;

        lxDropdownMenu.setParentController = setParentController;

        ////////////

        function setParentController(_parentCtrl)
        {
            lxDropdownMenu.parentCtrl = _parentCtrl;
        }
    }

    lxDropdownFilter.$inject = ['$timeout'];

    function lxDropdownFilter($timeout)
    {
        return {
            restrict: 'A',
            link: link
        };

        function link(scope, element)
        {
            element.bind('click', function(_event)
            {
                _event.stopPropagation();
            });

            $timeout(function()
            {
                element.find('input').focus();
            }, 200);
        }
    }
})();<|endoftext|>


<commit_before>/* ************************************************** *
 * ******************** REMIE
 * ************************************************** */
let REMIE = require('../libs/index.js'),
  remie = new (require('../libs/index.js'))(),
  i18next = require('i18next'),
  EventEmitter = require('events');
  //EventListener = require('events').EventListener;
  //console.log(EventListener)
var inherits = require('util').inherits;
err = 'server.400.notFound'
err.code = 'server.400.notFound'
err.message = 'Message in an error!'
//err.stack = 'errors stacked on errors'
//err.internalMessage = 'internal message'

let options = {}
options.internalMessage = 'internal Message'
options.level = 'warning'
//options.code = 'server.400.notFound'

let locale = 'server.400.notFound'

remie.i18next = i18next

inherits(REMIE, EventEmitter.EventEmitter)

//let error = remie.create(err, options, locale)
let error = remie.create(err, options)

//let error = remie.create('Something went wrong', options, locale)
//let error = remie.create("Something went wrong", {})
// |||
// vvv does not work if richErrorObject.error is undefined, null. must be object with property "message"
//let error2 = remie.toObject(error)
//let error2 = remie.create("Something else went wrong", {});
//let error3 = remie.create("Oh crap, what now", {});
console.log(error)

//module.exports = error

/* expected output:
RichError {
  error:
   { Error: server.500.generic
       at RichError.buildFromLocale (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/RichError.js:105:29)
       at RichError.build (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/RichError.js:76:27)
       at new RichError (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/RichError.js:56:10)
       at REMIE.create (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/index.js:38:12)
       at Object.<anonymous> (/Users/NicholasLivio/Documents/R2/REMIE-livio/examples/better-example.js:29:19)
       at Module._compile (module.js:541:32)
       at Object.Module._extensions..js (module.js:550:10)
       at Module.load (module.js:458:32)
       at tryModuleLoad (module.js:417:12)
       at Function.Module._load (module.js:409:3) code: 'server.500.generic' },
  internalOnly: false,
  internalMessage: 'internal Message',
  level: 'warning',
  messageData: undefined,
  options: { internalMessage: 'internal Message', level: 'warning' },
  referenceData: undefined,
  statusCode: 500 }
*/<commit_msg>added example of copy method<commit_after>/* ************************************************** *
 * ******************** REMIE
 * ************************************************** */
let REMIE = require('../libs/index.js'),
//  remie = new (require('../libs/index.js'))(),
  i18next = require('i18next'),
  EventEmitter = require('events');
  //EventListener = require('events').EventListener;
  //console.log(EventListener)
var inherits = require('util').inherits;
err = 'server.400.notFound'
//err.code = 'server.400.notFound'
//err.message = 'Message in an error!'
//err.stack = 'errors stacked on errors'
//err.internalMessage = 'internal message'

let options = {}
options.internalMessage = 'internal Message'
options.level = 'warning'
//options.code = 'server.400.notFound'

let locale = 'server.400.notFound'
let remie = new REMIE(err, options, locale) //rich Error
//let remie = REMIE.methodCaller()
//remie.i18next = i18next

inherits(REMIE, EventEmitter.EventEmitter)
//let error = remie.methodCaller() // remie
let other = remie.create(err, options, locale)
let copy = remie.copy(other)
console.log(other)
console.log(copy)
//if (error == other) {
//  console.log('wooh')
//}
//let error = remie.create(err, options, locale)
//let error = remie.create(err, options)

//let error = remie.create('Something went wrong', options, locale)
//let error = remie.create("Something went wrong", {})
// |||
// vvv does not work if richErrorObject.error is undefined, null. must be object with property "message"
//let error2 = remie.toObject(error)
//let error2 = remie.create("Something else went wrong", {});
//let error3 = remie.create("Oh crap, what now", {});
//console.log(error)

//module.exports = error

/* expected output:
RichError {
  error:
   { Error: server.500.generic
       at RichError.buildFromLocale (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/RichError.js:105:29)
       at RichError.build (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/RichError.js:76:27)
       at new RichError (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/RichError.js:56:10)
       at REMIE.create (/Users/NicholasLivio/Documents/R2/REMIE-livio/libs/index.js:38:12)
       at Object.<anonymous> (/Users/NicholasLivio/Documents/R2/REMIE-livio/examples/better-example.js:29:19)
       at Module._compile (module.js:541:32)
       at Object.Module._extensions..js (module.js:550:10)
       at Module.load (module.js:458:32)
       at tryModuleLoad (module.js:417:12)
       at Function.Module._load (module.js:409:3) code: 'server.500.generic' },
  internalOnly: false,
  internalMessage: 'internal Message',
  level: 'warning',
  messageData: undefined,
  options: { internalMessage: 'internal Message', level: 'warning' },
  referenceData: undefined,
  statusCode: 500 }
*/<|endoftext|>


<commit_before>import { combineReducers } from 'redux'
import { getTracker } from 'cozy-ui/react/helpers/tracker'
import { SHARINGS_DOCTYPE } from '../collections/SharingsCollection'
import { APPS_DOCTYPE } from '../collections/AppsCollection'
import {
  getDocument,
  createDocument,
  fetchCollection,
  makeFetchCollection,
  getCollection
} from '../reducer'

export const FETCH_SHARINGS = 'FETCH_SHARINGS'
const RECEIVE_SHARINGS_DATA = 'RECEIVE_SHARINGS_DATA'
const RECEIVE_FETCH_SHARINGS_ERROR = 'RECEIVE_FETCH_SHARINGS_ERROR'
const CREATE_SHARING = 'CREATE_SHARING'
const RECEIVE_NEW_SHARING = 'RECEIVE_NEW_SHARING'
const CREATE_SHARING_LINK = 'CREATE_SHARING_LINK'
const RECEIVE_NEW_SHARING_LINK = 'RECEIVE_NEW_SHARING_LINK'
const REVOKE_SHARING_LINK = 'REVOKE_SHARING_LINK'
const RECEIVE_SHARING_LINK_REVOKE = 'RECEIVE_SHARING_LINK_REVOKE'
const REVOKE_SHARING = 'REVOKE_SHARING'
const RECEIVE_SHARING_REVOKE = 'RECEIVE_SHARING_REVOKE'
const RECEIVE_ERROR = 'RECEIVE_ERROR'

const removeRecipient = (recipients, recipientId) => {
  const idx = recipients.findIndex(r => r.recipient.id === recipientId)
  return [...recipients.slice(0, idx), ...recipients.slice(idx + 1)]
}

const documents = (state = [], action) => {
  switch (action.type) {
    case RECEIVE_SHARINGS_DATA:
      return action.response.sharings
    case RECEIVE_NEW_SHARING:
      return [...state, action.response]
    case RECEIVE_SHARING_REVOKE:
      const idx = state.findIndex(
        s => s.attributes.sharing_id === action.sharingId
      )
      if (idx === -1) return state
      const sharing = state[idx]
      const loneRecipient =
        sharing.attributes.recipients === undefined || // for recipient-side revocation
        sharing.attributes.recipients.length === 1
      const newState = loneRecipient
        ? { ...sharing, attributes: { ...sharing.attributes, revoked: true } }
        : {
            ...sharing,
            attributes: {
              ...sharing.attributes,
              recipients: removeRecipient(
                sharing.attributes.recipients,
                action.recipientId
              )
            }
          }
      return [...state.slice(0, idx), newState, ...state.slice(idx + 1)]
    default:
      return state
  }
}

const doctypePermsetInitialState = {
  fetchStatus: 'loading',
  byMe: [],
  byLink: [],
  withMe: []
}

const permissions = (state = {}, action) => {
  let idx
  switch (action.type) {
    case FETCH_SHARINGS:
      if (state[action.doctype]) {
        return {
          ...state,
          [action.doctype]: { ...state[action.doctype], fetchStatus: 'loading' }
        }
      }
      return { ...state, [action.doctype]: doctypePermsetInitialState }
    case RECEIVE_SHARINGS_DATA:
      return {
        ...state,
        [action.doctype]: {
          fetchStatus: 'loaded',
          ...action.response.permissions
        }
      }
    case RECEIVE_FETCH_SHARINGS_ERROR:
      return { ...state, [action.doctype]: { fetchStatus: 'error' } }
    case RECEIVE_NEW_SHARING:
      return {
        ...state,
        [action.doctype]: {
          ...state[action.doctype],
          byMe: [
            ...state[action.doctype].byMe,
            {
              attributes: {
                permissions: {
                  rule0: { type: action.doctype, values: [action.id] }
                },
                source_id: action.response.attributes.sharing_id,
                type: 'io.cozy.sharings'
              }
            }
          ]
        }
      }
    case RECEIVE_NEW_SHARING_LINK:
      return {
        ...state,
        [action.doctype]: {
          ...state[action.doctype],
          byLink: [...state[action.doctype].byLink, action.response]
        }
      }
    case REVOKE_SHARING_LINK:
      idx = state[action.doctype].byLink.findIndex(
        p => action.permission._id === p._id
      )
      if (idx === -1) return state
      return {
        ...state,
        [action.doctype]: {
          ...state[action.doctype],
          byLink: [
            ...state[action.doctype].byLink.slice(0, idx),
            ...state[action.doctype].byLink.slice(idx + 1)
          ]
        }
      }
    default:
      return state
  }
}

export default combineReducers({ documents, permissions })

// actions
export const fetchSharings = (doctype, id = null, options = {}) => ({
  types: [FETCH_SHARINGS, RECEIVE_SHARINGS_DATA, RECEIVE_FETCH_SHARINGS_ERROR],
  doctype,
  id,
  options,
  promise: client =>
    client.getCollection(SHARINGS_DOCTYPE).findByDoctype(doctype),
  dependencies: [fetchApps()]
})

export const share = (document, recipients, sharingType, sharingDesc) => async (
  dispatch,
  getState
) => {
  const recipientIds = await Promise.all(
    recipients.map(
      recipient =>
        recipient.id ||
        dispatch(createContact(recipient)).then(c => c.data[0].id)
    )
  )
  trackSharingByEmail(document)
  return dispatch(
    createSharing(document, recipientIds, sharingType, sharingDesc)
  )
}

export const unshare = (document, recipient) => async (dispatch, getState) => {
  const sharing = getSharingForRecipient(getState(), document, recipient)
  const loneRecipient = sharing.attributes.recipients.length === 1
  return dispatch({
    types: [REVOKE_SHARING, RECEIVE_SHARING_REVOKE, RECEIVE_ERROR],
    doctype: document._type,
    id: document._id,
    sharingId: sharing.attributes.sharing_id,
    recipientId: recipient._id,
    promise: client =>
      loneRecipient
        ? client
            .getCollection(SHARINGS_DOCTYPE)
            .revoke(sharing.attributes.sharing_id)
        : client
            .getCollection(SHARINGS_DOCTYPE)
            .revokeForClient(
              sharing.attributes.sharing_id,
              sharing.attributes.recipients.find(
                r => r.recipient.id === recipient._id
              ).Client.client_id
            )
  })
}

export const leave = document => async (dispatch, getState) => {
  const sharings = getDocumentActiveSharings(
    getState(),
    document._type,
    document._id
  )
  const sharing = sharings.find(s => s.attributes.owner === false)
  return dispatch({
    types: [REVOKE_SHARING, RECEIVE_SHARING_REVOKE, RECEIVE_ERROR],
    doctype: document._type,
    id: document._id,
    sharingId: sharing.attributes.sharing_id,
    promise: client =>
      client
        .getCollection(SHARINGS_DOCTYPE)
        .revoke(sharing.attributes.sharing_id)
  })
}

export const shareByLink = document => {
  trackSharingByLink(document)
  return createSharingLink(document)
}

export const revokeLink = document => async (dispatch, getState) => {
  const perm = getSharingLinkPermission(
    getState(),
    document._type,
    document._id
  )
  return dispatch({
    types: [REVOKE_SHARING_LINK, RECEIVE_SHARING_LINK_REVOKE, RECEIVE_ERROR],
    doctype: document._type,
    id: document._id,
    permission: perm,
    promise: client => client.getCollection(SHARINGS_DOCTYPE).revokeLink(perm)
  })
}

const createSharing = (
  document,
  contactIds,
  sharingType = 'master-slave',
  description = ''
) => ({
  types: [CREATE_SHARING, RECEIVE_NEW_SHARING, RECEIVE_ERROR],
  doctype: document._type,
  id: document._id,
  promise: client =>
    client
      .getCollection(SHARINGS_DOCTYPE)
      .create(getPermissionsFor(document), contactIds, sharingType, description)
})

const createSharingLink = document => ({
  types: [CREATE_SHARING_LINK, RECEIVE_NEW_SHARING_LINK, RECEIVE_ERROR],
  doctype: document._type,
  id: document._id,
  promise: client =>
    client
      .getCollection(SHARINGS_DOCTYPE)
      .createLink(getPermissionsFor(document, true))
})

export const fetchApps = () =>
  makeFetchCollection('apps', APPS_DOCTYPE, client =>
    client.getCollection(APPS_DOCTYPE).all()
  )

const getApps = state => getCollection(state, 'apps').data

const getAppUrl = (state, appName) => {
  const apps = getApps(state)
  const app = apps.find(
    a => a.attributes.slug === appName && a.attributes.state === 'ready'
  )
  if (!app) {
    throw new Error(`Sharing link: app ${appName} not installed`)
  }
  return app.links.related
}

// TODO: this is a poor man's migration in order to normalize contacts
// and should be removed after a few weeks in prod
// Note for future-self: If you have no idea of what it means, please, erase this code.
// Note for time-travelers: please travel a little more back in time in order to advise us
// to get contacts right the first time
export const fetchContacts = () => {
  const action = fetchCollection('contacts', 'io.cozy.contacts')
  action.promise = async client => {
    const response = await client.fetchDocuments('contacts', 'io.cozy.contacts')
    const data = await Promise.all(
      response.data.map(contact => {
        return typeof contact.email !== 'string'
          ? contact
          : client
              .updateDocument({
                ...contact,
                email: [
                  {
                    address: contact.email,
                    primary: true
                  }
                ]
              })
              .then(resp => resp.data[0])
      })
    )

    return { ...response, data }
  }
  return action
}

const createContact = ({ email }) =>
  createDocument('io.cozy.contacts', {
    email: [{ address: email, primary: true }]
  })

const getPermissionsFor = (document, publicLink = false) => {
  const { _id, _type } = document
  const verbs = publicLink ? ['GET'] : ['ALL']
  // TODO: this works for albums, but it needs to be generalized and integrated
  // with cozy-client ; some sort of doctype "schema" will be needed here
  return isFile(document)
    ? {
        files: {
          type: 'io.cozy.files',
          verbs,
          values: [_id]
        }
      }
    : {
        collection: {
          type: _type,
          verbs,
          values: [_id]
        },
        files: {
          type: 'io.cozy.files',
          verbs,
          values: [`${_type}/${_id}`],
          selector: 'referenced_by'
        }
      }
}

// selectors
const getSharing = (state, id) =>
  state.cozy.sharings.documents.find(
    s => s.attributes && s.attributes.sharing_id === id
  )
const getContact = (state, id) => getDocument(state, 'io.cozy.contacts', id)
const getDoctypePermissions = (state, doctype) => {
  if (state.cozy.sharings.permissions[doctype]) {
    return state.cozy.sharings.permissions[doctype]
  }
  return doctypePermsetInitialState
}

const getSharingLink = (state, doctype, id) => {
  const perm = getSharingLinkPermission(state, doctype, id)
  return perm
    ? buildSharingLink(state, id, doctype, perm.attributes.codes.email)
    : null
}

const getSharingLinkPermission = (state, doctype, id) => {
  const perms = getDoctypePermissions(state, doctype)
  const type = isFile({ _type: doctype }) ? 'files' : 'collection'
  return perms.byLink.find(
    p =>
      p.attributes &&
      p.attributes.permissions &&
      p.attributes.permissions[type] &&
      p.attributes.permissions[type].values.indexOf(id) !== -1
  )
}

const getSharingForRecipient = (state, document, recipient) => {
  const sharings = getDocumentActiveSharings(
    state,
    document._type,
    document._id
  )
  return sharings.find(
    s =>
      s.attributes &&
      s.attributes.recipients &&
      s.attributes.recipients.find(
        r => r.recipient && r.recipient.id === recipient._id
      )
  )
}

const getDocumentActiveSharings = (state, doctype, id) => {
  const perms = getDoctypePermissions(state, doctype)
  return [
    ...perms.byMe.filter(
      perm => perm.attributes.permissions['rule0'].values.indexOf(id) !== -1
    ),
    ...perms.withMe.filter(
      perm => perm.attributes.permissions['rule0'].values.indexOf(id) !== -1
    )
  ]
    .map(p => getSharing(state, p.attributes.source_id))
    .filter(s => s && s.attributes && !s.attributes.revoked)
}

export const getSharings = (state, doctype, options = {}) => {
  const perms = getDoctypePermissions(state, doctype)
  const type = doctype === 'io.cozy.files' ? 'files' : 'collection'
  return {
    byMe: perms.byMe.map(p => p.attributes.permissions['rule0'].values[0]),
    withMe: perms.withMe.map(p => p.attributes.permissions['rule0'].values[0]),
    byLink: perms.byLink.map(p => p.attributes.permissions[type].values[0])
  }
}

export const getSharingStatus = (state, doctype, id) => {
  const sharings = getDocumentActiveSharings(state, doctype, id)
  return {
    shared: sharings.length !== 0,
    owner:
      sharings.length === 0 || sharings.some(s => s.attributes.owner === true),
    sharingType: sharings.some(
      s => s.attributes.sharing_type === 'master-master'
    )
      ? 'master-master'
      : 'master-slave',
    sharings
  }
}

export const getSharingDetails = (state, doctype, id, options = {}) => {
  const { shared, owner, sharingType, sharings } = getSharingStatus(
    state,
    doctype,
    id
  )
  const sharingLink = getSharingLink(state, doctype, id)
  return {
    shared,
    owner,
    sharingType,
    sharingLink,
    sharer:
      shared && !owner
        ? { name: 'John Doe', url: sharings[0].attributes.sharer.url }
        : null,
    readOnly: !owner && sharingType === 'master-slave',
    recipients: shared && owner ? getSharingRecipients(state, sharings) : [],
    byMe: shared && owner === true,
    withMe: shared && !owner,
    byLink: !!sharingLink
  }
}

const getSharingRecipients = (state, sharings) =>
  sharings
    .filter(sharing => sharing.attributes.recipients)
    .map(sharing =>
      sharing.attributes.recipients.map(info => ({
        contact: getContact(state, info.recipient.id),
        status: info.status,
        type: sharing.attributes.sharing_type
      }))
    )
    .reduce((a, b) => a.concat(b), [])

const getAppUrlForDoctype = (state, doctype) => {
  switch (doctype) {
    case 'io.cozy.files':
      return getAppUrl(state, 'drive')
    case 'io.cozy.photos.albums':
      return getAppUrl(state, 'photos')
    default:
      throw new Error(
        `Sharing link: don't know which app to use for doctype ${doctype}`
      )
  }
}

const buildSharingLink = (state, id, doctype, sharecode) => {
  const appUrl = getAppUrlForDoctype(state, doctype)
  return `${appUrl}public?sharecode=${sharecode}&id=${id}`
}

// helpers
const isFile = ({ _type, type }) =>
  _type === 'io.cozy.files' || type === 'directory' || type === 'file'

const track = (document, action) => {
  const tracker = getTracker()
  if (!tracker) {
    return
  }
  tracker.push([
    'trackEvent',
    isFile(document) ? 'Drive' : 'Photos',
    action,
    `${action}${isFile(document) ? 'File' : 'Album'}`
  ])
}
const trackSharingByLink = document => track(document, 'shareByLink')
const trackSharingByEmail = document => track(document, 'shareByEmail')
<commit_msg>fix: more sharing defensive code<commit_after>import { combineReducers } from 'redux'
import { getTracker } from 'cozy-ui/react/helpers/tracker'
import { SHARINGS_DOCTYPE } from '../collections/SharingsCollection'
import { APPS_DOCTYPE } from '../collections/AppsCollection'
import {
  getDocument,
  createDocument,
  fetchCollection,
  makeFetchCollection,
  getCollection
} from '../reducer'

export const FETCH_SHARINGS = 'FETCH_SHARINGS'
const RECEIVE_SHARINGS_DATA = 'RECEIVE_SHARINGS_DATA'
const RECEIVE_FETCH_SHARINGS_ERROR = 'RECEIVE_FETCH_SHARINGS_ERROR'
const CREATE_SHARING = 'CREATE_SHARING'
const RECEIVE_NEW_SHARING = 'RECEIVE_NEW_SHARING'
const CREATE_SHARING_LINK = 'CREATE_SHARING_LINK'
const RECEIVE_NEW_SHARING_LINK = 'RECEIVE_NEW_SHARING_LINK'
const REVOKE_SHARING_LINK = 'REVOKE_SHARING_LINK'
const RECEIVE_SHARING_LINK_REVOKE = 'RECEIVE_SHARING_LINK_REVOKE'
const REVOKE_SHARING = 'REVOKE_SHARING'
const RECEIVE_SHARING_REVOKE = 'RECEIVE_SHARING_REVOKE'
const RECEIVE_ERROR = 'RECEIVE_ERROR'

const removeRecipient = (recipients, recipientId) => {
  const idx = recipients.findIndex(r => r.recipient.id === recipientId)
  return [...recipients.slice(0, idx), ...recipients.slice(idx + 1)]
}

const documents = (state = [], action) => {
  switch (action.type) {
    case RECEIVE_SHARINGS_DATA:
      return action.response.sharings
    case RECEIVE_NEW_SHARING:
      return [...state, action.response]
    case RECEIVE_SHARING_REVOKE:
      const idx = state.findIndex(
        s => s.attributes.sharing_id === action.sharingId
      )
      if (idx === -1) return state
      const sharing = state[idx]
      const loneRecipient =
        sharing.attributes.recipients === undefined || // for recipient-side revocation
        sharing.attributes.recipients.length === 1
      const newState = loneRecipient
        ? { ...sharing, attributes: { ...sharing.attributes, revoked: true } }
        : {
            ...sharing,
            attributes: {
              ...sharing.attributes,
              recipients: removeRecipient(
                sharing.attributes.recipients,
                action.recipientId
              )
            }
          }
      return [...state.slice(0, idx), newState, ...state.slice(idx + 1)]
    default:
      return state
  }
}

const doctypePermsetInitialState = {
  fetchStatus: 'loading',
  byMe: [],
  byLink: [],
  withMe: []
}

const permissions = (state = {}, action) => {
  let idx
  switch (action.type) {
    case FETCH_SHARINGS:
      if (state[action.doctype]) {
        return {
          ...state,
          [action.doctype]: { ...state[action.doctype], fetchStatus: 'loading' }
        }
      }
      return { ...state, [action.doctype]: doctypePermsetInitialState }
    case RECEIVE_SHARINGS_DATA:
      return {
        ...state,
        [action.doctype]: {
          fetchStatus: 'loaded',
          ...action.response.permissions
        }
      }
    case RECEIVE_FETCH_SHARINGS_ERROR:
      return { ...state, [action.doctype]: { fetchStatus: 'error' } }
    case RECEIVE_NEW_SHARING:
      return {
        ...state,
        [action.doctype]: {
          ...state[action.doctype],
          byMe: [
            ...state[action.doctype].byMe,
            {
              attributes: {
                permissions: {
                  rule0: { type: action.doctype, values: [action.id] }
                },
                source_id: action.response.attributes.sharing_id,
                type: 'io.cozy.sharings'
              }
            }
          ]
        }
      }
    case RECEIVE_NEW_SHARING_LINK:
      return {
        ...state,
        [action.doctype]: {
          ...state[action.doctype],
          byLink: [...state[action.doctype].byLink, action.response]
        }
      }
    case REVOKE_SHARING_LINK:
      idx = state[action.doctype].byLink.findIndex(
        p => action.permission._id === p._id
      )
      if (idx === -1) return state
      return {
        ...state,
        [action.doctype]: {
          ...state[action.doctype],
          byLink: [
            ...state[action.doctype].byLink.slice(0, idx),
            ...state[action.doctype].byLink.slice(idx + 1)
          ]
        }
      }
    default:
      return state
  }
}

export default combineReducers({ documents, permissions })

// actions
export const fetchSharings = (doctype, id = null, options = {}) => ({
  types: [FETCH_SHARINGS, RECEIVE_SHARINGS_DATA, RECEIVE_FETCH_SHARINGS_ERROR],
  doctype,
  id,
  options,
  promise: client =>
    client.getCollection(SHARINGS_DOCTYPE).findByDoctype(doctype),
  dependencies: [fetchApps()]
})

export const share = (document, recipients, sharingType, sharingDesc) => async (
  dispatch,
  getState
) => {
  const recipientIds = await Promise.all(
    recipients.map(
      recipient =>
        recipient.id ||
        dispatch(createContact(recipient)).then(c => c.data[0].id)
    )
  )
  trackSharingByEmail(document)
  return dispatch(
    createSharing(document, recipientIds, sharingType, sharingDesc)
  )
}

export const unshare = (document, recipient) => async (dispatch, getState) => {
  const sharing = getSharingForRecipient(getState(), document, recipient)
  const loneRecipient = sharing.attributes.recipients.length === 1
  return dispatch({
    types: [REVOKE_SHARING, RECEIVE_SHARING_REVOKE, RECEIVE_ERROR],
    doctype: document._type,
    id: document._id,
    sharingId: sharing.attributes.sharing_id,
    recipientId: recipient._id,
    promise: client =>
      loneRecipient
        ? client
            .getCollection(SHARINGS_DOCTYPE)
            .revoke(sharing.attributes.sharing_id)
        : client
            .getCollection(SHARINGS_DOCTYPE)
            .revokeForClient(
              sharing.attributes.sharing_id,
              sharing.attributes.recipients.find(
                r => r.recipient.id === recipient._id
              ).Client.client_id
            )
  })
}

export const leave = document => async (dispatch, getState) => {
  const sharings = getDocumentActiveSharings(
    getState(),
    document._type,
    document._id
  )
  const sharing = sharings.find(s => s.attributes.owner === false)
  return dispatch({
    types: [REVOKE_SHARING, RECEIVE_SHARING_REVOKE, RECEIVE_ERROR],
    doctype: document._type,
    id: document._id,
    sharingId: sharing.attributes.sharing_id,
    promise: client =>
      client
        .getCollection(SHARINGS_DOCTYPE)
        .revoke(sharing.attributes.sharing_id)
  })
}

export const shareByLink = document => {
  trackSharingByLink(document)
  return createSharingLink(document)
}

export const revokeLink = document => async (dispatch, getState) => {
  const perm = getSharingLinkPermission(
    getState(),
    document._type,
    document._id
  )
  return dispatch({
    types: [REVOKE_SHARING_LINK, RECEIVE_SHARING_LINK_REVOKE, RECEIVE_ERROR],
    doctype: document._type,
    id: document._id,
    permission: perm,
    promise: client => client.getCollection(SHARINGS_DOCTYPE).revokeLink(perm)
  })
}

const createSharing = (
  document,
  contactIds,
  sharingType = 'master-slave',
  description = ''
) => ({
  types: [CREATE_SHARING, RECEIVE_NEW_SHARING, RECEIVE_ERROR],
  doctype: document._type,
  id: document._id,
  promise: client =>
    client
      .getCollection(SHARINGS_DOCTYPE)
      .create(getPermissionsFor(document), contactIds, sharingType, description)
})

const createSharingLink = document => ({
  types: [CREATE_SHARING_LINK, RECEIVE_NEW_SHARING_LINK, RECEIVE_ERROR],
  doctype: document._type,
  id: document._id,
  promise: client =>
    client
      .getCollection(SHARINGS_DOCTYPE)
      .createLink(getPermissionsFor(document, true))
})

export const fetchApps = () =>
  makeFetchCollection('apps', APPS_DOCTYPE, client =>
    client.getCollection(APPS_DOCTYPE).all()
  )

const getApps = state => getCollection(state, 'apps').data

const getAppUrl = (state, appName) => {
  const apps = getApps(state)
  const app = apps.find(
    a => a.attributes.slug === appName && a.attributes.state === 'ready'
  )
  if (!app) {
    throw new Error(`Sharing link: app ${appName} not installed`)
  }
  return app.links.related
}

// TODO: this is a poor man's migration in order to normalize contacts
// and should be removed after a few weeks in prod
// Note for future-self: If you have no idea of what it means, please, erase this code.
// Note for time-travelers: please travel a little more back in time in order to advise us
// to get contacts right the first time
export const fetchContacts = () => {
  const action = fetchCollection('contacts', 'io.cozy.contacts')
  action.promise = async client => {
    const response = await client.fetchDocuments('contacts', 'io.cozy.contacts')
    const data = await Promise.all(
      response.data.map(contact => {
        return typeof contact.email !== 'string'
          ? contact
          : client
              .updateDocument({
                ...contact,
                email: [
                  {
                    address: contact.email,
                    primary: true
                  }
                ]
              })
              .then(resp => resp.data[0])
      })
    )

    return { ...response, data }
  }
  return action
}

const createContact = ({ email }) =>
  createDocument('io.cozy.contacts', {
    email: [{ address: email, primary: true }]
  })

const getPermissionsFor = (document, publicLink = false) => {
  const { _id, _type } = document
  const verbs = publicLink ? ['GET'] : ['ALL']
  // TODO: this works for albums, but it needs to be generalized and integrated
  // with cozy-client ; some sort of doctype "schema" will be needed here
  return isFile(document)
    ? {
        files: {
          type: 'io.cozy.files',
          verbs,
          values: [_id]
        }
      }
    : {
        collection: {
          type: _type,
          verbs,
          values: [_id]
        },
        files: {
          type: 'io.cozy.files',
          verbs,
          values: [`${_type}/${_id}`],
          selector: 'referenced_by'
        }
      }
}

// selectors
const getSharing = (state, id) =>
  state.cozy.sharings.documents.find(
    s => s.attributes && s.attributes.sharing_id === id
  )
const getContact = (state, id) => getDocument(state, 'io.cozy.contacts', id)
const getDoctypePermissions = (state, doctype) => {
  if (state.cozy.sharings.permissions[doctype]) {
    return state.cozy.sharings.permissions[doctype]
  }
  return doctypePermsetInitialState
}

const getSharingLink = (state, doctype, id) => {
  const perm = getSharingLinkPermission(state, doctype, id)
  return perm &&
    perm.attributes &&
    perm.attributes.codes &&
    perm.attributes.codes.email
    ? buildSharingLink(state, id, doctype, perm.attributes.codes.email)
    : null
}

const getSharingLinkPermission = (state, doctype, id) => {
  const perms = getDoctypePermissions(state, doctype)
  const type = isFile({ _type: doctype }) ? 'files' : 'collection'
  return perms.byLink.find(
    p =>
      p.attributes &&
      p.attributes.permissions &&
      p.attributes.permissions[type] &&
      p.attributes.permissions[type].values.indexOf(id) !== -1
  )
}

const getSharingForRecipient = (state, document, recipient) => {
  const sharings = getDocumentActiveSharings(
    state,
    document._type,
    document._id
  )
  return sharings.find(
    s =>
      s.attributes &&
      s.attributes.recipients &&
      s.attributes.recipients.find(
        r => r.recipient && r.recipient.id === recipient._id
      )
  )
}

const getDocumentActiveSharings = (state, doctype, id) => {
  const perms = getDoctypePermissions(state, doctype)
  return [
    ...perms.byMe.filter(
      perm => perm.attributes.permissions['rule0'].values.indexOf(id) !== -1
    ),
    ...perms.withMe.filter(
      perm => perm.attributes.permissions['rule0'].values.indexOf(id) !== -1
    )
  ]
    .map(p => getSharing(state, p.attributes.source_id))
    .filter(s => s && s.attributes && !s.attributes.revoked)
}

export const getSharings = (state, doctype, options = {}) => {
  const perms = getDoctypePermissions(state, doctype)
  const type = doctype === 'io.cozy.files' ? 'files' : 'collection'
  return {
    byMe: perms.byMe.map(p => p.attributes.permissions['rule0'].values[0]),
    withMe: perms.withMe.map(p => p.attributes.permissions['rule0'].values[0]),
    byLink: perms.byLink.map(p => p.attributes.permissions[type].values[0])
  }
}

export const getSharingStatus = (state, doctype, id) => {
  const sharings = getDocumentActiveSharings(state, doctype, id)
  return {
    shared: sharings.length !== 0,
    owner:
      sharings.length === 0 || sharings.some(s => s.attributes.owner === true),
    sharingType: sharings.some(
      s => s.attributes.sharing_type === 'master-master'
    )
      ? 'master-master'
      : 'master-slave',
    sharings
  }
}

export const getSharingDetails = (state, doctype, id, options = {}) => {
  const { shared, owner, sharingType, sharings } = getSharingStatus(
    state,
    doctype,
    id
  )
  const sharingLink = getSharingLink(state, doctype, id)
  return {
    shared,
    owner,
    sharingType,
    sharingLink,
    sharer:
      shared && !owner
        ? { name: 'John Doe', url: sharings[0].attributes.sharer.url }
        : null,
    readOnly: !owner && sharingType === 'master-slave',
    recipients: shared && owner ? getSharingRecipients(state, sharings) : [],
    byMe: shared && owner === true,
    withMe: shared && !owner,
    byLink: !!sharingLink
  }
}

const getSharingRecipients = (state, sharings) =>
  sharings
    .filter(sharing => sharing.attributes.recipients)
    .map(sharing =>
      sharing.attributes.recipients.map(info => ({
        contact: getContact(state, info.recipient.id),
        status: info.status,
        type: sharing.attributes.sharing_type
      }))
    )
    .reduce((a, b) => a.concat(b), [])

const getAppUrlForDoctype = (state, doctype) => {
  switch (doctype) {
    case 'io.cozy.files':
      return getAppUrl(state, 'drive')
    case 'io.cozy.photos.albums':
      return getAppUrl(state, 'photos')
    default:
      throw new Error(
        `Sharing link: don't know which app to use for doctype ${doctype}`
      )
  }
}

const buildSharingLink = (state, id, doctype, sharecode) => {
  const appUrl = getAppUrlForDoctype(state, doctype)
  return `${appUrl}public?sharecode=${sharecode}&id=${id}`
}

// helpers
const isFile = ({ _type, type }) =>
  _type === 'io.cozy.files' || type === 'directory' || type === 'file'

const track = (document, action) => {
  const tracker = getTracker()
  if (!tracker) {
    return
  }
  tracker.push([
    'trackEvent',
    isFile(document) ? 'Drive' : 'Photos',
    action,
    `${action}${isFile(document) ? 'File' : 'Album'}`
  ])
}
const trackSharingByLink = document => track(document, 'shareByLink')
const trackSharingByEmail = document => track(document, 'shareByEmail')
<|endoftext|>


<commit_before>// countRevisions.js
// ------------------------------------------------------------------
//
// created: Mon Dec  3 13:31:48 2018
// last saved: <2019-September-25 17:41:18>

/* jshint esversion: 9, node: true, strict:implied */
/* global process, console, Buffer */

const edgejs     = require('apigee-edge-js'),
      common     = edgejs.utility,
      apigeeEdge = edgejs.edge,
      Getopt     = require('node-getopt'),
      util       = require('util'),
      version    = '20190925-1612',
      getopt     = new Getopt(common.commonOptions.concat([
        ['P' , 'prefix=ARG', 'optional. name prefix. query revision of proxies with names starting with this prefix.' ],
        ['S' , 'sharedflow', 'optional. query sharedflows. Default: query proxies.']
      ])).bindHelp();

// ========================================================

console.log(
  'Apigee Edge CountRevisions tool, version: ' + version + '\n' +
    'Node.js ' + process.version + '\n');

common.logWrite('start');

// process.argv array starts with 'node' and 'scriptname.js'
let opt = getopt.parse(process.argv.slice(2));
common.verifyCommonRequiredParameters(opt.options, getopt);
apigeeEdge.connect(common.optToOptions(opt))
  .then( org => {
    common.logWrite('connected');
    //console.log(org);
    const collection = (opt.options.sharedflow) ? org.sharedflows : org.proxies;
    return collection.get({})
      .then( items => {
        items = items
          .sort()
          .filter( name => (! opt.options.prefix) || name.startsWith(opt.options.prefix ));

        if ( !items || items.length == 0) {
          return Promise.resolve(true);
        }

        const reducer = (promise, itemname) =>
          promise .then( accumulator =>
                         collection
                         .get({ name: itemname })
                         .then( ({revision}) => [ ...accumulator, {itemname, count:revision.length} ] )
                       );

        return items
            .reduce(reducer, Promise.resolve([]))
            .then( arrayOfResults => common.logWrite('all done...\n' + JSON.stringify(arrayOfResults)) );
      });
  })
  .catch( e => console.error('error: ' + util.format(e) ));
<commit_msg>accept regex in countRevisions.js<commit_after>#! /usr/local/bin/node
// countRevisions.js
// ------------------------------------------------------------------
//
// created: Mon Dec  3 13:31:48 2018
// last saved: <2019-December-05 19:47:05>

/* jshint esversion: 9, node: true, strict:implied */
/* global process, console, Buffer */

const edgejs     = require('apigee-edge-js'),
      common     = edgejs.utility,
      apigeeEdge = edgejs.edge,
      Getopt     = require('node-getopt'),
      util       = require('util'),
      version    = '20191205-1947',
      getopt     = new Getopt(common.commonOptions.concat([
        ['P' , 'prefix=ARG', 'optional. name prefix. query revision of proxies with names starting with this prefix.' ],
        ['R' , 'regex=ARG', 'optional. a regular expression. query revision of proxies with names matching this pattern.' ],
        ['S' , 'sharedflow', 'optional. query sharedflows. Default: query proxies.']
      ])).bindHelp();

function isKeeper(opt) {
  if (opt.options.regex) {
    common.logWrite('using regex match (%s)',opt.options.regex);
    let re1 = new RegExp(opt.options.regex);
    return function(name) {
      return name.match(re1);
    };
  }

  if (opt.options.prefix) {
    common.logWrite('using prefix match (%s)',opt.options.prefix);
    return function (name) {
      return name.startsWith(opt.options.prefix);
    };
  }

  return () => true;
}

// ========================================================

console.log(
  'Apigee Edge CountRevisions tool, version: ' + version + '\n' +
    'Node.js ' + process.version + '\n');

common.logWrite('start');

// process.argv array starts with 'node' and 'scriptname.js'
let opt = getopt.parse(process.argv.slice(2));
common.verifyCommonRequiredParameters(opt.options, getopt);
apigeeEdge.connect(common.optToOptions(opt))
  .then( org => {
    common.logWrite('connected');
    //console.log(org);
    const collection = (opt.options.sharedflow) ? org.sharedflows : org.proxies;
    return collection.get({})
      .then( items => {
        items = items
          .sort()
          .filter( isKeeper(opt) );

        if ( !items || items.length == 0) {
          return Promise.resolve(true);
        }

        const reducer = (promise, itemname) =>
          promise .then( accumulator =>
                         collection
                         .get({ name: itemname })
                         .then( ({revision}) => [ ...accumulator, {itemname, count:revision.length} ] )
                       );

        return items
            .reduce(reducer, Promise.resolve([]))
            .then( arrayOfResults => common.logWrite('all done...\n' + JSON.stringify(arrayOfResults)) );
      });
  })
  .catch( e => console.error('error: ' + util.format(e) ));
<|endoftext|>


<commit_before>'use strict';

module.exports = function () {
	var map, iterator, result;
	if (typeof Map !== 'function') return false;
	try {
		// WebKit doesn't support arguments and crashes
		map = new Map([['raz', 'one'], ['dwa', 'two'], ['trzy', 'three']]);
	} catch (e) {
		return false;
	}
	if (map.size !== 3) return false;
	if (typeof map.clear !== 'function') return false;
	if (typeof map.delete !== 'function') return false;
	if (typeof map.entries !== 'function') return false;
	if (typeof map.forEach !== 'function') return false;
	if (typeof map.get !== 'function') return false;
	if (typeof map.has !== 'function') return false;
	if (typeof map.keys !== 'function') return false;
	if (typeof map.set !== 'function') return false;
	if (typeof map.values !== 'function') return false;

	iterator = map.entries();
	result = iterator.next();
	if (result.done !== false) return false;
	if (!result.value) return false;
	if (result.value[0] !== 'raz') return false;
	if (result.value[1] !== 'one') return false;
	return true;
};
<commit_msg>Restrict native Map detection<commit_after>'use strict';

module.exports = function () {
	var map, iterator, result;
	if (typeof Map !== 'function') return false;
	if (String(Map.prototype) !== '[object Map]') return false;
	try {
		// WebKit doesn't support arguments and crashes
		map = new Map([['raz', 'one'], ['dwa', 'two'], ['trzy', 'three']]);
	} catch (e) {
		return false;
	}
	if (map.size !== 3) return false;
	if (typeof map.clear !== 'function') return false;
	if (typeof map.delete !== 'function') return false;
	if (typeof map.entries !== 'function') return false;
	if (typeof map.forEach !== 'function') return false;
	if (typeof map.get !== 'function') return false;
	if (typeof map.has !== 'function') return false;
	if (typeof map.keys !== 'function') return false;
	if (typeof map.set !== 'function') return false;
	if (typeof map.values !== 'function') return false;

	iterator = map.entries();
	result = iterator.next();
	if (result.done !== false) return false;
	if (!result.value) return false;
	if (result.value[0] !== 'raz') return false;
	if (result.value[1] !== 'one') return false;
	return true;
};
<|endoftext|>


<commit_before>import {EMPTY, combineLatest, fromEvent, merge, timer} from 'rxjs'
import {Link} from 'route'
import {distinctUntilChanged, switchMap, take, takeUntil} from 'rxjs/operators'
import Icon from 'widget/icon'
import PropTypes from 'prop-types'
import React from 'react'
import Tooltip from 'widget/tooltip'
import lookStyles from 'style/look.module.css'
import styles from './button.module.css'

const CLICK_HOLD_DELAY_MS = 750
const CLICK_CANCEL_DELAY_MS = 250

const windowMouseUp$ = fromEvent(window, 'mouseup').pipe(distinctUntilChanged())

const download = (url, filename) => {
    // create hidden anchor, attach to DOM, click it and remove it from the DOM
    var downloadElement = document.createElement('a')
    downloadElement.setAttribute('style', 'display: none')
    downloadElement.setAttribute('href', url)
    downloadElement.setAttribute('download', filename)
    document.body.appendChild(downloadElement)
    downloadElement.click()
    downloadElement.remove()
}

export class Button extends React.Component {
    button = React.createRef()
    subscriptions = []

    stopPropagation() {
        const {link, stopPropagation = !link} = this.props
        return stopPropagation
    }

    active() {
        const {shown = true, disabled = false} = this.props
        return shown && !disabled
    }

    linked() {
        const {onMouseDown, onClick, onClickHold, link, downloadUrl} = this.props
        return onMouseDown || onClick || onClickHold || link || downloadUrl
    }

    nonInteractive() {
        return !this.active() || !this.linked()
    }

    classNames() {
        const {
            chromeless,
            className,
            additionalClassName,
            look = 'default',
            size = 'normal',
            shape = 'rectangle',
            onClickHold
        } = this.props
        return className ? className : [
            styles.button,
            chromeless ? styles.chromeless : null,
            styles[size],
            styles[shape],
            lookStyles.look,
            lookStyles[look],
            chromeless ? lookStyles.chromeless : null,
            this.nonInteractive() ? lookStyles.nonInteractive : null,
            onClickHold ? styles.hold : null,
            additionalClassName
        ].join(' ')
    }

    handleMouseDown(e) {
        const {onMouseDown} = this.props
        onMouseDown && onMouseDown(e)
        if (this.stopPropagation()) {
            e.stopPropagation()
        }
    }

    handleClick(e) {
        const {onClick, downloadUrl, downloadFilename} = this.props
        onClick && onClick(e)
        downloadUrl && download(downloadUrl, downloadFilename)
        if (this.stopPropagation()) {
            e.stopPropagation()
        }
    }

    handleClickHold(e) {
        const {onClickHold} = this.props
        onClickHold && onClickHold(e)
        if (this.stopPropagation()) {
            e.stopPropagation()
        }
    }

    // The Tooltip component stops propagation of events, thus the ref has to be on a wrapping element.
    renderWrapper(contents) {
        const {onClickHold} = this.props
        const style = {
            '--click-hold-delay-ms': `${CLICK_CANCEL_DELAY_MS}ms`,
            '--click-hold-duration-ms': `${CLICK_HOLD_DELAY_MS - CLICK_CANCEL_DELAY_MS}ms`
        }
        return onClickHold ? (

                {contents}

        ) : contents
    }

    renderLink(contents) {
        const {link, shown = true, disabled} = this.props
        return link && shown && !disabled ? (
            <Link to={link} onMouseDown={e => e.preventDefault()}>
                {contents}
            </Link>
        ) : contents
    }

    renderTooltip(contents) {
        const {tooltip, tooltipPlacement, tooltipDisabled, disabled} = this.props
        return tooltip && !tooltipDisabled && !disabled ? (
            <Tooltip msg={tooltip} placement={tooltipPlacement}>
                {contents}
            </Tooltip>
        ) : contents
    }

    renderButton(contents) {
        const {type = 'button', tabIndex, disabled, onClickHold} = this.props
        return (
            <button
                type={type}
                className={this.classNames()}
                tabIndex={tabIndex}
                disabled={disabled}
                onMouseDown={e => this.handleMouseDown(e)}
                onClick={e => onClickHold ? e.stopPropagation() : this.handleClick(e)}
            >
                {contents}
            </button>
        )
    }

    renderIcon() {
        const {icon, iconType, iconFlipHorizontal} = this.props
        return (
            <Icon
                name={icon}
                type={iconType}
                flip={iconFlipHorizontal ? 'horizontal' : null}
            />
        )
    }

    renderLabel() {
        const {label} = this.props
        return (
            {label}
        )
    }

    renderContents() {
        const {icon, iconPlacement = 'left', label, children} = this.props
        return children ? children : (

                {icon && iconPlacement === 'left' ? this.renderIcon() : null}
                {label ? this.renderLabel() : null}
                {icon && iconPlacement === 'right' ? this.renderIcon() : null}

        )
    }

    render() {
        return (
            this.renderWrapper(
                this.renderLink(
                    this.renderTooltip(
                        this.renderButton(
                            this.renderContents()
                        )
                    )
                )
            )
        )
    }

    componentDidMount() {
        const {onClickHold} = this.props

        const button = this.button.current

        const mouseDown$ = fromEvent(button, 'mousedown')
        const mouseUp$ = fromEvent(button, 'mouseup')
        const mouseEnter$ = fromEvent(button, 'mouseenter')
        const cancel$ = windowMouseUp$

        const mouseTrigger$ = combineLatest(mouseDown$, mouseEnter$)
        const mouseActivate$ = mouseUp$

        if (onClickHold) {

            // Click-hold is triggered if button pressed more than CLICK_HOLD_DELAY_MS.
            const clickHold$ =
                mouseTrigger$.pipe(
                    switchMap(() =>
                        timer(CLICK_HOLD_DELAY_MS).pipe(
                            takeUntil(cancel$),
                            switchMap(() =>
                                mouseActivate$.pipe(
                                    takeUntil(cancel$),
                                    take(1)
                                )
                            )
                        )
                    )
                )

            this.subscriptions.push(
                clickHold$.subscribe(e => {
                    const {onClickHold, disabled} = this.props
                    if (onClickHold && !disabled) {
                        this.handleClickHold(e)
                    }
                })
            )

            // Click event needs to be handled here for two reasons:
            // - to allow cancellation of click-hold without triggering click, when preesed longer than CLICK_CANCEL_DELAY_MS
            // - to avoid concurrent handling of both click and click-hold when pressed longer than CLICK_HOLD_DELAY_MS
            // Click is triggered only if button pressed less than CLICK_CANCEL_DELAY_MS.
            const click$ =
                mouseTrigger$.pipe(
                    switchMap(() =>
                        mouseActivate$.pipe(
                            takeUntil(
                                merge(
                                    cancel$,
                                    onClickHold ? timer(CLICK_CANCEL_DELAY_MS) : EMPTY
                                )
                            ),
                            take(1)
                        )
                    )
                )

            this.subscriptions.push(
                click$.subscribe(e => {
                    const {onClick, disabled} = this.props
                    if (onClick && !disabled) {
                        this.handleClick(e)
                    }
                })
            )
        }
    }

    componentWillUnmount() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe())
    }
}

Button.propTypes = {
    additionalClassName: PropTypes.string,
    children: PropTypes.any,
    chromeless: PropTypes.any,
    className: PropTypes.string,
    disabled: PropTypes.any,
    downloadFilename: PropTypes.any,
    downloadUrl: PropTypes.any,
    icon: PropTypes.string,
    iconFlipHorizontal: PropTypes.any,
    iconPlacement: PropTypes.oneOf(['left', 'right']),
    iconType: PropTypes.string,
    label: PropTypes.string,
    link: PropTypes.string,
    look: PropTypes.oneOf(['default', 'highlight', 'transparent', 'add', 'apply', 'cancel']),
    shape: PropTypes.oneOf(['rectangle', 'pill', 'circle', 'none']),
    shown: PropTypes.any,
    size: PropTypes.oneOf(['small', 'normal', 'large', 'x-large', 'xx-large']),
    stopPropagation: PropTypes.any,
    tabIndex: PropTypes.number,
    tooltip: PropTypes.string,
    tooltipDisabled: PropTypes.any,
    tooltipPlacement: PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
    type: PropTypes.oneOf(['button', 'submit', 'reset']),
    onClick: PropTypes.func,
    onClickHold: PropTypes.func,
    onMouseDown: PropTypes.func
}

export const ButtonGroup = ({children, type = 'horizontal-wrap', className}) =>

        {children}

ButtonGroup.propTypes = {
    children: PropTypes.any.isRequired,
    className: PropTypes.string,
    type: PropTypes.oneOf(['horizontal-wrap', 'horizontal-nowrap', 'horizontal-tight', 'vertical']),
    vertical: PropTypes.any,
    wrap: PropTypes.any
}
<commit_msg>Fixed Button shown prop.<commit_after>import {EMPTY, combineLatest, fromEvent, merge, timer} from 'rxjs'
import {Link} from 'route'
import {distinctUntilChanged, switchMap, take, takeUntil} from 'rxjs/operators'
import Icon from 'widget/icon'
import PropTypes from 'prop-types'
import React from 'react'
import Tooltip from 'widget/tooltip'
import lookStyles from 'style/look.module.css'
import styles from './button.module.css'

const CLICK_HOLD_DELAY_MS = 750
const CLICK_CANCEL_DELAY_MS = 250

const windowMouseUp$ = fromEvent(window, 'mouseup').pipe(distinctUntilChanged())

const download = (url, filename) => {
    // create hidden anchor, attach to DOM, click it and remove it from the DOM
    var downloadElement = document.createElement('a')
    downloadElement.setAttribute('style', 'display: none')
    downloadElement.setAttribute('href', url)
    downloadElement.setAttribute('download', filename)
    document.body.appendChild(downloadElement)
    downloadElement.click()
    downloadElement.remove()
}

export class Button extends React.Component {
    button = React.createRef()
    subscriptions = []

    stopPropagation() {
        const {link, stopPropagation = !link} = this.props
        return stopPropagation
    }

    active() {
        const {shown = true, disabled = false} = this.props
        return shown && !disabled
    }

    linked() {
        const {onMouseDown, onClick, onClickHold, link, downloadUrl} = this.props
        return onMouseDown || onClick || onClickHold || link || downloadUrl
    }

    nonInteractive() {
        return !this.active() || !this.linked()
    }

    classNames() {
        const {
            chromeless,
            className,
            additionalClassName,
            look = 'default',
            size = 'normal',
            shape = 'rectangle',
            onClickHold
        } = this.props
        return className ? className : [
            styles.button,
            chromeless ? styles.chromeless : null,
            styles[size],
            styles[shape],
            lookStyles.look,
            lookStyles[look],
            chromeless ? lookStyles.chromeless : null,
            this.nonInteractive() ? lookStyles.nonInteractive : null,
            onClickHold ? styles.hold : null,
            additionalClassName
        ].join(' ')
    }

    handleMouseDown(e) {
        const {onMouseDown} = this.props
        onMouseDown && onMouseDown(e)
        if (this.stopPropagation()) {
            e.stopPropagation()
        }
    }

    handleClick(e) {
        const {onClick, downloadUrl, downloadFilename} = this.props
        onClick && onClick(e)
        downloadUrl && download(downloadUrl, downloadFilename)
        if (this.stopPropagation()) {
            e.stopPropagation()
        }
    }

    handleClickHold(e) {
        const {onClickHold} = this.props
        onClickHold && onClickHold(e)
        if (this.stopPropagation()) {
            e.stopPropagation()
        }
    }

    // The Tooltip component stops propagation of events, thus the ref has to be on a wrapping element.
    renderWrapper(contents) {
        const {onClickHold} = this.props
        const style = {
            '--click-hold-delay-ms': `${CLICK_CANCEL_DELAY_MS}ms`,
            '--click-hold-duration-ms': `${CLICK_HOLD_DELAY_MS - CLICK_CANCEL_DELAY_MS}ms`
        }
        return onClickHold ? (

                {contents}

        ) : contents
    }

    renderLink(contents) {
        const {link, shown = true, disabled} = this.props
        return link && shown && !disabled ? (
            <Link to={link} onMouseDown={e => e.preventDefault()}>
                {contents}
            </Link>
        ) : contents
    }

    renderTooltip(contents) {
        const {tooltip, tooltipPlacement, tooltipDisabled, disabled} = this.props
        return tooltip && !tooltipDisabled && !disabled ? (
            <Tooltip msg={tooltip} placement={tooltipPlacement}>
                {contents}
            </Tooltip>
        ) : contents
    }

    renderButton(contents) {
        const {type = 'button', tabIndex, disabled, onClickHold} = this.props
        return (
            <button
                type={type}
                className={this.classNames()}
                tabIndex={tabIndex}
                disabled={disabled}
                onMouseDown={e => this.handleMouseDown(e)}
                onClick={e => onClickHold ? e.stopPropagation() : this.handleClick(e)}
            >
                {contents}
            </button>
        )
    }

    renderIcon() {
        const {icon, iconType, iconFlipHorizontal} = this.props
        return (
            <Icon
                name={icon}
                type={iconType}
                flip={iconFlipHorizontal ? 'horizontal' : null}
            />
        )
    }

    renderLabel() {
        const {label} = this.props
        return (
            {label}
        )
    }

    renderContents() {
        const {icon, iconPlacement = 'left', label, children} = this.props
        return children ? children : (

                {icon && iconPlacement === 'left' ? this.renderIcon() : null}
                {label ? this.renderLabel() : null}
                {icon && iconPlacement === 'right' ? this.renderIcon() : null}

        )
    }

    render() {
        const {shown = true} = this.props
        return shown ? (
            this.renderWrapper(
                this.renderLink(
                    this.renderTooltip(
                        this.renderButton(
                            this.renderContents()
                        )
                    )
                )
            )
        ) : null
    }

    componentDidMount() {
        const {onClickHold} = this.props

        if (onClickHold && this.button.current) {
            const button = this.button.current
            const mouseDown$ = fromEvent(button, 'mousedown')
            const mouseUp$ = fromEvent(button, 'mouseup')
            const mouseEnter$ = fromEvent(button, 'mouseenter')
            const cancel$ = windowMouseUp$
            const mouseTrigger$ = combineLatest(mouseDown$, mouseEnter$)
            const mouseActivate$ = mouseUp$

            // Click-hold is triggered if button pressed more than CLICK_HOLD_DELAY_MS.
            const clickHold$ =
                mouseTrigger$.pipe(
                    switchMap(() =>
                        timer(CLICK_HOLD_DELAY_MS).pipe(
                            takeUntil(cancel$),
                            switchMap(() =>
                                mouseActivate$.pipe(
                                    takeUntil(cancel$),
                                    take(1)
                                )
                            )
                        )
                    )
                )

            this.subscriptions.push(
                clickHold$.subscribe(e => {
                    const {onClickHold, disabled} = this.props
                    if (onClickHold && !disabled) {
                        this.handleClickHold(e)
                    }
                })
            )

            // Click event needs to be handled here for two reasons:
            // - to allow cancellation of click-hold without triggering click, when preesed longer than CLICK_CANCEL_DELAY_MS
            // - to avoid concurrent handling of both click and click-hold when pressed longer than CLICK_HOLD_DELAY_MS
            // Click is triggered only if button pressed less than CLICK_CANCEL_DELAY_MS.
            const click$ =
                mouseTrigger$.pipe(
                    switchMap(() =>
                        mouseActivate$.pipe(
                            takeUntil(
                                merge(
                                    cancel$,
                                    onClickHold ? timer(CLICK_CANCEL_DELAY_MS) : EMPTY
                                )
                            ),
                            take(1)
                        )
                    )
                )

            this.subscriptions.push(
                click$.subscribe(e => {
                    const {onClick, disabled} = this.props
                    if (onClick && !disabled) {
                        this.handleClick(e)
                    }
                })
            )
        }
    }

    componentWillUnmount() {
        this.subscriptions.forEach(subscription => subscription.unsubscribe())
    }
}

Button.propTypes = {
    additionalClassName: PropTypes.string,
    children: PropTypes.any,
    chromeless: PropTypes.any,
    className: PropTypes.string,
    disabled: PropTypes.any,
    downloadFilename: PropTypes.any,
    downloadUrl: PropTypes.any,
    icon: PropTypes.string,
    iconFlipHorizontal: PropTypes.any,
    iconPlacement: PropTypes.oneOf(['left', 'right']),
    iconType: PropTypes.string,
    label: PropTypes.string,
    link: PropTypes.string,
    look: PropTypes.oneOf(['default', 'highlight', 'transparent', 'add', 'apply', 'cancel']),
    shape: PropTypes.oneOf(['rectangle', 'pill', 'circle', 'none']),
    shown: PropTypes.any,
    size: PropTypes.oneOf(['small', 'normal', 'large', 'x-large', 'xx-large']),
    stopPropagation: PropTypes.any,
    tabIndex: PropTypes.number,
    tooltip: PropTypes.string,
    tooltipDisabled: PropTypes.any,
    tooltipPlacement: PropTypes.oneOf(['top', 'bottom', 'left', 'right']),
    type: PropTypes.oneOf(['button', 'submit', 'reset']),
    onClick: PropTypes.func,
    onClickHold: PropTypes.func,
    onMouseDown: PropTypes.func
}

export const ButtonGroup = ({children, type = 'horizontal-wrap', className}) =>

        {children}

ButtonGroup.propTypes = {
    children: PropTypes.any.isRequired,
    className: PropTypes.string,
    type: PropTypes.oneOf(['horizontal-wrap', 'horizontal-nowrap', 'horizontal-tight', 'vertical']),
    vertical: PropTypes.any,
    wrap: PropTypes.any
}
<|endoftext|>