Source

client/ux/mapField/mapField.js

/**
 * 
 * The Map field derives from [Field](kiss.ui.Field.html).
 * 
 * **Map** field displays a map with a text field to enter an address or geo coordinates.
 * 
 * @param {object} config
 * @param {string} [config.value] - Default address or geo coordinates like: latitude,longitude
 * @param {number} [config.zoom] - Zoom level (default 10, max 19)
 * @param {number} [config.mapHeight] - Height (the map width is defined by the field's width)
 * @param {number|string} [config.mapRatio] - Ratio between the field width and the map height (default 4/3). Can be a number or a string to evaluate, like: "4/3", "16/9", 1.77, 1.33, 2, etc. Use this property only if the height is not defined.
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-mapfield class="a-mapfield">
 *  <label class="field-label"></label>
 *  <input type="text|number|date" class="field-input"></input>
 * </a-field>
 * ```
 */
kiss.ux.MapField = class MapField extends kiss.ui.Field {
    /**
     * Its a Custom Web Component. Do not use the constructor directly with the **new** keyword.
     * Instead, use one of the 3 following methods:
     * 
     * Create the Web Component and call its **init** method:
     * ```
     * const myMapField = document.createElement("a-mapfield").init(config)
     * ```
     * 
     * Or use a shorthand to create one the various field types:
     * ```
     * const myMapField = createMapField({
     *  value: "-21,55",
     *  zoom: 15,
     *  width: 600,
     *  mapHeight: 400
     * })
     * 
     * myMapField.render()
     * ```
     * 
     * Or directly declare the config inside a container component:
     * ```
     * const myPanel = createPanel({
     *   title: "My panel",
     *   items: [
     *       {
     *           type: "mapfield",
     *           value: "-21,55",
     *           zoom: 15,
     *           width: 600,
     *           mapHeight: 400
     *       }
     *   ]
     * })
     * myPanel.render()
     * ```
     */
    constructor() {
        super()
    }

    /**
     * @ignore
     */
    init(config = {}) {
        config.type = "mapField"
        config.autoSize = true

        // Generates the text field to enter the address or geo coordinates
        super.init(config)

        // Ensure the map will be displayed below the field
        this.style.flexFlow = "row wrap"

        this._observeKeys()
        return this
    }

    /**
     * @ignore
     */
    _afterRender() {
        // Insert a map right after the field
        this._createMap()

        // Adjust the map height based on the field width, if no height is defined
        if (this.config.mapRatio && !this.config.mapHeight) {
            this._adjustMapRatio()
        }

        // Set the map's default position
        if (this.config.value) {
            this._setMapValue(this.config.value)
        }

        // Add a button to expand the map fullscreen
        this._addExpandButton()
    }

    /**
     * Add a map to the field
     * 
     * @private
     * @ignore
     */
    _createMap() {
        let zoom = this.config.zoom || 10
        if (zoom > 19) zoom = 19
        if (zoom < 1) zoom = 1

        this.map = createMap({
            zoom: this.config.zoom,
            width: this.config.width,
            height: this.config.mapHeight
        })

        this.map.style.order = 2
        this.map.style.flex = "1 1 100%"

        this.appendChild(this.map)
        this.map.render()
    }

    /**
     * Adjusts the map height based on the field width
     * 
     * @private
     * @ignore
     */
    _adjustMapRatio() {
        this.mapRatio = this.config.mapRatio
        if (typeof this.mapRatio == "string") {
            const mapRatio = eval(this.mapRatio)
            this.mapRatio = (isNaN(mapRatio)) ? (4 / 3) : mapRatio
        }

        setTimeout(() => {
            const width = this.getBoundingClientRect().width
            this.map.setHeight(width * 1 / this.mapRatio + "px")
        }, 50)
    }

    /**
     * Updates the field value internally
     * 
     * @private
     * @ignore
     * @param {*} updates 
     */
    _updateField(updates) {
        if (this.id in updates) {
            const newValue = updates[this.id]
            if (newValue || (newValue === 0) || (newValue === "")) {
                this.field.value = newValue
                this._setMapValue(newValue)
            }
        }
    }

    /**
     * Add a button to expand the map fullscreen
     * 
     * @private
     * @ignore
     */
    _addExpandButton() {
        setTimeout(() => {
            const fieldMap = this.map
            const mapExpandButton = document.createElement("button")
            mapExpandButton.innerHTML = "⛶"
            mapExpandButton.classList.add("a-mapfield-button")
            fieldMap.map.getViewport().appendChild(mapExpandButton)
            mapExpandButton.onclick = () => this.expandMap()
        }, 500)
    }

    /**
     * @ignore
     */
    _observeKeys() {
        const _this = this
        this.field.onkeydown = function (e) {
            if (e.key === "Enter") {
                _this._setMapValue(_this.field.value)
            }
        }
    }

    /**
     * @ignore
     */
    _setMapValue(input) {
        const geoloc = kiss.tools.isGeolocation(input)
        if (geoloc) {
            this.map.setGeolocation(geoloc)
        } else {
            this.map.setAddress(input)
        }
    }

    /**
     * Expand the map fullscreen
     * 
     * @returns this
     */
    expandMap() {
        let map = createMap({
            width: "100%",
            height: "100%",
            longitude: this.map.longitude,
            latitude: this.map.latitude,
            zoom: this.map.zoom
        })

        createPanel({
            title: this.config.label,
            closable: true,
            position: "absolute",
            top: 0,
            left: 0,
            padding: 0,
            width: "100%",
            height: "100%",
            items: [
                map
            ]
        }).render()

        return this
    }

    /**
     * Set a new address on the map
     * 
     * IMPORTANT: this methods uses Nominatim for geocoding, which is a free service but has limitations when it comes to the accuracy address street number.
     * 
     * @param {string} address 
     * @returns this
     * 
     * @example
     * myMapField.setAddress("10 Downing Street, London")
     */
    setAddress(address) {
        this.map.setAddress(address)
    }

    /**
     * Set a new geolocation on the map
     * 
     * @param {object} geoloc
     * @param {number} geoloc.longitude
     * @param {number} geoloc.latitude
     * @returns this
     * 
     * @example
     * myMapField.setGeolocation({
     *  longitude: 2.3483915,
     *  latitude: 48.8534951
     * })
     */
    setGeolocation(geoloc) {
        this.map.setGeolocation(geoloc)
    }
}

// Create a Custom Element
customElements.define("a-mapfield", kiss.ux.MapField)
const createMapField = (config) => document.createElement("a-mapfield").init(config)

;