Source

client/ui/elements/image.js

/**
 * 
 * The Image component derives from [Component](kiss.ui.Component.html).
 * 
 * @param {object} config
 * @param {string} config.src - The image source URL
 * @param {string} [config.alt] - Alternative text for the image
 * @param {string} [config.caption] - Text to be displayed below the image
 * @param {string} [config.objectFit] - fill (default) | contain | cover | scale-down | none
 * @param {string} [config.position]
 * @param {string} [config.top]
 * @param {string} [config.left]
 * @param {string} [config.right]
 * @param {string} [config.float]
 * @param {string} [config.display]
 * @param {string|number} [config.width]
 * @param {string|number} [config.height]
 * @param {string|number} [config.minWidth]
 * @param {string|number} [config.minHeight]
 * @param {string|number} [config.maxWidth]
 * @param {string|number} [config.maxHeight] 
 * @param {string} [config.margin]
 * @param {string} [config.padding]
 * @param {string} [config.border]
 * @param {string} [config.borderStyle]
 * @param {string} [config.borderWidth]
 * @param {string} [config.borderColor]
 * @param {string} [config.borderRadius]
 * @param {string} [config.boxShadow]
 * @param {string} [config.background]
 * @param {string} [config.backgroundColor]
 * @param {number} [config.zIndex]
 * @param {boolean} [config.draggable]
 * @param {string} [config.cursor]
 * @param {object} [config.record] - The record to bind the field to. This will automatically update the record when the field value changes, and the field will listen to database changes on the record.
 * @returns this
 * 
 * ## Generated markup
 * ```
 * <a-image class="a-image">
 *  <img class="image-content">
 * </a-image>
 * ```
 */
kiss.ui.Image = class Image extends kiss.ui.Component {
    /**
     * 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 myImage = document.createElement("a-image").init(config)
     * ```
     * 
     * Or use the shorthand for it:
    * ```
    * const myImage = createImage({
    *  src: "./logo.png",
    *  alt: "Company logo"
    * })
    * 
    * myImage.render()
    * ```
    * 
    * Or directly declare the config inside a container component:
    * ```
    * const myPanel = createPanel({
    *   title: "My panel",
    *   items: [
    *       {
    *           type: "image",
    *           src: "./logo.png",
    *           alt: "Company logo"
    *       }
    *   ]
    * })
    * myPanel.render()
    * ```
    */
    constructor() {
        super()
    }

    /**
     * Generates an Image from a JSON config
     * 
     * @ignore
     * @param {object} config - JSON config
     * @returns {HTMLElement}
     */
    init(config) {
        super.init(config)

        this.alt = config.alt || ""
        this.caption = config.caption || ""
        config.src = config.src || this._emptyImage()
        
        if (!config.caption) {
            // Template without caption
            this.innerHTML = `<img id="image-content-${this.id}" ${(config.src) ? 'src="' + config.src + '"' : ""} ${(config.alt) ? `alt="${config.alt}"` : ""} class="image-content" loading="lazy">`
        }
        else {
            // Template with caption
            this.innerHTML =
                `<figure>
                    <img id="image-content-${this.id}" ${(config.src) ? 'src="' + config.src + '"' : ""} ${(config.alt) ? `alt="${config.alt}"` : ""} class="image-content" loading="lazy">
                    <figcaption class="image-caption-text">${config.caption}</figcaption>
                </figure>`.removeExtraSpaces()
        }

	    // Attach event to handle token/session renewal
	    kiss.session.setupImg(this.querySelector('img'))

        // Set properties
        this.imageContent = this.querySelector(".image-content")

        this._setProperties(config, [
            [
                ["draggable"],
                [this]
            ],            
            [
                ["minWidth", "minHeight", "width", "height","maxWidth", "maxHeight", "margin", "position", "top", "left", "right", "float", "boxShadow", "zIndex", "border", "borderStyle", "borderWidth", "borderColor", "borderRadius", "background", "backgroundColor"],
                [this.style]

            ],
            [
                ["minWidth", "minHeight", "width", "height", "maxWidth", "maxHeight", "padding", "cursor", "objectFit", "borderRadius"],
                [this.imageContent.style]
            ]
        ])

        // Bind the image to a record, if any
        if (config.record) this._bindRecord(config.record)

        return this
    }

    /**
     * Bind the image to a record
     * (this subscribes the image to react to database changes)
     * 
     * @private
     * @ignore
     * @param {object} record
     * @returns this
     */
    _bindRecord(record) {
        this.record = record
        this.modelId = record.model.id
        this.recordId = record.id

        if (record[this.id]) {
            this.imageContent.src = this.initialValue = record[this.id]
        }

        // React to changes on a single record of the binded model
        this.subscriptions.push(
            subscribe("EVT_DB_UPDATE:" + this.modelId.toUpperCase(), (msgData) => {
                if ((msgData.modelId == this.modelId) && (msgData.id == this.recordId)) {
                    const updates = msgData.data
                    this._updateField(updates)
                }
            })
        )

        // React to changes on multiple records of the binded Model
        this.subscriptions.push(
            subscribe("EVT_DB_UPDATE_BULK", (msgData) => {
                const operations = msgData.data
                operations.forEach(operation => {
                    if ((operation.modelId == this.modelId) && (operation.recordId == this.recordId)) {
                        const updates = operation.updates
                        this._updateField(updates)
                    }
                })
            })
        )

        return this
    }

    /**
     * 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.imageContent.src = newValue
            }
        }
    }

    /**
     * Set the field value
     * 
     * @param {string} newValue - The new image source URL
     * @param {boolean} [rawUpdate] - If true, it doesn't update the associated record and doesn't trigger "change" event 
     * @returns this
     */
    setValue(newValue, rawUpdate) {
        if (rawUpdate) {
            this.imageContent.src = this.config.src = newValue
            return this
        }

        if (this.record) {
            // If the field is connected to a record, we update the database
            this.record.updateFieldDeep(this.id, newValue).then(success => {

                // Rollback the initial value if the update failed (ACL)
                if (!success) {
                    this.imageContent.src = this.config.src = this.initialValue || ""
                }
                else {
                    this.initialValue = newValue
                }
            })
        } else {
            // Otherwise, we just change the field value
            this.imageContent.src = this.config.src = newValue
        }

        return this
    }    

    /**
     * Get the src of the Image component
     * 
     * @returns {string} The image src
     */
    getValue() {
        return this.imageContent.src
    }

    /**
     * Set the image's width
     * 
     * @param {*} width - A valid CSS width value
     * @returns this
     */
    setWidth(width) {
        this.config.width = width
        this.style.width = this._computeSize("width", width)
        return this
    }

    /**
     * Set the image's height
     * 
     * @param {*} height - A valid CSS height value
     * @returns this
     */
    setHeight(height) {
        this.config.height = height
        this.style.height = this._computeSize("height", height)
        return this
    }
    
    /**
     * Update the alternative text for the image
     * 
     * @param {string} alt
     * @returns this
     */
    updateAltText(alt) {
        this.config.alt = this.alt = alt
        this.imageContent.alt = alt
        return this
    }

    /**
     * Update the caption of the image, if it exists.
     * 
     * @param {string} caption 
     * @returns this
     */
    updateCaption(caption) {
        this.config.caption = this.caption = caption
        const figcaption = this.querySelector(".image-caption-text")
        if (figcaption) figcaption.textContent = caption
        return this
    }

    /**
     * Returns the empty SVG image data URL
     * 
     * @private
     * @ignore
     * @return {string} The empty image data URL
     */
    _emptyImage() {
        return `data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGNsaXAtcnVsZT0iZXZlbm9kZCIgZD0iTTQgM0MyLjg5NTQzIDMgMiAzLjg5NTQzIDIgNVYxOUMyIDIwLjEwNDYgMi44OTU0MyAyMSA0IDIxSDIwQzIxLjEwNDYgMjEgMjIgMjAuMTA0NiAyMiAxOVY1QzIyIDMuODk1NDMgMjEuMTA0NiAzIDIwIDNINFpNMjAgNUg0VjE5SDIwVjVaTTcuMjUgOC41Qzc uMjUgOS4xOTAzNiA3LjgwOTY0IDkuNzUgOC41IDkuNzVDOS4xOTAzNiA5Ljc1IDkuNzUgOS4xOTAzNiA5Ljc1IDguNUM5Ljc1IDcuODA5NjQgOS4xOTAzNiA3LjI1IDguNSA3LjI1QzcuODA5NjQgNy4yNSA3LjI1IDcuODA5NjQgNy4yNSA4LjVaTTcuODExNDMgMTUuNDIxOUwxMC41Mjk0IDEyLjI1TDEzLjMxNTUgMTUuNDIxOUwxNS45MzY1IDEyLjY0ODdMMTkuNzUgMTcuMDY0NUg0LjI1T DcuODExNDMgMTUuNDIxOVoiIGZpbGw9ImN1cnJlbnRDb2xvciIvPjwvc3ZnPg==`
    }
}

// Create a Custom Element and add a shortcut to create it
customElements.define("a-image", kiss.ui.Image)

/**
 * Shorthand to create a new Image. See [kiss.ui.Image](kiss.ui.Image.html)
 * 
 * @param {object} config
 * @returns HTMLElement
 */
const createImage = (config) => document.createElement("a-image").init(config)

;