Hola, soy @ratapan y hoy quiero compartir con ustedes el primer custom element que he creado. Este elemento Input, que permite tomar un archivo de imagen, cortarlo y darle un nuevo tamaño, pueden personalizarlo tanto como quieran, es primera vez que hago uno, así que si tienen sugerencias, les agradecería.
¿Qué es un Custom Element?
Los Custom Elements son una parte fundamental de la suite de tecnologías de Web Components. Nos permiten crear nuestros propios elementos HTML, con su funcionalidad y estilos encapsulados, siendo todo esto nativo de la plataforma web.
Presentando rtp-crop
rtp-crop
es un custom element que desarrollé para manipular imágenes directamente en el navegador. Puede tomar una imagen, cortarla a un tamaño específico y cambiar su resolución.
Código del Custom Element
El código de rtp-crop
se divide en varias partes. Primero, tenemos los estilos CSS, que se tienen que insertar, ya que utiliza el shadow dom para crear el custom element, además podemos usar variables de CSS para personalizar todos a la vez:
//_rtp_image_crop.js
const styles = (height) => `
.rtp-crop{
min-height: 30px;
max-height:100%;
height: ${height};
min-width: 50px;
max-width:100%;
display: flex;
justify-content: center;
align-items: center;
position:relative;
}
.rtp-crop__label{
cursor: pointer;
padding: var(--input-padding);
border-radius: var(--input-radius);
color: var(--c-font);
background-color: var(--c-i-c);
margin-right:auto;
}
.rtp-crop__input{
display: none;
}
.rtp-crop__preview{
max-width:100%;
max-height:100%;
}`;
Luego, la función principal que cambia la resolución de la imagen y su extensión.
/**
* Reduces the resolution of an image file to specified dimensions.
*
* This function takes an image file as input along with the desired new width
* and height. It returns a Promise that resolves with an object containing the
* original image size, the new image size, and the reduced resolution image file.
* If the input image is not square, it will be cropped to fit the specified
* dimensions. The new image is returned in the WebP format.
*
* @param {File} imageFile - The image file to reduce resolution of.
* @param {string} type - The type of the final image.
* @param {number} newWidth - The desired width for the new image.
* @param {number} newHeight - The desired height for the new image.
* @returns {Promise<{img?: File, name?: string}>} A Promise that resolves with an object containing the new image file (`img`) and the name. The Promise is rejected if there is an error in processing the image.
*/
export async function reduceImageResolution(
imageFile,
type,
newWidth,
newHeight
) {
return new Promise((resolve, reject) => {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const image = new Image();
image.src = URL.createObjectURL(imageFile);
image.onload = () => {
const originalWidth = image.naturalWidth;
const originalHeight = image.naturalHeight;
const size = Math.min(originalWidth, originalHeight);
canvas.width = newWidth;
canvas.height = newHeight;
const sourceX = (originalWidth - size) / 2;
const sourceY = (originalHeight - size) / 2;
const sourceWidth = size;
const sourceHeight = size;
ctx.drawImage(
image,
sourceX,
sourceY,
sourceWidth,
sourceHeight,
0,
0,
newWidth,
newHeight
);
canvas.toBlob(
(blob) => {
if (blob) {
const reducedImage = new File([blob], imageFile.name, {
type:`image/${type}`,
});
resolve({
img: reducedImage,
});
} else {
reject(new Error("Error processing image"));
}
},
imageFile.type,
1
);
};
image.onerror = () => {
reject(new Error("Error loading image"));
};
});
}
La lógica, en el caso de que el valor de salida "size" la relación es de 1:1, lo que hará será tomar "File" y en caso de ser más ancho que alto, eliminará los costados hasta quedar centrado y en caso de ser más alto que ancho, se eliminaría arriba y abajo para dejar el centro como en estos casos, además de cambiar por defecto el formato a Webp o al de elección:
input | output |
---|---|
Y finalmente, la definición del custom element, este tiene diferentes valores default que podemos cambiar desde la declaración HTML de nuestro elemento, también a través del set value(val) generamos un evento tipo change para utilizar el valor de nuestro elemento:
///_rtp_image_crop.js
class RtpCropInput extends HTMLElement {
constructor() {
super();
this.shadow = this.attachShadow({ mode: "open" });
this._value = null;
// Styles
this.myStyles = document.createElement("style");
this.myStyles.innerHTML = styles();
// Container
this.inputContainer = document.createElement("div");
// Image
this.inputPreview = document.createElement("img");
this.inputPreview.alt = "Preview input image";
this.inputPreview.loading = "lazy";
this.inputPreview.style.display = "none";
// Label
this.inputLabel = document.createElement("label");
this.inputLabel.textContent = "Select an image";
// Input
this.inputThis = document.createElement("input");
this.inputThis.type = "file";
this.inputThis.accept = "image/*";
this.inputThis.id = "file-input"; // Setting an ID for the input
this.inputLabel.setAttribute("for", "file-input"); // Referencing the input ID in the label
//clases
this.inputContainer.className = "rtp-crop";
this.inputLabel.className = "rtp-crop__label";
this.inputThis.className = "rtp-crop__input";
this.inputPreview.className = "rtp-crop__preview";
// Append children
this.shadow.appendChild(this.myStyles);
this.shadow.appendChild(this.inputContainer);
this.inputContainer.appendChild(this.inputThis);
this.inputContainer.appendChild(this.inputLabel);
this.inputContainer.appendChild(this.inputPreview);
}
get value() {
return this._value;
}
set value(val) {
this._value = val;
this.dispatchEvent(new Event("change"));
}
connectedCallback() {
let size = this.getAttribute("output-size") || 800;
let type = this.getAttribute("output-type") || "webp";
this.myStyles.innerHTML = styles(
this.getAttribute("style-height") || "40px"
);
const previewImage = (file) => {
const reader = new FileReader();
reader.onload = (e) => {
this.value = file;
this.inputPreview.src = e.target.result;
this.inputPreview.style.display = "inline-block";
};
reader.readAsDataURL(file);
};
// Event listener for input
this.inputThis.addEventListener("change", async (ev) => {
const target = ev.target;
if (target.files && target.files[0]) {
const data = await reduceImageResolution(
target.files[0],
type,
size,
size
);
if (data.img) {
this.inputLabel.innerHTML = `Change image </br> ${
data.img.name.length > 25
? data.img.name.substring(0, 19) + "..."
: data.img.name
}`;
this.processedImage = data.img;
previewImage(data.img);
}
}
});
}
}
customElements.define("rtp-crop", RtpCropInput);
Uso en HTML
Para usar rtp-crop
, simplemente inclúyelo en tu HTML de la siguiente manera:
<script type="module" src="./webComponents/_rtp_image_crop.js"></script>
<article class="card">
<rtp-crop
id="input-crop"
output-size="500"
output-type="webp"
style-height="100px"/>
</article>
O de la siguiente manera, dejando los datos por defecto:
<script type="module" src="./webComponents/_rtp_image_crop.js">
<article class="card">
<rtp-crop id="input-crop"/>
</article>
Estilos Adicionales y Variables CSS
He definido varios estilos y variables CSS para personalizar la apariencia del elemento:
variables
//_variable.css
:root {
--header-h: 40px;
--mx-w: 1000px;
--input-padding: 2px 4px;
--input-radius: 4px;
--space-1: 4px;
--space-2: 8px;
--space-3: 16px;
--space-4: 24px;
--space-5: 32px;
}
:root.dark {
--c-bg: #222627;
--c-bg_2: #0a0000;
--c-font: #ffffff;
--c-font_2: #fff2d6;
--c-link: #fff8e8;
--c-select: #4c463a;
--c-i-a: #e3c4ad;
--c-i-b: #dca780;
--c-i-c: #a07250;
}
style.css
@import "./style/_typography.css";
@import "./style/_variable.css";
@import "./style/_reset.css";
.section {
height: 100dvh;
padding: var(--space-2) var(--space-3);
display: grid;
place-items: center;
}
.card {
width: 400px;
max-width: 500px;
border: 1px solid var(--c-font_2);
box-shadow: 4px 4px 8px var(--c-bg_2);
padding: var(--space-2) var(--space-3);
border-radius: var(--space-2);
-webkit-border-radius: var(--space-2);
-moz-border-radius: var(--space-2);
-ms-border-radius: var(--space-2);
-o-border-radius: var(--space-2);
}
Integración en JavaScript
Finalmente, el elemento se integra en el archivo JavaScript principal:
//main.js
import './webComponents/_rtp_image_crop.js';
const $ = (sele)=> document.querySelector(sele)
const cropInput = $('#input-crop')
cropInput.addEventListener("change",(event)=>{
console.log(event.target.value)
})
Conclusión
Crear este custom element ha sido una experiencia de aprendizaje increíble. Espero que rtp-crop
sea útil en tus proyectos y te inspire a crear tus propios elementos personalizados.
Top comments (2)
Espectacular post! increíble trabajo😉⭐
Buen post! justo lo que nesecitaba en este momento👌👌