Select Git revision
camera-capture.mjs

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.
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);