Skip to content

Übernehmen der Dark Mode Einstellungen in Widgets

Sebastian Bormann edited this page Aug 16, 2021 · 5 revisions

Übernehmen der Dark-Mode-Einstellungen in Widgets

(Betrifft Widget-Entwickler) Dazu gibt es den neuen Befehl "getOptions", welcher die Nutzer-Einstellungen von iQontrol als Objekt im Widget per postMessage empfängt. So kann man es im Widget implementieren:

//Ask for options
var options;
sendPostMessage("getOptions");

//send postMessages
function sendPostMessage(command, stateId, value){
	message = { command: command, stateId: stateId, value: value };
	window.parent.postMessage(message, "*");
}

//receive postMessages
window.addEventListener("message", receivePostMessage, false);
function receivePostMessage(event) { //event = {data: message data, origin: url of origin, source: id of sending element}
	if(event.data && event.data.command) switch(event.data.command){
		case "getState":
		//do what you like with the requested state...
		break;
		
		case "getOptions":
		console.log("Received OPTIONS");
		if(event.data.value){
			options = event.data.value;
			handleOptions();
		}
		break;
	}
}

In diesen Options gibt es die Option LayoutColorModeDarkEnable. Dies kann die Werte 'disabled', 'always' oder null (= default = reagiere auf die Dark-Mode-Vorgaben des Betriebssystems) haben.

Nachdem man somit nun die Einstellungen des Nutzers bekommen hat, kann man eine Funktion bauen, die entsprechend der Optionen die Dark-Mode-Einstellungen vornimmt.

Bsp.:

//Handle Options
function handleOptions(){
	if(typeof options !== "object") return;
	//Dark-Mode
	switch(options.LayoutColorModeDarkEnable){
		case "disabled":
		break;

		case "always":
		applyColorMode('dark');
		break;
		
		default:
		if(window.matchMedia){
			var darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
			applyColorMode(darkMode ? 'dark' : '');
			window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', darkModeEventListenerFunction);
			window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', darkModeEventListenerFunction);
			function darkModeEventListenerFunction(e){
				darkMode = e.matches;
				applyColorMode(darkMode ? 'dark' : '');
			}
		}
	}
	function applyColorMode(colorMode){
		$("html").removeClass(function(index, className){
			return (className.match (/(^|\s)color-mode-\S+/g) || []).join(' ');
		});	
		if(colorMode && colorMode != "") $("html").addClass("color-mode-" + colorMode);
	}
}

Wenn das ganze 'disabled' ist, passiert gar nichts. Bei 'always' wird applyColorMode('dark') aufgerufen. Und bei default wird ein Event-Listener aktiviert, der auf die Änderungen des Betriebssystems im Dark-Mode reagiert und entsprechend applyColorMode aufruft.

Die Funktion applyColorMode selbst schreibt oder entfernt jetzt eine Klasse color-mode-dark in den <html>-Tag, je nachdem, ob der Dark-Mode greifen soll, oder nicht.

Als drittes braucht man jetzt noch CSS-Styles, die auf die Klasse html.color-mode-dark reagieren:

html.color-mode-dark .myExampleArea {
	background: #757575;
	color: #a1a1a1;
}

Hier mal ein komplettes Beispiel anhand des JSON-Table-Widgets:

<!doctype html>
<html style="width: 100%; height: 100%; margin: 0px;">
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
	<meta name="widget-description" content="This is a json to table widget. It will display json-data of the datapoint linked to STATE or LEVEL as a table. (C) by Sebastian Bormann"/> 
	<meta name="widget-urlparameters" content="tableMode/columntoggle/Table Mode (mode how small screens are handled)/select/columntoggle,Hide columns as needed/reflow,Display the table stacked/none,Keep table as it is;colsSort//Order of Headings (semioclon-separated List of column headers, optional);colsFilter//Filter these Headings (semicolon-separated List of column headers, optional);translations//Translations (semicolon-separated list of comma-separated translations, for example filesize,Size of file);icon1Url//Icon 1/icon;icon1Caption//Caption that will be added to icon 1;icon1String//String that will be replaced by icon 1;icon2Url//Icon 2/icon;icon2Caption//Caption that will be added to icon 2;icon2String//String that will be replaced by icon 2;icon3Url//Icon 3/icon;icon3Caption//Caption that will be added to icon 3;icon3String//String that will be replaced by icon 3;icon4Url//Icon 4/icon;icon4Caption//Caption that will be added to icon 4;icon4String//String that will be replaced by icon 4;icon5Url//Icon 5/icon;icon5Caption//Caption that will be added to icon 5;icon5String//String that will be replaced by icon 5;useThisDatapoint//Use this Datapoint (if empty, the default Datapoint (e.g. the Datapoint defined in STATE of this device) will be used)/datapoint">
	<meta name="widget-options" content="{'noZoomOnHover': 'true', 'hideDeviceName': 'true', 'sizeInactive': 'xwideIfInactive highIfInactive', 'iconNoPointerEventsInactive': 'true', 'noOverlayInactive': 'true', 'hideDeviceNameIfInactive': 'true', 'hideStateIfInactive': 'true', 'sizeActive': 'xwideIfActive highIfActive', 'bigIconActive': 'false', 'iconNoPointerEventsActive': 'true', 'noOverlayActive': 'true', 'hideDeviceNameIfActive': 'true', 'hideStateIfActive': 'true', 'iconNoPointerEventsEnlarged': 'true', 'noOverlayEnlarged': 'true', 'hideDeviceNameIfEnlarged': 'true', 'hideStateIfEnlarged': 'true', 'sizeEnlarged': 'fullWidthIfEnlarged fullHeightIfEnlarged', 'bigIconEnlarged': 'true', 'popupAllowPostMessage': 'true', 'backgroundURLAllowPostMessage': 'true', 'backgroundURLNoPointerEvents': 'false', 'tileEnlargeShowButtonInactive': 'true', 'tileEnlargeShowButtonActive': 'true', 'tileEnlargeShowInPressureMenuInactive': 'true', 'tileEnlargeShowInPressureMenuActive': 'true'}"/>
 
	<link rel="stylesheet" href="/iqontrol/jquery/jquery.mobile-1.4.5.min.css"/>
	<script type="text/javascript" src="/iqontrol/jquery/jquery-1.11.3.min.js"></script>
	<script type="text/javascript" src="/iqontrol/jquery/jquery.mobile-1.4.5.min.js"></script>
	<style>
		::-webkit-scrollbar {
		  width: 5px;
		  height: 5px;
		}
		::-webkit-scrollbar-track {
		  background: #eee;
		}
		::-webkit-scrollbar-thumb {
		  background: #aaa; 
		}
		::-webkit-scrollbar-thumb:hover {
		  background: #888; 
		}
		.ui-table-columntoggle-btn {
			border: none !important;
			box-shadow: none !important;
			padding: 9px 20px 8px 20px !important;
			background: transparent !important;
			position: absolute !important;
			top: -8px !important;
			right: -8px !important;
			font-weight: 900 !important;
		}
		.ui-table-columntoggle-btn:hover {
			background: rgba(0,0,0,0.1) !important;
		}
		.jsonTableIcon {
			width: 24px;
			height: 24px;
			border: 0;
			margin: -3px 0px -8px 0px;
		}
		html.color-mode-dark .ui-overlay-a, html.color-mode-dark .ui-page-theme-a, html.color-mode-dark .ui-page-theme-a .ui-panel-wrapper {
			background-color: #00000045;
			color: #b9b9b9;
			text-shadow: 0 1px 0 #3c3c3c
		}
		html.color-mode-dark .table-stripe tbody tr:nth-child(odd) td, html.color-mode-dark .table-stripe tbody tr:nth-child(odd) th {
			background-color: #ffffff24;
		}
	</style>
	<title>iQontrol JSON to TABLE Widget</title>
</head>
<body style="width: 100%; height: 100%; margin: 0; overflow: auto;">
	<div id="jsonTableContainer" style="width: 100%; height: 100%; overflow: auto;">
	</div>
	<script type="text/javascript">
		//Declarations
		var USE_THIS_DATAPOINT;
		var STATE;
		var LEVEL;
		var options;

		//UrlParameters
		var tableMode = getUrlParameter("tableMode") || "columntoggle";
		var colsSort = (getUrlParameter("colsSort") || "").split(';');
		var colsFilter = (getUrlParameter("colsFilter") || "").split(';');
		var translations = (getUrlParameter("translations") || "").split(';');
		for(var i = 0; i < translations.length; i++){
			let entry = translations[i].split(',');
			if(entry.length < 2) entry.push(entry[0]);
			translations[i] = {searchValue: entry[0], newValue: entry[1]};
		}
		var iconReplacements = [];
		for(var i = 1; i <= 5; i++){
			var iconUrl = getUrlParameter("icon" + i + "Url");
            if (iconUrl && iconUrl.indexOf('http') !== 0) iconUrl = '/iqontrol/' + iconUrl
			var caption = getUrlParameter("icon" + i + "Caption");
			var string = getUrlParameter("icon" + i + "String");
			if(iconUrl && string) iconReplacements.push({searchValue: string, newValue: "<img src='" + iconUrl + "' class='jsonTableIcon'>" + (caption ? "&nbsp;" + caption : "")});
		}
		var translationsAndIconReplacements = iconReplacements.concat(translations);

		//Document ready
		$(document).ready(function(){
			//Subscribe to Datapoints
			console.log("Subscribe to Datapoints");
			sendPostMessage("getOptions");
			if(getUrlParameter("useThisDatapoint")) sendPostMessage("getStateSubscribed", getUrlParameter("useThisDatapoint")); else USE_THIS_DATAPOINT = null;
			sendPostMessage("getWidgetDeviceStateSubscribed", "STATE");
			sendPostMessage("getWidgetDeviceStateSubscribed", "LEVEL");
		});
		
		//Handle Options
		function handleOptions(){
			if(typeof options !== "object") return;
			//Dark-Mode
			switch(options.LayoutColorModeDarkEnable){
				case "disabled":
				break;

				case "always":
				applyColorMode('dark');
				break;
				
				default:
				if(window.matchMedia){
					var darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
					applyColorMode(darkMode ? 'dark' : '');
					window.matchMedia('(prefers-color-scheme: dark)').removeEventListener('change', darkModeEventListenerFunction);
					window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', darkModeEventListenerFunction);
					function darkModeEventListenerFunction(e){
						darkMode = e.matches;
						applyColorMode(darkMode ? 'dark' : '');
					}
				}
			}
			function applyColorMode(colorMode){
				$("html").removeClass(function(index, className){
					return (className.match (/(^|\s)color-mode-\S+/g) || []).join(' ');
				});	
				if(colorMode && colorMode != "") $("html").addClass("color-mode-" + colorMode);
			}
		}

		//Create Table
		function createTable(){
			if(typeof USE_THIS_DATAPOINT == "undefined" || typeof STATE == "undefined" || typeof LEVEL == "undefined") return;
			var dp = USE_THIS_DATAPOINT || LEVEL || STATE || null;
			if(dp == null || typeof dp.val == "undefined" || dp.val == null || dp.val == "") return;
			dataObject = tryParseJSON(dp.val);
			console.log("parsed JSON");
			console.log(dataObject);
			//Get cols
			var cols = [];
			for (var line = 0; line < dataObject.length; line++) {
				for (var key in dataObject[line]) {
					if (cols.indexOf(key) === -1) {
						cols.push(key);
					}
				}
			}
			//Sort cols
			var colsSorted = [];			
			for (var col = 0; col < colsSort.length; col++) {
				if(cols.indexOf(colsSort[col]) > -1) colsSorted.push(colsSort[col]);
			}
			for (var col = 0; col < cols.length; col++) { //add the remaining cols
				if(colsSorted.indexOf(colsSort[col]) == -1) colsSorted.push(cols[col]);
			}
			//Filter cols
			var colsSortedAndFiltered = [];
			for (var col = 0; col < colsSorted.length; col++) {
				if(colsFilter.indexOf(colsSorted[col]) == -1) colsSortedAndFiltered.push(colsSorted[col]);
			}			
			//Build table
			var tableString = "<table id='jsonTable' data-role='table' data-mode='" + tableMode + "' class='ui-responsive table-stroke table-stripe' data-column-btn-mini='true' data-column-btn-text='&#8942;' style='font-size: smaller;'>";
			//Add headers
			tableString += "<thead>";
			tableString += "<tr>";
			for (var col = 0; col < colsSortedAndFiltered.length; col++) {
				tableString += "<th data-priority='" + (col > 0 ? col : "") + "'>" + multiReplace(colsSortedAndFiltered[col] || (col + 1).toString(), translations, true) + "</th>";
			}
			tableString += "</tr>";
			tableString += "</thead>";		
			//Add Body
			tableString += "<tbody>";
			for (var line = 0; line < dataObject.length; line++) {
				tableString += "<tr>";
				for (var col = 0; col < colsSortedAndFiltered.length; col++) {
					tableString += "<td>" + multiReplace(dataObject[line][colsSortedAndFiltered[col]] || "", translationsAndIconReplacements, true) + "</td>";
				}
				tableString += "</tr>";
			}
			tableString += "</tbody>";
			//Append table
			tableString += "</table>";
			//$('#jsonTable-popup-popup').remove();
			$('#jsonTableContainer').html(tableString);
			$('#jsonTable').table();
		}

		//send postMessages
		function sendPostMessage(command, stateId, value){
			message = { command: command, stateId: stateId, value: value };
			window.parent.postMessage(message, "*");
		}

		//receive postMessages
		window.addEventListener("message", receivePostMessage, false);
		function receivePostMessage(event) { //event = {data: message data, origin: url of origin, source: id of sending element}
			if(event.data && event.data.command) switch(event.data.command){
				case "getState":
					if(event.data.stateId) switch(event.data.stateId){
						case getUrlParameter("useThisDatapoint") || "---undefined---":
							console.log("Received USE_THIS_DATAPOINT");
							USE_THIS_DATAPOINT = event.data.value;
							createTable();
						break;

						case "STATE":
							console.log("Received STATE");
							STATE = event.data.value;
							createTable();
						break;

						case "LEVEL":
							console.log("Received LEVEL");
							LEVEL = event.data.value;
							createTable();
						break;
					}
				break;
				
				case "getOptions":
				console.log("Received OPTIONS");
				if(event.data.value){
					options = event.data.value;
					handleOptions();
				}
				break;
			}
		}
		
		//GetUrlParameter
		function getUrlParameter(name) {
			name = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
			var regex = new RegExp('[\\?&]' + name + '=([^&#]*)');
			var results = regex.exec(location.search);
			return results === null ? null : decodeURIComponent(results[1].replace(/\+/g, ' '));
		};

		//tryParseJSON
		function tryParseJSON(jsonString){ //Returns parsed object or false, if jsonString is not valid
			try {
				var o = JSON.parse(jsonString);
				// Handle non-exception-throwing cases:
				// Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
				// but... JSON.parse(null) returns null, and typeof null === "object",
				// so we must check for that, too. Thankfully, null is falsey, so this suffices:
				if (o && typeof o === "object") {
					return o;
				}
			}
			catch (e) { }
			return false;
		};
		
		//multiReplace
		function multiReplace(string, replacementObj, onlyExactMatches){ //Replaces multiple replacements in string. replacementObj = [{searchValue: "", newValue: ""}, ...]
			replacementObj.forEach(function(replacement){
				if(onlyExactMatches){
					if(string == replacement.searchValue) string = replacement.newValue;
				} else {
					var regex = new RegExp(replacement.searchValue, "g");
					string = string.toString().replace(regex, replacement.newValue);
				}
			});
			return string;
		}
	</script>
</body>
</html>

Viel Spaß beim Nachbauen!