export const API_MISUSE = new SyntaxError(
"codemelted.js module logic was not used properly!"
);
export const API_NOT_IMPLEMENTED = new SyntaxError(
"NOT IMPLEMENTED LOGIC. DO NOT CALL!"
);
export const API_TYPE_VIOLATION = new SyntaxError(
"codemelted.js module encountered a parameter of an unexpected type!"
);
export const API_UNSUPPORTED_RUNTIME = new SyntaxError(
"codemelted.js module function called on an unsupported " +
"JavaScript runtime!"
);
export class CProtocolHandler {
#id = "";
async get_message(request="") { throw API_NOT_IMPLEMENTED; }
id() { return this.#id; }
is_running() { throw API_NOT_IMPLEMENTED; }
async post_message(data) { throw API_NOT_IMPLEMENTED; }
terminate() { throw API_NOT_IMPLEMENTED; }
constructor(id) {
this.#id = id;
}
}
export class CResult {
#error = null;
#value = null;
error() { return this.#error; }
is_error() {
if (this.error() instanceof Error) {
return true;
}
return this.error() != null && this.error().length > 0;
}
is_ok() { return !this.is_error(); }
value() { return this.#value; }
constructor({value = null, error = null} = {}) {
if (value && error) { throw API_MISUSE; }
this.#value = value;
this.#error = error;
}
}
export function if_def(property, obj = globalThis) {
return property in obj;
}
export class CTimerResult {
#id = -1;
get is_running() { return this.#id !== -1; }
stop() {
if (!this.is_running) {
throw API_MISUSE;
}
clearInterval(this.#id);
this.#id = -1;
}
constructor(task, interval) {
this.#id = setInterval(task, interval);
}
}
export function async_sleep(delay) {
json_check_type({type: "number", data: delay, should_throw: true});
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, delay);
});
}
export function async_task({task, data, delay = 0}) {
json_check_type({type: "function", data: task, should_throw: true});
json_check_type({type: "number", data: delay, should_throw: true});
return new Promise((resolve) => {
try {
setTimeout(() => {
let answer = task(data);
resolve(new CResult(answer));
}, delay);
} catch (err) {
logger_log({
level: LOGGER.error,
data: `async_task() error occurred. ${err}`
});
resolve(new CResult({error: err}));
}
});
}
export function async_timer({task, interval}) {
json_check_type({
type: "function",
data: task,
count: 0,
should_throw: true
});
json_check_type({type: "number", data: interval, should_throw: true});
return new CTimerResult(task, interval);
}
export function async_worker() {
throw API_NOT_IMPLEMENTED;
}
export function db_exists() {
throw API_NOT_IMPLEMENTED;
}
export function db_manage() {
throw API_NOT_IMPLEMENTED;
}
export function db_query() {
throw API_NOT_IMPLEMENTED;
}
export function db_update() {
throw API_NOT_IMPLEMENTED;
}
export function db_version() {
throw API_NOT_IMPLEMENTED;
}
export class CGeodeticData {
#timestamp = new Date();
#latitude = NaN;
#longitude = NaN;
#altitude = null;
#heading = null;
#speed = null;
#alpha = null;
#beta = null;
#gamma = null;
get timestamp() { return this.#timestamp; }
get latitude() { return this.#latitude; }
get longitude() { return this.#longitude; }
get altitude() { return this.#altitude; }
get heading() { return this.#heading; }
get speed() { return this.#speed; }
get alpha() { return this.#alpha; }
get beta() { return this.#beta; }
get gamma() { return this.#gamma; }
updateDeviceOrientation(data) {
if (json_check_type({type: DeviceOrientationEvent, data: data})) {
this.#timestamp = new Date();
this.#alpha = data.alpha ?? NaN;
this.#beta = data.beta ?? NaN;
this.#gamma = data.gamma ?? NaN;
}
}
updateGeolocation(data) {
if (json_check_type({type: GeolocationCoordinates, data: data})) {
this.#timestamp = new Date();
this.#latitude = data.latitude;
this.#longitude = data.longitude;
this.#altitude = data.altitude;
this.#heading = data.heading;
this.#speed = data.speed;
}
}
constructor() { }
}
export class COrientationProtocol extends CProtocolHandler {
#data = new CGeodeticData();
#onDeviceOrientation;
#watchId;
#errorRx = null;
async get_message(request="") {
if (this.#errorRx) {
const result = new CResult({
error: this.#errorRx
});
this.#errorRx = null;
return result;
}
return new CResult({
value: Object.assign({}, this.#data),
});
}
is_running() {
return this.#watchId != -1;
}
post_message(data) { throw API_NOT_IMPLEMENTED; }
terminate() {
if (!this.is_running) {
throw API_MISUSE;
}
globalThis.navigator.geolocation.clearWatch(this.#watchId);
this.#watchId = -1;
globalThis.removeEventListener(
"deviceorientation",
this.#onDeviceOrientation
);
}
constructor(options) {
super("COrientationProtocol");
this.#onDeviceOrientation =
( e) => {
this.#data.updateDeviceOrientation(e);
};
globalThis.addEventListener(
"deviceorientation",
this.#onDeviceOrientation
);
this.#watchId = globalThis.navigator.geolocation.watchPosition(
( position) => {
this.#data.updateGeolocation(position.coords);
},
( err) => {
this.#errorRx = err.message;
},
options
)
}
}
export class CSerialPortProtocol extends CProtocolHandler {
#port;
async get_message(request) {
try {
if (!this.is_running) {
throw API_MISUSE;
}
let resp = null;
let data = null;
switch (request) {
case SERIAL_PORT_DATA_REQUEST.ClearToSend:
resp = await this.#port.getSignals();
data = resp["clearToSend"];
break;
case SERIAL_PORT_DATA_REQUEST.CarrierDetect:
resp = await this.#port.getSignals();
data = resp["dataCarrierDetect"];
break;
case SERIAL_PORT_DATA_REQUEST.DataSetReady:
resp = await this.#port.getSignals();
data = resp["dataSetReady"];
break;
case SERIAL_PORT_DATA_REQUEST.RingIndicator:
resp = await this.#port.getSignals();
data = resp["ringIndicator"];
break;
case SERIAL_PORT_DATA_REQUEST.DataBytes:
if (!this.#port.readable) {
return new CResult({
error: "Serial port was unreadable at this time"
});
}
const reader = this.#port.readable.getReader();
const { value, done } = await reader.read();
data = value;
reader.releaseLock();
break;
default:
throw API_MISUSE;
}
return new CResult({value: data});
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CSerialPortProtocol::get_message() error occurred. ${err}`
});
return new CResult({error: err});
}
}
is_running() {
return this.#port.connected;
}
async post_message(data) {
try {
json_check_type({type: "object", data: data, should_throw: true});
let request = data.request;
switch (request) {
case SERIAL_PORT_DATA_REQUEST.DataTerminalReady:
json_check_type({
type: "boolean",
data: data.data,
should_throw: true
});
await this.#port.setSignals("dataTerminalReady", data.data);
break;
case SERIAL_PORT_DATA_REQUEST.RequestToSend:
json_check_type({
type: "boolean",
data: data.data,
should_throw: true
});
await this.#port.setSignals("requestToSend", data.data);
break;
case SERIAL_PORT_DATA_REQUEST.Break:
json_check_type({
type: "boolean",
data: data.data,
should_throw: true
});
await this.#port.setSignals("break", data.data);
break;
case SERIAL_PORT_DATA_REQUEST.DataBytes:
json_check_type({
type: Uint8Array,
data: data.data,
should_throw: true
});
const writer = this.#port.writable.getWriter();
await writer.write(data.data);
writer.releaseLock();
break;
default:
throw API_MISUSE;
}
return new CResult();
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CSerialPortProtocol::post_message() error occurred. ${err}`
});
return new CResult({error: err});
}
}
terminate() {
if (!this.is_running) {
throw API_MISUSE;
}
this.#port.close();
}
constructor(port) {
super(
`CSerialPort_${port.getInfo().usbVendorId}` +
`_${[port.getInfo().usbProductId]}`
);
this.#port = port;
}
}
export const SERIAL_PORT_DATA_REQUEST = Object.freeze({
Break: "Break",
CarrierDetect: "CarrierDetect",
ClearToSend: "ClearToSend",
DataSetReady: "DataSetReady",
DataTerminalReady: "DataTerminalReady",
RequestToSend: "RequestToSend",
RingIndicator: "RingIndicator",
DataBytes: "DataBytes",
});
export function hw_request_bluetooth() {
throw API_NOT_IMPLEMENTED;
}
export function hw_request_midi() {
throw API_NOT_IMPLEMENTED;
}
export function hw_request_orientation(options) {
if (!hw_support_orientation()) {
throw API_UNSUPPORTED_RUNTIME;
}
return new COrientationProtocol(options)
}
export async function hw_request_serial_port() {
if (!hw_support_serial_port()) {
throw API_UNSUPPORTED_RUNTIME;
}
try {
const port = await globalThis.navigator.serial.requestPort();
return new CSerialPortProtocol(port);
} catch (err) {
return null;
}
}
export function hw_request_usb() {
throw API_NOT_IMPLEMENTED;
}
export function hw_support_bluetooth() {
return if_def("navigator") &&
if_def("bluetooth", globalThis["navigator"]);
}
export function hw_support_midi() {
return if_def("navigator") &&
if_def("requestMIDIAccess", globalThis["navigator"]);
}
export function hw_support_orientation() {
return if_def("navigator") &&
if_def("geolocation", globalThis["navigator"]);
}
export function hw_support_serial_port() {
return if_def("navigator") && if_def("serial", globalThis["navigator"]);
}
export function hw_support_usb() {
return if_def("navigator") && if_def("usb", globalThis["navigator"]);
}
export function json_atob(data) {
json_check_type({type: "string", data: data, should_throw: true});
try {
return globalThis.atob(data);
} catch (err) {
return null;
}
}
export function json_btoa(data) {
json_check_type({type: "string", data: data, should_throw: true});
try {
return globalThis.btoa(data);
} catch (err) {
return null;
}
}
export function json_create_array(data) {
if (json_check_type({type: Array, data: data})) {
let stringified = json_stringify(data);
if (stringified) {
return json_parse(stringified) ?? [];
}
}
return [];
}
export function json_create_object(data) {
if (json_check_type({type: "object", data})) {
return Object.assign({}, data);
}
return {};
}
export function json_check_type({
type,
data,
count = undefined,
should_throw = false
}) {
try {
const isExpectedType = typeof type !== "string"
? (data instanceof type)
: typeof data === type;
let valid = typeof count === "number"
? isExpectedType && data.length === count
: isExpectedType;
if (should_throw && !valid) {
logger_log({
level: LOGGER.error,
data: "json_check_type() - type specified not of an expected type."
})
throw API_TYPE_VIOLATION;
}
return valid;
} catch (err) {
throw API_MISUSE;
}
}
export function json_has_key({data, key, should_throw = false}) {
json_check_type({type: "object", data: data, should_throw: true});
json_check_type({type: "string", data: key, should_throw: true});
var hasKey = Object.hasOwn(data, key);
if (should_throw && !hasKey) {
throw API_TYPE_VIOLATION;
}
return hasKey;
}
export function json_parse(data) {
try {
return JSON.parse(data);
} catch (ex) {
return null;
}
}
export function json_stringify(data) {
try {
return JSON.stringify(data);
} catch (ex) {
return null;
}
}
export function json_valid_url({data, should_throw = false}) {
json_check_type({type: "string", data: data, should_throw: true});
let url = undefined;
try {
url = new URL(data);
} catch (_err) {
url = undefined;
}
let valid = json_check_type({type: URL, data: url});
if (should_throw && !valid) {
throw API_TYPE_VIOLATION;
}
return valid;
}
export class CLogRecord {
#time = new Date();
#level;
#data = undefined;
get time() { return this.#time; }
get level() { return this.#level; }
get data() { return this.#data; }
constructor(level, data) {
this.#level = level;
this.#data = data;
}
}
export const LOGGER = Object.freeze({
debug: { level: 0, label: "DEBUG" },
info: { level: 1, label: "INFO" },
warning: { level: 2, label: "WARNING" },
error: { level: 3, label: "ERROR" },
off: { level: 4, label: "OFF" },
});
let _logger_level = LOGGER.error;
let _logger_handler = null;
export function logger_handler(handler) {
if (json_check_type({type: "function", data: handler, count: 1})) {
_logger_handler = handler;
} else if (handler === null) {
_logger_handler = handler;
} else {
throw API_TYPE_VIOLATION;
}
}
export function logger_level(level) {
if (level) {
json_check_type({type: "object", data: level, should_throw: true});
json_has_key({data: level, key: "level", should_throw: true});
json_has_key({data: level, key: "label", should_throw: true});
_logger_level = level;
}
return _logger_level.label;
}
export function logger_log({level, data}) {
json_check_type({type: "object", data: level, should_throw: true});
if (!data) {
throw API_TYPE_VIOLATION;
}
if (_logger_level.label == "OFF") {
return;
}
const record = new CLogRecord(level, data);
if (record.level.level >= _logger_level.level) {
switch (record.level.label) {
case "DEBUG":
case "INFO":
console.info(
record.time.toISOString(),
record.level.label,
record.data
);
case "WARNING":
console.warn(
record.time.toISOString(),
record.level.label,
record.data
);
break;
case "ERROR":
console.error(
record.time.toISOString(),
record.level.label,
record.data
);
break;
}
if (_logger_handler) {
_logger_handler(record);
}
}
}
export class CBroadcastChannelProtocol extends CProtocolHandler {
#channel
#rxData = [];
#rxError = [];
async get_message(request="") {
return new Promise((resolve) => {
if (!this.is_running) {
resolve(new CResult({error: API_MISUSE}));
}
setTimeout(() => {
if (this.#rxError.length > 0) {
resolve(new CResult({error: this.#rxError.shift()}));
} else {
if (this.#rxData.length > 0) {
resolve(new CResult({value: this.#rxData.shift()}));
} else {
resolve(new CResult());
}
}
});
});
}
is_running() {
return this.#channel.onmessage !=null
&& this.#channel.onmessageerror != null;
}
async post_message(data) {
try {
if (!this.is_running()) {
throw API_MISUSE;
}
this.#channel.postMessage(data);
return new CResult();
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CBroadcastChannelProtocol::post_message() error ` +
`occurred. ${err}`
})
return new CResult({error: err});
}
}
terminate() {
if (!this.is_running()) {
logger_log({
level: LOGGER.error,
data: `CBroadcastChannelProtocol::terminate() error. ${API_MISUSE}`
});
throw API_MISUSE;
}
this.#channel.onmessage = null;
this.#channel.onmessageerror = null;
this.#channel.close();
this.#rxData.length = 0;
this.#rxError.length = 0;
}
constructor(url) {
super(`CBroadcastChannelProtocol-${url}`);
this.#channel = new globalThis.BroadcastChannel(url);
this.#channel.onmessage = (evt) => {
setTimeout(() => {
this.#rxData.push(evt);
});
};
this.#channel.onmessageerror = (evt) => {
setTimeout(() => {
this.#rxError.push(evt);
});
};
}
}
export const CONNECT_REQUEST = Object.freeze({
BroadcastChannel: "broadcast_channel",
EventSource: "event_source",
WebSocket: "web_socket",
WebRTC: "web_rtc",
});
export class CEventSourceProtocol extends CProtocolHandler {
static get CONNECTING() { return 0; }
static get OPEN() { return 1; }
static get CLOSED() { return 2; }
#sse
#rxData = [];
#rxError = [];
get state() {
return this.is_running() ?
this.#sse.readyState
: -1;
}
async get_message(request="") {
return new Promise((resolve) => {
if (!this.is_running()) {
resolve(new CResult({error: API_MISUSE}));
}
setTimeout(() => {
if (this.#rxError.length > 0) {
resolve(new CResult({error: this.#rxError.shift()}));
} else {
if (this.#rxData.length > 0) {
resolve(new CResult({value: this.#rxData.shift()}));
} else {
resolve(new CResult());
}
}
});
});
}
is_running() {
return this.#sse.onmessage !=null
&& this.#sse.onerror != null;
}
async post_message(data) { throw API_NOT_IMPLEMENTED; }
terminate() {
if (!this.is_running()) {
throw API_MISUSE;
}
this.#sse.close();
this.#sse.onerror = null;
this.#sse.onmessage = null;
this.#rxData.length = 0;
this.#rxError.length = 0;
}
constructor(url) {
super(`CEventSourceProtocol-${url}`);
this.#sse = new globalThis.EventSource(url);
this.#sse.onerror = (evt) => {
setTimeout(() => {
this.#rxError.push(evt);
});
}
this.#sse.onmessage = (evt) => {
setTimeout(() => {
this.#rxData.push(evt);
});
}
}
}
export class CFetchResult extends CResult {
#status = -1;
get asBinary() {
return json_check_type({type: Uint8Array,
data: this.value()})
? this.value()
: null;
}
get asBlob() {
return json_check_type({type: Blob, data: this.value()})
? this.value()
: null;
}
get asFormData() {
return json_check_type({type: FormData, data: this.value()})
? this.value()
: null;
}
get asObject() {
return json_check_type({type: "object", data: this.value()})
? this.value()
: null;
}
get asString() {
return json_check_type({type: "string", data: this.value()})
? this.value()
: null;
}
is_error() {
return super.is_error() || this.status < 200 || this.status > 299;
}
get status() { return this.#status; }
constructor({status, data, error=null}) {
super({value: data, error: error});
this.#status = status;
}
}
export class CWebSocketProtocol extends CProtocolHandler {
static get CONNECTING() { return 0; }
static get OPEN() { return 1; }
static get CLOSING() { return 2; }
static get CLOSED() { return 3; }
#url;
#socket
#rxData = [];
#rxError = [];
get state() {
return this.is_running() ?
this.#socket.readyState
: -1;
}
async get_message(request="") {
if (!this.is_running) {
throw API_MISUSE;
}
return new Promise((resolve) => {
setTimeout(() => {
if (this.#rxError.length > 0) {
resolve(new CResult({error: this.#rxError.shift()}));
} else {
if (this.#rxData.length > 0) {
resolve(new CResult({value: this.#rxData.shift()}));
} else {
resolve(new CResult());
}
}
});
});
}
is_running() {
return this.#socket.onmessage !=null
&& this.#socket.onerror != null;
}
async post_message(data) {
if (!this.is_running) {
throw API_MISUSE;
}
try {
this.#socket.send(data);
return new CResult();
} catch (err) {
this.#closeSocket();
this.#connectSocket();
return new CResult({error: err});
}
}
terminate() {
if (!this.is_running) {
throw API_MISUSE;
}
this.#closeSocket();
}
constructor({url}) {
super(`CWebSocketProtocol-${url ?? 'server'}`);
this.#url = url;
this.#connectSocket();
}
#connectSocket() {
this.#socket = new globalThis.WebSocket(this.#url);
this.#socket.onmessage = ( evt) => {
setTimeout(() => {
this.#rxData.push(evt);
});
}
this.#socket.onerror = ( evt) => {
setTimeout(() => {
this.#rxError.push(evt);
});
}
}
#closeSocket() {
this.#socket.close();
this.#socket.onmessage = null;
this.#socket.onerror = null;
this.#rxData.length = 0;
this.#rxError.length = 0;
}
}
export function network_beacon({url, data}) {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
return globalThis.navigator.sendBeacon(url, data);
}
export function network_connect({request, url}) {
switch (request) {
case CONNECT_REQUEST.BroadcastChannel:
return new CBroadcastChannelProtocol(url);
case CONNECT_REQUEST.EventSource:
return new CEventSourceProtocol(url);
case CONNECT_REQUEST.WebSocket:
return new CWebSocketProtocol({url: url});
case CONNECT_REQUEST.WebRTC:
throw API_NOT_IMPLEMENTED;
default:
throw API_MISUSE;
}
}
export async function network_fetch({url, options}) {
try {
const resp = await globalThis.fetch(url, options);
const contentType = resp.headers.get("Content-Type") ?? "";
const status = resp.status;
const data = contentType.includes("application/json")
? await resp.json()
: contentType.includes("form-data")
? await resp.formData()
: contentType.includes("application/octet-stream")
? await resp.blob()
: contentType.includes("text/")
? await resp.text()
: "";
return new CFetchResult({status: status, data: data});
} catch (err) {
return new CFetchResult({status: 418, error: err});
}
}
export const MATH_FORMULA = Object.freeze({
GeodeticDistance: "GeodeticDistance",
GeodeticHeading: "GeodeticHeading",
GeodeticSpeed: "GeodeticSpeed",
TemperatureCelsiusToFahrenheit: "TemperatureCelsiusToFahrenheit",
TemperatureCelsiusToKelvin: "TemperatureCelsiusToKelvin",
TemperatureFahrenheitToCelsius: "TemperatureFahrenheitToCelsius",
TemperatureFahrenheitToKelvin: "TemperatureFahrenheitToKelvin",
TemperatureKelvinToCelsius: "TemperatureKelvinToCelsius",
TemperatureKelvinToFahrenheit: "TemperatureKelvinToFahrenheit"
});
function _geodetic_distance(start_latitude, start_longitude,
end_latitude, end_longitude) {
let lat1 = start_latitude * Math.PI / 180.0;
let lon1 = start_longitude * Math.PI / 180.0;
let lat2 = end_latitude * Math.PI / 180.0;
let lon2 = end_longitude * Math.PI / 180.0;
let r = 6378100.0;
let rho1 = r * Math.cos(lat1);
let z1 = r * Math.sin(lat1);
let x1 = rho1 * Math.cos(lon1);
let y1 = rho1 * Math.sin(lon1);
let rho2 = r * Math.cos(lat2);
let z2 = r * Math.sin(lat2);
let x2 = rho2 * Math.cos(lon2);
let y2 = rho2 * Math.sin(lon2);
let dot = x1 * x2 + y1 * y2 + z1 * z2;
let cos_theta = dot / (r * r);
let theta = Math.acos(cos_theta);
return r * theta;
}
function _geodetic_heading(start_latitude, start_longitude,
end_latitude, end_longitude) {
let lat1 = start_latitude * (Math.PI / 180.0);
let lon1 = start_longitude * (Math.PI / 180.0);
let lat2 = end_latitude * (Math.PI / 180.0);
let lon2 = end_longitude * (Math.PI / 180.0);
let y = Math.sin(lon2 - lon1) * Math.cos(lat2);
let x = (Math.cos(lat1) * Math.sin(lat2)) -
(Math.sin(lat1) * Math.cos(lat2) * Math.cos(lon2 - lon1));
let rtnval = Math.atan2(y, x) * (180.0 / Math.PI);
return (rtnval + 360.0) % 360.0;
}
function _geodetic_speed(start_latitude, start_longitude,
start_milliseconds, end_latitude, end_longitude, end_milliseconds) {
let dist_meters = _geodetic_distance(
start_latitude, start_longitude,
end_latitude, end_longitude
);
let time_s = (end_milliseconds - start_milliseconds) / 1000.0;
return dist_meters / time_s;
}
export function npu_compute() {
throw API_NOT_IMPLEMENTED;
}
export function npu_math({formula, args}) {
json_check_type({type: "string", data: formula, should_throw: true});
json_check_type({type: Array, data: args, should_throw: true});
args.forEach((v) => {
json_check_type({type: "number", data: v, should_throw: true});
});
try {
switch (formula) {
case MATH_FORMULA.GeodeticDistance:
return _geodetic_distance(args[0], args[1], args[2], args[3]);
case MATH_FORMULA.GeodeticHeading:
return _geodetic_heading(args[0], args[1], args[2], args[3]);
case MATH_FORMULA.GeodeticSpeed:
return _geodetic_speed(args[0], args[1], args[2], args[3], args[4],
args[5]);
case MATH_FORMULA.TemperatureCelsiusToFahrenheit:
return (args[0] * 9.0 / 5.0) + 32.0;
case MATH_FORMULA.TemperatureCelsiusToKelvin:
return args[0] + 273.15;
case MATH_FORMULA.TemperatureFahrenheitToCelsius:
return (args[0] - 32.0) * (5.0 / 9.0);
case MATH_FORMULA.TemperatureFahrenheitToKelvin:
return (args[0] - 32.0) * (5.0 / 9.0) + 273.15;
case MATH_FORMULA.TemperatureKelvinToCelsius:
return args[0] - 273.15;
case MATH_FORMULA.TemperatureKelvinToFahrenheit:
return (args[0] - 273.15) * (9.0 / 5.0) + 32.0;
}
} catch (err) {
throw API_MISUSE;
}
throw API_NOT_IMPLEMENTED;
}
export const EVENT_REQUEST = Object.freeze({
Add: "add",
Remove: "remove",
});
export function runtime_cpu_count() {
if (runtime_is_browser() || runtime_is_deno() ||
runtime_is_worker()) {
return globalThis.navigator.hardwareConcurrency;
}
throw API_UNSUPPORTED_RUNTIME;
}
export function runtime_environment(name) {
if (runtime_is_browser()) {
let params = new URLSearchParams(globalThis.location.search);
return params.get(name);
}
throw API_UNSUPPORTED_RUNTIME;
}
export function runtime_event({
action,
type,
listener,
obj = undefined,
}) {
json_check_type({type: "string", data: type, should_throw: true});
json_check_type({
type: "function",
data: listener,
count: 1,
should_throw: true
});
if (action === "add") {
if (obj) {
obj.addEventListener(type, listener);
} else {
globalThis.addEventListener(type, listener);
}
} else if (action === "remove") {
if (obj) {
obj.removeEventListener(type, listener);
} else {
globalThis.removeEventListener(type, listener);
}
} else {
throw API_MISUSE;
}
}
export function runtime_hostname() {
if (runtime_is_browser()) {
return globalThis.location.hostname;
}
throw API_UNSUPPORTED_RUNTIME;
}
export function runtime_is_browser() {
return if_def("document");
}
export function runtime_is_deno() {
return if_def("Deno") && if_def("version", globalThis["Deno"]);
}
export function runtime_is_nodejs() {
return if_def("process") && !runtime_is_deno();
}
export function runtime_is_worker() {
return if_def("WorkerGlobalScope");
}
export function runtime_name() {
if (runtime_is_browser()) {
const userAgent = globalThis.navigator.userAgent.toLowerCase();
if (userAgent.includes("firefox/")) {
return "firefox";
} else if (userAgent.includes("opr/")
|| userAgent.includes("presto/")) {
return "opera";
} else if (userAgent.includes("mobile/")
|| userAgent.includes("version/")) {
return "safari";
} else if (userAgent.includes("edg/")) {
return "edge";
} else if (userAgent.includes("chrome/")) {
return "chrome";
} else {
return "UNKNOWN BROwSER";
}
} else if (runtime_is_deno()) {
return "deno";
} else if (runtime_is_nodejs()) {
return "nodejs";
} else if (runtime_is_worker()) {
return "worker";
}
return "UNDETERMINED";
}
export function runtime_online() {
if (runtime_is_browser()) {
return globalThis.navigator.onLine;
}
throw API_UNSUPPORTED_RUNTIME;
}
class CCookieStorage {
static #expireDays = 365;
static #keyList = [];
static clear() {
let cookies = globalThis.document.cookie.split(";");
for (let i = 0; i < cookies.length; i++) {
globalThis.document.cookie =
`${cookies[i]}=;expires=Thu, 01 Jan 1970 00:00:00 UTC`;
}
CCookieStorage.initKeyList();
}
static getItem(key) {
let name = `${key}=`;
let decodedCookie = decodeURIComponent(globalThis.document.cookie);
let ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == " ") {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return null;
}
static key(index) {
return index < CCookieStorage.#keyList.length
? CCookieStorage.#keyList[index]
: null;
}
static get length() { return CCookieStorage.#keyList.length; }
static setItem(key, value) {
const d = new Date();
d.setTime(d.getTime() + (this.#expireDays * 24 * 60 * 60 * 1000));
let expires = `expires=${d.toUTCString()}`;
globalThis.document.cookie = `${key}=${value};${expires};path=/`;
CCookieStorage.initKeyList();
}
static removeItem(key) {
globalThis.document.cookie =
`${key}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;`;
CCookieStorage.initKeyList();
}
static initKeyList() {
CCookieStorage.#keyList = [];
let decodedCookie = decodeURIComponent(globalThis.document.cookie);
let ca = decodedCookie.split(";");
for (let i = 0; i < ca.length; i++) {
let key = ca[i].split("=");
if (!key[0].includes("expires") && !key[0].includes("path")) {
CCookieStorage.#keyList.push((key[0]));
}
}
}
}
export const STORAGE_TYPE = Object.freeze({
Cookie: "cookie",
Local: "local",
Session: "session",
});
export function storage_clear(type = STORAGE_TYPE.Local) {
if (runtime_is_nodejs() || runtime_is_worker()) {
throw API_UNSUPPORTED_RUNTIME;
}
switch (type) {
case STORAGE_TYPE.Cookie:
CCookieStorage.clear();
break;
case STORAGE_TYPE.Local:
globalThis.localStorage.clear();
break;
case STORAGE_TYPE.Session:
globalThis.sessionStorage.clear();
break;
default:
throw API_MISUSE;
}
}
export function storage_get({type = STORAGE_TYPE.Local, key}) {
if (runtime_is_nodejs() || runtime_is_worker()) {
throw API_UNSUPPORTED_RUNTIME;
}
json_check_type({type: "string", data: key, should_throw: true});
switch (type) {
case STORAGE_TYPE.Cookie:
return CCookieStorage.getItem(key);
case STORAGE_TYPE.Local:
return globalThis.localStorage.getItem(key);
case STORAGE_TYPE.Session:
return globalThis.sessionStorage.getItem(key);
default:
throw API_MISUSE;
}
}
export function storage_key({type, index}) {
if (runtime_is_nodejs() || runtime_is_worker()) {
throw API_UNSUPPORTED_RUNTIME;
}
json_check_type({type: "number", data: index, should_throw: true});
switch (type) {
case STORAGE_TYPE.Cookie:
return CCookieStorage.key(index);
case STORAGE_TYPE.Local:
return index < globalThis.localStorage.length
? globalThis.localStorage.key(index)
: null;
case STORAGE_TYPE.Session:
return index < globalThis.sessionStorage.length
? globalThis.sessionStorage.key(index)
: null;
default:
throw API_MISUSE;
}
}
export function storage_length(type = STORAGE_TYPE.Local) {
if (runtime_is_nodejs() || runtime_is_worker()) {
throw API_UNSUPPORTED_RUNTIME;
}
switch (type) {
case STORAGE_TYPE.Cookie:
return CCookieStorage.length;
case STORAGE_TYPE.Local:
return globalThis.localStorage.length;
case STORAGE_TYPE.Session:
return globalThis.sessionStorage.length;
default:
throw API_MISUSE;
}
}
export function storage_remove({type = STORAGE_TYPE.Local, key}) {
if (runtime_is_nodejs() || runtime_is_worker()) {
throw API_UNSUPPORTED_RUNTIME;
}
json_check_type({type: "string", data: key, should_throw: true});
switch (type) {
case STORAGE_TYPE.Cookie:
CCookieStorage.removeItem(key);
break;
case STORAGE_TYPE.Local:
globalThis.localStorage.removeItem(key);
break;
case STORAGE_TYPE.Session:
globalThis.sessionStorage.removeItem(key);
break;
default:
throw API_MISUSE;
}
}
export function storage_set({type = STORAGE_TYPE.Local, key, value}) {
if (runtime_is_nodejs() || runtime_is_worker()) {
throw API_UNSUPPORTED_RUNTIME;
}
json_check_type({type: "string", data: key, should_throw: true});
json_check_type({type: "string", data: value, should_throw: true});
switch (type) {
case STORAGE_TYPE.Cookie:
CCookieStorage.setItem(key, value);
break;
case STORAGE_TYPE.Local:
globalThis.localStorage.setItem(key, value);
break;
case STORAGE_TYPE.Session:
globalThis.sessionStorage.setItem(key, value);
break;
default:
throw API_MISUSE;
}
}
export const ACTION_REQUEST = Object.freeze({
Audio: "Audio",
Focus: "Focus",
MoveBy: "MoveBy",
MoveTo: "MoveTo",
Print: "Print",
PostMessage: "PostMessage",
ResizeBy: "ResizeBy",
ResizeTo: "ResizeTo",
Scroll: "Scroll",
ScrollBy: "ScrollBy",
ScrollTo: "ScrollTo",
Share: "Share",
Vibrate: "Vibrate",
});
export class CAudioPlayer {
#not_loaded_err = "No loaded src detected. You must call load() before " +
"using CAudioPlayer.";
#audio_player = null;
#tts_utterance = null;
#state;
#handler = null;
get loop() {
try {
if (this.#audio_player) {
return this.#audio_player.loop;
} else if (this.#tts_utterance) {
throw "loop property not supported by 'tts' type"
} else {
logger_log({
level: LOGGER.error,
data: this.#not_loaded_err
})
throw API_MISUSE;
}
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::loop - ${err}`
})
throw API_MISUSE;
}
}
set loop(v) {
json_check_type({type: "boolean", data: v, should_throw: true});
try {
if (this.#audio_player) {
this.#audio_player.loop = v;
} else if (this.#tts_utterance) {
throw "loop property not supported by 'tts' type"
} else {
throw this.#not_loaded_err
}
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::loop - ${err}`
});
throw API_MISUSE;
}
}
get rate() {
try {
if (this.#audio_player) {
return this.#audio_player.playbackRate;
} else if (this.#tts_utterance) {
return this.#tts_utterance.rate;
} else {
throw this.#not_loaded_err;
}
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::rate - ${err}`
});
throw API_MISUSE;
}
}
set rate(v) {
json_check_type({type: "number", data: v, should_throw: true});
let rate = v > 11
? 11
: v < 0.1
? 0.1
: v;
try {
if (this.#audio_player) {
this.#audio_player.playbackRate = rate;
} else if (this.#tts_utterance) {
this.#tts_utterance.rate = rate;
} else {
throw this.#not_loaded_err;
}
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::rate - ${err}`
});
throw API_MISUSE;
}
}
set onended(handler) {
if (handler) {
json_check_type({type: "function", data: handler, should_throw: true});
this.#handler = handler;
} else {
this.#handler = null;
}
}
get state() {
return this.#state;
}
get volume() {
try {
if (this.#audio_player) {
return this.#audio_player.volume;
} else if (this.#tts_utterance) {
return this.#tts_utterance.volume;
} else {
throw this.#not_loaded_err;
}
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::volume - ${err}`
})
throw API_MISUSE;
}
}
set volume(v) {
json_check_type({type: "number", data: v, should_throw: true});
let volume = v < 0
? 0
: v > 1
? 1
: v;
try {
if (this.#audio_player) {
this.#audio_player.volume = volume;
} else if (this.#tts_utterance) {
this.#tts_utterance.volume = volume;
} else {
throw this.#not_loaded_err;
}
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::volume - ${err}`
})
throw API_MISUSE;
}
}
load(type, data) {
json_check_type({type: "string", data: data, should_throw: true});
json_check_type({type: "string", data: data, should_throw: true});
this.#audio_player = null;
this.#tts_utterance = null;
if (type === "audio") {
this.#audio_player = new Audio(data);
this.#audio_player.onended = () => {
this.#state = "stopped";
if (this.#handler) {
this.#handler(new Event("stopped"));
}
}
} else if (type === "tts") {
this.#tts_utterance = new SpeechSynthesisUtterance(data);
this.#tts_utterance.onend = () => {
this.#state = "stopped";
if (this.#handler) {
this.#handler(new Event("stopped"));
}
}
globalThis.speechSynthesis.cancel();
} else {
logger_log({
level: LOGGER.error,
data: "CAudioPlayer::load() - valid types are 'audio' / 'tts'"
});
throw API_MISUSE;
}
}
async pause() {
try {
if (this.#state != "playing") {
throw "not in a 'playing' state";
} else if (this.#audio_player) {
this.#audio_player.pause();
} else if (this.#tts_utterance) {
globalThis.speechSynthesis.pause();
} else {
throw this.#not_loaded_err;
}
this.#state = "paused";
return new CResult();
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::pause() - ${err}`
});
return new CResult({error: err});
}
}
async play() {
try {
if (this.#state != "stopped") {
throw "not in a 'stopped' state"
} else if (this.#audio_player) {
await this.#audio_player.play();
} else if (this.#tts_utterance) {
globalThis.speechSynthesis.speak(this.#tts_utterance);
} else {
throw this.#not_loaded_err;
}
this.#state = "playing";
return new CResult();
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::play() - ${err}`
});
return new CResult({error: err});
}
}
async resume() {
try {
if (this.#state != "paused") {
throw "not in a 'paused' state";
} else if (this.#audio_player) {
await this.#audio_player.play();
} else if (this.#tts_utterance) {
globalThis.speechSynthesis.resume();
} else {
throw this.#not_loaded_err;
}
this.#state = "playing";
return new CResult();
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::resume() - ${err}`
});
return new CResult({error: err});
}
}
stop() {
if (this.#state === "stopped") {
logger_log({
level: LOGGER.error,
data: "CAudioPlayer::stop() already in a stop stopped state"
});
throw API_MISUSE;
}
try {
if (this.#audio_player) {
this.#audio_player.load();
this.#audio_player.currentTime = 0;
} else if (this.#tts_utterance) {
globalThis.speechSynthesis.cancel();
} else {
throw this.#not_loaded_err;
}
this.#state = "stopped";
return new CResult();
} catch (err) {
logger_log({
level: LOGGER.error,
data: `CAudioPlayer::stop() - ${err}`
});
return new CResult({error: err});
}
}
constructor() {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
this.#state = "stopped";
}
}
class CDialog {
static returnValue;
static close(id, returnValue) {
const dlg = globalThis.document.getElementById(id);
CDialog.returnValue = returnValue;
dlg.close();
const divDialog = globalThis.document.getElementById(`div_${id}`);
globalThis.document.body.removeChild(divDialog);
}
static show(icon, id, title, content, width="250px", height) {
const html = `
<style>
.codemelted-dialog {
background-color: black;
flex-flow: column;
border: 5px solid black;
padding: 0;
width: ${width};
height: ${height};
overflow: hidden;
}
dialog::backdrop {
background: rgba(0, 0, 0, 0.50);
}
.codemelted-dialog-title {
flex: 0 1 auto;
font-size: large;
font-weight: bolder;
text-align: center;
vertical-align: middle;
background-color: black;
color: white;
display: grid;
grid-template-columns: auto 1fr auto;
border-bottom: 3px solid black;
}
.codemelted-dialog-title button {
margin-left: 5px;
cursor: pointer;
}
.codemelted-dialog-content {
background-color: gray;
color: white;
border: 0;
margin: 0;
padding: 0;
width: 100%;
flex: 1 1 auto;
}
.codemelted-dialog-content div {
padding: 10px;
}
.codemelted-dialog-content input[type=text] {
margin-left: 10px;
margin-right: 10px;
width: 85%;
}
.codemelted-dialog-content select {
margin-left: 10px;
margin-right: 10px;
margin-bottom: 10px;
width: 150px;
}
.codemelted-dialog-content button {
cursor: pointer;
height: 25px;
width: 75px;
}
.codemelted-align-center {
padding: 5px;
text-align: center;
}
</style>
<dialog id="${id}" class="codemelted-dialog">
<div class="codemelted-dialog-title">
<label>${icon}</label>
<label style="margin-top: 2px;">${title}</label>
<button id="${id}CloseDialog">X</button>
</div>
${content}
</dialog>
`;
const divDialog = globalThis.document.createElement("div");
divDialog.id = `div_${id}`;
divDialog.innerHTML = html;
globalThis.document.body.appendChild(divDialog);
const dialog = globalThis.document.getElementById(id);
dialog.showModal();
}
}
export const DIALOG_REQUEST = Object.freeze({
Alert: "alert",
Choose: "choose",
Close: "close",
Confirm: "confirm",
Loading: "loading",
Prompt: "prompt",
});
export const IS_REQUEST = Object.freeze({
PWA: "PWA",
SecureContext: "SecureContext",
TouchEnabled: "TouchEnabled",
});
export const SCHEMA_TYPE = Object.freeze({
File: "file:",
Http: "http://",
Https: "https://",
Mailto: "mailto:",
Sms: "sms:",
Tel: "tel:",
});
export const SCREEN_REQUEST = Object.freeze({
AvailableHeight: "availHeight",
AvailableWidth: "availWidth",
ColorDepth: "colorDepth",
DevicePixelRatio: "devicePixelRatio",
Height: "height",
InnerHeight: "innerHeight",
InnerWidth: "innerWidth",
OuterHeight: "outerHeight",
OuterWidth: "outerWidth",
PixelDepth: "pixelDepth",
ScreenLeft: "screenLeft",
ScreenOrientationAngle: "screenOrientationAngle",
ScreenOrientationType: "screenOrientationType",
ScreenTop: "screenTop",
ScreenX: "screenX",
ScreenY: "screenY",
ScrollX: "scrollX",
ScrollY: "scrollY",
Width: "width",
});
export const TARGET_TYPE = Object.freeze({
Blank: "_blank",
Parent: "_parent",
Self: "_self",
Top: "_top",
});
export const WIDGET_REQUEST = Object.freeze({
CssVariable: "CssVariable",
Define: "Define",
ElementById: "ElementById",
});
export async function ui_action({
request,
data,
target_origin="*",
pattern=[],
x,
y
}) {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
let value = null;
switch (request) {
case ACTION_REQUEST.Audio:
value = new CAudioPlayer();
break;
case ACTION_REQUEST.Focus:
globalThis.focus();
break;
case ACTION_REQUEST.MoveBy:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.moveBy(x, y);
break;
case ACTION_REQUEST.MoveTo:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.moveTo(x, y);
break;
case ACTION_REQUEST.PostMessage:
globalThis.postMessage(data, target_origin);
break;
case ACTION_REQUEST.Print:
globalThis.print();
break;
case ACTION_REQUEST.ResizeBy:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.resizeBy(x, y);
break;
case ACTION_REQUEST.ResizeTo:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.resizeTo(x, y);
break;
case ACTION_REQUEST.Scroll:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.scroll(x, y);
break;
case ACTION_REQUEST.ScrollBy:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.scrollBy(x, y);
break;
case ACTION_REQUEST.ScrollTo:
json_check_type({type: "number", data: x, should_throw: true});
json_check_type({type: "number", data: y, should_throw: true});
globalThis.scrollTo(x, y);
break;
case ACTION_REQUEST.Share:
try {
await globalThis.navigator.share(data);
} catch (err) {
logger_log({
level: LOGGER.error,
data: `ui_action() share failed. ${err}`
});
return new CResult({error: err});
}
case ACTION_REQUEST.Vibrate:
json_check_type({type: Array, data: pattern, should_throw: true});
try {
globalThis.navigator.vibrate(pattern);
} catch (err) {
logger_log({
level: LOGGER.error,
data: `ui_action() vibrate failed. ${err}`
});
return new CResult({error: err});
}
default:
throw API_MISUSE;
}
return new CResult({value: value});
}
export function ui_dialog({
request,
title,
message="",
choices = [],
returnValue,
use_native=false,
width,
height
}) {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
json_check_type({type: Array, data: choices, should_throw: true});
json_check_type({type: "string", data: request, should_throw: true});
json_check_type({type: "string", data: title, should_throw: true});
if (!json_check_type({type: "string", data: message}) &&
!json_check_type({type: globalThis.HTMLElement, data: message})) {
throw API_TYPE_VIOLATION;
}
const id = title.replace(/\s/g, '');
switch (request) {
case DIALOG_REQUEST.Alert:
return new Promise((resolve) => {
if (use_native) {
globalThis.alert(data);
resolve(new CResult());
} else {
const content = `
<div class="codemelted-dialog-content">
<div>${message}</div>
<div class="codemelted-align-center">
<button id="${id}OK">OK</button>
</div>
</div>
`;
setTimeout(() => {
const dlg = globalThis.document.getElementById(id);
dlg.onclose = () => {
resolve(new CResult());
}
const closeBtn = globalThis.document.getElementById(
`${id}CloseDialog`
);
closeBtn.onclick = () => {
CDialog.close(id);
resolve(new CResult());
};
const ok = globalThis.document.getElementById(`${id}OK`);
ok.onclick = () => {
CDialog.close(id);
resolve(new CResult());
};
});
CDialog.show("💬", id, title, content, width, height);
}
});
case DIALOG_REQUEST.Choose:
return new Promise((resolve) => {
const selectOptions = [];
choices.forEach((e) => {
selectOptions.push(`<option value="${e}">${e}</option>`);
});
const content = `
<div class="codemelted-dialog-content">
<div>${message}:</div>
<select id="${id}Select">
${selectOptions.toString()}
</select>
<div class="codemelted-align-center">
<button id="${id}OK">OK</button>
<button id="${id}Cancel">Cancel</button>
</div>
</div>
`;
setTimeout(() => {
const dlg = globalThis.document.getElementById(id);
dlg.onclose = () => {
resolve(new CResult());
}
const closeBtn = globalThis.document.getElementById(
`${id}CloseDialog`
);
closeBtn.onclick = () => {
CDialog.close(id, null);
resolve(new CResult());
};
const cancel = globalThis.document.getElementById(`${id}Cancel`);
cancel.onclick = () => {
CDialog.close(id, null);
resolve(new CResult());
};
const cmbSelect = globalThis.document.getElementById(
`${id}Select`);
const ok = globalThis.document.getElementById(`${id}OK`);
ok.onclick = () => {
CDialog.close(id, cmbSelect.value);
const rtnval = CDialog.returnValue
? CDialog.returnValue
: "";
resolve(new CResult({value: rtnval}));
};
});
CDialog.show("🤔", id, title, content, width, height);
});
case DIALOG_REQUEST.Close:
return new Promise((resolve) => {
CDialog.close(id, returnValue);
resolve(new CResult());
});
case DIALOG_REQUEST.Confirm:
return new Promise((resolve) => {
if (use_native) {
let answer = globalThis.confirm(data);
resolve(new CResult({value: answer}))
} else {
const content = `
<div class="codemelted-dialog-content">
<div>${message}</div>
<div class="codemelted-align-center">
<button id="${id}OK">OK</button>
<button id="${id}Cancel">Cancel</button>
</div>
</div>
`;
setTimeout(() => {
const dlg = globalThis.document.getElementById(id);
dlg.onclose = () => {
resolve(new CResult());
}
const closeBtn = globalThis.document.getElementById(
`${id}CloseDialog`
);
closeBtn.onclick = () => {
CDialog.close(id, false);
resolve(new CResult({value: CDialog.returnValue}));
};
const ok = globalThis.document.getElementById(`${id}OK`);
ok.onclick = () => {
CDialog.close(id, true);
resolve(new CResult({value: CDialog.returnValue}));
};
const cancel = globalThis.document.getElementById(`${id}Cancel`);
cancel.onclick = () => {
CDialog.close(id, false);
resolve(new CResult({value: CDialog.returnValue}));
};
});
CDialog.show("🙋♂️", id, title, content, width, height);
}
});
case DIALOG_REQUEST.Loading:
return new Promise((resolve) => {
const content = `
<div class="codemelted-dialog-content">
<div>${message}</div>
<div id="${id}-${request}"></div>
</div>
`;
setTimeout(() => {
const txtProcessing = globalThis.document.getElementById(
`${id}-${request}`
);
let x = 0;
const timerId = setInterval(() => {
let dots = "";
if (x === 0) {
dots = " .";
} else if (x === 1) {
dots = " . .";
} else if (x == 2) {
dots = " . . .";
x = -1;
}
txtProcessing.innerHTML = `Processing${dots}`;
x += 1;
}, 500);
const dlg = globalThis.document.getElementById(id);
dlg.onclose = () => {
resolve(new CResult({value: CDialog.returnValue}));
clearInterval(timerId);
}
});
CDialog.show("⏳", id, title, content, width, height);
});
case DIALOG_REQUEST.Prompt:
return new Promise((resolve) => {
if (use_native) {
let answer = globalThis.prompt(data);
resolve(new CResult({value: answer}));
} else {
const content = `
<div class="codemelted-dialog-content">
<div>${message}:</div>
<input id="${id}Text" type="text" />
<div class="codemelted-align-center">
<button id="${id}OK">OK</button>
<button id="${id}Cancel">Cancel</button>
</div>
</div>
`;
setTimeout(() => {
const dlg = globalThis.document.getElementById(id);
dlg.onclose = () => {
resolve(new CResult());
}
const closeBtn = globalThis.document.getElementById(
`${id}CloseDialog`
);
closeBtn.onclick = () => {
CDialog.close(id, null);
resolve(new CResult({value: CDialog.returnValue}));
};
const txtField = globalThis.document.getElementById(`${id}Text`);
const ok = globalThis.document.getElementById(`${id}OK`);
ok.onclick = () => {
CDialog.close(id, txtField.value);
const rtnval = CDialog.returnValue
? CDialog.returnValue
: "";
resolve(new CResult({value: rtnval}));
};
const cancel = globalThis.document.getElementById(`${id}Cancel`);
cancel.onclick = () => {
CDialog.close(id, null);
resolve(new CResult({value: CDialog.returnValue}));
};
});
CDialog.show("🤨", id, title, content, width, height);
}
});
default:
throw API_MISUSE;
}
}
export function ui_is(request) {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
switch (request) {
case IS_REQUEST.PWA:
return globalThis.matchMedia("(display-mode: standalone)").matches;
case IS_REQUEST.SecureContext:
return globalThis.isSecureContext;
case IS_REQUEST.TouchEnabled:
return globalThis.navigator.maxTouchPoints > 0;
default:
throw API_MISUSE;
}
}
export function ui_open({
schema,
popup_window = false,
url,
mailto = [],
cc = [],
bcc = [],
subject = "",
body = "",
target = TARGET_TYPE.Self,
width=900,
height=600
}) {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
json_check_type({type: "boolean", data: popup_window, should_throw: true});
json_check_type({type: "string", data: target, should_throw: true});
json_check_type({type: "number", data: width, should_throw: true});
json_check_type({type: "number", data: height, should_throw: true});
json_check_type({type: Array, data: mailto, should_throw: true});
json_check_type({type: Array, data: cc, should_throw: true});
json_check_type({type: Array, data: bcc, should_throw: true});
json_check_type({type: "string", data: subject, should_throw: true});
json_check_type({type: "string", data: body, should_throw: true});
let urlToLaunch = schema;
if (schema === "file:" ||
schema === "http://" ||
schema === "https://" ||
schema === "sms:" ||
schema === "tel:") {
json_check_type({type: "string", data: url, should_throw: true});
urlToLaunch += url;
} else if (schema === "mailto:") {
if (url) {
json_check_type({type: "string", data: url, should_throw: true});
urlToLaunch += url;
} else {
if (mailto.length > 0) {
mailto.forEach((addr) => {
urlToLaunch += `${addr};`;
});
urlToLaunch.substring(0, urlToLaunch.length - 1);
}
let delimiter = "?";
if (cc.length > 0) {
urlToLaunch += `${delimiter}cc=`;
delimiter = "&";
cc.forEach((addr) => {
urlToLaunch += `${addr};`;
});
urlToLaunch.substring(0, urlToLaunch.length - 1);
}
if (bcc.length > 0) {
urlToLaunch += `${delimiter}bcc=`;
delimiter = "&";
bcc.forEach((addr) => {
urlToLaunch += `${addr};`;
});
urlToLaunch.substring(0, urlToLaunch.length - 1);
}
if (subject.trim().length > 0) {
urlToLaunch += `${delimiter}subject=${subject.trim()}`;
delimiter = "&";
}
if (body.trim().length > 0) {
urlToLaunch += `${delimiter}body=${body.trim()}`;
delimiter = "&";
}
}
} else {
throw API_MISUSE;
}
if (popup_window) {
let top = (ui_screen(SCREEN_REQUEST.Height) - height) / 2;
let left = (ui_screen(SCREEN_REQUEST.Width) - width) / 2;
let settings = `toolbar=no, location=no, ` +
`directories=no, status=no, menubar=no, ` +
`scrollbars=no, resizable=yes, copyhistory=no, ` +
`width=${width}, height=${height}, top=${top}, left=${left}`;
return globalThis.open(urlToLaunch, "_blank", settings);
}
return globalThis.open(urlToLaunch, target);
}
export function ui_screen(request) {
if (!runtime_is_browser()) {
throw API_UNSUPPORTED_RUNTIME;
}
switch (request) {
case SCREEN_REQUEST.AvailableHeight:
return globalThis.screen.availHeight;
case SCREEN_REQUEST.AvailableWidth:
return globalThis.screen.availWidth;
case SCREEN_REQUEST.ColorDepth:
return globalThis.screen.colorDepth;
case SCREEN_REQUEST.DevicePixelRatio:
return globalThis.devicePixelRatio;
case SCREEN_REQUEST.Height:
return globalThis.screen.height;
case SCREEN_REQUEST.InnerHeight:
return globalThis.innerHeight;
case SCREEN_REQUEST.InnerWidth:
return globalThis.innerWidth;
case SCREEN_REQUEST.OuterHeight:
return globalThis.outerHeight;
case SCREEN_REQUEST.OuterWidth:
return globalThis.outerWidth;
case SCREEN_REQUEST.PixelDepth:
return globalThis.screen.pixelDepth;
case SCREEN_REQUEST.ScreenLeft:
return globalThis.screenLeft;
case SCREEN_REQUEST.ScreenOrientationAngle:
return globalThis.screen.orientation.angle;
case SCREEN_REQUEST.ScreenOrientationType:
return globalThis.screen.orientation.type;
case SCREEN_REQUEST.ScreenTop:
return globalThis.screenTop;
case SCREEN_REQUEST.ScreenX:
return globalThis.screenX
case SCREEN_REQUEST.ScreenY:
return globalThis.screenY
case SCREEN_REQUEST.ScrollX:
return globalThis.scrollX;
case SCREEN_REQUEST.ScrollY:
return globalThis.scrollY;
case SCREEN_REQUEST.Width:
return globalThis.screen.width;
default:
throw API_MISUSE;
}
}
export function ui_widget({request, data}) {
if (!if_def("HTMLElement")) {
throw API_UNSUPPORTED_RUNTIME;
}
if (request === WIDGET_REQUEST.CssVariable) {
json_check_type({type: "string", data: data, should_throw: true});
let cs = globalThis.window.getComputedStyle(
globalThis.document.documentElement
);
return cs.getPropertyValue(data);
} else if (request === WIDGET_REQUEST.Define) {
const get_css_var = (v) => {
let css_var = ui_widget({
request: WIDGET_REQUEST.CssVariable,
data: v
}) ?? "";
return css_var.length > 0 ? css_var : v;
}
globalThis.customElements.define("codemelted-ui-app-bar",
class extends globalThis.HTMLElement {
connectedCallback() {
let type = this.getAttribute("cm_type");
if (type === "header") {
this.style.top = "0";
} else if (type === "footer") {
this.style.bottom = "0";
} else {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-app-bar requires cm_type attribute"
});
throw API_MISUSE;
}
this.style.left = "0";
this.style.right = "0";
this.style.position = "fixed";
let attributes = [
"cm_bg_color",
"cm_border",
"cm_fg_color",
"cm_height",
"cm_z_index",
];
attributes.forEach((attr_val, index) => {
let attr = this.getAttribute(attr_val)
if (!attr) {
logger_log({
level: LOGGER.error,
data: `codemelted-ui-app-bar requires ${attr_val}`
});
throw API_MISUSE;
}
let css_value = get_css_var(attr);
switch (index) {
case 0:
this.style.backgroundColor = css_value.length > 0
? css_value : attr;
break;
case 1:
this.style.border = css_value.length > 0
? css_value : attr;
break;
case 2:
this.style.color = css_value.length > 0
? css_value : attr;
break;
case 3:
this.style.height = css_value.length > 0
? css_value : attr;
let height_margin = parseInt(
this.style.height.replaceAll("px", "")
) + 1;
if (type === "header") {
globalThis.document.body.style.marginTop =
`${height_margin}px`;
} else {
globalThis.document.body.style.marginBottom =
`${height_margin}px`;
}
break;
case 4:
this.style.zIndex = css_value.length > 0
? css_value : attr;
break;
default:
logger_log({
level: LOGGER.error,
data: `codemelted-ui-app-bar unknown ${attr_val}`
});
throw API_NOT_IMPLEMENTED;
}
});
}
constructor() { super(); }
}
);
globalThis.customElements.define("codemelted-ui-app-sheet",
class extends globalThis.HTMLElement {
static observedAttributes = [
"cm_title",
"cm_img_src",
"cm_img_type"
];
#header;
#render_header() {
let cm_title = this.getAttribute("cm_title");
let cm_img_type = this.getAttribute("cm_img_type");
let cm_img_src = this.getAttribute("cm_img_src");
let img_src = "";
if (!cm_title) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-app-sheet cm_title attribute is required"
});
throw API_MISUSE;
} else if (!cm_img_src) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-app-sheet cm_img_src attribute is required"
});
throw API_MISUSE;
} else if (cm_img_type === "emoji") {
img_src = `<h2>${cm_img_src}</h2>`;
} else if (cm_img_type === "img") {
img_src = `<img style="height: 49px;" src="${cm_img_src} />"`;
} else {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-app-sheet cm_img_type attribute valid " +
"values are 'emoji' / 'img'"
});
throw API_MISUSE;
}
let btn_id = `id${globalThis.window.crypto.randomUUID()}`;
let btn_style = "border: none; cursor: pointer; " +
"background-color: transparent;"
this.#header.innerHTML=`
${img_src}
<h2>${cm_title}</h2>
<button id=${btn_id} style="${btn_style}">❌</button>
`;
setTimeout(() => {
let btn = globalThis.document.getElementById(btn_id);
if (!btn) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-app-sheet close button could " +
"not be setup."
})
throw API_NOT_IMPLEMENTED;
}
btn.onclick = (evt) => {
this.hide();
};
});
}
connectedCallback() {
this.style.position = "fixed";
this.style.flexDirection = "column";
this.style.top = "0";
this.style.left = "0";
this.style.right = "0";
this.style.bottom = "0";
this.style.display = "none";
let attributes = [
"cm_bg_color",
"cm_fg_color",
"cm_border",
"cm_z_index",
];
attributes.forEach((attr, i) => {
let attr_val = this.getAttribute(attr);
if (!attr_val) {
logger_log({
level: LOGGER.error,
data: `codemelted-ui-app-sheet ${attr} required`
});
throw API_MISUSE;
}
let css_val = get_css_var(attr_val);
let css = css_val.length > 0 ? css_val : attr_val;
switch (i) {
case 0:
this.style.backgroundColor = css;
break;
case 1:
this.style.color = css;
break;
case 2:
this.#header.style.border = css;
break;
case 3:
this.style.zIndex = css;
break;
default:
console.error(
`codemelted-ui-app-sheet ${attr} not implemented.`
);
throw API_NOT_IMPLEMENTED;
}
});
if (this.firstChild?.nextSibling instanceof globalThis.HTMLElement) {
this.firstChild.nextSibling.style.flexGrow = "1";
}
this.#render_header();
this.insertBefore(this.#header, this.firstChild);
}
constructor() {
super();
this.#header = globalThis.document.createElement("header");
this.#header.style.display = "grid";
this.#header.style.height = "50px";
this.#header.style.display = "grid";
this.#header.style.alignSelf = "center";
this.#header.style.alignContent = "center";
this.#header.style.alignItems = "center";
this.#header.style.textAlign = "center";
this.#header.style.width = "100%";
this.#header.style.gridTemplateColumns = "75px auto 75px";
}
show() {
this.#render_header();
this.style.display = "flex";
}
hide() { this.style.display = "none"; }
}
);
globalThis.customElements.define("codemelted-ui-grid-layout",
class extends globalThis.HTMLElement {
connectedCallback() {
this.style.display = "grid";
let type = this.getAttribute("cm_type");
let grid_template = this.getAttribute("cm_grid_template");
if (!type) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-grid-layout expects cm_type attribute"
});
throw API_MISUSE;
} else if (!grid_template) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-grid-layout expects cm_grid_template " +
"attribute"
});
throw API_MISUSE;
}
if (type === "columns") {
let css_value = get_css_var(grid_template);
this.style.gridTemplateColumns = css_value.length > 0
? css_value : grid_template;
} else if (type === "rows") {
let css_value = get_css_var(grid_template);
this.style.gridTemplateRows = css_value.length > 0
? css_value : grid_template;
} else {
console.error(
"codemelted-ui-grid-layout cm_type must be 'rows' / 'columns'"
);
throw API_MISUSE;
}
}
constructor() {
super();
}
}
);
globalThis.customElements.define("codemelted-ui-button",
class extends globalThis.HTMLElement {
connectedCallback() {
let btn = globalThis.document.createElement("button");
this.title = this.getAttribute("cm_tooltip") ?? "";
let label = this.getAttribute("cm_label");
let img_src = this.getAttribute("cm_img_src");
if (!label && !img_src) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-button expects at least one of the " +
"cm_label / cm_img attribute"
});
throw API_MISUSE;
} else if (label && !img_src) {
btn.innerHTML = label;
} else if (!label && img_src) {
btn.innerHTML = `<img src=${img_src} />`
} else {
btn.innerHTML = `<img src=${img_src} /> ${label}`;
}
btn.style.display = "block";
btn.style.cursor = "pointer";
this.appendChild(btn);
}
constructor() { super(); }
}
);
globalThis.customElements.define("codemelted-ui-combobox",
class extends globalThis.HTMLElement {
connectedCallback() {
let select = globalThis.document.createElement("select");
select.style.cursor = "pointer";
let cm_size = parseInt(this.getAttribute("cm_size") ?? "0");
if (cm_size === 0) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-combobox requires cm_size attribute"
});
throw API_MISUSE;
}
for (let i = 0; i < cm_size; i++) {
let cm_option = this.getAttribute(`cm_option${i+1}`) ?? "";
let options = cm_option.split(",");
if (!options || options.length != 2) {
logger_log({
level: LOGGER.error,
data: `codemelted-ui-combobox cm_option${i+1} not ` +
"valid 'option,value' format"
});
throw API_MISUSE;
}
let option = globalThis.document.createElement("option");
option.text = options[0];
option.value = options[1];
select.appendChild(option);
}
this.appendChild(select);
}
constructor() { super(); }
}
);
globalThis.customElements.define("codemelted-ui-expansion-tile",
class extends globalThis.HTMLElement {
connectedCallback() {
let cm_label = this.getAttribute("cm_label");
if (!cm_label) {
logger_log({
level: LOGGER.error,
data: "codemelted-ui-expansion-tile requires cm_label attribute"
});
throw API_MISUSE;
}
let child_nodes = Array.from(this.children);
child_nodes.forEach((el) => {
if (el.tagName.toLowerCase() === "codemelted-ui-button") {
el.innerHTML = "";
} else if (el.tagName.toLowerCase() === "codemelted-ui-combobox") {
el.innerHTML = "";
}
});
this.innerHTML = `
<style>
.cm_exp_tile_summary {
user-select: none;
text-align: left;
font-weight: bold;
cursor: pointer;
padding: 5px;
}
.cm_exp_tile_summary::before {
content: '+';
margin-right: 10px;
display: inline-block;
transition: transform 0.3s;
}
.cm_exp_tile_details[open] summary::before {
content: '-';
transform: rotate(0deg);
}
</style>
<details class="cm_exp_tile_details">
<summary class="cm_exp_tile_summary">${cm_label}</summary>
</details>
`;
child_nodes.forEach((el) => {
this.children[1].appendChild(el);
});
}
constructor() { super(); }
}
);
} else if (request === WIDGET_REQUEST.ElementById) {
json_check_type({type: "string", data: data, should_throw: true});
let widget = globalThis.document.getElementById(data);
if (!widget) {
logger_log({
level: LOGGER.error,
data: `codemelted::ui_widget() did not find ${data} element by id`
});
throw API_MISUSE;
}
return widget;
} else {
logger_log({
level: LOGGER.error,
data: `codemelted::ui_widget() unknown ${request}`
});
throw API_MISUSE;
}
}