Proyectos de Subversion Moodle

Rev

Autoría | Ultima modificación | Ver Log |

{"version":3,"file":"imagedetails.min.js","sources":["../src/imagedetails.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Tiny media plugin image details class for Moodle.\n *\n * @module      tiny_media/imagedetails\n * @copyright   2024 Meirza <meirza.arson@moodle.com>\n * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Config from 'core/config';\nimport ModalEvents from 'core/modal_events';\nimport Notification from 'core/notification';\nimport Pending from 'core/pending';\nimport Selectors from './selectors';\nimport Templates from 'core/templates';\nimport {getString} from 'core/str';\nimport {ImageInsert} from 'tiny_media/imageinsert';\nimport {\n    bodyImageInsert,\n    footerImageInsert,\n    showElements,\n    hideElements,\n    isPercentageValue,\n} from 'tiny_media/imagehelpers';\n\nexport class ImageDetails {\n    DEFAULTS = {\n        WIDTH: 160,\n        HEIGHT: 160,\n    };\n\n    rawImageDimensions = null;\n\n    constructor(\n        root,\n        editor,\n        currentModal,\n        canShowFilePicker,\n        canShowDropZone,\n        currentUrl,\n        image,\n    ) {\n        this.root = root;\n        this.editor = editor;\n        this.currentModal = currentModal;\n        this.canShowFilePicker = canShowFilePicker;\n        this.canShowDropZone = canShowDropZone;\n        this.currentUrl = currentUrl;\n        this.image = image;\n    }\n\n    init = function() {\n        this.currentModal.setTitle(getString('imagedetails', 'tiny_media'));\n        this.imageTypeChecked();\n        this.presentationChanged();\n        this.storeImageDimensions(this.image);\n        this.setImageDimensions();\n        this.registerEventListeners();\n    };\n\n    /**\n     * Loads and displays a preview image based on the provided URL, and handles image loading events.\n     */\n    loadInsertImage = async function() {\n        const templateContext = {\n            elementid: this.editor.id,\n            showfilepicker: this.canShowFilePicker,\n            showdropzone: this.canShowDropZone,\n        };\n\n        Promise.all([bodyImageInsert(templateContext, this.root), footerImageInsert(templateContext, this.root)])\n            .then(() => {\n                const imageinsert = new ImageInsert(\n                    this.root,\n                    this.editor,\n                    this.currentModal,\n                    this.canShowFilePicker,\n                    this.canShowDropZone,\n                );\n                imageinsert.init();\n                return;\n            })\n            .catch(error => {\n                window.console.log(error);\n            });\n    };\n\n    storeImageDimensions(image) {\n        // Store dimensions of the raw image, falling back to defaults for images without dimensions (e.g. SVG).\n        this.rawImageDimensions = {\n            width: image.width || this.DEFAULTS.WIDTH,\n            height: image.height || this.DEFAULTS.HEIGHT,\n        };\n\n        const getCurrentWidth = (element) => {\n            if (element.value === '') {\n                element.value = this.rawImageDimensions.width;\n            }\n            return element.value;\n        };\n\n        const getCurrentHeight = (element) => {\n            if (element.value === '') {\n                element.value = this.rawImageDimensions.height;\n            }\n            return element.value;\n        };\n\n        const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);\n        const currentWidth = getCurrentWidth(widthInput);\n\n        const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);\n        const currentHeight = getCurrentHeight(heightInput);\n\n        const preview = this.root.querySelector(Selectors.IMAGE.elements.preview);\n        preview.setAttribute('src', image.src);\n        preview.style.display = '';\n\n        // Ensure the checkbox always in unchecked status when an image loads at first.\n        const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n        if (isPercentageValue(currentWidth) && isPercentageValue(currentHeight)) {\n            constrain.checked = currentWidth === currentHeight;\n        } else if (image.width === 0 || image.height === 0) {\n            // If we don't have both dimensions of the image, we can't auto-size it, so disable control.\n            constrain.disabled = 'disabled';\n        } else {\n            // This is the same as comparing to 3 decimal places.\n            const widthRatio = Math.round(100 * parseInt(currentWidth, 10) / image.width);\n            const heightRatio = Math.round(100 * parseInt(currentHeight, 10) / image.height);\n            constrain.checked = widthRatio === heightRatio;\n        }\n\n        /**\n         * Sets the selected size option based on current width and height values.\n         *\n         * @param {number} currentWidth - The current width value.\n         * @param {number} currentHeight - The current height value.\n         */\n        const setSelectedSize = (currentWidth, currentHeight) => {\n            if (this.rawImageDimensions.width === currentWidth &&\n                this.rawImageDimensions.height === currentHeight\n            ) {\n                this.currentWidth = this.rawImageDimensions.width;\n                this.currentHeight = this.rawImageDimensions.height;\n                this.sizeChecked('original');\n            } else {\n                this.currentWidth = currentWidth;\n                this.currentHeight = currentHeight;\n                this.sizeChecked('custom');\n            }\n        };\n\n        setSelectedSize(Number(currentWidth), Number(currentHeight));\n    }\n\n    /**\n     * Handles the selection of image size options and updates the form inputs accordingly.\n     *\n     * @param {string} option - The selected image size option (\"original\" or \"custom\").\n     */\n    sizeChecked(option) {\n        const widthInput = this.root.querySelector(Selectors.IMAGE.elements.width);\n        const heightInput = this.root.querySelector(Selectors.IMAGE.elements.height);\n        if (option === \"original\") {\n            this.sizeOriginalChecked();\n            widthInput.value = this.rawImageDimensions.width;\n            heightInput.value = this.rawImageDimensions.height;\n        } else if (option === \"custom\") {\n            this.sizeCustomChecked();\n            widthInput.value = this.currentWidth;\n            heightInput.value = this.currentHeight;\n\n            // If the current size is equal to the original size, then check the Keep proportion checkbox.\n            if (this.currentWidth === this.rawImageDimensions.width && this.currentHeight === this.rawImageDimensions.height) {\n                const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n                constrainField.checked = true;\n            }\n        }\n        this.autoAdjustSize();\n    }\n\n    autoAdjustSize(forceHeight = false) {\n        // If we do not know the image size, do not do anything.\n        if (!this.rawImageDimensions) {\n            return;\n        }\n\n        const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);\n        const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);\n\n        const normalizeFieldData = (fieldData) => {\n            fieldData.isPercentageValue = !!isPercentageValue(fieldData.field.value);\n            if (fieldData.isPercentageValue) {\n                fieldData.percentValue = parseInt(fieldData.field.value, 10);\n                fieldData.pixelSize = this.rawImageDimensions[fieldData.type] / 100 * fieldData.percentValue;\n            } else {\n                fieldData.pixelSize = parseInt(fieldData.field.value, 10);\n                fieldData.percentValue = fieldData.pixelSize / this.rawImageDimensions[fieldData.type] * 100;\n            }\n\n            return fieldData;\n        };\n\n        const getKeyField = () => {\n            const getValue = () => {\n                if (forceHeight) {\n                    return {\n                        field: heightField,\n                        type: 'height',\n                    };\n                } else {\n                    return {\n                        field: widthField,\n                        type: 'width',\n                    };\n                }\n            };\n\n            const currentValue = getValue();\n            if (currentValue.field.value === '') {\n                currentValue.field.value = this.rawImageDimensions[currentValue.type];\n            }\n\n            return normalizeFieldData(currentValue);\n        };\n\n        const getRelativeField = () => {\n            if (forceHeight) {\n                return normalizeFieldData({\n                    field: widthField,\n                    type: 'width',\n                });\n            } else {\n                return normalizeFieldData({\n                    field: heightField,\n                    type: 'height',\n                });\n            }\n        };\n\n        // Now update with the new values.\n        const constrainField = this.root.querySelector(Selectors.IMAGE.elements.constrain);\n        if (constrainField.checked) {\n            const keyField = getKeyField();\n            const relativeField = getRelativeField();\n            // We are keeping the image in proportion.\n            // Calculate the size for the relative field.\n            if (keyField.isPercentageValue) {\n                // In proportion, so the percentages are the same.\n                relativeField.field.value = keyField.field.value;\n                relativeField.percentValue = keyField.percentValue;\n            } else {\n                relativeField.pixelSize = Math.round(\n                    keyField.pixelSize / this.rawImageDimensions[keyField.type] * this.rawImageDimensions[relativeField.type]\n                );\n                relativeField.field.value = relativeField.pixelSize;\n            }\n        }\n\n        // Store the custom width and height to reuse.\n        this.currentWidth = Number(widthField.value) !== this.rawImageDimensions.width ? widthField.value : this.currentWidth;\n        this.currentHeight = Number(heightField.value) !== this.rawImageDimensions.height ? heightField.value : this.currentHeight;\n    }\n\n    /**\n     * Sets the dimensions of the image preview element based on user input and constraints.\n     */\n    setImageDimensions = () => {\n        const imagePreviewBox = this.root.querySelector(Selectors.IMAGE.elements.previewBox);\n        const image = this.root.querySelector(Selectors.IMAGE.elements.preview);\n        const widthField = this.root.querySelector(Selectors.IMAGE.elements.width);\n        const heightField = this.root.querySelector(Selectors.IMAGE.elements.height);\n\n        const updateImageDimensions = () => {\n            // Get the latest dimensions of the preview box for responsiveness.\n            const boxWidth = imagePreviewBox.clientWidth;\n            const boxHeight = imagePreviewBox.clientHeight;\n            // Get the new width and height for the image.\n            const dimensions = this.fitSquareIntoBox(widthField.value, heightField.value, boxWidth, boxHeight);\n            image.style.width = `${dimensions.width}px`;\n            image.style.height = `${dimensions.height}px`;\n        };\n        // If the client size is zero, then get the new dimensions once the modal is shown.\n        if (imagePreviewBox.clientWidth === 0) {\n            // Call the shown event.\n            this.currentModal.getRoot().on(ModalEvents.shown, () => {\n                updateImageDimensions();\n            });\n        } else {\n            updateImageDimensions();\n        }\n    };\n\n    /**\n     * Handles the selection of the \"Original Size\" option and updates the form elements accordingly.\n     */\n    sizeOriginalChecked() {\n        this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = true;\n        this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = false;\n        hideElements(Selectors.IMAGE.elements.properties, this.root);\n    }\n\n    /**\n     * Handles the selection of the \"Custom Size\" option and updates the form elements accordingly.\n     */\n    sizeCustomChecked() {\n        this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked = false;\n        this.root.querySelector(Selectors.IMAGE.elements.sizeCustom).checked = true;\n        showElements(Selectors.IMAGE.elements.properties, this.root);\n    }\n\n    /**\n     * Handles changes in the image presentation checkbox and enables/disables the image alt text input accordingly.\n     */\n    presentationChanged() {\n        const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation);\n        const alt = this.root.querySelector(Selectors.IMAGE.elements.alt);\n        alt.disabled = presentation.checked;\n\n        // Counting the image description characters.\n        this.handleKeyupCharacterCount();\n    }\n\n    /**\n     * This function checks whether an image URL is local (within the same website's domain) or external (from an external source).\n     * Depending on the result, it dynamically updates the visibility and content of HTML elements in a user interface.\n     * If the image is local then we only show it's filename.\n     * If the image is external then it will show full URL and it can be updated.\n     */\n    imageTypeChecked() {\n        const regex = new RegExp(`${Config.wwwroot}`);\n\n        // True if the URL is from external, otherwise false.\n        const isExternalUrl = regex.test(this.currentUrl) === false;\n\n        // Hide the URL input.\n        hideElements(Selectors.IMAGE.elements.url, this.root);\n\n        if (!isExternalUrl) {\n            // Split the URL by '/' to get an array of segments.\n            const segments = this.currentUrl.split('/');\n            // Get the last segment, which should be the filename.\n            const filename = segments.pop().split('?')[0];\n            // Show the file name.\n            this.setFilenameLabel(decodeURI(filename));\n        } else {\n\n            this.setFilenameLabel(decodeURI(this.currentUrl));\n        }\n    }\n\n    /**\n     * Set the string for the URL label element.\n     *\n     * @param {string} label - The label text to set.\n     */\n    setFilenameLabel(label) {\n        const urlLabelEle = this.root.querySelector(Selectors.IMAGE.elements.fileNameLabel);\n        if (urlLabelEle) {\n            urlLabelEle.innerHTML = label;\n            urlLabelEle.setAttribute(\"title\", label);\n        }\n    }\n\n    toggleAriaInvalid(selectors, predicate) {\n        selectors.forEach((selector) => {\n            const elements = this.root.querySelectorAll(selector);\n            elements.forEach((element) => element.setAttribute('aria-invalid', predicate));\n        });\n    }\n\n    hasErrorUrlField() {\n        const urlError = this.currentUrl === '';\n        if (urlError) {\n            showElements(Selectors.IMAGE.elements.urlWarning, this.root);\n        } else {\n            hideElements(Selectors.IMAGE.elements.urlWarning, this.root);\n        }\n        this.toggleAriaInvalid([Selectors.IMAGE.elements.url], urlError);\n\n        return urlError;\n    }\n\n    hasErrorAltField() {\n        const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n        const presentation = this.root.querySelector(Selectors.IMAGE.elements.presentation).checked;\n        const imageAltError = alt === '' && !presentation;\n        if (imageAltError) {\n            showElements(Selectors.IMAGE.elements.altWarning, this.root);\n        } else {\n            hideElements(Selectors.IMAGE.elements.urlWaaltWarningrning, this.root);\n        }\n        this.toggleAriaInvalid([Selectors.IMAGE.elements.alt, Selectors.IMAGE.elements.presentation], imageAltError);\n\n        return imageAltError;\n    }\n\n    updateWarning() {\n        const urlError = this.hasErrorUrlField();\n        const imageAltError = this.hasErrorAltField();\n\n        return urlError || imageAltError;\n    }\n\n    getImageContext() {\n        // Check if there are any accessibility issues.\n        if (this.updateWarning()) {\n            return null;\n        }\n\n        const classList = [];\n        const constrain = this.root.querySelector(Selectors.IMAGE.elements.constrain).checked;\n        const sizeOriginal = this.root.querySelector(Selectors.IMAGE.elements.sizeOriginal).checked;\n        if (constrain || sizeOriginal) {\n            // If the Auto size checkbox is checked or the Original size is checked, then apply the responsive class.\n            classList.push(Selectors.IMAGE.styles.responsive);\n        } else {\n            // Otherwise, remove it.\n            classList.pop(Selectors.IMAGE.styles.responsive);\n        }\n\n        return {\n            url: this.currentUrl,\n            alt: this.root.querySelector(Selectors.IMAGE.elements.alt).value,\n            width: this.root.querySelector(Selectors.IMAGE.elements.width).value,\n            height: this.root.querySelector(Selectors.IMAGE.elements.height).value,\n            presentation: this.root.querySelector(Selectors.IMAGE.elements.presentation).checked,\n            customStyle: this.root.querySelector(Selectors.IMAGE.elements.customStyle).value,\n            classlist: classList.join(' '),\n        };\n    }\n\n    setImage() {\n        const pendingPromise = new Pending('tiny_media:setImage');\n        const url = this.currentUrl;\n        if (url === '') {\n            return;\n        }\n\n        // Check if there are any accessibility issues.\n        if (this.updateWarning()) {\n            pendingPromise.resolve();\n            return;\n        }\n\n        // Check for invalid width or height.\n        const width = this.root.querySelector(Selectors.IMAGE.elements.width).value;\n        if (!isPercentageValue(width) && isNaN(parseInt(width, 10))) {\n            this.root.querySelector(Selectors.IMAGE.elements.width).focus();\n            pendingPromise.resolve();\n            return;\n        }\n\n        const height = this.root.querySelector(Selectors.IMAGE.elements.height).value;\n        if (!isPercentageValue(height) && isNaN(parseInt(height, 10))) {\n            this.root.querySelector(Selectors.IMAGE.elements.height).focus();\n            pendingPromise.resolve();\n            return;\n        }\n\n        Templates.render('tiny_media/image', this.getImageContext())\n        .then((html) => {\n            this.editor.insertContent(html);\n            this.currentModal.destroy();\n            pendingPromise.resolve();\n\n            return html;\n        })\n        .catch(error => {\n            window.console.log(error);\n        });\n    }\n\n    /**\n     * Deletes the image after confirming with the user and loads the insert image page.\n     */\n    deleteImage() {\n        Notification.deleteCancelPromise(\n            getString('deleteimage', 'tiny_media'),\n            getString('deleteimagewarning', 'tiny_media'),\n        ).then(() => {\n            hideElements(Selectors.IMAGE.elements.altWarning, this.root);\n            // Removing the image in the preview will bring the user to the insert page.\n            this.loadInsertImage();\n            return;\n        }).catch(error => {\n            window.console.log(error);\n        });\n    }\n\n    registerEventListeners() {\n        const submitAction = this.root.querySelector(Selectors.IMAGE.actions.submit);\n        submitAction.addEventListener('click', (e) => {\n            e.preventDefault();\n            this.setImage();\n        });\n\n        const deleteImageEle = this.root.querySelector(Selectors.IMAGE.actions.deleteImage);\n        deleteImageEle.addEventListener('click', () => {\n            this.deleteImage();\n        });\n        deleteImageEle.addEventListener(\"keydown\", (e) => {\n            if (e.key === \"Enter\") {\n                this.deleteImage();\n            }\n        });\n\n        this.root.addEventListener('change', (e) => {\n            const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n            if (presentationEle) {\n                this.presentationChanged();\n            }\n\n            const constrainEle = e.target.closest(Selectors.IMAGE.elements.constrain);\n            if (constrainEle) {\n                this.autoAdjustSize();\n            }\n\n            const sizeOriginalEle = e.target.closest(Selectors.IMAGE.elements.sizeOriginal);\n            if (sizeOriginalEle) {\n                this.sizeChecked('original');\n            }\n\n            const sizeCustomEle = e.target.closest(Selectors.IMAGE.elements.sizeCustom);\n            if (sizeCustomEle) {\n                this.sizeChecked('custom');\n            }\n        });\n\n        this.root.addEventListener('blur', (e) => {\n            if (e.target.nodeType === Node.ELEMENT_NODE) {\n\n                const presentationEle = e.target.closest(Selectors.IMAGE.elements.presentation);\n                if (presentationEle) {\n                    this.presentationChanged();\n                }\n            }\n        }, true);\n\n        // Character count.\n        this.root.addEventListener('keyup', (e) => {\n            const altEle = e.target.closest(Selectors.IMAGE.elements.alt);\n            if (altEle) {\n                this.handleKeyupCharacterCount();\n            }\n        });\n\n        this.root.addEventListener('input', (e) => {\n            const widthEle = e.target.closest(Selectors.IMAGE.elements.width);\n            if (widthEle) {\n                // Avoid empty value.\n                widthEle.value = widthEle.value === \"\" ? 0 : Number(widthEle.value);\n                this.autoAdjustSize();\n            }\n\n            const heightEle = e.target.closest(Selectors.IMAGE.elements.height);\n            if (heightEle) {\n                // Avoid empty value.\n                heightEle.value = heightEle.value === \"\" ? 0 : Number(heightEle.value);\n                this.autoAdjustSize(true);\n            }\n        });\n    }\n\n    handleKeyupCharacterCount() {\n        const alt = this.root.querySelector(Selectors.IMAGE.elements.alt).value;\n        const current = this.root.querySelector('#currentcount');\n        current.innerHTML = alt.length;\n    }\n\n    /**\n     * Calculates the dimensions to fit a square into a specified box while maintaining aspect ratio.\n     *\n     * @param {number} squareWidth - The width of the square.\n     * @param {number} squareHeight - The height of the square.\n     * @param {number} boxWidth - The width of the box.\n     * @param {number} boxHeight - The height of the box.\n     * @returns {Object} An object with the new width and height of the square to fit in the box.\n     */\n    fitSquareIntoBox = (squareWidth, squareHeight, boxWidth, boxHeight) => {\n        if (squareWidth < boxWidth && squareHeight < boxHeight) {\n          // If the square is smaller than the box, keep its dimensions.\n          return {\n            width: squareWidth,\n            height: squareHeight,\n          };\n        }\n        // Calculate the scaling factor based on the minimum scaling required to fit in the box.\n        const widthScaleFactor = boxWidth / squareWidth;\n        const heightScaleFactor = boxHeight / squareHeight;\n        const minScaleFactor = Math.min(widthScaleFactor, heightScaleFactor);\n        // Scale the square's dimensions based on the aspect ratio and the minimum scaling factor.\n        const newWidth = squareWidth * minScaleFactor;\n        const newHeight = squareHeight * minScaleFactor;\n        return {\n          width: newWidth,\n          height: newHeight,\n        };\n    };\n}\n"],"names":["constructor","root","editor","currentModal","canShowFilePicker","canShowDropZone","currentUrl","image","WIDTH","HEIGHT","setTitle","imageTypeChecked","presentationChanged","storeImageDimensions","this","setImageDimensions","registerEventListeners","async","templateContext","elementid","id","showfilepicker","showdropzone","Promise","all","then","ImageInsert","init","catch","error","window","console","log","imagePreviewBox","querySelector","Selectors","IMAGE","elements","previewBox","preview","widthField","width","heightField","height","updateImageDimensions","boxWidth","clientWidth","boxHeight","clientHeight","dimensions","fitSquareIntoBox","value","style","getRoot","on","ModalEvents","shown","squareWidth","squareHeight","widthScaleFactor","heightScaleFactor","minScaleFactor","Math","min","rawImageDimensions","DEFAULTS","currentWidth","element","getCurrentWidth","currentHeight","getCurrentHeight","setAttribute","src","display","constrain","checked","disabled","widthRatio","round","parseInt","heightRatio","sizeChecked","setSelectedSize","Number","option","widthInput","heightInput","sizeOriginalChecked","sizeCustomChecked","autoAdjustSize","forceHeight","normalizeFieldData","fieldData","isPercentageValue","field","percentValue","pixelSize","type","getKeyField","currentValue","keyField","relativeField","sizeOriginal","sizeCustom","properties","presentation","alt","handleKeyupCharacterCount","isExternalUrl","RegExp","Config","wwwroot","test","url","setFilenameLabel","decodeURI","filename","split","pop","label","urlLabelEle","fileNameLabel","innerHTML","toggleAriaInvalid","selectors","predicate","forEach","selector","querySelectorAll","hasErrorUrlField","urlError","urlWarning","hasErrorAltField","imageAltError","altWarning","urlWaaltWarningrning","updateWarning","getImageContext","classList","push","styles","responsive","customStyle","classlist","join","setImage","pendingPromise","Pending","resolve","isNaN","focus","render","html","insertContent","destroy","deleteImage","deleteCancelPromise","loadInsertImage","actions","submit","addEventListener","e","preventDefault","deleteImageEle","key","target","closest","nodeType","Node","ELEMENT_NODE","widthEle","heightEle","length"],"mappings":"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"}