<commit_before>import './map.module.css'
import {NEVER, Observable, Subject} from 'rxjs'
import {connect, select} from 'store'
import {map, mergeMap, takeUntil} from 'rxjs/operators'
import {msg} from 'translate'
import GoogleMapsLoader from 'google-maps'
import Notifications from 'widget/notifications'
import Portal from 'widget/portal'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import actionBuilder from 'action-builder'
import api from 'api'

export let sepalMap = null
export let google = null
export let googleMap = null

const contextById = {}
let currentContextId

const initListeners = []
const onInit = listener => {
    if (google)
        listener(google)
    else
        initListeners.push(listener)
}

export const initGoogleMapsApi$ = () => {
    const loadGoogleMapsApiKey$ =
        api.map.loadApiKey$().pipe(
            map(({apiKey}) => apiKey)
        )

    const loadGoogleMapsApi$ = apiKey => Observable.create(observer => {
        GoogleMapsLoader.KEY = apiKey
        GoogleMapsLoader.VERSION = '3.35'
        GoogleMapsLoader.LIBRARIES = ['drawing']
        GoogleMapsLoader.load(g => {
            google = g
            initListeners.forEach(listener => listener(google))
            observer.next(apiKey)
            observer.complete()
        })
    })

    return loadGoogleMapsApiKey$.pipe(
        mergeMap(loadGoogleMapsApi$),
        map(apiKey => actionBuilder('SET_GOOGLE_MAPS_API_INITIALIZED', {apiKey: apiKey})
            .set('map.apiKey', apiKey)
            .build()
        )
    )
}

const createMap = mapElement => {
    googleMap = new google.maps.Map(mapElement, {
        zoom: 3,
        minZoom: 3,
        maxZoom: 17,
        center: new google.maps.LatLng(16.7794913, 9.6771556),
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        zoomControl: false,
        mapTypeControl: false,
        scaleControl: false,
        streetViewControl: false,
        rotateControl: false,
        fullscreenControl: false,
        backgroundColor: '#131314',
        gestureHandling: 'greedy'
    })
    googleMap.mapTypes.set('styled_map', new google.maps.StyledMapType(defaultStyle, {name: 'sepalMap'}))
    googleMap.setMapTypeId('styled_map')
    googleMap.addListener('zoom_changed', () =>
        actionBuilder('SET_MAP_ZOOM')
            .set('map.zoom', googleMap.getZoom())
            .dispatch()
    )

    const drawingOptions = {
        fillColor: '#FBFAF2',
        fillOpacity: 0.07,
        strokeColor: '#c5b397',
        strokeOpacity: 1,
        strokeWeight: 2,
        clickable: false,
        editable: false,
        zIndex: 1
    }

    sepalMap = {
        getKey() {
            return GoogleMapsLoader.KEY
        },

        getZoom() {
            return googleMap.getZoom()
        },
        setZoom(zoom) {
            return googleMap.setZoom(zoom)
        },
        zoomIn() {
            googleMap.setZoom(googleMap.getZoom() + 1)
        },
        zoomOut() {
            googleMap.setZoom(googleMap.getZoom() - 1)
        },
        isMaxZoom() {
            return googleMap.getZoom() === googleMap.maxZoom
        },
        isMinZoom() {
            return googleMap.getZoom() === googleMap.minZoom
        },
        fitBounds(bounds) {
            const googleBounds = toGoogleBounds(bounds)
            const boundsChanged = !googleMap.getBounds().equals(googleBounds)
            if (boundsChanged)
                googleMap.fitBounds(googleBounds)
        },
        getBounds() {
            return fromGoogleBounds(googleMap.getBounds())
        },
        onBoundsChanged(listener) {
            return googleMap.addListener('bounds_changed', listener)
        },
        addListener(mapObject, event, listener) {
            return google.maps.event.addListener(mapObject, event, listener)
        },
        removeListener(listener) {
            if (listener)
                google.maps.event.removeListener(listener)
        },
        getContext(contextId) {
            let context = contextById[contextId]
            if (!context) {
                const layerById = {}
                const setZooming = zooming =>
                    actionBuilder('SET_MAP_ZOOMING')
                        .set(['map', contextId, 'zooming'], zooming)
                        .dispatch()
                context = {
                    setLayer({id, layer, destroy$ = NEVER, onInitialized, onError}) {
                        const existingLayer = layerById[id]
                        const unchanged = layer === existingLayer || (existingLayer && existingLayer.equals(layer))
                        if (unchanged)
                            return false
                        this.removeLayer(id)
                        if (layer) {
                            layerById[id] = layer
                            layer.__removed$ = new Subject()
                            layer.initialize$()
                                .pipe(
                                    takeUntil(destroy$),
                                    takeUntil(layer.__removed$)
                                )
                                .subscribe(
                                    () => {
                                        layer.__initialized__ = true
                                        currentContextId === contextId && layer.addToMap(googleMap)
                                        onInitialized && onInitialized(layer)
                                    },
                                    error => {
                                        if (onError)
                                            onError(error)
                                        else
                                            Notifications.error({message: msg('map.layer.error'), error})
                                    }
                                )
                        }
                        return true
                    },
                    hideLayer(id, hidden) {
                        const layer = layerById[id]
                        if (layer)
                            layer.hide(googleMap, hidden)
                    },
                    removeLayer(id) {
                        const layer = layerById[id]
                        if (!layer)
                            return
                        layer.__removed$.next()
                        if (currentContextId === contextId)
                            layer.removeFromMap(googleMap)
                        delete layerById[id]
                    },
                    isLayerInitialized(id) {
                        return !!(this.hasLayer(id) && layerById[id].__initialized__)
                    },
                    hasLayer(id) {
                        return !!layerById[id]
                    },
                    fitLayer(id) {
                        const layer = layerById[id]
                        if (layer && layer.bounds && currentContextId === contextId) {
                            const bounds = layer.bounds
                            sepalMap.fitBounds(bounds)
                        }
                    },
                    addToMap() {
                        Object.keys(layerById).forEach(id => {
                            const layer = layerById[id]
                            if (layer.__initialized__)
                                layer.addToMap(googleMap)
                        })
                    },
                    removeFromMap() {
                        Object.keys(layerById).forEach(id => layerById[id].removeFromMap(googleMap))
                    },
                    zoomArea() {
                        setZooming(true)
                        this._drawingManager = new google.maps.drawing.DrawingManager({
                            drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
                            drawingControl: false,
                            rectangleOptions: drawingOptions
                        })
                        const drawingListener = e => {
                            const rectangle = e.overlay
                            rectangle.setMap(null)
                            googleMap.fitBounds(rectangle.bounds)
                            this.cancelZoomArea()
                        }
                        google.maps.event.addListener(this._drawingManager, 'overlaycomplete', drawingListener)
                        this._drawingManager.setMap(googleMap)

                    },
                    cancelZoomArea() {
                        setZooming(false)
                        this.disableDrawingMode()
                    },
                    isZooming() {
                        const zooming = select(['map', contextId, 'zooming'])
                        return !!zooming
                    },
                    drawPolygon(id, callback) {
                        this._drawingPolygon = {id, callback}
                        this._drawingManager = this._drawingManager || new google.maps.drawing.DrawingManager({
                            drawingMode: google.maps.drawing.OverlayType.POLYGON,
                            drawingControl: false,
                            drawingControlOptions: {
                                position: google.maps.ControlPosition.TOP_CENTER,
                                drawingModes: ['polygon']
                            },
                            circleOptions: drawingOptions,
                            polygonOptions: drawingOptions,
                            rectangleOptions: drawingOptions
                        })
                        const drawingListener = e => {
                            const polygon = e.overlay
                            polygon.setMap(null)
                            const toPolygonPath = polygon => polygon.getPaths().getArray()[0].getArray().map(latLng =>
                                [latLng.lng(), latLng.lat()]
                            )
                            callback(toPolygonPath(polygon))
                        }
                        google.maps.event.addListener(this._drawingManager, 'overlaycomplete', drawingListener)
                        this._drawingManager.setMap(googleMap)
                    },
                    pauseDrawingMode() {
                        if (this._drawingManager) {
                            this._drawingManager.setMap(null)
                            google.maps.event.clearListeners(this._drawingManager, 'overlaycomplete')
                        }
                    },
                    disableDrawingMode() {
                        this.pauseDrawingMode()
                        this._drawingPolygon = null
                    },
                }
                contextById[contextId] = context
            }
            return context
        },
        setContext(contextId) {
            if (currentContextId === contextId)
                return
            const prevContextId = currentContextId
            if (prevContextId) {
                const context = this.getContext(prevContextId)
                context.pauseDrawingMode()
                context.removeFromMap()
            }

            currentContextId = contextId
            if (contextId) {
                const context = this.getContext(contextId)
                context.addToMap()
                context._drawingPolygon && context.drawPolygon(context._drawingPolygon.id, context._drawingPolygon.callback)
                context.isZooming() && context.zoomArea()
            }

        },
        clearContext(contextId) {
            if (contextId === currentContextId)
                sepalMap.clear()
        },
        removeContext(contextId) {
            if (contextId === currentContextId) {
                sepalMap.clear()
            }
            delete contextById[contextId]
        },
        clear() {
            sepalMap.setContext()
        }
    }
}

export const fromGoogleBounds = googleBounds => {
    const sw = googleBounds.getSouthWest()
    const ne = googleBounds.getNorthEast()
    return [
        [sw.lng(), sw.lat()],
        [ne.lng(), ne.lat()]
    ]
}

const toGoogleBounds = bounds => {
    return new google.maps.LatLngBounds(
        {lng: bounds[0][0], lat: bounds[0][1]},
        {lng: bounds[1][0], lat: bounds[1][1]}
    )
}

export const polygonOptions = fill => ({
    fillColor: '#FBFAF2',
    fillOpacity: fill ? 0.07 : 0.000000000000000000000000000001,
    strokeColor: '#FBFAF2',
    strokeOpacity: 0.5,
    strokeWeight: 1
})

// https://developers.google.com/maps/documentation/javascript/style-reference
const defaultStyle = [
    {stylers: [{visibility: 'simplified'}]},
    {stylers: [{color: '#131314'}]},
    {featureType: 'transit.station', stylers: [{visibility: 'off'}]},
    {featureType: 'poi', stylers: [{visibility: 'off'}]},
    {featureType: 'water', stylers: [{color: '#191919'}, {lightness: 4}]},
    {elementType: 'labels.text.fill', stylers: [{visibility: 'off'}, {lightness: 25}]}
]

const mapStateToProps = () => ({
    apiKey: select('map.apiKey')
})

class Map extends React.Component {
    state = {initialized: false}
    mapElement = React.createRef()

    render() {
        return

    }

    componentDidUpdate() {
        const apiKey = this.props.apiKey
        if (apiKey && !this.state.initialized) { // Create map once there is an API key
            this.setState(prevState => ({...prevState, initialized: true}))
            createMap(this.mapElement.current)
            this.initialized = true
        }
    }
}

Map.propTypes = {
    className: PropTypes.string
}

export default connect(mapStateToProps)(Map)

export class MapLayer extends React.Component {
    state = {
        shown:
            false, projection: null
    }

    constructor(props) {
        super(props)
        this.overlay = new ReactOverlayView(this)
        this.overlay.setMap(googleMap)
    }

    render() {
        const {shown, projection} = this.state
        const {className, children} = this.props
        const mapPanes = this.overlay.getPanes()
        const content = (

                <ProjectionContext.Provider value={projection}>
                    {children}
                </ProjectionContext.Provider>

        )
        return shown && mapPanes
            ? <Portal content={content} container={mapPanes.overlayMouseTarget}/>
            : null
    }

    componentWillUnmount() {
        this.overlay.setMap(null)
    }
}

class WrappedMapObject extends React.Component {
    render() {
        const {lat, lng, width, height, className, children} = this.props
        const shown = googleMap.getBounds().contains({lng, lat})
        if (!shown)
            return null
        return (
            <ProjectionContext.Consumer>
                {projection => {
                    if (!projection)
                        return null
                    const point = projection.fromLatLngToDivPixel(new google.maps.LatLng(lat, lng))
                    const style = {
                        position: 'absolute',
                        top: `calc(${point.y}px - ${height} / 2)`,
                        left: `calc(${point.x}px - ${width} /2)`
                    }
                    return

                        {children}

                }
                }
            </ProjectionContext.Consumer>
        )
    }

    componentWillUnmount() {

    }
}

export const MapObject = connect(state => ({projectionChange: state.map.projectionChange}))(WrappedMapObject)

const ProjectionContext = React.createContext()

let ReactOverlayView
onInit(google =>
    ReactOverlayView = class ReactOverlayView extends google.maps.OverlayView {
        constructor(component) {
            super()
            this.component = component
            this.xyz = null

        }

        onAdd() {
            this.show(true)
        }

        draw() {
            const projection = this.getProjection() // TODO: Zooming changes the projection...
            const point = projection.fromLatLngToDivPixel(new google.maps.LatLng(0, 0))
            const xyz = [point.x, point.y, sepalMap.getZoom()]
            if (!_.isEqual(this.xyz, xyz)) {
                this.xyz = xyz
                this.component.setState(prevState => ({...prevState, projection}))
                actionBuilder('PROJECTION_CHANGED', {xyz})
                    .set('map.projectionChange', xyz)
                    .dispatch()
            }
        }

        onRemove() {
            this.show(false)
            google.maps.event.removeListener(this.projectionChangeListener)
        }

        show(shown) {
            this.component.setState(prevState => ({...prevState, shown: shown}))
        }
    })
<commit_msg>Refactoring.<commit_after>import './map.module.css'
import {NEVER, Observable, Subject} from 'rxjs'
import {connect, select} from 'store'
import {map, mergeMap, takeUntil} from 'rxjs/operators'
import {msg} from 'translate'
import GoogleMapsLoader from 'google-maps'
import Notifications from 'widget/notifications'
import Portal from 'widget/portal'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import actionBuilder from 'action-builder'
import api from 'api'

export let sepalMap = null
export let google = null
export let googleMap = null

const contextById = {}
let currentContextId

const initListeners = []
const onInit = listener => {
    if (google)
        listener(google)
    else
        initListeners.push(listener)
}

export const initGoogleMapsApi$ = () => {
    const loadGoogleMapsApiKey$ =
        api.map.loadApiKey$().pipe(
            map(({apiKey}) => apiKey)
        )

    const loadGoogleMapsApi$ = apiKey => Observable.create(observer => {
        GoogleMapsLoader.KEY = apiKey
        GoogleMapsLoader.VERSION = '3.35'
        GoogleMapsLoader.LIBRARIES = ['drawing']
        GoogleMapsLoader.load(g => {
            google = g
            initListeners.forEach(listener => listener(google))
            observer.next(apiKey)
            observer.complete()
        })
    })

    return loadGoogleMapsApiKey$.pipe(
        mergeMap(loadGoogleMapsApi$),
        map(apiKey => actionBuilder('SET_GOOGLE_MAPS_API_INITIALIZED', {apiKey: apiKey})
            .set('map.apiKey', apiKey)
            .build()
        )
    )
}

const createMap = mapElement => {
    const mapOptions = {
        zoom: 3,
        minZoom: 3,
        maxZoom: 17,
        center: new google.maps.LatLng(16.7794913, 9.6771556),
        mapTypeId: google.maps.MapTypeId.ROADMAP,
        zoomControl: false,
        mapTypeControl: false,
        scaleControl: false,
        streetViewControl: false,
        rotateControl: false,
        fullscreenControl: false,
        backgroundColor: '#131314',
        gestureHandling: 'greedy'
    }

    // https://developers.google.com/maps/documentation/javascript/style-reference
    const sepalStyle = new google.maps.StyledMapType([
        {stylers: [{visibility: 'simplified'}]},
        {stylers: [{color: '#131314'}]},
        {featureType: 'transit.station', stylers: [{visibility: 'off'}]},
        {featureType: 'poi', stylers: [{visibility: 'off'}]},
        {featureType: 'water', stylers: [{color: '#191919'}, {lightness: 4}]},
        {elementType: 'labels.text.fill', stylers: [{visibility: 'off'}, {lightness: 25}]}
    ], {name: 'sepalMap'})

    googleMap = new google.maps.Map(mapElement, mapOptions)
    googleMap.mapTypes.set('sepalStyle', sepalStyle)
    googleMap.setMapTypeId('sepalStyle')

    googleMap.addListener('zoom_changed', () =>
        actionBuilder('SET_MAP_ZOOM')
            .set('map.zoom', googleMap.getZoom())
            .dispatch()
    )

    const drawingOptions = {
        fillColor: '#FBFAF2',
        fillOpacity: 0.07,
        strokeColor: '#c5b397',
        strokeOpacity: 1,
        strokeWeight: 2,
        clickable: false,
        editable: false,
        zIndex: 1
    }

    sepalMap = {
        getKey() {
            return GoogleMapsLoader.KEY
        },
        getZoom() {
            return googleMap.getZoom()
        },
        setZoom(zoom) {
            return googleMap.setZoom(zoom)
        },
        zoomIn() {
            googleMap.setZoom(googleMap.getZoom() + 1)
        },
        zoomOut() {
            googleMap.setZoom(googleMap.getZoom() - 1)
        },
        isMaxZoom() {
            return googleMap.getZoom() === googleMap.maxZoom
        },
        isMinZoom() {
            return googleMap.getZoom() === googleMap.minZoom
        },
        fitBounds(bounds) {
            const googleBounds = toGoogleBounds(bounds)
            const boundsChanged = !googleMap.getBounds().equals(googleBounds)
            if (boundsChanged)
                googleMap.fitBounds(googleBounds)
        },
        getBounds() {
            return fromGoogleBounds(googleMap.getBounds())
        },
        onBoundsChanged(listener) {
            return googleMap.addListener('bounds_changed', listener)
        },
        addListener(mapObject, event, listener) {
            return google.maps.event.addListener(mapObject, event, listener)
        },
        removeListener(listener) {
            if (listener)
                google.maps.event.removeListener(listener)
        },
        getContext(contextId) {
            let context = contextById[contextId]
            if (!context) {
                const layerById = {}
                const setZooming = zooming =>
                    actionBuilder('SET_MAP_ZOOMING')
                        .set(['map', contextId, 'zooming'], zooming)
                        .dispatch()
                context = {
                    setLayer({id, layer, destroy$ = NEVER, onInitialized, onError}) {
                        const existingLayer = layerById[id]
                        const unchanged = layer === existingLayer || (existingLayer && existingLayer.equals(layer))
                        if (unchanged)
                            return false
                        this.removeLayer(id)
                        if (layer) {
                            layerById[id] = layer
                            layer.__removed$ = new Subject()
                            layer.initialize$()
                                .pipe(
                                    takeUntil(destroy$),
                                    takeUntil(layer.__removed$)
                                )
                                .subscribe(
                                    () => {
                                        layer.__initialized__ = true
                                        currentContextId === contextId && layer.addToMap(googleMap)
                                        onInitialized && onInitialized(layer)
                                    },
                                    error => {
                                        if (onError)
                                            onError(error)
                                        else
                                            Notifications.error({message: msg('map.layer.error'), error})
                                    }
                                )
                        }
                        return true
                    },
                    hideLayer(id, hidden) {
                        const layer = layerById[id]
                        if (layer)
                            layer.hide(googleMap, hidden)
                    },
                    removeLayer(id) {
                        const layer = layerById[id]
                        if (!layer)
                            return
                        layer.__removed$.next()
                        if (currentContextId === contextId)
                            layer.removeFromMap(googleMap)
                        delete layerById[id]
                    },
                    isLayerInitialized(id) {
                        return !!(this.hasLayer(id) && layerById[id].__initialized__)
                    },
                    hasLayer(id) {
                        return !!layerById[id]
                    },
                    fitLayer(id) {
                        const layer = layerById[id]
                        if (layer && layer.bounds && currentContextId === contextId) {
                            const bounds = layer.bounds
                            sepalMap.fitBounds(bounds)
                        }
                    },
                    addToMap() {
                        Object.keys(layerById).forEach(id => {
                            const layer = layerById[id]
                            if (layer.__initialized__)
                                layer.addToMap(googleMap)
                        })
                    },
                    removeFromMap() {
                        Object.keys(layerById).forEach(id => layerById[id].removeFromMap(googleMap))
                    },
                    zoomArea() {
                        setZooming(true)
                        this._drawingManager = new google.maps.drawing.DrawingManager({
                            drawingMode: google.maps.drawing.OverlayType.RECTANGLE,
                            drawingControl: false,
                            rectangleOptions: drawingOptions
                        })
                        const drawingListener = e => {
                            const rectangle = e.overlay
                            rectangle.setMap(null)
                            googleMap.fitBounds(rectangle.bounds)
                            this.cancelZoomArea()
                        }
                        google.maps.event.addListener(this._drawingManager, 'overlaycomplete', drawingListener)
                        this._drawingManager.setMap(googleMap)

                    },
                    cancelZoomArea() {
                        setZooming(false)
                        this.disableDrawingMode()
                    },
                    isZooming() {
                        const zooming = select(['map', contextId, 'zooming'])
                        return !!zooming
                    },
                    drawPolygon(id, callback) {
                        this._drawingPolygon = {id, callback}
                        this._drawingManager = this._drawingManager || new google.maps.drawing.DrawingManager({
                            drawingMode: google.maps.drawing.OverlayType.POLYGON,
                            drawingControl: false,
                            drawingControlOptions: {
                                position: google.maps.ControlPosition.TOP_CENTER,
                                drawingModes: ['polygon']
                            },
                            circleOptions: drawingOptions,
                            polygonOptions: drawingOptions,
                            rectangleOptions: drawingOptions
                        })
                        const drawingListener = e => {
                            const polygon = e.overlay
                            polygon.setMap(null)
                            const toPolygonPath = polygon => polygon.getPaths().getArray()[0].getArray().map(latLng =>
                                [latLng.lng(), latLng.lat()]
                            )
                            callback(toPolygonPath(polygon))
                        }
                        google.maps.event.addListener(this._drawingManager, 'overlaycomplete', drawingListener)
                        this._drawingManager.setMap(googleMap)
                    },
                    pauseDrawingMode() {
                        if (this._drawingManager) {
                            this._drawingManager.setMap(null)
                            google.maps.event.clearListeners(this._drawingManager, 'overlaycomplete')
                        }
                    },
                    disableDrawingMode() {
                        this.pauseDrawingMode()
                        this._drawingPolygon = null
                    },
                }
                contextById[contextId] = context
            }
            return context
        },
        setContext(contextId) {
            if (currentContextId === contextId)
                return
            const prevContextId = currentContextId
            if (prevContextId) {
                const context = this.getContext(prevContextId)
                context.pauseDrawingMode()
                context.removeFromMap()
            }

            currentContextId = contextId
            if (contextId) {
                const context = this.getContext(contextId)
                context.addToMap()
                context._drawingPolygon && context.drawPolygon(context._drawingPolygon.id, context._drawingPolygon.callback)
                context.isZooming() && context.zoomArea()
            }

        },
        clearContext(contextId) {
            if (contextId === currentContextId)
                sepalMap.clear()
        },
        removeContext(contextId) {
            if (contextId === currentContextId) {
                sepalMap.clear()
            }
            delete contextById[contextId]
        },
        clear() {
            sepalMap.setContext()
        }
    }
}

export const fromGoogleBounds = googleBounds => {
    const sw = googleBounds.getSouthWest()
    const ne = googleBounds.getNorthEast()
    return [
        [sw.lng(), sw.lat()],
        [ne.lng(), ne.lat()]
    ]
}

const toGoogleBounds = bounds => {
    return new google.maps.LatLngBounds(
        {lng: bounds[0][0], lat: bounds[0][1]},
        {lng: bounds[1][0], lat: bounds[1][1]}
    )
}

export const polygonOptions = fill => ({
    fillColor: '#FBFAF2',
    fillOpacity: fill ? 0.07 : 0.000000000000000000000000000001,
    strokeColor: '#FBFAF2',
    strokeOpacity: 0.5,
    strokeWeight: 1
})

const mapStateToProps = () => ({
    apiKey: select('map.apiKey')
})

class Map extends React.Component {
    state = {initialized: false}
    mapElement = React.createRef()

    render() {
        return

    }

    componentDidUpdate() {
        const apiKey = this.props.apiKey
        if (apiKey && !this.state.initialized) { // Create map once there is an API key
            this.setState(prevState => ({...prevState, initialized: true}))
            createMap(this.mapElement.current)
            this.initialized = true
        }
    }
}

Map.propTypes = {
    className: PropTypes.string
}

export default connect(mapStateToProps)(Map)

export class MapLayer extends React.Component {
    state = {
        shown:
            false, projection: null
    }

    constructor(props) {
        super(props)
        this.overlay = new ReactOverlayView(this)
        this.overlay.setMap(googleMap)
    }

    render() {
        const {shown, projection} = this.state
        const {className, children} = this.props
        const mapPanes = this.overlay.getPanes()
        const content = (

                <ProjectionContext.Provider value={projection}>
                    {children}
                </ProjectionContext.Provider>

        )
        return shown && mapPanes
            ? <Portal content={content} container={mapPanes.overlayMouseTarget}/>
            : null
    }

    componentWillUnmount() {
        this.overlay.setMap(null)
    }
}

class WrappedMapObject extends React.Component {
    render() {
        const {lat, lng, width, height, className, children} = this.props
        const shown = googleMap.getBounds().contains({lng, lat})
        if (!shown)
            return null
        return (
            <ProjectionContext.Consumer>
                {projection => {
                    if (!projection)
                        return null
                    const point = projection.fromLatLngToDivPixel(new google.maps.LatLng(lat, lng))
                    const style = {
                        position: 'absolute',
                        top: `calc(${point.y}px - ${height} / 2)`,
                        left: `calc(${point.x}px - ${width} / 2)`
                    }
                    return

                        {children}

                }
                }
            </ProjectionContext.Consumer>
        )
    }

    componentWillUnmount() {

    }
}

export const MapObject = connect(state => ({projectionChange: state.map.projectionChange}))(WrappedMapObject)

const ProjectionContext = React.createContext()

let ReactOverlayView
onInit(google =>
    ReactOverlayView = class ReactOverlayView extends google.maps.OverlayView {
        constructor(component) {
            super()
            this.component = component
            this.xyz = null

        }

        onAdd() {
            this.show(true)
        }

        draw() {
            const projection = this.getProjection() // TODO: Zooming changes the projection...
            const point = projection.fromLatLngToDivPixel(new google.maps.LatLng(0, 0))
            const xyz = [point.x, point.y, sepalMap.getZoom()]
            if (!_.isEqual(this.xyz, xyz)) {
                this.xyz = xyz
                this.component.setState(prevState => ({...prevState, projection}))
                actionBuilder('PROJECTION_CHANGED', {xyz})
                    .set('map.projectionChange', xyz)
                    .dispatch()
            }
        }

        onRemove() {
            this.show(false)
            google.maps.event.removeListener(this.projectionChangeListener)
        }

        show(shown) {
            this.component.setState(prevState => ({...prevState, shown: shown}))
        }
    })
<|endoftext|>


<commit_before>import {Input} from 'widget/input'
import {ScrollableList} from 'widget/list'
import {Shape} from 'widget/shape'
import {Subject, merge} from 'rxjs'
import {compose} from 'compose'
import {debounceTime, distinctUntilChanged, filter} from 'rxjs/operators'
import FloatingBox from 'widget/floatingBox'
import Keybinding from 'widget/keybinding'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import escapeStringRegexp from 'escape-string-regexp'
import styles from './searchBox.module.css'
import withSubscriptions from 'subscription'

class _SearchBox extends React.Component {
    inputRef = React.createRef()
    containerRef = React.createRef()
    listRef = React.createRef()

    search$ = new Subject()
    select$ = new Subject()
    showOptions$ = new Subject()

    state = {
        value: '',
        showOptions: true
    }

    constructor() {
        super()
        this.showOptions = this.showOptions.bind(this)
        this.hideOptions = this.hideOptions.bind(this)
        this.selectOption = this.selectOption.bind(this)
    }

    render() {
        const {placeholder, className} = this.props
        const {value, showOptions} = this.state
        return (
            <Keybinding keymap={{
                Escape: () => this.setValue(''),
                ArrowDown: () => this.showOptions()
            }}>
                <Shape
                    look='transparent'
                    size='large'
                    ref={this.containerRef}
                    shape='pill'>
                    <Input
                        className={[styles.search, className].join(' ')}
                        type='search'
                        ref={this.inputRef}
                        value={value}
                        placeholder={placeholder}
                        autoFocus
                        border={false}
                        onFocus={this.showOptions}
                        onClick={this.showOptions}
                        onChange={e => this.setValue(e.target.value)}
                    />
                </Shape>
                {showOptions ? this.renderOptions() : null}
            </Keybinding>
        )
    }

    renderOptions() {
        const {placement, options, optionsClassName, optionTooltipPlacement, alignment} = this.props
        return options && options.length
            ? (
                <FloatingBox
                    element={this.containerRef.current}
                    placement={placement}
                    onBlur={this.hideOptions}>
                    <ScrollableList
                        air='more'
                        className={optionsClassName || styles.options}
                        options={options}
                        onSelect={this.selectOption}
                        onCancel={this.hideOptions}
                        tooltipPlacement={optionTooltipPlacement}
                        autoHighlight
                        keyboard
                        alignment={alignment}
                    />
                </FloatingBox>
            )
            : null
    }

    showOptions() {
        this.showOptions$.next(true)
    }

    hideOptions() {
        this.showOptions$.next(false)
    }

    selectOption(option) {
        const {onSelect} = this.props
        onSelect && onSelect(option)
        this.hideOptions()
    }

    setValue(value) {
        this.setState({value})
        this.search$.next(value)
        this.showOptions()
    }

    componentDidMount() {
        const {onSearchValue, onSearchValues, debounce, addSubscription} = this.props
        const {search$, showOptions$} = this

        const debouncedShowOptions$ = merge(
            showOptions$.pipe(
                filter(show => !show)
            ),
            showOptions$.pipe(
                debounceTime(250),
            )
        ).pipe(
            distinctUntilChanged()
        )

        const debouncedSearch$ = merge(
            search$.pipe(
                filter(value => !value) // skip debouncing when empty
            ),
            search$.pipe(
                debounceTime(debounce)
            )
        ).pipe(
            distinctUntilChanged()
        )

        addSubscription(
            debouncedShowOptions$.subscribe(
                showOptions => this.setState({showOptions})
            ),
            debouncedSearch$.subscribe(
                value => {
                    onSearchValue && onSearchValue(value)
                    onSearchValues && onSearchValues(
                        _.chain(value.split(/\s+/))
                            .map(filter => escapeStringRegexp(filter.trim()))
                            .compact()
                            .value()
                    )
                }
            )
        )
    }
}

export const SearchBox = compose(
    _SearchBox,
    withSubscriptions()
)

SearchBox.propTypes = {
    className: PropTypes.string,
    debounce: PropTypes.number,
    options: PropTypes.array,
    optionsClassName: PropTypes.string,
    optionTooltipPlacement: PropTypes.string,
    placeholder: PropTypes.string,
    shape: PropTypes.string,
    value: PropTypes.string,
    onSearchValue: PropTypes.func,
    onSearchValues: PropTypes.func,
    onSelect: PropTypes.func
}

SearchBox.defaultProps = {
    debounce: 250
}
<commit_msg>Set initial value when mounting search box.<commit_after>import {Input} from 'widget/input'
import {ScrollableList} from 'widget/list'
import {Shape} from 'widget/shape'
import {Subject, merge} from 'rxjs'
import {compose} from 'compose'
import {debounceTime, distinctUntilChanged, filter} from 'rxjs/operators'
import FloatingBox from 'widget/floatingBox'
import Keybinding from 'widget/keybinding'
import PropTypes from 'prop-types'
import React from 'react'
import _ from 'lodash'
import escapeStringRegexp from 'escape-string-regexp'
import styles from './searchBox.module.css'
import withSubscriptions from 'subscription'

class _SearchBox extends React.Component {
    inputRef = React.createRef()
    containerRef = React.createRef()
    listRef = React.createRef()

    search$ = new Subject()
    select$ = new Subject()
    showOptions$ = new Subject()

    state = {
        value: '',
        showOptions: true
    }

    constructor() {
        super()
        this.showOptions = this.showOptions.bind(this)
        this.hideOptions = this.hideOptions.bind(this)
        this.selectOption = this.selectOption.bind(this)
    }

    render() {
        const {placeholder, className} = this.props
        const {value, showOptions} = this.state
        return (
            <Keybinding keymap={{
                Escape: () => this.setValue(''),
                ArrowDown: () => this.showOptions()
            }}>
                <Shape
                    look='transparent'
                    size='large'
                    ref={this.containerRef}
                    shape='pill'>
                    <Input
                        className={[styles.search, className].join(' ')}
                        type='search'
                        ref={this.inputRef}
                        value={value}
                        placeholder={placeholder}
                        autoFocus
                        border={false}
                        onFocus={this.showOptions}
                        onClick={this.showOptions}
                        onChange={e => this.setValue(e.target.value)}
                    />
                </Shape>
                {showOptions ? this.renderOptions() : null}
            </Keybinding>
        )
    }

    renderOptions() {
        const {placement, options, optionsClassName, optionTooltipPlacement, alignment} = this.props
        return options && options.length
            ? (
                <FloatingBox
                    element={this.containerRef.current}
                    placement={placement}
                    onBlur={this.hideOptions}>
                    <ScrollableList
                        air='more'
                        className={optionsClassName || styles.options}
                        options={options}
                        onSelect={this.selectOption}
                        onCancel={this.hideOptions}
                        tooltipPlacement={optionTooltipPlacement}
                        autoHighlight
                        keyboard
                        alignment={alignment}
                    />
                </FloatingBox>
            )
            : null
    }

    showOptions() {
        this.showOptions$.next(true)
    }

    hideOptions() {
        this.showOptions$.next(false)
    }

    selectOption(option) {
        const {onSelect} = this.props
        onSelect && onSelect(option)
        this.hideOptions()
    }

    setValue(value) {
        this.setState({value})
        this.search$.next(value)
        this.showOptions()
    }

    componentDidMount() {
        const {value, onSearchValue, onSearchValues, debounce, addSubscription} = this.props
        const {search$, showOptions$} = this
        this.setValue(value)

        const debouncedShowOptions$ = merge(
            showOptions$.pipe(
                filter(show => !show)
            ),
            showOptions$.pipe(
                debounceTime(250),
            )
        ).pipe(
            distinctUntilChanged()
        )

        const debouncedSearch$ = merge(
            search$.pipe(
                filter(value => !value) // skip debouncing when empty
            ),
            search$.pipe(
                debounceTime(debounce)
            )
        ).pipe(
            distinctUntilChanged()
        )

        addSubscription(
            debouncedShowOptions$.subscribe(
                showOptions => this.setState({showOptions})
            ),
            debouncedSearch$.subscribe(
                value => {
                    onSearchValue && onSearchValue(value)
                    onSearchValues && onSearchValues(
                        _.chain(value.split(/\s+/))
                            .map(filter => escapeStringRegexp(filter.trim()))
                            .compact()
                            .value()
                    )
                }
            )
        )
    }
}

export const SearchBox = compose(
    _SearchBox,
    withSubscriptions()
)

SearchBox.propTypes = {
    className: PropTypes.string,
    debounce: PropTypes.number,
    options: PropTypes.array,
    optionsClassName: PropTypes.string,
    optionTooltipPlacement: PropTypes.string,
    placeholder: PropTypes.string,
    shape: PropTypes.string,
    value: PropTypes.string,
    onSearchValue: PropTypes.func,
    onSearchValues: PropTypes.func,
    onSelect: PropTypes.func
}

SearchBox.defaultProps = {
    debounce: 250
}
<|endoftext|>


<commit_before>/* tooltip js
----------------------------------------------------------

@package:	Zotonic 2009
@Author: 	Tim Benniks <tim@timbenniks.nl>

Copyright 2009 Tim Benniks

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

---------------------------------------------------------- */

$.widget("ui.tooltip",
{
	_init: function()
	{
		self = this;
		obj  = this.element;

		if(this.title == '')
		{
			obj.unbind(self.options.inevent, self.options.outevent);
			return false;
		}

		obj.bind(self.options.inevent, function(e)
		{
			this.tip 		= this.title;
			var tip_content = this.tip;
			this.title 		= "";

			tip = $('

')
					.addClass('tooltip')
					.html(tip_content)
					.css({top: e.pageY + self.options.offsetY, left: e.pageX + self.options.offsetX, width: self.options.width, maxWidth: self.options.maxwidth });

			$(document.body).append(tip);

			var left = $(this).position().left;
			var top  = $(this).position().top;

			tip.css({top: top - 30});

			if(left + tip.width() > $(window).width())
			{
				tip.css({left: Math.ceil(left) - Math.ceil((obj.width() / 2)) - Math.ceil((tip.width() / 2 ))});
			}
			else
			{
				tip.css({left: Math.ceil(left) + Math.ceil((obj.width() / 2)) - Math.ceil((tip.width() / 2 ))});
			}

			tip.stop().animate({opacity: 'show'}, 200);
		});

		obj.bind(self.options.outevent, function(e)
		{
			tip.stop().animate({opacity: 'show'}, 200, function()
			{
				$(this).remove();
			});

			self.destroy();
			this.title = this.tip;
		});
	},
	destroy: function()
	{
		obj.unbind(self.options.inevent, self.options.outevent);
		$(document).unbind('mousemove');
	}
});

$.fn.tooltip.destroy = function()
{
	$('.tooltip').tooltip('destroy');
}

$.ui.tooltip.defaults = {
	offsetY: 	0,
	offsetX: 	0,
	inevent: 	'mouseover',
	outevent: 	'mouseout',
	width: 		'auto',
	maxwidth: 	'330px'
}<commit_msg>Fixed for Opera<commit_after>/* tooltip js
----------------------------------------------------------

@package:	Zotonic 2009
@Author: 	Tim Benniks <tim@timbenniks.nl>

Copyright 2009 Tim Benniks

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

---------------------------------------------------------- */

$.widget("ui.tooltip",
{
	_init: function()
	{
		var self = this;
		var obj  = this.element;
		var tip  = undefined;

		if(this.title == '')
		{
			obj.unbind(self.options.inevent, self.options.outevent);
			return false;
		}

		obj.bind(self.options.inevent, function(e)
		{
			this.tip 		= this.title;
			var tip_content = this.tip;
			this.title 		= "";

			tip = $('

')
					.addClass('tooltip')
					.html(tip_content)
					.css({top: e.pageY + self.options.offsetY, left: e.pageX + self.options.offsetX, width: self.options.width, maxWidth: self.options.maxwidth });

			$(document.body).append(tip);

			var left = $(this).position().left;
			var top  = $(this).position().top;

			tip.css({top: top - 30});

			if(left + tip.width() > $(window).width())
			{
				tip.css({left: Math.ceil(left) - Math.ceil((obj.width() / 2)) - Math.ceil((tip.width() / 2 ))});
			}
			else
			{
				tip.css({left: Math.ceil(left) + Math.ceil((obj.width() / 2)) - Math.ceil((tip.width() / 2 ))});
			}

			tip.stop().animate({opacity: 'show'}, 200);
		});

		obj.bind(self.options.outevent, function(e)
		{
			tip.stop().animate({opacity: 'show'}, 200, function()
			{
				$(this).remove();
			});

			self.destroy();
			this.title = this.tip;
		});
	},
	destroy: function()
	{
	    var self = this;
		var obj  = this.element;

		obj.unbind(self.options.inevent, self.options.outevent);
		$(document).unbind('mousemove');
	}
});

$.fn.tooltip.destroy = function()
{
	$('.tooltip').tooltip('destroy');
}

$.ui.tooltip.defaults = {
	offsetY: 	0,
	offsetX: 	0,
	inevent: 	'mouseover',
	outevent: 	'mouseout',
	width: 		'auto',
	maxwidth: 	'330px'
}<|endoftext|>


<commit_before>import { HtmlService, ClipboardService } from './scripts/services';
import { Selectors } from './scripts/constants';
import $ from 'jquery';
import './scripts/contents.js';

// chrome.runtime.onMessage.addListener((request, sender, response) => {
//     if (request.action === 'reload') {
//         console.log('Page content changed. Reloading copy to clipboard.');
//         HtmlService();
//         ClipboardService();
//     }
// });

$(document).ready(() => {
    console.log('Initializing copy to clipboard...');
    HtmlService();
    ClipboardService();
});
<commit_msg>Fixed issue where content is not consistently injected to page.<commit_after>import { HtmlService, ClipboardService } from './scripts/services';
import { Selectors } from './scripts/constants';
import $ from 'jquery';
import './scripts/contents.js';

// chrome.runtime.onMessage.addListener((request, sender, response) => {
//     if (request.action === 'reload') {
//         console.log('Page content changed. Reloading copy to clipboard.');
//         HtmlService();
//         ClipboardService();
//     }
// });

$(document).ready(() => {
    console.log('Initializing copy to clipboard... XXX');

    setTimeout(() => {
        HtmlService();
        ClipboardService();
    }, 1000);

});
<|endoftext|>


<commit_before>// Generic function to make an AJAX call
var getJSON = function(dataURL, successCallback, errorCallback) {
    // Return the $.ajax promise
    return $.ajax({
      type: 'GET',
      dataType: 'jsonp',
      url: dataURL,
      success: successCallback,
      error: errorCallback
    });
}

$(document).ready(function() {

  var position = navigator.geolocation.getCurrentPosition(geolocate),
      marineURL = 'https://api.darksky.net/forecast/5dee4e51a4b71918ffe3ad18e83db386/';

  function geolocate(pos) {
    console.log(pos);
    console.log(marineURL + pos.coords.latitude + ',' + pos.coords.longitude);
    return marineURL + pos.coords;
  }

  getJSON(position, loadMarine);

  function loadMarine(data) {
    console.log(data);
    $('#marineforecast').html(data.main.humidity);
  }
});

(function getWeather() {
  var weatherDOM = $('#openweathermap'),
      metric = false;

  $.ajax({
    type: 'GET',
    url: '//api.openweathermap.org/data/2.5/weather?id=5847155&appid=9d7e64f21bfc706e679ffc6cd895b306',
    dataType: 'jsonp',
    success: function(data) {
      console.log(data);

      if (metric === false) {
      	// define temperature as fahrenheit
      	var temperature = Math.round(((data.main.temp - 273.15) * 1.8) + 32) + '°F';
      	var minTemperature = Math.round(((data.main.temp_min - 273.15) * 1.8) + 32) + '°F';
      	var maxTemperature = Math.round(((data.main.temp_max - 273.15) * 1.8) + 32) + '°F';
      } else {
      	// define temperature as celsius
      	var temperature = Math.round(data.main.temp - 273.15) + '°C';
      	var minTemperature = Math.round(data.main.temp_min - 273.15) + '°C';
      	var maxTemperature = Math.round(data.main.temp_max - 273.15) + '°C';
      }
      // TODO apply KO bindings
      // set temperature
      weatherDOM.html(temperature);
    },
    error: function(jqXHR, textStatus, errorThrown) {
      console.warn(jqXHR + textStatus);
      weatherDOM.html(errorThrown);
  	}
  });
})();
<commit_msg>darksky api working<commit_after>(function getWeather() {
  var weatherDOM = $('#openweathermap'),
      marineDOM = $('#marineforecast'),
      metric = false;

  $.ajax({
    type: 'GET',
    url: '//api.openweathermap.org/data/2.5/weather?id=5847155&appid=9d7e64f21bfc706e679ffc6cd895b306',
    dataType: 'jsonp',
    success: function(data) {
      console.log(data);

      if (metric === false) {
      	// define temperature as fahrenheit
      	var temperature = Math.round(((data.main.temp - 273.15) * 1.8) + 32) + '°F';
      	var minTemperature = Math.round(((data.main.temp_min - 273.15) * 1.8) + 32) + '°F';
      	var maxTemperature = Math.round(((data.main.temp_max - 273.15) * 1.8) + 32) + '°F';
      } else {
      	// define temperature as celsius
      	var temperature = Math.round(data.main.temp - 273.15) + '°C';
      	var minTemperature = Math.round(data.main.temp_min - 273.15) + '°C';
      	var maxTemperature = Math.round(data.main.temp_max - 273.15) + '°C';
      }
      // TODO apply KO bindings
      // set temperature
      weatherDOM.html(temperature);
    },
    error: function(jqXHR, textStatus, errorThrown) {
      console.warn(jqXHR + textStatus);
      weatherDOM.html(errorThrown);
  	}
  });

  $.ajax({
    type: 'GET',
    url: 'https://api.darksky.net/forecast/5dee4e51a4b71918ffe3ad18e83db386/58.109190,-135.444947',
    dataType: 'jsonp',
    success: function(data) {
      console.log(data);
      marineDOM.html(data.currently.summary);
    },
    error: function(jqXHR, textStatus, errorThrown) {
      console.warn(jqXHR + textStatus);
      marineDOM.html(errorThrown);
  	}
  });
})();
<|endoftext|>


<commit_before>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Oobe network screen implementation.
 */

login.createScreen('NetworkScreen', 'connect', function() {
  return {
    EXTERNAL_API: [
      'enableContinueButton',
      'setTimezone',
      'showError'
    ],

    /**
     * Dropdown element for networks selection.
     */
    dropdown_: null,

    /** @override */
    decorate: function() {
      Oobe.setupSelect($('language-select'),
                       loadTimeData.getValue('languageList'),
                       'networkOnLanguageChanged');

      Oobe.setupSelect($('keyboard-select'),
                       loadTimeData.getValue('inputMethodsList'),
                       'networkOnInputMethodChanged');

      Oobe.setupSelect($('timezone-select'),
                       loadTimeData.getValue('timezoneList'),
                       'networkOnTimezoneChanged');

      this.dropdown_ = $('networks-list');
      cr.ui.DropDown.decorate(this.dropdown_);
    },

    onBeforeShow: function(data) {
      cr.ui.DropDown.show('networks-list', true, -1);
    },

    onBeforeHide: function() {
      cr.ui.DropDown.hide('networks-list');
      this.enableContinueButton(false);
    },

    /**
     * Header text of the screen.
     * @type {string}
     */
    get header() {
      return loadTimeData.getString('networkScreenTitle');
    },

    /**
     * Buttons in oobe wizard's button strip.
     * @type {array} Array of Buttons.
     */
    get buttons() {
      var buttons = [];

      var continueButton = this.ownerDocument.createElement('button');
      continueButton.disabled = true;
      continueButton.id = 'continue-button';
      continueButton.textContent = loadTimeData.getString('continueButton');
      continueButton.classList.add('preserve-disabled-state');
      continueButton.addEventListener('click', function(e) {
        chrome.send('networkOnExit');
        e.stopPropagation();
      });
      buttons.push(continueButton);

      return buttons;
    },

    /**
     * Returns a control which should receive an initial focus.
     */
    get defaultControl() {
      return $('language-select');
    },

    /**
     * Enables/disables continue button.
     * @param {boolean} enable Should the button be enabled?
     */
    enableContinueButton: function(enable) {
      $('continue-button').disabled = !enable;
    },

    /**
     * Sets the current timezone.
     * @param {string} timezoneId The timezone ID to select.
     */
    setTimezone: function(timezoneId) {
      $('timezone-select').value = timezoneId;
    },

    /**
     * Shows the network error message.
     * @param {string} message Message to be shown.
     */
    showError: function(message) {
      var error = document.createElement('div');
      var messageDiv = document.createElement('div');
      messageDiv.className = 'error-message-bubble';
      messageDiv.textContent = message;
      error.appendChild(messageDiv);

      $('bubble').showContentForElement($('networks-list'),
                                        cr.ui.Bubble.Attachment.BOTTOM,
                                        error);
    }
  };
});

<commit_msg>Network alert echoed in ChromeVox.<commit_after>// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview Oobe network screen implementation.
 */

login.createScreen('NetworkScreen', 'connect', function() {
  return {
    EXTERNAL_API: [
      'enableContinueButton',
      'setTimezone',
      'showError'
    ],

    /**
     * Dropdown element for networks selection.
     */
    dropdown_: null,

    /** @override */
    decorate: function() {
      Oobe.setupSelect($('language-select'),
                       loadTimeData.getValue('languageList'),
                       'networkOnLanguageChanged');

      Oobe.setupSelect($('keyboard-select'),
                       loadTimeData.getValue('inputMethodsList'),
                       'networkOnInputMethodChanged');

      Oobe.setupSelect($('timezone-select'),
                       loadTimeData.getValue('timezoneList'),
                       'networkOnTimezoneChanged');

      this.dropdown_ = $('networks-list');
      cr.ui.DropDown.decorate(this.dropdown_);
    },

    onBeforeShow: function(data) {
      cr.ui.DropDown.show('networks-list', true, -1);
    },

    onBeforeHide: function() {
      cr.ui.DropDown.hide('networks-list');
      this.enableContinueButton(false);
    },

    /**
     * Header text of the screen.
     * @type {string}
     */
    get header() {
      return loadTimeData.getString('networkScreenTitle');
    },

    /**
     * Buttons in oobe wizard's button strip.
     * @type {array} Array of Buttons.
     */
    get buttons() {
      var buttons = [];

      var continueButton = this.ownerDocument.createElement('button');
      continueButton.disabled = true;
      continueButton.id = 'continue-button';
      continueButton.textContent = loadTimeData.getString('continueButton');
      continueButton.classList.add('preserve-disabled-state');
      continueButton.addEventListener('click', function(e) {
        chrome.send('networkOnExit');
        e.stopPropagation();
      });
      buttons.push(continueButton);

      return buttons;
    },

    /**
     * Returns a control which should receive an initial focus.
     */
    get defaultControl() {
      return $('language-select');
    },

    /**
     * Enables/disables continue button.
     * @param {boolean} enable Should the button be enabled?
     */
    enableContinueButton: function(enable) {
      $('continue-button').disabled = !enable;
    },

    /**
     * Sets the current timezone.
     * @param {string} timezoneId The timezone ID to select.
     */
    setTimezone: function(timezoneId) {
      $('timezone-select').value = timezoneId;
    },

    /**
     * Shows the network error message.
     * @param {string} message Message to be shown.
     */
    showError: function(message) {
      var error = document.createElement('div');
      var messageDiv = document.createElement('div');
      messageDiv.className = 'error-message-bubble';
      messageDiv.textContent = message;
      error.appendChild(messageDiv);
      error.setAttribute('role', 'alert');

      $('bubble').showContentForElement($('networks-list'),
                                        cr.ui.Bubble.Attachment.BOTTOM,
                                        error);
    }
  };
});

<|endoftext|>


<commit_before>/**
 * Manage files associated with datasets. When uploading a file creates a new dataset
 * @param collectionId
 * @param data
 * @param field - JSON data key ('versions')
 * @param idField - HTML id for the template ('version' or 'correction')
 */

function editDatasetVersion(collectionId, data, field, idField) {
  var downloadExtensions, uriUpload, file;
  var lastIndex;
  if (data[field]) {
    lastIndex = data[field].length;
  } else {
    lastIndex = 0;
    data[field] = [];
  }
  var uploadedNotSaved = {uploaded: false, saved: false, fileUrl: "", oldLabel: data.description.versionLabel};
  $(".workspace-edit").scrollTop(Florence.globalVars.pagePos);
  //Add
  if (data.type === 'timeseries_dataset') {
    downloadExtensions = /\.csdb$/;
  } else if (data.type === 'dataset') {
    downloadExtensions = /\.csv$|.xls$|.zip$/;
  }

  var ajaxRequest = [];
  var templateData = $.extend(true, {}, data);
  $(templateData[field]).each(function (index, version) {
    var dfd = $.Deferred();
    if (version.correctionNotice) {
      templateData[field][index].type = true;
    } else {
      templateData[field][index].type = false;
    }
    templateData[field][index].label = version.label;
    dfd.resolve();
    ajaxRequest.push(dfd);
  });

  $.when.apply($, ajaxRequest).then(function () {
    var html = templates.editorCorrection({idField: idField});
    $('#' + idField).replaceWith(html);
    initialiseDatasetVersion(collectionId, data, templateData, field, idField);
  });

  $("#add-" + idField).one('click', function () {
    addTheVersion();
  });

  function addTheVersion() {
    var position = $(".workspace-edit").scrollTop();
    Florence.globalVars.pagePos = position + 200;

    // todo: Move this HTML into a handlebars template.
    $('#' + idField + '-section').append(
      '
' +
      '  <form id="UploadForm">' +
      '    <textarea class="auto-size" type="text" placeholder="Add a label here (E.g. Revised, Final, etc" id="label"></textarea>' +
      '    <input type="file" title="Select a file and click Submit" name="files">' +
      '
' +
      '    <button type="submit" form="UploadForm" value="submit">Submit</button>' +
      '    <button class="btn-page-cancel" id="file-cancel">Cancel</button>' +
      '  </form>' +
      '

' +
      '

' +
      '
');

    // The label field is not used for corrections, just use existing version label.
    if (idField === "correction") {
      var $versionLabel = $('#UploadForm #label')
      $versionLabel.text(uploadedNotSaved.oldLabel);
      $versionLabel.hide();
    }

    $('#file-cancel').one('click', function (e) {
      e.preventDefault();
      $('#' + lastIndex).remove();
      if (uploadedNotSaved.uploaded === true && uploadedNotSaved.saved === false) {
        data.description.versionLabel = uploadedNotSaved.oldLabel;
        deleteContent(collectionId, uploadedNotSaved.fileUrl,
          onSuccess = function () {
          },
          onError = function (error) {
            handleApiError(error);
          }
        );
      }
      initialiseDatasetVersion(collectionId, data, templateData, field, idField);
    });

    $('#UploadForm').submit(function (e) {
      e.preventDefault();
      e.stopImmediatePropagation();

      var formdata = new FormData();

      function showUploadedItem(source) {
        $('#list').append(source);
      }

      var versionLabel = this[0].value;
      file = this[1].files[0];
      if (!file) {
        sweetAlert('Please select a file to upload');
        return;
      }

      document.getElementById("response").innerHTML = "Uploading . . .";

      var fileNameNoSpace = file.name.replace(/\s*/g, "").toLowerCase();
      uriUpload = data.uri + '/' + fileNameNoSpace;
      var safeUriUpload = checkPathSlashes(uriUpload);

      if (!!file.name.match(downloadExtensions)) {
        showUploadedItem(fileNameNoSpace);
        if (formdata) {
          formdata.append("name", file);
        }
      } else {
        sweetAlert('This file type is not supported');
        $('#' + lastIndex).remove();
        editDatasetVersion(collectionId, data, field, idField);
        return;
      }

      if (formdata) {
        $.ajax({
          url: "/zebedee/content/" + collectionId + "?uri=" + safeUriUpload,
          type: 'POST',
          data: formdata,
          cache: false,
          processData: false,
          contentType: false,
          success: function () {
            document.getElementById("response").innerHTML = "File uploaded successfully";
            uploadedNotSaved.uploaded = true;
            uploadedNotSaved.fileUrl = safeUriUpload;
            // create the new version/correction
            saveNewCorrection(collectionId, data.uri,
              function (response) {
                var tmpDate = Florence.collection.date ? Florence.collection.date : (new Date()).toISOString();
                if (idField === "correction") {
                  data[field].push({
                    correctionNotice: " ",
                    updateDate: tmpDate,
                    uri: response,
                    label: versionLabel
                  });
                } else {
                  data[field].push({
                    correctionNotice: "",
                    updateDate: tmpDate,
                    uri: response,
                    label: versionLabel
                  });
                  data.description.versionLabel = versionLabel; // only update the version label for versions not corrections.
                }
                data.downloads = [{file: fileNameNoSpace}];
                uploadedNotSaved.saved = true;
                $("#" + idField).find('.edit-section__content').prepend('
');
                $("#" + idField + '-section').remove();
                saveDatasetVersion(collectionId, data.uri, data, field, idField);
              }, function (response) {
                if (response.status === 409) {
                  sweetAlert("You can add only one " + idField + " before publishing.");
                  deleteContent(collectionId, uploadedNotSaved.fileUrl);
                }
                else if (response.status === 404) {
                  sweetAlert("You can only add " + idField + "s to content that has been published.");
                  deleteContent(collectionId, uploadedNotSaved.fileUrl);
                }
                else {
                  handleApiError(response);
                }
              }
            );
          },
          error: function (response) {
            console.log("Error in uploading this file");
            handleApiError(response);
          }
        });
      }
    });
  }
}

function refreshDatasetVersion(collectionId, data, field, idField) {
  var ajaxRequest = [];
  var templateData = $.extend(true, {}, data);
  $(templateData[field]).each(function (index, version) {
    var dfd = $.Deferred();
    if (version.correctionNotice) {
      templateData[field][index].type = true;
    } else {
      templateData[field][index].type = false;
    }
    templateData[field][index].label = version.label;
    dfd.resolve();
    ajaxRequest.push(dfd);
  });

  $.when.apply($, ajaxRequest).then(function () {
    initialiseDatasetVersion(collectionId, data, templateData, field, idField);
  });

}

function initialiseDatasetVersion(collectionId, data, templateData, field, idField) {
  // Load
  var list = templateData[field];
  var correction;
  if (idField === 'correction') {
    correction = true;
  } else {
    correction = false;
  }
  var dataTemplate = {list: list, idField: idField, correction: correction};
  var html = templates.workEditT8VersionList(dataTemplate);
  $('#sortable-' + idField).replaceWith(html);
  $(data[field]).each(function (index) {
    dateTmp = data[field][index].updateDate;
    var dateTmpFormatted = $.datepicker.formatDate('dd MM yy', new Date(dateTmp));
    $('#' + idField + '-date_' + index).val(dateTmpFormatted).datepicker({dateFormat: 'dd MM yy'}).on('change', function () {
      data[field][index].updateDate = new Date($('#' + idField + '-date_' + index).datepicker('getDate')).toISOString();
      saveDatasetVersion(collectionId, data.uri, data, field, idField);
    });
    if (idField === 'correction') {
      $('#' + idField + '-edit_' + index).click(function () {
        var markdown = data[field][index].correctionNotice;
        var editedSectionValue = {title: 'Correction notice', markdown: markdown};
        var saveContent = function (updatedContent) {
          data[field][index].correctionNotice = updatedContent;
          saveDatasetVersion(collectionId, data.uri, data, field, idField);
        };
        loadMarkdownEditor(editedSectionValue, saveContent, data, 'notEmpty');
      });
    }
    $('#' + idField + '-edit-label_' + index).click(function () {
      var markdown = data[field][index].label ? data[field][index].label : "";
      var editedSectionValue = {title: 'Label content', markdown: markdown};
      var saveContent = function (updatedContent) {
        data[field][index].label = updatedContent;
        if (index === data[field].length - 1) {
          data.description.versionLabel = updatedContent;
        }
        saveDatasetVersion(collectionId, data.uri, data, field, idField);
      };
      loadMarkdownEditor(editedSectionValue, saveContent, data);
    });
    // Delete
    $('#' + idField + '-delete_' + index).click(function () {
      swal ({
        title: "Warning",
        text: "This will revert all changes you have made in this file. Are you sure you want to delete this " + idField + "?",
        type: "warning",
        showCancelButton: true,
        confirmButtonText: "Delete",
        cancelButtonText: "Cancel",
        closeOnConfirm: false
      }, function(result){
        if (result === true) {
          swal({
            title: "Deleted",
            text: "This " + idField + " version has been deleted",
            type: "success",
            timer: 2000
          });
          var pathToDelete = data.uri;
          var fileToDelete = pathToDelete + data.downloads[0].file;  //Saves always the latest
          var uriToDelete = $(this).parent().children('#' + idField + '-edition_' + index).attr(idField + '-url');
          deleteUnpublishedVersion(collectionId, uriToDelete, function () {
            var position = $(".workspace-edit").scrollTop();
            Florence.globalVars.pagePos = position;
            $(this).parent().remove();
            // delete uploaded file
            deleteContent(collectionId, fileToDelete, function () {
              console.log("File deleted");
            }, function (error) {
              handleApiError(error);
            });
            // delete modified data.json and revert to pubished
            deleteContent(collectionId, pathToDelete, function () {
              loadPageDataIntoEditor(pathToDelete, collectionId);
              refreshPreview(pathToDelete);
            }, function (error) {
              handleApiError(error);
            });
          }, function (response) {
            if (response.status === 404) {
              sweetAlert("You cannot delete a " + idField + " that has been published.");
            }
            else {
              handleApiError(response);
            }
          });
        }
      });
    });
  });
}

function saveDatasetVersion(collectionId, path, data, field, idField) {
  putContent(collectionId, path, JSON.stringify(data),
    function () {
      Florence.Editor.isDirty = false;
      refreshDatasetVersion(collectionId, data, field, idField);
      refreshPreview(path);
    },
    function (response) {
      if (response.status === 400) {
        sweetAlert("Cannot edit this page", "It is already part of another collection.");
      }
      else {
        handleApiError(response);
      }
    }
  );
}

<commit_msg>Change behaviour in dataset corrections to input the notice when it is created<commit_after>/**
 * Manage files associated with datasets. When uploading a file creates a new dataset
 * @param collectionId
 * @param data
 * @param field - JSON data key ('versions')
 * @param idField - HTML id for the template ('version' or 'correction')
 */

function editDatasetVersion(collectionId, data, field, idField) {
  var downloadExtensions, uriUpload, file;
  var lastIndex;
  if (data[field]) {
    lastIndex = data[field].length;
  } else {
    lastIndex = 0;
    data[field] = [];
  }
  var uploadedNotSaved = {uploaded: false, saved: false, fileUrl: "", oldLabel: data.description.versionLabel};
  $(".workspace-edit").scrollTop(Florence.globalVars.pagePos);
  //Add
  if (data.type === 'timeseries_dataset') {
    downloadExtensions = /\.csdb$/;
  } else if (data.type === 'dataset') {
    downloadExtensions = /\.csv$|.xls$|.zip$/;
  }

  var ajaxRequest = [];
  var templateData = $.extend(true, {}, data);
  $(templateData[field]).each(function (index, version) {
    var dfd = $.Deferred();
    if (version.correctionNotice) {
      templateData[field][index].type = true;
    } else {
      templateData[field][index].type = false;
    }
    templateData[field][index].label = version.label;
    dfd.resolve();
    ajaxRequest.push(dfd);
  });

  $.when.apply($, ajaxRequest).then(function () {
    var html = templates.editorCorrection({idField: idField});
    $('#' + idField).replaceWith(html);
    initialiseDatasetVersion(collectionId, data, templateData, field, idField);
  });

  $("#add-" + idField).one('click', function () {
    addTheVersion();
  });

  function addTheVersion() {
    var position = $(".workspace-edit").scrollTop();
    Florence.globalVars.pagePos = position + 200;

    // todo: Move this HTML into a handlebars template.
    $('#' + idField + '-section').append(
      '
' +
      '  <form id="UploadForm">' +
      '    <textarea class="auto-size" type="text" placeholder="Add a label here (E.g. Revised, Final, etc" id="label"></textarea>' +
      '    <input type="file" title="Select a file and click Submit" name="files">' +
      '
' +
      '    <button type="submit" form="UploadForm" value="submit">Submit</button>' +
      '    <button class="btn-page-cancel" id="file-cancel">Cancel</button>' +
      '  </form>' +
      '

' +
      '

' +
      '
');

    // The label field is not used for corrections, just use existing version label.
    if (idField === "correction") {
      var $versionLabel = $('#UploadForm #label')
      $versionLabel.text(uploadedNotSaved.oldLabel);
      $versionLabel.hide();
    }

    $('#file-cancel').one('click', function (e) {
      e.preventDefault();
      $('#' + lastIndex).remove();
      if (uploadedNotSaved.uploaded === true && uploadedNotSaved.saved === false) {
        data.description.versionLabel = uploadedNotSaved.oldLabel;
        deleteContent(collectionId, uploadedNotSaved.fileUrl,
          onSuccess = function () {
          },
          onError = function (error) {
            handleApiError(error);
          }
        );
      }
      initialiseDatasetVersion(collectionId, data, templateData, field, idField);
    });

    $('#UploadForm').submit(function (e) {
      e.preventDefault();
      e.stopImmediatePropagation();

      var formdata = new FormData();

      function showUploadedItem(source) {
        $('#list').append(source);
      }

      var versionLabel = this[0].value;
      file = this[1].files[0];
      if (!file) {
        sweetAlert('Please select a file to upload');
        return;
      }

      document.getElementById("response").innerHTML = "Uploading . . .";

      var fileNameNoSpace = file.name.replace(/\s*/g, "").toLowerCase();
      uriUpload = data.uri + '/' + fileNameNoSpace;
      var safeUriUpload = checkPathSlashes(uriUpload);

      if (!!file.name.match(downloadExtensions)) {
        showUploadedItem(fileNameNoSpace);
        if (formdata) {
          formdata.append("name", file);
        }
      } else {
        sweetAlert('This file type is not supported');
        $('#' + lastIndex).remove();
        editDatasetVersion(collectionId, data, field, idField);
        return;
      }

      if (formdata) {
        $.ajax({
          url: "/zebedee/content/" + collectionId + "?uri=" + safeUriUpload,
          type: 'POST',
          data: formdata,
          cache: false,
          processData: false,
          contentType: false,
          success: function () {
            document.getElementById("response").innerHTML = "File uploaded successfully";
            uploadedNotSaved.uploaded = true;
            uploadedNotSaved.fileUrl = safeUriUpload;
            // create the new version/correction
            saveNewCorrection(collectionId, data.uri,
              function (response) {
                var tmpDate = Florence.collection.publishDate ? Florence.collection.publishDate : (new Date()).toISOString();
                if (idField === "correction") {
                  data[field].push({
                    correctionNotice: " ",
                    updateDate: tmpDate,
                    uri: response,
                    label: versionLabel
                  });
                  // Enter a notice
                  var editedSectionValue = {title: 'Correction notice', markdown: ''};
                  var saveContent = function (updatedContent) {
                    data[field][data[field].length - 1].correctionNotice = updatedContent;
                    data.downloads = [{file: fileNameNoSpace}];
                    uploadedNotSaved.saved = true;
                    $("#" + idField).find('.edit-section__content').prepend('
');
                    $("#" + idField + '-section').remove();
                    saveDatasetVersion(collectionId, data.uri, data, field, idField);
                  };
                  loadMarkdownEditor(editedSectionValue, saveContent, data, 'notEmpty');
                } else {
                  data[field].push({
                    correctionNotice: "",
                    updateDate: tmpDate,
                    uri: response,
                    label: versionLabel
                  });
                  data.description.versionLabel = versionLabel; // only update the version label for versions not corrections.
                  data.downloads = [{file: fileNameNoSpace}];
                  uploadedNotSaved.saved = true;
                  $("#" + idField).find('.edit-section__content').prepend('
');
                  $("#" + idField + '-section').remove();
                  saveDatasetVersion(collectionId, data.uri, data, field, idField);
                }
              }, function (response) {
                if (response.status === 409) {
                  sweetAlert("You can add only one " + idField + " before publishing.");
                  deleteContent(collectionId, uploadedNotSaved.fileUrl);
                }
                else if (response.status === 404) {
                  sweetAlert("You can only add " + idField + "s to content that has been published.");
                  deleteContent(collectionId, uploadedNotSaved.fileUrl);
                }
                else {
                  handleApiError(response);
                }
              }
            );
          },
          error: function (response) {
            console.log("Error in uploading this file");
            handleApiError(response);
          }
        });
      }
    });
  }
}

function refreshDatasetVersion(collectionId, data, field, idField) {
  var ajaxRequest = [];
  var templateData = $.extend(true, {}, data);
  $(templateData[field]).each(function (index, version) {
    var dfd = $.Deferred();
    if (version.correctionNotice) {
      templateData[field][index].type = true;
    } else {
      templateData[field][index].type = false;
    }
    templateData[field][index].label = version.label;
    dfd.resolve();
    ajaxRequest.push(dfd);
  });

  $.when.apply($, ajaxRequest).then(function () {
    initialiseDatasetVersion(collectionId, data, templateData, field, idField);
  });

}

function initialiseDatasetVersion(collectionId, data, templateData, field, idField) {
  // Load
  var list = templateData[field];
  var correction;
  if (idField === 'correction') {
    correction = true;
  } else {
    correction = false;
  }
  var dataTemplate = {list: list, idField: idField, correction: correction};
  var html = templates.workEditT8VersionList(dataTemplate);
  $('#sortable-' + idField).replaceWith(html);
  $(data[field]).each(function (index) {
    dateTmp = data[field][index].updateDate;
    var dateTmpFormatted = $.datepicker.formatDate('dd MM yy', new Date(dateTmp));
    $('#' + idField + '-date_' + index).val(dateTmpFormatted).datepicker({dateFormat: 'dd MM yy'}).on('change', function () {
      data[field][index].updateDate = new Date($('#' + idField + '-date_' + index).datepicker('getDate')).toISOString();
      saveDatasetVersion(collectionId, data.uri, data, field, idField);
    });
    if (idField === 'correction') {
      $('#' + idField + '-edit_' + index).click(function () {
        var markdown = data[field][index].correctionNotice;
        var editedSectionValue = {title: 'Correction notice', markdown: markdown};
        var saveContent = function (updatedContent) {
          data[field][index].correctionNotice = updatedContent;
          saveDatasetVersion(collectionId, data.uri, data, field, idField);
        };
        loadMarkdownEditor(editedSectionValue, saveContent, data, 'notEmpty');
      });
    }
    $('#' + idField + '-edit-label_' + index).click(function () {
      var markdown = data[field][index].label ? data[field][index].label : "";
      var editedSectionValue = {title: 'Label content', markdown: markdown};
      var saveContent = function (updatedContent) {
        data[field][index].label = updatedContent;
        if (index === data[field].length - 1) {
          data.description.versionLabel = updatedContent;
        }
        saveDatasetVersion(collectionId, data.uri, data, field, idField);
      };
      loadMarkdownEditor(editedSectionValue, saveContent, data);
    });
    // Delete
    $('#' + idField + '-delete_' + index).click(function () {
      swal ({
        title: "Warning",
        text: "This will revert all changes you have made in this file. Are you sure you want to delete this " + idField + "?",
        type: "warning",
        showCancelButton: true,
        confirmButtonText: "Delete",
        cancelButtonText: "Cancel",
        closeOnConfirm: false
      }, function(result){
        if (result === true) {
          swal({
            title: "Deleted",
            text: "This " + idField + " version has been deleted",
            type: "success",
            timer: 2000
          });
          var pathToDelete = data.uri;
          var fileToDelete = pathToDelete + data.downloads[0].file;  //Saves always the latest
          var uriToDelete = $(this).parent().children('#' + idField + '-edition_' + index).attr(idField + '-url');
          deleteUnpublishedVersion(collectionId, uriToDelete, function () {
            var position = $(".workspace-edit").scrollTop();
            Florence.globalVars.pagePos = position;
            $(this).parent().remove();
            // delete uploaded file
            deleteContent(collectionId, fileToDelete, function () {
              console.log("File deleted");
            }, function (error) {
              handleApiError(error);
            });
            // delete modified data.json and revert to pubished
            deleteContent(collectionId, pathToDelete, function () {
              loadPageDataIntoEditor(pathToDelete, collectionId);
              refreshPreview(pathToDelete);
            }, function (error) {
              handleApiError(error);
            });
          }, function (response) {
            if (response.status === 404) {
              sweetAlert("You cannot delete a " + idField + " that has been published.");
            }
            else {
              handleApiError(response);
            }
          });
        }
      });
    });
  });
}

function saveDatasetVersion(collectionId, path, data, field, idField) {
  putContent(collectionId, path, JSON.stringify(data),
    function () {
      Florence.Editor.isDirty = false;
      refreshDatasetVersion(collectionId, data, field, idField);
      refreshPreview(path);
    },
    function (response) {
      if (response.status === 400) {
        sweetAlert("Cannot edit this page", "It is already part of another collection.");
      }
      else {
        handleApiError(response);
      }
    }
  );
}

<|endoftext|>


<commit_before>/*
 * This program is part of the OpenLMIS logistics management information system platform software.
 * Copyright © 2013 VillageReach
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License along with this program.  If not, see http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.
 */

 (function() {

    'use strict';

    /**
     *
     * @ngdoc service
     * @name  openlmis-auth.accessTokenInterceptorInterceptor
     * @description Adds access token stored by the Authorization Service to all requests to the OpenLMIS Server
     *
     */
    angular
        .module('openlmis-auth')
        .factory('accessTokenInterceptor', factory)
        .config(config);

    config.$inject = ['$httpProvider'];

    factory.$inject = [
        '$q', '$injector', 'openlmisUrlService', 'authorizationService', 'accessTokenFactory'
    ];

    function config($httpProvider) {
        $httpProvider.interceptors.push('accessTokenInterceptor');
    }

    function factory($q, $injector, openlmisUrlService, authorizationService,
                     accessTokenFactory) {

        var interceptor = {
            request: request,
            responseError: responseError
        };
        return interceptor;

        /**
         *
         * @ngdoc function
         * @name request
         * @methodOf openlmis-auth.accessTokenInterceptorInterceptor
         *
         * @param  {object} config HTTP Config object
         * @return {object} A modified configuration object
         *
         * @description
         * Checks the request config url with openlmisUrlService, and if there is a match an access
         * token is added to the url
         */
        function request(config) {
            if(openlmisUrlService.check(config.url) && authorizationService.isAuthenticated()
                    // we don't want to add the token to template requests
                    && !isHtml(config.url)) {
                config.url = accessTokenFactory.addAccessToken(config.url);
            }
            return config;
        }

        /**
         *
         * @ngdoc function
         * @name  response
         * @methodOf openlmis-auth.accessTokenInterceptorInterceptor
         *
         * @param  {object} response HTTP Response
         * @return {Promise} Rejected promise
         *
         * @description
         * Takes a failed response that is a 401 and clears a user's credentials, forcing them to
         * login OR takes 403 response and shows a modal with authorization error.
         *
         */
        function responseError(response) {
            if (response.status === 401) {
                authorizationService.clearAccessToken();
                authorizationService.clearUser();
                authorizationService.clearRights();
            } else if (response.status === 403) {
                $injector.get('alertService').error('error.authorization');
            }
            return $q.reject(response);
        }

        function isHtml(url) {
            return url.lastIndexOf('.html') === url.length - '.html'.length;
        }
    }

})();
<commit_msg>OLMIS-1434: Fixed JSDocs in the access-token.interceptor.js<commit_after>/*
 * This program is part of the OpenLMIS logistics management information system platform software.
 * Copyright © 2013 VillageReach
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License along with this program.  If not, see http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.
 */

 (function() {

    'use strict';

    /**
     *
     * @ngdoc service
     * @name  openlmis-auth.accessTokenInterceptor
     * @description Adds access token stored by the Authorization Service to all requests to the OpenLMIS Server
     *
     */
    angular
        .module('openlmis-auth')
        .factory('accessTokenInterceptor', factory)
        .config(config);

    config.$inject = ['$httpProvider'];

    factory.$inject = [
        '$q', '$injector', 'openlmisUrlService', 'authorizationService', 'accessTokenFactory'
    ];

    function config($httpProvider) {
        $httpProvider.interceptors.push('accessTokenInterceptor');
    }

    function factory($q, $injector, openlmisUrlService, authorizationService,
                     accessTokenFactory) {

        var interceptor = {
            request: request,
            responseError: responseError
        };
        return interceptor;

        /**
         *
         * @ngdoc function
         * @name request
         * @methodOf openlmis-auth.accessTokenInterceptor
         *
         * @param  {object} config HTTP Config object
         * @return {object} A modified configuration object
         *
         * @description
         * Checks the request config url with openlmisUrlService, and if there is a match an access
         * token is added to the url
         */
        function request(config) {
            if(openlmisUrlService.check(config.url) && authorizationService.isAuthenticated()
                    // we don't want to add the token to template requests
                    && !isHtml(config.url)) {
                config.url = accessTokenFactory.addAccessToken(config.url);
            }
            return config;
        }

        /**
         *
         * @ngdoc function
         * @name  response
         * @methodOf openlmis-auth.accessTokenInterceptor
         *
         * @param  {object} response HTTP Response
         * @return {Promise} Rejected promise
         *
         * @description
         * Takes a failed response that is a 401 and clears a user's credentials, forcing them to
         * login OR takes 403 response and shows a modal with authorization error.
         *
         */
        function responseError(response) {
            if (response.status === 401) {
                authorizationService.clearAccessToken();
                authorizationService.clearUser();
                authorizationService.clearRights();
            } else if (response.status === 403) {
                $injector.get('alertService').error('error.authorization');
            }
            return $q.reject(response);
        }

        function isHtml(url) {
            return url.lastIndexOf('.html') === url.length - '.html'.length;
        }
    }

})();
<|endoftext|>


<commit_before>/*
 * This program is part of the OpenLMIS logistics management information system platform software.
 * Copyright © 2013 VillageReach
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License along with this program.  If not, see http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.
 */

 (function(){
    "use strict";

    angular.module('openlmis-login')
        .run(loginRequiredInterceptor);

    /**
    * @ngdoc function
    * @name  openlmis-login.loginRequiredInterceptor
    *
    * @description
    * When there is 401 unauthorized status code after request, the user is shown login modal window. After authenticate request is retried.
    *
    */
    loginRequiredInterceptor.$inject = ['$rootScope', '$compile', 'bootbox', '$templateRequest', 'loadingModalService', 'authService', 'accessTokenFactory'];
    function loginRequiredInterceptor($rootScope, $compile, bootbox, $templateRequest, loadingModalService, authService, accessTokenFactory) {
        var noRetryRequest;
        var dialog;

        $rootScope.$on('event:auth-loginRequired', onLoginRequired);

        /**
          *
          * @ngdoc function
          * @name onLoginRequired
          * @param {Object} event event
          * @param {boolean} noRetryRequest true if should no retry request
          *
          * @description
          * Make and show login modal, close loading modal.
          *
          */
        function onLoginRequired(event, _noRetryRequest_) {
          var scope = $rootScope.$new();
          noRetryRequest = _noRetryRequest_;

          $templateRequest('openlmis-login/login-form.html').then(function (html) {
            dialog = bootbox.dialog({
              message: $compile(html)(scope),
              closeButton: false,
              className: 'login-modal'
            });
          });
          loadingModalService.close();
        }

        $rootScope.$on('auth.login-modal', function () {
          dialog.modal('hide');
          if (noRetryRequest === true) {
            $rootScope.$emit('event:auth-loggedIn');
          } else {
            authService.loginConfirmed(null, function (config) {
              config.url = accessTokenFactory.updateAccessToken(config.url);
              return config;
            });
          }
        });
    }
})();
<commit_msg>OLMIS-1886: Fixed memory leak in loginModalInterceptor<commit_after>/*
 * This program is part of the OpenLMIS logistics management information system platform software.
 * Copyright © 2013 VillageReach
 *
 * This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License along with this program.  If not, see http://www.gnu.org/licenses.  For additional information contact info@OpenLMIS.org.
 */

(function() {

    'use strict';

    angular
        .module('openlmis-login')
        .run(loginRequiredInterceptor);

    /**
     * @ngdoc function
     * @name  openlmis-login.loginRequiredInterceptor
     *
     * @description
     * When there is 401 unauthorized status code after request, the user is shown login modal
     * window. After authenticate request is retried.
     *
     */
    loginRequiredInterceptor.$inject = [
        '$rootScope', '$compile', 'bootbox', '$templateRequest', 'loadingModalService',
        'authService', 'accessTokenFactory'
    ];

    function loginRequiredInterceptor($rootScope, $compile, bootbox, $templateRequest,
                                      loadingModalService, authService, accessTokenFactory) {

        var noRetryRequest, dialog, dialogScope;

        $rootScope.$on('event:auth-loginRequired', onLoginRequired);
        $rootScope.$on('auth.login-modal', onLoginModal);

        /**
         *
         * @ngdoc function
         * @name onLoginRequired
         * @param {Object} event event
         * @param {boolean} noRetryRequest true if should no retry request
         *
         * @description
         * Make and show login modal, close loading modal.
         *
         */
        function onLoginRequired(event, _noRetryRequest_) {
            noRetryRequest = _noRetryRequest_;

            if (dialogScope) dialogScope.$destroy();
            dialogScope = $rootScope.$new();

            $templateRequest('openlmis-login/login-form.html').then(function(html) {
                dialog = bootbox.dialog({
                    message: $compile(html)(dialogScope),
                    closeButton: false,
                    className: 'login-modal'
                });
            });
            loadingModalService.close();
        }

        function onLoginModal() {
            dialog.modal('hide');
            dialog = undefined;
            dialogScope.$destroy();

            if (noRetryRequest === true) {
                $rootScope.$emit('event:auth-loggedIn');
            } else {
                authService.loginConfirmed(null, function(config) {
                    config.url = accessTokenFactory.updateAccessToken(config.url);
                    return config;
                });
            }
        }
    }
})();
<|endoftext|>