a map layer shows up

a map layer shows up

<template>
    <div class="map" id="map" tabindex="0">
        <gk-info v-ref:info></gk-info>
        <select @change="setScale($event.target.value)" id="menu-scale" v-cloak>
            <option value="{{ scale }}" selected>{{ scaleString }}</option>
            <option v-for="s in fixedScales" value="{{ s }}">{{ getScaleString(s) }}</option>
        </select>
    </div>
</template>

<script>
    import gkInfo from './info.vue'
    export default {
        components: { gkInfo },
        data: function() {
            var resolutions = [0.17578125, 0.087890625, 0.0439453125, 0.02197265625, 0.010986328125, 0.0054931640625, 0.00274658203125, 0.001373291015625, 0.0006866455078125, 0.0003433227539062, 0.0001716613769531, 858306884766e-16, 429153442383e-16, 214576721191e-16, 107288360596e-16, 53644180298e-16, 26822090149e-16, 13411045074e-16]
            return {
                // default matrix from KMI
                resolutions: resolutions,
                matrixSets: {
                    'EPSG:4326': {
                        '1024': {
                            'name': 'gda94',
                            'resolutions': resolutions,
                            'minLevel': 0,
                            'maxLevel': 17
                        }
                    }
                },
                // overridable defaults for WMTS and WFS loading
                defaultWMTSSrc: 'https://kmi.dpaw.wa.gov.au/geoserver/gwc/service/wmts',
                defaultWFSSrc: 'https://kmi.dpaw.wa.gov.au/geoserver/wfs',
                // fixed scales for the scale selector (1:1K increments)
                fixedScales: [0.25, 0.5, 1, 2, 2.5, 5, 10, 20, 25, 50, 80, 100, 125, 250, 500, 1000, 2000, 3000, 5000, 10000, 25000],
                scale: 0
            }
        },
        // parts of the template to be computed live
        computed: {
            // scale string for the current map zoom level
            scaleString: function() {
                return this.getScaleString(this.scale);
            },
            // because the viewport size changes when the tab pane opens, don't cache the map width and height
            mapWidth: {
                cache: false,
                get: function get() {
                    if (this.$el) {
                        return this.$el.clientWidth
                    }
                    return 0
                }
            },
            mapHeight: {
                cache: false,
                get: function get() {
                    if (this.$el) {
                        return this.$el.clientHeight
                    }
                    return 0
                }
            }
        },
        // methods callable from inside the template
        methods: {
            // force OL to approximate a fixed scale (1:1K increments)
            setScale: function(scale) {
                while (Math.abs(self.getScale() - scale) > 0.001) {
                    self.map.getView().setResolution(self.map.getView().getResolution() * scale / self.getScale())
                }
                this.scale = scale;
                console.log(scale);
                ev.target.selectedIndex = 0;
            },
            // return the scale (1:1K increments)
            getScale: function () {
                var self = this.$root
                var size = self.map.getSize()
                var center = self.map.getView().getCenter()
                var extent = self.map.getView().calculateExtent(size)
                var distance = self.wgs84Sphere.haversineDistance([extent[0], center[1]], center) * 2
                return distance * self.dpmm / size[0]
            },
            // get the fixed scale (1:1K increments) closest to current scale
            getFixedScale: function () {
                var scale = this.getScale()
                var closest = null
                $.each(this.fixedScales, function () {
                    if (closest === null || Math.abs(this - scale) < Math.abs(closest - scale)) {
                        closest = this
                    }
                })
                return closest
            },
            // generate a human-readable scale string
            getScaleString: function (scale) {
                if (Math.round(scale * 100) / 100 < 1.0) {
                    return '1:' + (Math.round(scale * 100000) / 100).toLocaleString()
                } else if (Math.round(scale * 100) / 100 >= 1000.0) {
                    return '1:' + (Math.round(scale / 10) / 100).toLocaleString() + 'M'
                }
                return '1:' + (Math.round(scale * 100) / 100).toLocaleString() + 'K'
            },
            // reusable tile loader hook to update a loading indicator
            tileLoaderHook: function (tileSource, tileLayer) {
                // number of tiles currently in flight
                var numLoadingTiles = 0
                // number of misses for the current set
                var badTiles = 0
                var tileLoader = tileSource.getTileLoadFunction()
                return function (tile, src) {
                    if (numLoadingTiles === 0) {
                        tileLayer.progress = 'loading'
                        badTiles = 0
                    }
                    numLoadingTiles++
                    var image = tile.getImage()
                    // to hell with you, cross origin policy!
                    image.crossOrigin = 'anonymous'
                    image.onload = function () {
                        numLoadingTiles--
                        if (numLoadingTiles === 0) {
                            if (badTiles > 0) {
                                tileLayer.progress = 'error'
                            } else {
                                tileLayer.progress = 'idle'
                            }
                        }
                    }
                    image.onerror = function () {
                        badTiles++
                        image.onload()
                    }
                    tileLoader(tile, src)
                }
            },
            // loader for layers with a "time" axis, e.g. live satellite imagery
            createTimelineLayer: function (self) {
                var options = this
                options.params = $.extend({
                    FORMAT: 'image/jpeg',
                    SRS: 'EPSG:4326',
                }, options.params || {})

                // technically, we can specify a WMS source and a layer without
                // either the source URL or the layerID. which is good, because
                // we need to do that later on in a callback.
                var tileSource = new ol.source.TileWMS({
                    params: options.params,
                    tileGrid: new ol.tilegrid.TileGrid({
                        extent: [-180, -90, 180, 90],
                        resolutions: self.resolutions,
                        tileSize: [1024, 1024]
                    })
                })

                var tileLayer = new ol.layer.Tile({
                    opacity: options.opacity || 1,
                    source: tileSource
                })

                // hook the tile loading function to update progress indicator
                tileLayer.progress = ''
                tileSource.setTileLoadFunction(self.tileLoaderHook(tileSource, tileLayer))

                // hook to swap the tile layer when timeIndex changes
                tileLayer.on('propertychange', function (event) {
                    if (event.key === 'timeIndex') {
                        tileSource.updateParams({
                            'layers': options.timeline[event.target.get(event.key)][1]
                        })
                    }
                })

                // helper function to update the time index
                options.updateTimeline = function () {
                    // fetch the latest timestamp-to-layerID map from the source URL
                    $.getJSON(options.source, function (data) {
                        tileLayer.set('updated', moment().toLocaleString())
                        tileSource.setUrls(data.servers)
                        options.timeline = data.layers.reverse()
                        tileLayer.set('timeIndex', options.timeIndex || options.timeline.length - 1)
                        self.ui.layers.update()
                    })
                }

                options.updateTimeline()
                // if the "refresh" option is set, set a timer
                // to update the source
                if (options.refresh) {
                    tileLayer.refresh = setInterval(function () {
                        options.updateTimeline()
                    }, options.refresh * 1000)
                }

                // set properties for use in layer selector
                tileLayer.set('name', options.name)
                tileLayer.set('id', options.id)
                return tileLayer
            },
            // loader for vector layers with hover querying
            createWFSLayer: function (self) {
                var options = this
                var url = self.defaultWFSSrc
                // default overridable params sent to the WFS source
                options.params = $.extend({
                    version: '1.1.0',
                    service: 'WFS',
                    request: 'GetFeature',
                    outputFormat: 'application/json',
                    srsname: 'EPSG:4326',
                    typename: options.id,
                }, options.params || {})

                var vectorSource = new ol.source.Vector()
                var vector = new ol.layer.Vector({
                    opacity: options.opacity || 1,
                    source: vectorSource,
                    style: options.style
                })
                vector.progress = ''

                vectorSource.loadSource = function () {
                    if (options.cql_filter) {
                        options.params.cql_filter = options.cql_filter
                    } else if (options.params.cql_filter) {
                        delete options.params.cql_filter
                    }
                    vector.progress = 'loading'
                    $.ajax({
                        url: url + '?' + $.param(options.params),
                        success: function (response, stat, xhr) {
                            var features = self.geojson.readFeatures(response)
                            vectorSource.clear(true)
                            vectorSource.addFeatures(features)
                            vector.progress = 'idle'
                        },
                        error: function () {
                            vector.progress = 'error'
                        },
                        dataType: 'json',
                        xhrFields: {
                            withCredentials: true
                        }
                    })
                }

                if (options.onadd) {
                    vectorSource.on('addfeature', function (event) {
                        options.onadd(event.feature)
                    })
                }

                // if the "refresh" option is set, set a timer
                // to update the source
                if (options.refresh) {
                    vector.set('updated', moment().toLocaleString())
                    vectorSource.refresh = setInterval(function () {
                        vector.set('updated', moment().toLocaleString())
                        vectorSource.loadSource()
                    }, options.refresh * 1000)
                }
                vectorSource.loadSource()

                vector.set('name', options.name)
                vector.set('id', options.id)
                return vector
            },
            // loader to create a WMTS layer from a kmi datasource
            createTileLayer: function (self) {
                var layer = this
                if (layer.base) {
                    layer.format = 'image/jpeg'
                }
                layer = $.extend({
                    opacity: 1,
                    name: 'Mapbox Outdoors',
                    id: 'dpaw:mapbox_outdoors',
                    format: 'image/png',
                    tileSize: 1024,
                    style: '',
                    projection: 'EPSG:4326',
                    wmts_url: self.defaultWMTSSrc,
                }, layer)

                // create a tile grid using the stock KMI resolutions
                var matrixSet = self.matrixSets[layer.projection][layer.tileSize]
                var tileGrid = new ol.tilegrid.WMTS({
                    origin: ol.extent.getTopLeft([-180, -90, 180, 90]),
                    resolutions: matrixSet.resolutions,
                    matrixIds: matrixSet.matrixIds,
                    tileSize: layer.tileSize
                })

                // override getZForResolution on tile grid object;
                // for weird zoom levels, the default is to round up or down to the
                // nearest integer to determine which tiles to use.
                // because we want the printing rasters to contain as much detail as
                // possible, we rig it here to always round up.
                tileGrid.origGetZForResolution = tileGrid.getZForResolution
                tileGrid.getZForResolution = function (resolution, opt_direction) {
                    return tileGrid.origGetZForResolution(resolution, -1)
                }

                // create a tile source
                var tileSource = new ol.source.WMTS({
                    url: layer.wmts_url,
                    layer: layer.id,
                    matrixSet: matrixSet.name,
                    format: layer.format,
                    style: layer.style,
                    projection: layer.projection,
                    wrapX: true,
                    tileGrid: tileGrid
                })

                var tileLayer = new ol.layer.Tile({
                    opacity: layer.opacity || 1,
                    source: tileSource
                })

                // hook the tile loading function to update progress indicator
                tileLayer.progress = ''
                tileSource.setTileLoadFunction(self.tileLoaderHook(tileSource, tileLayer))

                // if the "refresh" option is set, set a timer
                // to force a reload of the tile content
                if (layer.refresh) {
                    tileLayer.set('updated', moment().toLocaleString())
                    tileLayer.refresh = setInterval(function () {
                        tileLayer.set('updated', moment().toLocaleString())
                        tileSource.setUrl(layer.wmts_url + '?time=' + moment.utc().unix())
                    }, layer.refresh * 1000)
                }
                // set properties for use in layer selector
                tileLayer.set('name', layer.name)
                tileLayer.set('id', layer.id)
                return tileLayer
            },
            getMapLayer: function (id) {
                return self.map.getLayers().getArray().find(function (layer) {
                    return layer.get('id') === id
                })
            },
            getLayer: function (id) {
                return this.$root.catalogue.catalogue.getArray().find(function (layer) {
                    return layer.id === id
                })
            },
            // initialise map
            init: function (catalogue, layers, options) {
                var self = this;
                this.$root.catalogue.catalogue = new ol.Collection()
                options = options || {}

                var getMapLayer = function () {
                    return self.getMapLayer(this.id)
                }

                var initLayer = function(initFunc, mapObj) {
                    return function () {
                        return initFunc(mapObj)
                    }
                }

                this.$root.catalogue.catalogue.on('add', function (event) {
                    var l = event.element
                    l.olLayer = getMapLayer
                    l.id = l.id || l.identifier
                    l.name = l.name || l.title
                    l.init = initLayer( l.init || self.createTileLayer, self ) // override based on layer type
                })

                this.$root.catalogue.catalogue.extend(catalogue)

                var initialLayers = layers.reverse().map(function (id) {
                    return self.getLayer(id).init()
                })

                self.map = new ol.Map({
                    logo: false,
                    renderer: 'canvas',
                    target: 'map',
                    layers: initialLayers,
                    view: new ol.View({
                        projection: 'EPSG:4326',
                        center: [123.75, -24.966],
                        zoom: 6,
                        maxZoom: 21,
                        minZoom: 5
                    }),
                    controls: [
                        new ol.control.Zoom(),
                        new ol.control.ScaleLine(),
                        new ol.control.FullScreen({
                            source: $('body').get(0),
                            label: $('<i/>', {
                                class: 'fa fa-expand'
                            })[0]
                        }),
                        new ol.control.Control({
                            element: $('#menu-scale').get(0)
                        }),
                        new ol.control.Attribution()
                    ],
                    interactions: ol.interaction.defaults({
                        altShiftDragRotate: false,
                        pinchRotate: false,
                        dragPan: false,
                        doubleClickZoom: false,
                        keyboard: false
                    })
                })
                var params = {}
                decodeURIComponent(location.search).slice(1).split('&').forEach(function (p) {
                    var t = p.split('=')
                    params[t[0]] = parseFloat(t[1])
                })
                if (params.scale) {
                    self.map.getView().setCenter([params.lon, params.lat])
                    self.setScale(params.scale / 1000)
                }
                // add some default interactions
                self.dragPanInter = new ol.interaction.DragPan()
                self.doubleClickZoomInter = new ol.interaction.DoubleClickZoom()
                self.keyboardPanInter = new ol.interaction.KeyboardPan()
                self.keyboardZoomInter = new ol.interaction.KeyboardZoom()
                self.map.addInteraction(self.dragPanInter)
                self.map.addInteraction(self.doubleClickZoomInter)
                self.map.addInteraction(self.keyboardPanInter)
                self.map.addInteraction(self.keyboardZoomInter)

                // Create the graticule component
                self.graticule = new ol.LabelGraticule()
                self.graticule.setMap(self.map)

                // setup scale events
                self.map.on('postrender', function () {
                    if (self.mapScaleControl) {
                        self.mapScaleControl.scale = self.getScale()
                    }
                    if (self.mapExportControls) {
                        history.replaceState(null, null, location.pathname + '?' + self.mapExportControls.shortUrl)
                    }
                })
                if (document.querySelector('#info')) {
                    self.initInfo()
                }
                if (document.querySelector('#menu-tab-layers')) {
                    self.initLayers()
                }
                if (document.querySelector('#menu-tab-annotations')) {
                    self.initAnnotations()
                }

                $.get('static/images/legend.svg', function (tmpl) {
                    $('#legendsvg').html(tmpl)
                    self.initMapControls()
                }, 'text')
                // display hover popups
                self.map.on('pointermove', this.$refs.info.onPointerMove)
            }
        },
        ready: function() {
            // generate matrix IDs from name and level number
            $.each(this.matrixSets, function (projection, innerMatrixSets) {
                $.each(innerMatrixSets, function (tileSize, matrixSet) {
                    var matrixIds = new Array(matrixSet.maxLevel - matrixSet.minLevel + 1)
                    for (var z = matrixSet.minLevel; z <= matrixSet.maxLevel; ++z) {
                        matrixIds[z] = matrixSet.name + ':' + z
                    }
                    matrixSet.matrixIds = matrixIds
                })
            })
        }
    }
</script>

<style>
    /* full-height page for slippy map, thanks */

    html,
    body {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        overflow: hidden;
    }

    [v-cloak] {
        display: none;
    }
    /* mousehover info pane, hide for mobile */

    @media screen and (max-width: 40em) {
        #info {
            display: none;
        }
    }

    #info {
        position: absolute;
        z-index: 100;
        padding: 2em;
        background-color: #424f5a;
        max-width: 40%;
    }

    #info .content {
        max-height: 30vh;
        overflow: auto;
    }
    /* full-height the foundation off-canvas stuff also */

    .off-canvas-wrapper {
        height: 100%;
    }

    .off-canvas-wrapper-inner {
        height: 100%;
        background-color: #424f5a;
        color: #fbfbfb;
    }
    /* SVG icons for buttons (include margin for aligning to text) */

    .icon {
        width: 20px;
        height: 20px;
        margin-bottom: -5px;
    }

    .off-canvas-content {
        height: 100%;
    }

    .off-canvas {
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: flex-start;
        align-items: stretch;
        align-content: stretch;
    }

    .off-canvas .tabs-content {
        height: 100%;
        flex: 1;
    }
    /* slippy map widget */

    .map {
        width: 100%;
        height: 100%;
        min-height: 20px;
        flex: 1;
        background: url('../static/images/bg.png') repeat;
        background-position: center;
        position: relative;
        outline: 0;
    }

    .ol-overviewmap .ol-overviewmap-map {
        border: 0px;
        margin: 0px;
        width: 30vw;
        height: 30vh;
    }

    .ol-overviewmap .ol-overviewmap-map .ol-overviewmap-box {
        display: none;
    }
    /* position fixes for top-right OpenLayers map widgets */

    .ol-scale-line {
        top: 72px;
        left: auto;
        right: 72px;
        bottom: auto;
        border: 1px solid transparent;
    }

    .ol-full-screen {
        top: 16px;
        right: 16px;
        padding: 0;
    }

    .ol-zoom {
        top: 72px;
        left: auto;
        right: 16px;
        bottom: auto;
        padding: 0;
    }

    .ol-zoom button,
    .ol-full-screen button {
        width: 48px;
        height: 48px;
    }

    #menu-scale {
        position: absolute;
        top: 16px;
        right: 72px;
        width: 96px;
        cursor: pointer;
        font-size: 0.8rem;
        border: 0;
        margin: 1px;
        color: white;
        height: 48px;
    }
    /* for mobile screens, size the pane to fill the viewport */

    .off-canvas.position-left {
        left: -100vw;
        width: 100vw;
    }

    .position-left.reveal-responsive {
        left: 0;
        position: fixed;
        z-index: 0;
    }

    .position-left.reveal-responsive ~ .off-canvas-content {
        margin-left: 100vw;
    }

    #side-pane-close {
        display: none;
    }

    .position-left.reveal-responsive #side-pane-close {
        display: block;
        position: absolute;
        right: 0;
        top: 0;
    }
    /* for non-mobile screens, have a fixed 400px left pane */

    @media screen and (min-width: 40em) {
        /* widen the off canvas area*/
        .off-canvas.position-left {
            left: -424px;
            width: 424px;
        }
        .position-left.reveal-responsive {
            left: 0;
            position: fixed;
            z-index: auto;
        }
        .position-left.reveal-responsive ~ .off-canvas-content {
            margin-left: 424px;
        }
        #side-pane-close {
            display: none;
        }
        .position-left.reveal-responsive #side-pane-close {
            display: none;
        }
    }
    /* raise our widgets above the slippy map */

    .map-widget {
        position: absolute;
        z-index: 1;
    }
    /* remove some of the annoying tab box styles */

    #offCanvasLeft {
        background-color: transparent;
    }

    #offCanvasLeft .tabs-content {
        background-color: transparent;
        border: 0;
    }
    /* fix tool labels/padding */

    .tool-label,
    .off-canvas label.tool-label {
        font-size: 80%;
        float: left;
        width: 200px;
    }

    .tool-slice {
        margin-bottom: 4px;
    }

    .tool-slice input,
    .tool-slice select,
    .tool-slice button {
        margin: 0px;
    }

    .tool-slice .button.selected {
        border: 2px dotted #ffffff;
    }

    ;
    .button-group input {
        display: none;
    }

    .button-group input:checked + label,
    .button-group input:checked + label:active {
        background-color: rgba(0, 60, 136, 0.7);
    }
    /* make range sliders visible */

    input[type="range"] {
        background-color: transparent;
    }

    .tool-slice input[type="range"] {
        width: 100%;
    }
    /* make tool button text bigger */

    .button-group.expanded .button-tool {
        padding: 0.5em;
        font-size: 18px;
    }

    #menu-tabs {
        background-color: transparent;
        border: 0;
    }

    #menu-pagesize {
        font-weight: bold;
    }
    /* styling for tabs used in the side-pane */

    .off-canvas .tabs {
        background-color: rgba(37, 45, 51, 0.9);
        border: 0;
    }

    .off-canvas .tabs a {
        color: white;
        border: 1px solid transparent;
        text-align: center;
        padding: 16px 4px;
    }

    .off-canvas .tabs a:hover {
        background-color: rgba(128, 128, 128, 0.23);
    }

    .off-canvas .tabs a:focus {
        background-color: transparent;
    }

    .off-canvas .tabs a[aria-selected="true"] {
        background-color: rgba(128, 128, 128, 0.46);
        border: 1px solid rgba(37, 45, 51, 0.9);
    }
    /* rig the tabs to use flexbox for full-width */

    .off-canvas .tabs {
        display: flex;
        width: 100%;
    }

    .off-canvas .tabs .tabs-title {
        flex: 1 1 0px;
    }

    .off-canvas .tabs a {
        height: 100%;
    }
    /* styling for switch widgets used in the side pane */

    .off-canvas .switch-paddle {
        background: rgba(37, 45, 51, 0.9);
    }

    .off-canvas .switch-input,
    .off-canvas .switch {
        margin: 0;
    }

    .off-canvas label {
        color: white;
        line-height: inherit;
        font-size: inherit;
        font-weight: inherit;
    }

    .side-label {
        margin-left: 1em;
    }
    /* Set height of internal layers to allow for bottom details */

    .layers-topframe {
        height: 60vh;
        border-bottom: 1px solid rgba(37, 45, 51, 0.9);
    }

    .scroller {
        overflow-y: auto;
    }
    /* remove extra padding for tab boxes inside other tab boxes */

    #layers-list {
        padding: 1rem 0 0 0;
    }
    /* styling for movable row boxes */

    .layer-row,
    .feature-row {
        color: white;
        background-color: rgba(37, 45, 51, 0.5);
        padding: 4px;
        cursor: pointer;
    }

    .layer-row:nth-child(even),
    .feature-row:nth-child(even) {
        background-color: rgba(37, 45, 51, 0.3);
    }

    .layer-row .layer-title {
        font-size: 120%;
    }

    .layer-row .layer-id {
        font-style: italic;
        font-size: 75%;
    }
    /* Add dotted line box to dragged layers*/

    .layer-row.gu-transit {
        border: 2px dashed #ffffff;
        border-radius: 4px;
    }
    /* Add status indicator to left hand side of layer rows */

    .layer-row {
        border-left: 3px solid transparent;
    }

    @keyframes layer-pulse {
        0% {
            border-left-color: #ffcf41;
        }
        50% {
            border-left-color: #edb100;
        }
        100% {
            border-left-color: #ffcf41;
        }
    }

    .layer-row.idle {
        border-left-color: #71c837;
    }

    .layer-row.loading {
        animation-name: layer-pulse;
        animation-duration: 1s;
        animation-iteration-count: infinite;
    }

    .layer-row.error {
        border-left-color: #ec5840;
    }
    /* custom F6 tab style overrides for the side buttons  */

    .side-button > a {
        background-color: rgba(37, 45, 51, 0.7);
        border-left: 3px solid transparent;
        padding: 1em;
        font-size: 24px;
        color: white;
    }

    .side-button .icon {
        width: 24px;
        height: 24px;
    }

    .side-button > a:hover {
        background-color: rgba(37, 45, 51, 0.9);
    }

    .side-button > a:focus {
        background-color: rgba(37, 45, 51, 0.7);
    }

    .side-button > a[aria-selected="true"] {
        background-color: rgba(37, 45, 51, 0.8);
        border-left: 3px solid #2199e8;
    }
    /* fix absurdly large margins on button groups */

    .button-group {
        margin-bottom: 1px;
    }
    /* colouring changes to OpenLayers widgets to match side buttons*/

    .ol-control,
    .ol-control:hover {
        background-color: transparent;
    }

    .ol-control button,
    .ol-control button:focus,
    .ol-scale-line,
    #menu-scale {
        background-color: rgba(37, 45, 51, 0.7);
    }

    .ol-control button:hover,
    .ol-scale-line:hover,
    #menu-scale:hover {
        background-color: rgba(37, 45, 51, 0.9);
    }
</style>

<template>
    <div class="map" id="map" tabindex="0">
        <gk-info v-ref:info></gk-info>
        <select @change="setScale($event.target.value)" id="menu-scale" v-cloak>
            <option value="{{ scale }}" selected>{{ scaleString }}</option>
            <option v-for="s in fixedScales" value="{{ s }}">{{ getScaleString(s) }}</option>
        </select>
    </div>
</template>

<script>
    import gkInfo from './info.vue'
    export default {
        components: { gkInfo },
        data: function() {
            return {
                // default matrix from KMI
                resolutions: [0.17578125, 0.087890625, 0.0439453125, 0.02197265625, 0.010986328125, 0.0054931640625, 0.00274658203125, 0.001373291015625, 0.0006866455078125, 0.0003433227539062, 0.0001716613769531, 858306884766e-16, 429153442383e-16, 214576721191e-16, 107288360596e-16, 53644180298e-16, 26822090149e-16, 13411045074e-16],
                _matrixSets: {
                    'EPSG:4326': {
                        '1024': {
                            'name': 'gda94',
                            'resolutions': this.resolutions,
                            'minLevel': 0,
                            'maxLevel': 17
                        }
                    }
                },
                // overridable defaults for WMTS and WFS loading
                defaultWMTSSrc: 'https://kmi.dpaw.wa.gov.au/geoserver/gwc/service/wmts',
                defaultWFSSrc: 'https://kmi.dpaw.wa.gov.au/geoserver/wfs',
                // fixed scales for the scale selector (1:1K increments)
                fixedScales: [0.25, 0.5, 1, 2, 2.5, 5, 10, 20, 25, 50, 80, 100, 125, 250, 500, 1000, 2000, 3000, 5000, 10000, 25000],
                scale: 0
            }
        },
        // parts of the template to be computed live
        computed: {
            // scale string for the current map zoom level
            scaleString: function() {
                return this.getScaleString(this.scale);
            },
            // because the viewport size changes when the tab pane opens, don't cache the map width and height
            mapWidth: {
                cache: false,
                get: function get() {
                    return this.$el.clientWidth
                }
            },
            mapHeight: {
                cache: false,
                get: function get() {
                    return this.$el.clientHeight
                }
            }
        },
        // methods callable from inside the template
        methods: {
            // force OL to approximate a fixed scale (1:1K increments)
            setScale: function(scale) {
                while (Math.abs(self.getScale() - scale) > 0.001) {
                    self.map.getView().setResolution(self.map.getView().getResolution() * scale / self.getScale())
                }
                this.scale = scale;
                console.log(scale);
                ev.target.selectedIndex = 0;
            },
            // return the scale (1:1K increments)
            getScale: function () {
                var self = this.$root
                var size = self.map.getSize()
                var center = self.map.getView().getCenter()
                var extent = self.map.getView().calculateExtent(size)
                var distance = self.wgs84Sphere.haversineDistance([extent[0], center[1]], center) * 2
                return distance * self.dpmm / size[0]
            },
            // get the fixed scale (1:1K increments) closest to current scale
            getFixedScale: function () {
                var scale = this.getScale()
                var closest = null
                $.each(this.fixedScales, function () {
                    if (closest === null || Math.abs(this - scale) < Math.abs(closest - scale)) {
                        closest = this
                    }
                })
                return closest
            },
            // generate a human-readable scale string
            getScaleString: function (scale) {
                if (Math.round(scale * 100) / 100 < 1.0) {
                    return '1:' + (Math.round(scale * 100000) / 100).toLocaleString()
                } else if (Math.round(scale * 100) / 100 >= 1000.0) {
                    return '1:' + (Math.round(scale / 10) / 100).toLocaleString() + 'M'
                }
                return '1:' + (Math.round(scale * 100) / 100).toLocaleString() + 'K'
            },
            // reusable tile loader hook to update a loading indicator
            tileLoaderHook: function (tileSource, tileLayer) {
                // number of tiles currently in flight
                var numLoadingTiles = 0
                // number of misses for the current set
                var badTiles = 0
                var tileLoader = tileSource.getTileLoadFunction()
                return function (tile, src) {
                    if (numLoadingTiles === 0) {
                        tileLayer.progress = 'loading'
                        badTiles = 0
                    }
                    numLoadingTiles++
                    var image = tile.getImage()
                    // to hell with you, cross origin policy!
                    image.crossOrigin = 'anonymous'
                    image.onload = function () {
                        numLoadingTiles--
                        if (numLoadingTiles === 0) {
                            if (badTiles > 0) {
                                tileLayer.progress = 'error'
                            } else {
                                tileLayer.progress = 'idle'
                            }
                        }
                    }
                    image.onerror = function () {
                        badTiles++
                        image.onload()
                    }
                    tileLoader(tile, src)
                }
            },
            // loader for layers with a "time" axis, e.g. live satellite imagery
            createTimelineLayer: function () {
                var options = this
                options.params = $.extend({
                    FORMAT: 'image/jpeg',
                    SRS: 'EPSG:4326',
                }, options.params || {})

                // technically, we can specify a WMS source and a layer without
                // either the source URL or the layerID. which is good, because
                // we need to do that later on in a callback.
                var tileSource = new ol.source.TileWMS({
                    params: options.params,
                    tileGrid: new ol.tilegrid.TileGrid({
                        extent: [-180, -90, 180, 90],
                        resolutions: self.resolutions,
                        tileSize: [1024, 1024]
                    })
                })

                var tileLayer = new ol.layer.Tile({
                    opacity: options.opacity || 1,
                    source: tileSource
                })

                // hook the tile loading function to update progress indicator
                tileLayer.progress = ''
                tileSource.setTileLoadFunction(_tileLoaderHook(tileSource, tileLayer))

                // hook to swap the tile layer when timeIndex changes
                tileLayer.on('propertychange', function (event) {
                    if (event.key === 'timeIndex') {
                        tileSource.updateParams({
                            'layers': options.timeline[event.target.get(event.key)][1]
                        })
                    }
                })

                // helper function to update the time index
                options.updateTimeline = function () {
                    // fetch the latest timestamp-to-layerID map from the source URL
                    $.getJSON(options.source, function (data) {
                        tileLayer.set('updated', moment().toLocaleString())
                        tileSource.setUrls(data.servers)
                        options.timeline = data.layers.reverse()
                        tileLayer.set('timeIndex', options.timeIndex || options.timeline.length - 1)
                        self.ui.layers.update()
                    })
                }

                options.updateTimeline()
                // if the "refresh" option is set, set a timer
                // to update the source
                if (options.refresh) {
                    tileLayer.refresh = setInterval(function () {
                        options.updateTimeline()
                    }, options.refresh * 1000)
                }

                // set properties for use in layer selector
                tileLayer.set('name', options.name)
                tileLayer.set('id', options.id)
                return tileLayer
            },
            // loader for vector layers with hover querying
            createWFSLayer: function (self) {
                var options = this
                var url = self.defaultWFSSrc
                // default overridable params sent to the WFS source
                options.params = $.extend({
                    version: '1.1.0',
                    service: 'WFS',
                    request: 'GetFeature',
                    outputFormat: 'application/json',
                    srsname: 'EPSG:4326',
                    typename: options.id,
                }, options.params || {})

                var vectorSource = new ol.source.Vector()
                var vector = new ol.layer.Vector({
                    opacity: options.opacity || 1,
                    source: vectorSource,
                    style: options.style
                })
                vector.progress = ''

                vectorSource.loadSource = function () {
                    if (options.cql_filter) {
                        options.params.cql_filter = options.cql_filter
                    } else if (options.params.cql_filter) {
                        delete options.params.cql_filter
                    }
                    vector.progress = 'loading'
                    $.ajax({
                        url: url + '?' + $.param(options.params),
                        success: function (response, stat, xhr) {
                            var features = self.geojson.readFeatures(response)
                            vectorSource.clear(true)
                            vectorSource.addFeatures(features)
                            vector.progress = 'idle'
                        },
                        error: function () {
                            vector.progress = 'error'
                        },
                        dataType: 'json',
                        xhrFields: {
                            withCredentials: true
                        }
                    })
                }

                if (options.onadd) {
                    vectorSource.on('addfeature', function (event) {
                        options.onadd(event.feature)
                    })
                }

                // if the "refresh" option is set, set a timer
                // to update the source
                if (options.refresh) {
                    vector.set('updated', moment().toLocaleString())
                    vectorSource.refresh = setInterval(function () {
                        vector.set('updated', moment().toLocaleString())
                        vectorSource.loadSource()
                    }, options.refresh * 1000)
                }
                vectorSource.loadSource()

                vector.set('name', options.name)
                vector.set('id', options.id)
                return vector
            },
            // loader to create a WMTS layer from a kmi datasource
            createTileLayer: function (self) {
                var layer = this
                if (layer.base) {
                    layer.format = 'image/jpeg'
                }
                layer = $.extend({
                    opacity: 1,
                    name: 'Mapbox Outdoors',
                    id: 'dpaw:mapbox_outdoors',
                    format: 'image/png',
                    tileSize: 1024,
                    style: '',
                    projection: 'EPSG:4326',
                    wmts_url: self.defaultWMTSSrc,
                }, layer)

                // create a tile grid using the stock KMI resolutions
                var matrixSet = self._matrixSets[layer.projection][layer.tileSize]
                var tileGrid = new ol.tilegrid.WMTS({
                    origin: ol.extent.getTopLeft([-180, -90, 180, 90]),
                    resolutions: matrixSet.resolutions,
                    matrixIds: matrixSet.matrixIds,
                    tileSize: layer.tileSize
                })

                // override getZForResolution on tile grid object;
                // for weird zoom levels, the default is to round up or down to the
                // nearest integer to determine which tiles to use.
                // because we want the printing rasters to contain as much detail as
                // possible, we rig it here to always round up.
                tileGrid.origGetZForResolution = tileGrid.getZForResolution
                tileGrid.getZForResolution = function (resolution, opt_direction) {
                    return tileGrid.origGetZForResolution(resolution, -1)
                }

                // create a tile source
                var tileSource = new ol.source.WMTS({
                    url: layer.wmts_url,
                    layer: layer.id,
                    matrixSet: matrixSet.name,
                    format: layer.format,
                    style: layer.style,
                    projection: layer.projection,
                    wrapX: true,
                    tileGrid: tileGrid
                })

                var tileLayer = new ol.layer.Tile({
                    opacity: layer.opacity || 1,
                    source: tileSource
                })

                // hook the tile loading function to update progress indicator
                tileLayer.progress = ''
                tileSource.setTileLoadFunction(_tileLoaderHook(tileSource, tileLayer))

                // if the "refresh" option is set, set a timer
                // to force a reload of the tile content
                if (layer.refresh) {
                    tileLayer.set('updated', moment().toLocaleString())
                    tileLayer.refresh = setInterval(function () {
                        tileLayer.set('updated', moment().toLocaleString())
                        tileSource.setUrl(layer.wmts_url + '?time=' + moment.utc().unix())
                    }, layer.refresh * 1000)
                }
                // set properties for use in layer selector
                tileLayer.set('name', layer.name)
                tileLayer.set('id', layer.id)
                return tileLayer
            },
            getMapLayer: function (id) {
                return self.map.getLayers().getArray().find(function (layer) {
                    return layer.get('id') === id
                })
            },
            getLayer: function (id) {
                return this.$root.catalogue.catalogue.getArray().find(function (layer) {
                    return layer.id === id
                })
            },
            // initialise map
            init: function (catalogue, layers, options) {
                var self = this;
                this.$root.catalogue.catalogue = new ol.Collection()
                options = options || {}

                var getMapLayer = function () {
                    return self.getMapLayer(this.id)
                }

                this.$root.catalogue.catalogue.on('add', function (event) {
                    var l = event.element
                    l.olLayer = getMapLayer
                    l.id = l.id || l.identifier
                    l.name = l.name || l.title
                    l.init = l.init || self.createTileLayer // override based on layer type
                })

                this.$root.catalogue.catalogue.extend(catalogue)

                var initialLayers = layers.reverse().map(function (id) {
                    return self.getLayer(id).init(self)
                })

                self.map = new ol.Map({
                    logo: false,
                    renderer: 'canvas',
                    target: 'map',
                    layers: initialLayers,
                    view: new ol.View({
                        projection: 'EPSG:4326',
                        center: [123.75, -24.966],
                        zoom: 6,
                        maxZoom: 21,
                        minZoom: 5
                    }),
                    controls: [
                        new ol.control.Zoom(),
                        new ol.control.ScaleLine(),
                        new ol.control.FullScreen({
                            source: $('body').get(0),
                            label: $('<i/>', {
                                class: 'fa fa-expand'
                            })[0]
                        }),
                        new ol.control.Control({
                            element: $('#menu-scale').get(0)
                        }),
                        new ol.control.Attribution()
                    ],
                    interactions: ol.interaction.defaults({
                        altShiftDragRotate: false,
                        pinchRotate: false,
                        dragPan: false,
                        doubleClickZoom: false,
                        keyboard: false
                    })
                })
                var params = {}
                decodeURIComponent(location.search).slice(1).split('&').forEach(function (p) {
                    var t = p.split('=')
                    params[t[0]] = parseFloat(t[1])
                })
                if (params.scale) {
                    self.map.getView().setCenter([params.lon, params.lat])
                    self.setScale(params.scale / 1000)
                }
                // add some default interactions
                self.dragPanInter = new ol.interaction.DragPan()
                self.doubleClickZoomInter = new ol.interaction.DoubleClickZoom()
                self.keyboardPanInter = new ol.interaction.KeyboardPan()
                self.keyboardZoomInter = new ol.interaction.KeyboardZoom()
                self.map.addInteraction(self.dragPanInter)
                self.map.addInteraction(self.doubleClickZoomInter)
                self.map.addInteraction(self.keyboardPanInter)
                self.map.addInteraction(self.keyboardZoomInter)

                // Create the graticule component
                self.graticule = new ol.LabelGraticule()
                self.graticule.setMap(self.map)

                // setup scale events
                self.map.on('postrender', function () {
                    if (self.mapScaleControl) {
                        self.mapScaleControl.scale = self.getScale()
                    }
                    if (self.mapExportControls) {
                        history.replaceState(null, null, location.pathname + '?' + self.mapExportControls.shortUrl)
                    }
                })
                if (document.querySelector('#info')) {
                    self.initInfo()
                }
                if (document.querySelector('#menu-tab-layers')) {
                    self.initLayers()
                }
                if (document.querySelector('#menu-tab-annotations')) {
                    self.initAnnotations()
                }

                $.get('static/images/legend.svg', function (tmpl) {
                    $('#legendsvg').html(tmpl)
                    self.initMapControls()
                }, 'text')
                // display hover popups
                self.map.on('pointermove', this.$refs.info.onPointerMove)
            }
        },
        ready: function() {
            // generate matrix IDs from name and level number
            $.each(this._matrixSets, function (projection, innerMatrixSets) {
                $.each(innerMatrixSets, function (tileSize, matrixSet) {
                    var matrixIds = new Array(matrixSet.maxLevel - matrixSet.minLevel + 1)
                    for (var z = matrixSet.minLevel; z <= matrixSet.maxLevel; ++z) {
                        matrixIds[z] = matrixSet.name + ':' + z
                    }
                    matrixSet.matrixIds = matrixIds
                })
            })
        }
    }
</script>

<style>
    /* full-height page for slippy map, thanks */

    html,
    body {
        width: 100%;
        height: 100%;
        margin: 0;
        padding: 0;
        overflow: hidden;
    }

    [v-cloak] {
        display: none;
    }
    /* mousehover info pane, hide for mobile */

    @media screen and (max-width: 40em) {
        #info {
            display: none;
        }
    }

    #info {
        position: absolute;
        z-index: 100;
        padding: 2em;
        background-color: #424f5a;
        max-width: 40%;
    }

    #info .content {
        max-height: 30vh;
        overflow: auto;
    }
    /* full-height the foundation off-canvas stuff also */

    .off-canvas-wrapper {
        height: 100%;
    }

    .off-canvas-wrapper-inner {
        height: 100%;
        background-color: #424f5a;
        color: #fbfbfb;
    }
    /* SVG icons for buttons (include margin for aligning to text) */

    .icon {
        width: 20px;
        height: 20px;
        margin-bottom: -5px;
    }

    .off-canvas-content {
        height: 100%;
    }

    .off-canvas {
        height: 100%;
        display: flex;
        flex-direction: column;
        justify-content: flex-start;
        align-items: stretch;
        align-content: stretch;
    }

    .off-canvas .tabs-content {
        height: 100%;
        flex: 1;
    }
    /* slippy map widget */

    .map {
        width: 100%;
        height: 100%;
        min-height: 20px;
        flex: 1;
        background: url('../static/images/bg.png') repeat;
        background-position: center;
        position: relative;
        outline: 0;
    }

    .ol-overviewmap .ol-overviewmap-map {
        border: 0px;
        margin: 0px;
        width: 30vw;
        height: 30vh;
    }

    .ol-overviewmap .ol-overviewmap-map .ol-overviewmap-box {
        display: none;
    }
    /* position fixes for top-right OpenLayers map widgets */

    .ol-scale-line {
        top: 72px;
        left: auto;
        right: 72px;
        bottom: auto;
        border: 1px solid transparent;
    }

    .ol-full-screen {
        top: 16px;
        right: 16px;
        padding: 0;
    }

    .ol-zoom {
        top: 72px;
        left: auto;
        right: 16px;
        bottom: auto;
        padding: 0;
    }

    .ol-zoom button,
    .ol-full-screen button {
        width: 48px;
        height: 48px;
    }

    #menu-scale {
        position: absolute;
        top: 16px;
        right: 72px;
        width: 96px;
        cursor: pointer;
        font-size: 0.8rem;
        border: 0;
        margin: 1px;
        color: white;
        height: 48px;
    }
    /* for mobile screens, size the pane to fill the viewport */

    .off-canvas.position-left {
        left: -100vw;
        width: 100vw;
    }

    .position-left.reveal-responsive {
        left: 0;
        position: fixed;
        z-index: 0;
    }

    .position-left.reveal-responsive ~ .off-canvas-content {
        margin-left: 100vw;
    }

    #side-pane-close {
        display: none;
    }

    .position-left.reveal-responsive #side-pane-close {
        display: block;
        position: absolute;
        right: 0;
        top: 0;
    }
    /* for non-mobile screens, have a fixed 400px left pane */

    @media screen and (min-width: 40em) {
        /* widen the off canvas area*/
        .off-canvas.position-left {
            left: -424px;
            width: 424px;
        }
        .position-left.reveal-responsive {
            left: 0;
            position: fixed;
            z-index: auto;
        }
        .position-left.reveal-responsive ~ .off-canvas-content {
            margin-left: 424px;
        }
        #side-pane-close {
            display: none;
        }
        .position-left.reveal-responsive #side-pane-close {
            display: none;
        }
    }
    /* raise our widgets above the slippy map */

    .map-widget {
        position: absolute;
        z-index: 1;
    }
    /* remove some of the annoying tab box styles */

    #offCanvasLeft {
        background-color: transparent;
    }

    #offCanvasLeft .tabs-content {
        background-color: transparent;
        border: 0;
    }
    /* fix tool labels/padding */

    .tool-label,
    .off-canvas label.tool-label {
        font-size: 80%;
        float: left;
        width: 200px;
    }

    .tool-slice {
        margin-bottom: 4px;
    }

    .tool-slice input,
    .tool-slice select,
    .tool-slice button {
        margin: 0px;
    }

    .tool-slice .button.selected {
        border: 2px dotted #ffffff;
    }

    ;
    .button-group input {
        display: none;
    }

    .button-group input:checked + label,
    .button-group input:checked + label:active {
        background-color: rgba(0, 60, 136, 0.7);
    }
    /* make range sliders visible */

    input[type="range"] {
        background-color: transparent;
    }

    .tool-slice input[type="range"] {
        width: 100%;
    }
    /* make tool button text bigger */

    .button-group.expanded .button-tool {
        padding: 0.5em;
        font-size: 18px;
    }

    #menu-tabs {
        background-color: transparent;
        border: 0;
    }

    #menu-pagesize {
        font-weight: bold;
    }
    /* styling for tabs used in the side-pane */

    .off-canvas .tabs {
        background-color: rgba(37, 45, 51, 0.9);
        border: 0;
    }

    .off-canvas .tabs a {
        color: white;
        border: 1px solid transparent;
        text-align: center;
        padding: 16px 4px;
    }

    .off-canvas .tabs a:hover {
        background-color: rgba(128, 128, 128, 0.23);
    }

    .off-canvas .tabs a:focus {
        background-color: transparent;
    }

    .off-canvas .tabs a[aria-selected="true"] {
        background-color: rgba(128, 128, 128, 0.46);
        border: 1px solid rgba(37, 45, 51, 0.9);
    }
    /* rig the tabs to use flexbox for full-width */

    .off-canvas .tabs {
        display: flex;
        width: 100%;
    }

    .off-canvas .tabs .tabs-title {
        flex: 1 1 0px;
    }

    .off-canvas .tabs a {
        height: 100%;
    }
    /* styling for switch widgets used in the side pane */

    .off-canvas .switch-paddle {
        background: rgba(37, 45, 51, 0.9);
    }

    .off-canvas .switch-input,
    .off-canvas .switch {
        margin: 0;
    }

    .off-canvas label {
        color: white;
        line-height: inherit;
        font-size: inherit;
        font-weight: inherit;
    }

    .side-label {
        margin-left: 1em;
    }
    /* Set height of internal layers to allow for bottom details */

    .layers-topframe {
        height: 60vh;
        border-bottom: 1px solid rgba(37, 45, 51, 0.9);
    }

    .scroller {
        overflow-y: auto;
    }
    /* remove extra padding for tab boxes inside other tab boxes */

    #layers-list {
        padding: 1rem 0 0 0;
    }
    /* styling for movable row boxes */

    .layer-row,
    .feature-row {
        color: white;
        background-color: rgba(37, 45, 51, 0.5);
        padding: 4px;
        cursor: pointer;
    }

    .layer-row:nth-child(even),
    .feature-row:nth-child(even) {
        background-color: rgba(37, 45, 51, 0.3);
    }

    .layer-row .layer-title {
        font-size: 120%;
    }

    .layer-row .layer-id {
        font-style: italic;
        font-size: 75%;
    }
    /* Add dotted line box to dragged layers*/

    .layer-row.gu-transit {
        border: 2px dashed #ffffff;
        border-radius: 4px;
    }
    /* Add status indicator to left hand side of layer rows */

    .layer-row {
        border-left: 3px solid transparent;
    }

    @keyframes layer-pulse {
        0% {
            border-left-color: #ffcf41;
        }
        50% {
            border-left-color: #edb100;
        }
        100% {
            border-left-color: #ffcf41;
        }
    }

    .layer-row.idle {
        border-left-color: #71c837;
    }

    .layer-row.loading {
        animation-name: layer-pulse;
        animation-duration: 1s;
        animation-iteration-count: infinite;
    }

    .layer-row.error {
        border-left-color: #ec5840;
    }
    /* custom F6 tab style overrides for the side buttons  */

    .side-button > a {
        background-color: rgba(37, 45, 51, 0.7);
        border-left: 3px solid transparent;
        padding: 1em;
        font-size: 24px;
        color: white;
    }

    .side-button .icon {
        width: 24px;
        height: 24px;
    }

    .side-button > a:hover {
        background-color: rgba(37, 45, 51, 0.9);
    }

    .side-button > a:focus {
        background-color: rgba(37, 45, 51, 0.7);
    }

    .side-button > a[aria-selected="true"] {
        background-color: rgba(37, 45, 51, 0.8);
        border-left: 3px solid #2199e8;
    }
    /* fix absurdly large margins on button groups */

    .button-group {
        margin-bottom: 1px;
    }
    /* colouring changes to OpenLayers widgets to match side buttons*/

    .ol-control,
    .ol-control:hover {
        background-color: transparent;
    }

    .ol-control button,
    .ol-control button:focus,
    .ol-scale-line,
    #menu-scale {
        background-color: rgba(37, 45, 51, 0.7);
    }

    .ol-control button:hover,
    .ol-scale-line:hover,
    #menu-scale:hover {
        background-color: rgba(37, 45, 51, 0.9);
    }
</style>


2.14     + Отчётный поиск результатов

2.14
    + Отчётный поиск результатов

<template>

        <button class="btn btn-blue-nb btn-ell dropdown-toggle" type="button" data-toggle="dropdown"
                style="width: 200px;text-align: left!important;"> {{selected_base.title}}
        </button>

{{row.title}}

      <input type="text" class="form-control" v-model="query" placeholder="Введите запрос" autofocus
             :maxlength="query_limit" @keyup.enter="search">
      <button style="margin-right: -2px" class="btn last btn-blue-nb" type="button"
                                            :disabled="!query_valid || inLoading" @click="search">Поиск</button>

      <colgroup>
        <col width="127">
        <col>
        <col width="127">
        <col>
      </colgroup>

        ФИО:
        {{selected_card.family}} {{selected_card.name}}
          {{selected_card.twoname}}

        Номер карты:
        {{selected_card.num}}

        Дата рождения:
        {{selected_card.birthday}} ({{selected_card.age}})
        Пол:
        {{selected_card.sex}}

          Номер истории:

            <input type="text" class="form-control" maxlength="11" v-model="history_num"
                                          :disabled="!selected_base.history_number"/>

        Работа от имени:

          <select-picker-b v-model="directive_department" :options="directive_departments_select"/>

          <select-picker-b v-model="directive_doc" :options="directive_docs_select"/>

    <slot name="for_card" v-if="loaded" style="margin-top: 5px"/>
    <slot name="for_all" style="margin-top: 5px"/>

    <modal ref="modal" v-show="showModal" @close="hide_modal" show-footer="true">
      Найдено несколько карт

          <colgroup>
            <col width="95">
            <col width="150">
            <col>
            <col width="140">
          </colgroup>

            Категория
            Карта
            ФИО, пол
            Дата рождения

            {{row.type_title}}
            {{row.num}}
            {{row.family}} {{row.name}} {{row.twoname}}, {{row.sex}}
            {{row.birthday}}

        Показано не более 10 карт

    </modal>

</template>


[Misc] Fix ridiculous typo

[Misc] Fix ridiculous typo

<template>

      <livedata-entry-selector-all></livedata-entry-selector-all>

        {{ property.name }}

</template>


Remove reactive script search

Remove reactive script search

- Reactive query parameters might confuse users.
- Searching script is triggered by hitting search button.

<template>

                        <colgroup>
                            <col width="400px"/>
                            <col width="*"/>
                        </colgroup>

                                <input type="text" class="search-query span3" placeholder="Keywords" v-model="query" @keyup.enter="search">
                                <button class="btn" @click="search">

                                </button>

                                    SVN

                        <colgroup>
                            <col width="600px"/>
                            <col width="340px"/>
                        </colgroup>

                                <template v-if="$route.name !== 'scriptSearch'">

                                </template>

        <create-script-modal :currentPath="currentPath"></create-script-modal>
        <create-folder-modal :currentPath="currentPath"></create-folder-modal>
        <upload-file-modal :currentPath="currentPath"></upload-file-modal>

</template>


Fix mime type icon height

Fix mime type icon height

Whenever a mime type icon is displayed, apply min-height to make it look
bigger.

Signed-off-by: Vincent Petry <87e1f221a672a14a323e57bb65eaea19d3ed3804@nextcloud.com>

<template>
	<file-preview v-bind="filePreview"
		:tabindex="wrapperTabIndex"
		class="file-preview"
		:class="{ 'file-preview--viewer-available': isViewerAvailable, 'file-preview--upload-editor': isUploadEditor }"
		@click="handleClick"
		@keydown.enter="handleClick">

		{{ name }}
		<button v-if="isUploadEditor"
			tabindex="1"
			:aria-label="removeAriaLabel"
			class="remove-file primary">
			<Close class="remove-file__icon" decorative @click="$emit('remove-file', id)" />
		</button>
		<ProgressBar v-if="isTemporaryUpload && !isUploadEditor" :value="uploadProgress" />
	</file-preview>
</template>


Fixed issue with setting an clearing visits

<template>
  <section>
    <marionette-view
      v-if="typeOfView == 'marionette'"
      :key="$route.fullPath"
      :options="options"
      :fetchOnLoad="true"
      :mview="mview"
      :breadcrumbs="bc"
    />
    <!-- For a MarionetteView - collection, model and params are wrapped in the
         options property for pure Vue views they are better as individual
         properties -->
    <em-dc-list
      v-if="typeOfView == 'EmDcList'"
      :collection="collection"
      :model="model"
      :params="params"
      :breadcrumbs="bc"
    />
  </section>
</template>

<script>
// Allow us to map store values to local computed properties
import { mapGetters } from 'vuex'

import MarionetteView from 'app/views/marionette/marionette-wrapper.vue'

import DCList from 'modules/dc/datacollections'
import GenericDCList from 'modules/types/gen/dc/datacollections'
import SMDCList from 'modules/types/sm/dc/datacollections'
import TomoDCList from 'modules/types/tomo/dc/datacollections'
import EmDcList from 'modules/types/em/dc-list/em-dc-list.vue'
import POWDCList from 'modules/types/pow/dc/datacollections'
import SAXSDCList from 'modules/types/saxs/dc/datacollections'
import XPDFDCList from 'modules/types/xpdf/dc/datacollections'

import DCCol from 'collections/datacollections'
import Proposal from 'models/proposal'
import Visit from 'models/visit'

let dc_views = {
  mx: DCList,
  sm: SMDCList,
  gen: GenericDCList,
  tomo: TomoDCList,
  em: EmDcList,
  pow: POWDCList,
  saxs: SAXSDCList,
  xpdf: XPDFDCList,
}

export default {
    name: 'dc',
    components: {
        'marionette-view': MarionetteView,
        'em-dc-list': EmDcList,
    },
    props: {
        'id': Number,
        'visit' : String,
        'page' : Number,
        'search': String,
        'dcg': String,
        'pjid': Number,
        'ty': String,
    },
    data: function() {
        return {
            ready: false,
            mview: undefined, // don't use `null` it has `typeof` = `object`
            model: null,
            collection: null,
            params: null,
            bc : [{ title: 'Data Collections', url: '/dc' }],
            error: '', // Used to provide context to proposal lookup
        }
    },
    computed: {
        options: function() {
            return {
                collection: this.collection,
                model: this.model,
                params: this.params
            }
        },
        typeOfView: function() {
            // Vue components are objects
            if (typeof this.mview == 'object') {
                return this.mview.name
            }
            // Marionette views are functions (and need the `ready` flag set)
            if (typeof this.mview == 'function' && this.ready) {
                return 'marionette'
            }
            // Anything else, including the default `undefined`
            // Don't use `null` as a default: `typeof null === 'object'`
            return 'not-ready'
        },
        // Combine with local computed properties, spread operator
        // Allows us to use this.currentProposal mapped to vuex state/getters
        ...mapGetters('proposal', ['currentProposal'])
    },
    created: function() {
        // Setup backbone collection and params that will be passed into marionette view
        this.collection = new DCCol(null, {
                        state: { currentPage: this.page ? parseInt(this.page) : 1, pageSize: app.mobile() ? 5 : 15},
                        queryParams: { visit: this.visit, s: this.search, t: this.ty, id: this.id, dcg: this.dcg, PROCESSINGJOBID: this.pjid }
                    })
        this.params = { visit: this.visit, search: this.search, type: this.ty, id: this.id, dcg: this.dcg, pjid: this.pjid }
    },
    mounted: function() {
        this.initialiseView()
    },
    methods: {
        initialiseView: function() {
            // Determine what our model should be...
            // The model is either a visit or proposal used to determine proposalType in the mview
            this.setModel()

            // Start loading animation
            this.$store.commit('loading', true)

            // Fetch the model then set the breadcrumbs
            this.$store.dispatch('getModel', this.model).then( () => {
                if (this.model.has('VISIT')) {
                    this.$store.commit('proposal/setVisit', this.model.get('VISIT'))
                } else {
                    this.$store.commit('proposal/clearVisit')
                }

                // Stop loading animation.
                // Note - not cancelled in finally block but in success/error blocks
                // This avoids premature cancelling of mview loading data collections
                this.$store.commit('loading', false)

                // Set breadcrumbs now we have the model
                this.setBreadcrumbs()

                // Not using lookup to set the proposal type...?
                // This is from original router/controller logic
                let proposalType = this.model.get('TYPE')

                // Determine correct marionette view - defaults to mx
                this.setView(proposalType)
            }, () => {
                // Error getting model
                this.$store.commit('proposal/clearVisit')
                // Again cancel the loading animation here
                this.$store.commit('loading', false)
                console.log(this.$options.name + " Error getting model " + this.error)
                app.alert({ title: 'Error getting model', message: this.error})
            }).finally( () => {
                // Only render when complete
                this.ready = true
            })
        },

        // Set the model to either a visit or proposal
        setModel: function() {
            // We need to fetch a visit or proposal to determine the proposal type
            if (this.visit) {
                app.cookie(this.visit.split('-')[0])
                // Sets the proposal based on visit path parameter
                this.model = new Visit({ VISIT: this.visit })
                this.error = 'The specified visit does not exist'
            } else {
                // Lookup the current proposal data
                this.model = new Proposal({ PROPOSAL: this.currentProposal })
                this.error = 'The specified proposal does not exist'
            }
        },
        // Set Breadcrumbs - depends on if visit provided
        setBreadcrumbs: function() {
            if (this.visit) {
                this.bc.push({ title: this.model.get('BL') })
                this.bc.push({ title: this.visit })
            } else {
                this.bc.push({ title: this.currentProposal })
            }
        },
        // Set marionette view based on a passed proposal type
        setView: function(proposalType) {
            this.mview = dc_views[proposalType] || dc_views['mx']
        },
    },
}
</script>

<template>
  <section>
    <marionette-view
      v-if="typeOfView == 'marionette'"
      :key="$route.fullPath"
      :options="options"
      :fetchOnLoad="true"
      :mview="mview"
      :breadcrumbs="bc"
    />
    <!-- For a MarionetteView - collection, model and params are wrapped in the
         options property for pure Vue views they are better as individual
         properties -->
    <em-dc-list
      v-if="typeOfView == 'EmDcList'"
      :collection="collection"
      :model="model"
      :params="params"
      :breadcrumbs="bc"
    />
  </section>
</template>

<script>
// Allow us to map store values to local computed properties
import { mapGetters } from 'vuex'

import MarionetteView from 'app/views/marionette/marionette-wrapper.vue'

import DCList from 'modules/dc/datacollections'
import GenericDCList from 'modules/types/gen/dc/datacollections'
import SMDCList from 'modules/types/sm/dc/datacollections'
import TomoDCList from 'modules/types/tomo/dc/datacollections'
import EmDcList from 'modules/types/em/dc-list/em-dc-list.vue'
import POWDCList from 'modules/types/pow/dc/datacollections'
import SAXSDCList from 'modules/types/saxs/dc/datacollections'
import XPDFDCList from 'modules/types/xpdf/dc/datacollections'

import DCCol from 'collections/datacollections'
import Proposal from 'models/proposal'
import Visit from 'models/visit'

let dc_views = {
  mx: DCList,
  sm: SMDCList,
  gen: GenericDCList,
  tomo: TomoDCList,
  em: EmDcList,
  pow: POWDCList,
  saxs: SAXSDCList,
  xpdf: XPDFDCList,
}

export default {
    name: 'dc',
    components: {
        'marionette-view': MarionetteView,
        'em-dc-list': EmDcList,
    },
    props: {
        'id': Number,
        'visit' : String,
        'page' : Number,
        'search': String,
        'dcg': String,
        'pjid': Number,
        'ty': String,
    },
    data: function() {
        return {
            ready: false,
            mview: undefined, // don't use `null` it has `typeof` = `object`
            model: null,
            collection: null,
            params: null,
            bc : [{ title: 'Data Collections', url: '/dc' }],
            error: '', // Used to provide context to proposal lookup
        }
    },
    computed: {
        options: function() {
            return {
                collection: this.collection,
                model: this.model,
                params: this.params
            }
        },
        typeOfView: function() {
            // Vue components are objects
            if (typeof this.mview == 'object') {
                return this.mview.name
            }
            // Marionette views are functions (and need the `ready` flag set)
            if (typeof this.mview == 'function' && this.ready) {
                return 'marionette'
            }
            // Anything else, including the default `undefined`
            // Don't use `null` as a default: `typeof null === 'object'`
            return 'not-ready'
        },
        // Combine with local computed properties, spread operator
        // Allows us to use this.currentProposal mapped to vuex state/getters
        ...mapGetters('proposal', ['currentProposal'])
    },
    created: function() {
        // Setup backbone collection and params that will be passed into marionette view
        this.collection = new DCCol(null, {
                        state: { currentPage: this.page ? parseInt(this.page) : 1, pageSize: app.mobile() ? 5 : 15},
                        queryParams: { visit: this.visit, s: this.search, t: this.ty, id: this.id, dcg: this.dcg, PROCESSINGJOBID: this.pjid }
                    })
        this.params = { visit: this.visit, search: this.search, type: this.ty, id: this.id, dcg: this.dcg, pjid: this.pjid }
    },
    mounted: function() {
        this.initialiseView()
    },
    methods: {
        initialiseView: function() {
            // Determine what our model should be...
            // The model is either a visit or proposal used to determine proposalType in the mview
            this.setModel()

            // Start loading animation
            this.$store.commit('loading', true)

            // Fetch the model then set the breadcrumbs
            this.$store.dispatch('getModel', this.model).then( () => {
                this.$store.commit('proposal/setVisit', this.model.get('VISIT'))
                // Stop loading animation.
                // Note - not cancelled in finally block but in success/error blocks
                // This avoids premature cancelling of mview loading data collections
                this.$store.commit('loading', false)

                // Set breadcrumbs now we have the model
                this.setBreadcrumbs()

                // Not using lookup to set the proposal type...?
                // This is from original router/controller logic
                let proposalType = this.model.get('TYPE')

                // Determine correct marionette view - defaults to mx
                this.setView(proposalType)
            }, () => {
                // Error getting model
                this.$store.commit('proposal/clearVisit')
                // Again cancel the loading animation here
                this.$store.commit('loading', false)
                console.log(this.$options.name + " Error getting model " + this.error)
                app.alert({ title: 'Error getting model', message: this.error})
            }).finally( () => {
                // Only render when complete
                this.ready = true
            })
        },

        // Set the model to either a visit or proposal
        setModel: function() {
            // We need to fetch a visit or proposal to determine the proposal type
            if (this.visit) {
                app.cookie(this.visit.split('-')[0])
                // Sets the proposal based on visit path parameter
                this.model = new Visit({ VISIT: this.visit })
                this.error = 'The specified visit does not exist'
            } else {
                // Lookup the current proposal data
                this.model = new Proposal({ PROPOSAL: this.currentProposal })
                this.error = 'The specified proposal does not exist'
            }
        },
        // Set Breadcrumbs - depends on if visit provided
        setBreadcrumbs: function() {
            if (this.visit) {
                this.bc.push({ title: this.model.get('BL') })
                this.bc.push({ title: this.visit })
            } else {
                this.bc.push({ title: this.currentProposal })
            }
        },
        // Set marionette view based on a passed proposal type
        setView: function(proposalType) {
            this.mview = dc_views[proposalType] || dc_views['mx']
        },
    },
}
</script>


Complete goods.vue

Complete goods.vue

<template>

 0}">
             0}">

{{totalCount}}

 0}">¥{{totalPrice}}

另需配送费¥{{deliveryPrice}}元

= minPrice}">

{{payDesc}}

          购物车
          清除

              {{food.name}}

                ¥{{food.count*food.price}}

                <cartcontrol :food="food"></cartcontrol>

</template>

<template>

 0}">
             0}">

{{totalCount}}

 0}">¥{{totalPrice}}

另需配送费¥{{deliveryPrice}}元

= minPrice}">

{{payDesc}}

</template>


Fix# mainWrapper too high

Fix# mainWrapper too high

<template>
    <div id="app" tabindex="0"
        @keydown.space="togglePlayback"
        @keydown.j = "playNext"
        @keydown.k = "playPrev"
        @keydown.f = "search"
        @keydown.177 = "playPrev"
        @keydown.176 = "playNext"
        @keydown.179 = "togglePlayback"
    >
        <main-wrapper></main-wrapper>
        <site-footer></site-footer>
        <overlay :state.sync="overlayState"></overlay>
    </div>
</template>

<script>
    import $ from 'jquery';

    import siteFooter from './components/site-footer/index.vue';
    import mainWrapper from './components/main-wrapper/index.vue';
    import overlay from './components/shared/overlay.vue';

    import sharedStore from './stores/shared';
    import preferenceStore from './stores/preference';
    import playback from './services/playback';

    export default {
        components: { siteFooter, mainWrapper, overlay },

        replace: false,

        data() {
            return {
                prefs: preferenceStore.state,

                overlayState: {
                    showing: true,
                    dismissable: false,
                    type: 'loading',
                    message: '',
                },
            };
        },

        ready() {
            this.showOverlay();

            // Make the most important HTTP request to get all necessary data from the server.
            // Afterwards, init all mandatory stores and services.
            sharedStore.init(() => {
                playback.init(this);

                this.hideOverlay();

                // Ask for user's notification permission.
                this.requestNotifPermission();

                // Let all other components know we're ready.
                this.$broadcast('koel:ready');
            });
        },

        methods: {
            /**
             * Toggle playback when user presses Space key.
             *
             * @param {Object} e The keydown event
             */
            togglePlayback(e) {
                if ($(e.target).is('input,textarea,button,select')) {
                    return true;
                }

                // Ah... Good ol' jQuery. Whatever play/pause control is there, we blindly click it.
                $('#mainFooter .play:visible, #mainFooter .pause:visible').click();
                e.preventDefault();
            },

            /**
             * Play the previous song when user presses K.
             *
             * @param {Object} e The keydown event
             */
            playPrev(e) {
                if ($(e.target).is('input,textarea')) {
                    return true;
                }

                playback.playPrev();
                e.preventDefault();
            },

            /**
             * Play the next song when user presses J.
             *
             * @param {Object} e The keydown event
             */
            playNext(e) {
                if ($(e.target).is('input,textarea')) {
                    return true;
                }

                playback.playNext();
                e.preventDefault();
            },

            /**
             * Put focus into the search field when user presses F.
             *
             * @param {Object} e The keydown event
             */
            search(e) {
                if ($(e.target).is('input,textarea') || e.metaKey || e.ctrlKey) {
                    return true;
                }

                $('#searchForm input[type="search"]').focus().select();
                e.preventDefault();
            },

            /**
             * Request for notification permission if it's not provided and the user is OK with notifs.
             */
            requestNotifPermission() {
                if (window.Notification && this.prefs.notify && Notification.permission !== 'granted') {
                    Notification.requestPermission(result => {
                        if (result === 'denied') {
                            preferenceStore.set('notify', false);
                        }
                    });
                }
            },

            /**
             * Load (display) a main panel (view).
             *
             * @param string view The view, which can be found under components/main-wrapper/main-content.
             */
            loadMainView(view) {
                this.$broadcast('main-content-view:load', view);
            },

            /**
             * Load a playlist into the main panel.
             *
             * @param {Object} playlist The playlist object
             */
            loadPlaylist(playlist) {
                this.$broadcast('playlist:load', playlist);
                this.loadMainView('playlist');
            },

            /**
             * Load the Favorites view.
             */
            loadFavorites() {
                this.loadMainView('favorites');
            },

            /**
             * Shows the overlay.
             *
             * @param {String}  message     The message to display.
             * @param {String}  type        (loading|success|info|warning|error)
             * @param {Boolean} dismissable Whether to show the Close button
             */
            showOverlay(message = 'Just a little patience…', type = 'loading', dismissable = false) {
                this.overlayState.message = message;
                this.overlayState.type = type;
                this.overlayState.dismissable = dismissable;
                this.overlayState.showing = true;
            },

            /**
             * Hides the overlay.
             */
            hideOverlay() {
                this.overlayState.showing = false;
            },

            /**
             * Shows the close button, allowing the user to close the overlay.
             */
            setOverlayDimissable() {
                this.overlayState.dismissable = true;
            },
        },
    };

    /**
     * Modified version of orderBy that is case insensitive
     *
     * @source https://github.com/vuejs/vue/blob/dev/src/filters/array-filters.js
     */
    Vue.filter('caseInsensitiveOrderBy', (arr, sortKey, reverse) => {
        if (!sortKey) {
            return arr;
        }

        var order = (reverse && reverse < 0) ? -1 : 1

        // sort on a copy to avoid mutating original array
        return arr.slice().sort((a, b) => {
            a = Vue.util.isObject(a) ? Vue.parsers.path.getPath(a, sortKey) : a
            b = Vue.util.isObject(b) ? Vue.parsers.path.getPath(b, sortKey) : b

            a = a === undefined ? a : a.toLowerCase()
            b = b === undefined ? b : b.toLowerCase()

            return a === b ? 0 : a > b ? order : -order
        });
    });

    // Register the global directives
    Vue.directive('koel-focus', require('./directives/focus'));
</script>

<style lang="sass">
    @import "../../sass/partials/_vars.scss";
    @import "../../sass/partials/_mixins.scss";
    @import "../../sass/partials/_shared.scss";

    #app {
        display: flex;
        flex-direction: column;

        background: $colorMainBgr;
        color: $colorMainText;

        font-family: $fontFamily;
        font-size: $fontSize;
        line-height: $fontSize * 1.5;
        font-weight: $fontWeight_Thin;

        padding-bottom: $footerHeight;
    }
</style>

<template>
    <div id="app" tabindex="0"
        @keydown.space="togglePlayback"
        @keydown.j = "playNext"
        @keydown.k = "playPrev"
        @keydown.f = "search"
        @keydown.177 = "playPrev"
        @keydown.176 = "playNext"
        @keydown.179 = "togglePlayback"
    >
        <main-wrapper></main-wrapper>
        <site-footer></site-footer>
        <overlay :state.sync="overlayState"></overlay>
    </div>
</template>

<script>
    import $ from 'jquery';

    import siteFooter from './components/site-footer/index.vue';
    import mainWrapper from './components/main-wrapper/index.vue';
    import overlay from './components/shared/overlay.vue';

    import sharedStore from './stores/shared';
    import preferenceStore from './stores/preference';
    import playback from './services/playback';

    export default {
        components: { siteFooter, mainWrapper, overlay },

        replace: false,

        data() {
            return {
                prefs: preferenceStore.state,

                overlayState: {
                    showing: true,
                    dismissable: false,
                    type: 'loading',
                    message: '',
                },
            };
        },

        ready() {
            this.showOverlay();

            // Make the most important HTTP request to get all necessary data from the server.
            // Afterwards, init all mandatory stores and services.
            sharedStore.init(() => {
                playback.init(this);

                this.hideOverlay();

                // Ask for user's notification permission.
                this.requestNotifPermission();

                // Let all other components know we're ready.
                this.$broadcast('koel:ready');
            });
        },

        methods: {
            /**
             * Toggle playback when user presses Space key.
             *
             * @param {Object} e The keydown event
             */
            togglePlayback(e) {
                if ($(e.target).is('input,textarea,button,select')) {
                    return true;
                }

                // Ah... Good ol' jQuery. Whatever play/pause control is there, we blindly click it.
                $('#mainFooter .play:visible, #mainFooter .pause:visible').click();
                e.preventDefault();
            },

            /**
             * Play the previous song when user presses K.
             *
             * @param {Object} e The keydown event
             */
            playPrev(e) {
                if ($(e.target).is('input,textarea')) {
                    return true;
                }

                playback.playPrev();
                e.preventDefault();
            },

            /**
             * Play the next song when user presses J.
             *
             * @param {Object} e The keydown event
             */
            playNext(e) {
                if ($(e.target).is('input,textarea')) {
                    return true;
                }

                playback.playNext();
                e.preventDefault();
            },

            /**
             * Put focus into the search field when user presses F.
             *
             * @param {Object} e The keydown event
             */
            search(e) {
                if ($(e.target).is('input,textarea') || e.metaKey || e.ctrlKey) {
                    return true;
                }

                $('#searchForm input[type="search"]').focus().select();
                e.preventDefault();
            },

            /**
             * Request for notification permission if it's not provided and the user is OK with notifs.
             */
            requestNotifPermission() {
                if (window.Notification && this.prefs.notify && Notification.permission !== 'granted') {
                    Notification.requestPermission(result => {
                        if (result === 'denied') {
                            preferenceStore.set('notify', false);
                        }
                    });
                }
            },

            /**
             * Load (display) a main panel (view).
             *
             * @param string view The view, which can be found under components/main-wrapper/main-content.
             */
            loadMainView(view) {
                this.$broadcast('main-content-view:load', view);
            },

            /**
             * Load a playlist into the main panel.
             *
             * @param {Object} playlist The playlist object
             */
            loadPlaylist(playlist) {
                this.$broadcast('playlist:load', playlist);
                this.loadMainView('playlist');
            },

            /**
             * Load the Favorites view.
             */
            loadFavorites() {
                this.loadMainView('favorites');
            },

            /**
             * Shows the overlay.
             *
             * @param {String}  message     The message to display.
             * @param {String}  type        (loading|success|info|warning|error)
             * @param {Boolean} dismissable Whether to show the Close button
             */
            showOverlay(message = 'Just a little patience…', type = 'loading', dismissable = false) {
                this.overlayState.message = message;
                this.overlayState.type = type;
                this.overlayState.dismissable = dismissable;
                this.overlayState.showing = true;
            },

            /**
             * Hides the overlay.
             */
            hideOverlay() {
                this.overlayState.showing = false;
            },

            /**
             * Shows the close button, allowing the user to close the overlay.
             */
            setOverlayDimissable() {
                this.overlayState.dismissable = true;
            },
        },
    };

    /**
     * Modified version of orderBy that is case insensitive
     *
     * @source https://github.com/vuejs/vue/blob/dev/src/filters/array-filters.js
     */
    Vue.filter('caseInsensitiveOrderBy', (arr, sortKey, reverse) => {
        if (!sortKey) {
            return arr;
        }

        var order = (reverse && reverse < 0) ? -1 : 1

        // sort on a copy to avoid mutating original array
        return arr.slice().sort((a, b) => {
            a = Vue.util.isObject(a) ? Vue.parsers.path.getPath(a, sortKey) : a
            b = Vue.util.isObject(b) ? Vue.parsers.path.getPath(b, sortKey) : b

            a = a === undefined ? a : a.toLowerCase()
            b = b === undefined ? b : b.toLowerCase()

            return a === b ? 0 : a > b ? order : -order
        });
    });

    // Register the global directives
    Vue.directive('koel-focus', require('./directives/focus'));
</script>

<style lang="sass">
    @import "../../sass/partials/_vars.scss";
    @import "../../sass/partials/_mixins.scss";
    @import "../../sass/partials/_shared.scss";

    #app {
        display: flex;
        min-height: 100vh;
        flex-direction: column;

        background: $colorMainBgr;
        color: $colorMainText;

        font-family: $fontFamily;
        font-size: $fontSize;
        line-height: $fontSize * 1.5;
        font-weight: $fontWeight_Thin;

        padding-bottom: $footerHeight;
    }
</style>


feat: add dismiss button to chapter

feat: add dismiss button to chapter