Skip to content
Snippets Groups Projects
Select Git revision
  • 592e01efaee1629bfaa4b7884a63229ad9d527fc
  • master default protected
  • 1.31
  • 4.38.2
  • 4.38.1
  • 4.38.0
  • 4.37.2
  • 4.37.1
  • 4.37.0
  • 4.36.0
  • 4.35.0
  • 4.34.1
  • 4.34.0
  • 4.33.1
  • 4.33.0
  • 4.32.2
  • 4.32.1
  • 4.32.0
  • 4.31.0
  • 4.30.1
  • 4.30.0
  • 4.29.1
  • 4.29.0
23 results

camera-capture.mjs

Blame
  • Volker Schukai's avatar
    Volker Schukai authored
    - Added `318.html` and `318.mjs` for documenting layout problems related to the split panel.
    - Updated `camera-capture.mjs` to enhance formatting and structure in imports and methods, ensuring better readability and maintainability.
    - Refined error handling and logging throughout the component to prevent potential uncaught exceptions.
    - Organized component dependencies more clearly to eliminate redundancies and improve performance.
    - Improved inline comments to clarify the intent and functionality of existing code.
    
    This refactoring not only resolves layout-related issues but also aids future developers in navigating and understanding the codebase.
    592e01ef
    History
    camera-capture.mjs 15.99 KiB
    /**
     * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
     * Node module: @schukai/monster
     *
     * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
     * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
     *
     * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
     * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
     * For more information about purchasing a commercial license, please contact schukai GmbH.
     *
     * SPDX-License-Identifier: AGPL-3.0
     */
    
    import { instanceSymbol } from "../../constants.mjs";
    import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
    import { CustomElement } from "../../dom/customelement.mjs";
    import {
    	assembleMethodSymbol,
    	registerCustomElement,
    } from "../../dom/customelement.mjs";
    import { CameraCaptureStyleSheet } from "./stylesheet/camera-capture.mjs";
    import "../form/button.mjs";
    import "../state/state.mjs";
    import { getLocaleOfDocument } from "../../dom/locale.mjs";
    import { addErrorAttribute } from "../../dom/error.mjs";
    import { Queue } from "../../types/queue.mjs";
    import { fireCustomEvent } from "../../dom/events.mjs";
    
    import "../layout/full-screen.mjs";
    
    export { CameraCapture };
    
    /**
     * @private
     * @type {symbol}
     */
    const controlElementSymbol = Symbol("copyElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const videoElementSymbol = Symbol("videoElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const takePictureButtonElementSymbol = Symbol("takePictureButtonElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const canvasElementSymbol = Symbol("canvasElement");
    
    /**
     * @private
     * @type {symbol}
     */
    const queueSymbol = Symbol("queue");
    
    /**
     * @private
     * @type {symbol}
     */
    const emptyHistoryStateElementSymbol = Symbol("emptyHistoryStateElement");
    
    /**
     * This is a camera capture component.
     *
     * @fragments /fragments/components/content/camera-capture/
     *
     * @example /examples/components/content/camera-capture-simple/
     *
     * @since 3.111.0
     * @copyright schukai GmbH
     * @summary A simple but powerful camera capture component. It can be used to capture images from the camera.
     * @fires monster-camera-capture-captured
     */
    class CameraCapture extends CustomElement {
    	/**
    	 * Constructor for the CameraCapture class.
    	 * Calls the parent class constructor.
    	 */
    	constructor() {
    		super();
    
    		this[queueSymbol] = new Queue();
    	}
    
    	/**
    	 * This method is called by the `instanceof` operator.
    	 * @return {symbol}
    	 */
    	static get [instanceSymbol]() {
    		return Symbol.for(
    			"@schukai/monster/components/content/camera-capture@instance",
    		);
    	}
    
    	/**
    	 *
    	 * @return {Components.Content.Copy
    	 */
    	[assembleMethodSymbol]() {
    		super[assembleMethodSymbol]();
    		initControlReferences.call(this);
    		initEventHandler.call(this);
    		initCameraControl.call(this);
    		return this;
    	}
    
    	/**
    	 * To set the options via the HTML Tag, the attribute `data-monster-options` must be used.
    	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
    	 *
    	 * The individual configuration values can be found in the table.
    	 *
    	 * @property {Object} templates Template definitions
    	 * @property {string} templates.main Main template
    	 * @property {Object} actions Callbacks
    	 * @property {string} actions.click="throw Error" Callback when clicked
    	 * @property {Object} features Features
    	 * @property {boolean} features.stripTags=true Strip tags from the copied text
    	 * @property {boolean} features.preventOpenEventSent=false Prevent open event from being sent
    	 * @property {Object} popper Popper configuration
    	 * @property {string} popper.placement="top" Popper placement
    	 * @property {string[]} popper.middleware=["autoPlacement", "shift", "offset:15", "arrow"] Popper middleware
    	 * @property {boolean} disabled=false Disabled state
    	 */
    	get defaults() {
    		return Object.assign({}, super.defaults, {
    			templates: {
    				main: getTemplate(),
    			},
    
    			disabled: false,
    			features: {},
    
    			labels: getTranslations(),
    		});
    	}
    
    	/**
    	 * @return {string}
    	 */
    	static getTag() {
    		return "monster-camera-capture";
    	}
    
    	/**
    	 * @return {CSSStyleSheet[]}
    	 */
    	static getCSSStyleSheet() {
    		return [CameraCaptureStyleSheet];
    	}
    
    	/**
    	 * Retrieve the next image from the queue.
    	 * If the queue is empty, it returns `null`.
    	 * @returns {string|null}
    	 */
    	getNextImage() {
    		if (!this[queueSymbol].isEmpty()) {
    			const next = this[queueSymbol].poll();
    			if (!next) {
    				return null;
    			}
    			return next;
    		}
    		return null;
    	}
    
    	/**
    	 * Capture an image from the camera and add it to the queue.
    	 * The image is returned as a data URL.
    	 * @returns {string}
    	 */
    	capture() {
    		this[canvasElementSymbol].width = this[videoElementSymbol].videoWidth;
    		this[canvasElementSymbol].height = this[videoElementSymbol].videoHeight;
    		const ctx = this[canvasElementSymbol].getContext("2d");
    		ctx.drawImage(
    			this[videoElementSymbol],
    			0,
    			0,
    			this[canvasElementSymbol].width,
    			this[canvasElementSymbol].height,
    		);
    		const dataURL = this[canvasElementSymbol].toDataURL("image/png");
    		this[queueSymbol].add(dataURL);
    		return dataURL;
    	}
    }
    
    /**
     * @private
     */
    function initCameraControl() {
    	const self = this;
    
    	if (
    		!navigator ||
    		!navigator.mediaDevices ||
    		!navigator.mediaDevices.getUserMedia
    	) {
    		addErrorAttribute(self, "Browser not supported");
    		return;
    	}
    
    	navigator.mediaDevices.enumerateDevices().then((devices) => {
    		const cameras = devices.filter((device) => device.kind === "videoinput");
    
    		if (cameras.length === 0) {
    			addErrorAttribute(self, getTranslations().cameraNotSupportedOrNotAllowed);
    			return;
    		}
    
    		// Nur Dropdown anzeigen, wenn mehr als 1 Kamera vorhanden
    		if (cameras.length > 1) {
    			const select = document.createElement("select");
    			select.setAttribute("data-monster-role", "cameraSelector");
    			select.style.marginBottom = "0.5rem";
    
    			cameras.forEach((camera, index) => {
    				const option = document.createElement("option");
    				option.value = camera.deviceId;
    				option.text = camera.label || `Kamera ${index + 1}`;
    				select.appendChild(option);
    			});
    
    			select.addEventListener("change", () => {
    				startCameraWithDeviceId.call(self, select.value);
    			});
    
    			// Vor dem Video-Element einfügen
    			self[controlElementSymbol].insertBefore(select, self[videoElementSymbol]);
    		}
    
    		// Mit der ersten Kamera starten
    		startCameraWithDeviceId.call(self, cameras[0].deviceId);
    	});
    }
    
    function startCameraWithDeviceId(deviceId) {
    	const self = this;
    	navigator.mediaDevices
    		.getUserMedia({ video: { deviceId: { exact: deviceId } } })
    		.then(function (stream) {
    			self[takePictureButtonElementSymbol].style.display = "block";
    			self[videoElementSymbol].style.display = "block";
    			self[emptyHistoryStateElementSymbol].style.display = "none";
    
    			self[videoElementSymbol].srcObject = stream;
    		})
    		.catch(function (e) {
    			addErrorAttribute(self, e);
    		});
    }
    
    /**
     * @private
     * @returns {{takePicture: string}}
     */
    function getTranslations() {
    	const locale = getLocaleOfDocument();
    	switch (locale.language) {
    		case "de":
    			return {
    				takePicture: "Bild aufnehmen",
    				cameraNotSupportedOrNotAllowed:
    					"Die Kamera wird nicht unterstützt oder die Berechtigung wurde nicht erteilt.",
    			};
    
    		case "es":
    			return {
    				takePicture: "Tomar una foto",
    				cameraNotSupportedOrNotAllowed:
    					"La cámara no es compatible o no se ha otorgado permiso.",
    			};
    
    		case "zh":
    			return {
    				takePicture: "拍照",
    				cameraNotSupportedOrNotAllowed: "相机不受支持或未授予权限。",
    			};
    
    		case "hi":
    			return {
    				takePicture: "तस्वीर खींचें",
    				cameraNotSupportedOrNotAllowed:
    					"कैमरा समर्थित नहीं है या अनुमति नहीं दी गई है।",
    			};
    
    		case "bn":
    			return {
    				takePicture: "ছবি তুলুন",
    				cameraNotSupportedOrNotAllowed: "ক্যামেরা সমর্থিত নয় বা অনুমতি দেয়া হয়নি।",
    			};
    
    		case "pt":
    			return {
    				takePicture: "Tirar uma foto",
    				cameraNotSupportedOrNotAllowed:
    					"A câmera não é suportada ou a permissão não foi concedida.",
    			};
    
    		case "ru":
    			return {
    				takePicture: "Сделать фото",
    				cameraNotSupportedOrNotAllowed:
    					"Камера не поддерживается или разрешение не предоставлено.",
    			};
    
    		case "ja":
    			return {
    				takePicture: "写真を撮る",
    				cameraNotSupportedOrNotAllowed:
    					"カメラがサポートされていないか、許可が付与されていません。",
    			};
    
    		case "pa":
    			return {
    				takePicture: "ਤਸਵੀਰ ਖਿੱਚੋ",
    				cameraNotSupportedOrNotAllowed:
    					"ਕੈਮਰਾ ਦਾ ਸਮਰਥਨ ਨਹੀਂ ਹੈ ਜਾਂ ਅਨੁਮਤੀ ਨਹੀਂ ਦਿੱਤੀ ਗਈ ਹੈ।",
    			};
    
    		case "mr":
    			return {
    				takePicture: "फोटो घ्या",
    				cameraNotSupportedOrNotAllowed:
    					"कॅमेरा समर्थित नाही किंवा परवानगी दिलेली नाही.",
    			};
    
    		case "fr":
    			return {
    				takePicture: "Prendre une photo",
    				cameraNotSupportedOrNotAllowed:
    					"La caméra n'est pas prise en charge ou l'autorisation n'a pas été accordée.",
    			};
    
    		case "it":
    			return {
    				takePicture: "Scattare una foto",
    				cameraNotSupportedOrNotAllowed:
    					"La fotocamera non è supportata o l'autorizzazione non è stata concessa.",
    			};
    
    		case "nl":
    			return {
    				takePicture: "Maak een foto",
    				cameraNotSupportedOrNotAllowed:
    					"De camera wordt niet ondersteund of er is geen toestemming verleend.",
    			};
    
    		case "sv":
    			return {
    				takePicture: "Ta ett foto",
    				cameraNotSupportedOrNotAllowed:
    					"Kameran stöds inte eller tillståndet har inte beviljats.",
    			};
    
    		case "pl":
    			return {
    				takePicture: "Zrób zdjęcie",
    				cameraNotSupportedOrNotAllowed:
    					"Kamera nie jest obsługiwana lub nie udzielono zgody.",
    			};
    
    		case "da":
    			return {
    				takePicture: "Tag et billede",
    				cameraNotSupportedOrNotAllowed:
    					"Kameraen understøttes ikke eller tilladelsen er ikke givet.",
    			};
    
    		case "fi":
    			return {
    				takePicture: "Ota kuva",
    				cameraNotSupportedOrNotAllowed:
    					"Kameraa ei tueta tai lupaa ei ole myönnetty.",
    			};
    
    		case "no":
    			return {
    				takePicture: "Ta et bilde",
    				cameraNotSupportedOrNotAllowed:
    					"Kameraen støttes ikke eller tillatelsen er ikke gitt.",
    			};
    
    		case "cs":
    			return {
    				takePicture: "Vyfotit",
    				cameraNotSupportedOrNotAllowed:
    					"Fotoaparát není podporován nebo povolení nebylo uděleno.",
    			};
    
    		case "en":
    		default:
    			return {
    				takePicture: "Take a picture",
    				cameraNotSupportedOrNotAllowed:
    					"The camera is not supported or permission has not been granted.",
    			};
    	}
    }
    
    /**
     * @private
     * @return {initEventHandler}
     * @fires monster-camera-capture-captured
     */
    function initEventHandler() {
    	const self = this;
    
    	this[takePictureButtonElementSymbol].setOption("actions.click", function () {
    		self.capture();
    
    		fireCustomEvent(self, "monster-camera-capture-captured", {
    			element: self,
    		});
    	});
    
    	return this;
    }
    
    /**
     * @private
     * @return {void}
     */
    function initControlReferences() {
    	this[controlElementSymbol] = this.shadowRoot.querySelector(
    		`[${ATTRIBUTE_ROLE}="control"]`,
    	);
    
    	this[takePictureButtonElementSymbol] = this.shadowRoot.querySelector(
    		`[data-monster-role="takePicture"]`,
    	);
    
    	this[videoElementSymbol] = this.shadowRoot.querySelector(`video`);
    
    	this[canvasElementSymbol] = this.shadowRoot.querySelector(`canvas`);
    
    	// data-monster-role="emptyHistoryState"
    	this[emptyHistoryStateElementSymbol] = this.shadowRoot.querySelector(
    		`[data-monster-role="emptyHistoryState"]`,
    	);
    }
    
    /**
     * @private
     * @return {string}
     */
    function getTemplate() {
    	// language=HTML
    	return `
            <div data-monster-role="control" part="control">
                <monster-full-screen part="full-screen" data-monster-role="full-screen" data-monster-option-selector="[data-monster-role=control]"></monster-full-screen>
                <monster-state data-monster-role="emptyHistoryState">
                    <svg slot="visual" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
                         width="350"
                         height="350" viewBox="0 0 92.604 92.604">
                        <defs>
                            <linearGradient id="a" x1="336.587" x2="372.879" y1="218.625" y2="218.625"
                                            gradientUnits="userSpaceOnUse" spreadMethod="pad">
                                <stop offset="0" style="stop-color:#fefe00"/>
                                <stop offset="1" style="stop-color:#ff43c6"/>
                            </linearGradient>
                            <linearGradient xlink:href="#a" id="p" x1="-2894.157" x2="-805.215" y1="-4285.143"
                                            y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
                                            gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
                            <linearGradient xlink:href="#a" id="q" x1="-2894.157" x2="-805.215" y1="-4285.143"
                                            y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
                                            gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
                            <linearGradient xlink:href="#a" id="o" x1="-2894.157" x2="-805.215" y1="-4285.143"
                                            y2="-2196.201" gradientTransform="translate(135.207 194.415)scale(.04433)"
                                            gradientUnits="userSpaceOnUse" spreadMethod="pad"/>
                        </defs>
                        <g style="font-variation-settings:normal;opacity:1;vector-effect:none;fill:url(#o);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1">
                            <path d="M8.983 34.64a9.33 9.33 0 0 1 9.33-9.33H74.29a9.33 9.33 0 0 1 9.33 9.33v13.995a9.33 9.33 0 0 1-9.33 9.33h-22.07c.673.755 1.54 1.51 2.478 2.215a32.7 32.7 0 0 0 4.23 2.66l.066.027.014.01a2.332 2.332 0 0 1-1.045 4.417H34.64a2.332 2.332 0 0 1-1.045-4.417l.014-.01.065-.033a23 23 0 0 0 1.25-.69 33 33 0 0 0 2.981-1.965c.933-.7 1.806-1.46 2.477-2.215h-22.07a9.33 9.33 0 0 1-9.329-9.33Zm9.33-4.665a4.665 4.665 0 0 0-4.665 4.665v13.995a4.665 4.665 0 0 0 4.665 4.664H74.29a4.665 4.665 0 0 0 4.665-4.664V34.64a4.665 4.665 0 0 0-4.665-4.665z"
                                  style="font-variation-settings:normal;vector-effect:none;fill:url(#p);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/>
                            <path d="M46.302 36.972a4.665 4.665 0 1 0 0 9.33 4.665 4.665 0 0 0 0-9.33m-9.33 4.665a9.33 9.33 0 1 1 18.66 0 9.33 9.33 0 0 1-18.66 0m32.655 0a2.332 2.332 0 1 1-4.665 0 2.332 2.332 0 0 1 4.665 0"
                                  style="font-variation-settings:normal;vector-effect:none;fill:url(#q);fill-opacity:1;stroke-width:.0440952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;-inkscape-stroke:none;stop-color:#000;stop-opacity:1"/>
                        </g>
                    </svg>
    
                    <p class="camera-not-supported-text" data-monster-replace="path:labels.cameraNotSupportedOrNotAllowed | default:there was an error:string"></p>
                </monster-state>
    
                
    
                <video autoplay style="display:none"></video>
                <canvas style="display:none;"></canvas>
                <div>
                    <monster-button part="takePictureButton" style="display:none"
                                    data-monster-role="takePicture" data-monster-attributes="classes.takePictureButton"
                                    data-monster-replace="path:labels.takePicture"></monster-button>
                </div>
            </div>`;
    }
    
    registerCustomElement(CameraCapture);