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" onerror="this.src='${this._emptyImage()}'">`
		}
		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" onerror="this.src='${this._emptyImage()}'">
                    <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 (newValue === "") newValue = this._emptyImage()
        
		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
					this.dispatchEvent(new Event("change"))
				}
			})
		} 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.getAttribute("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
	 * @returns {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)