741 lines
16 KiB
JavaScript
741 lines
16 KiB
JavaScript
var bricks = window.bricks || {};
|
|
bricks.bug = false;
|
|
|
|
/*
|
|
We use ResizeObserver to implements dom object resize event
|
|
*/
|
|
bricks.resize_observer = new ResizeObserver(entries => {
|
|
for (let entry of entries){
|
|
const cr = entry.contentRect;
|
|
const ele = entry.target;
|
|
const w = ele.bricks_widget;
|
|
// console.log('size=', cr, 'element=', ele, w);
|
|
if (w){
|
|
w.dispatch('element_resize', cr);
|
|
}
|
|
}
|
|
});
|
|
|
|
/* MutationObserver for add to DOM or remove from DOM
|
|
event:
|
|
domon: add to dom
|
|
domoff: remove from dom
|
|
*/
|
|
bricks.dom_on_off_observer=new MutationObserver((mutations)=>{
|
|
for (let m of mutations) {
|
|
for (let n of m.removedNodes) {
|
|
if (n.bricks_widget){
|
|
var w = n.bricks_widget;
|
|
w.dispatch('domoff');
|
|
}
|
|
}
|
|
for (let n of m.addedNodes) {
|
|
if (n.bricks_widget){
|
|
var w = n.bricks_widget;
|
|
w.dispatch('domon');
|
|
}
|
|
}
|
|
}
|
|
});
|
|
bricks.resize_observer.observe(document.body,
|
|
{ childList: true, subtree: true });
|
|
bricks.dom_on_off_observer.observe(document.body,
|
|
{ childList: true, subtree: true });
|
|
function addParamsToUrl(url, params, widget) {
|
|
const urlObj = new URL(url, window.baseURI); // 处理相对和绝对路径
|
|
Object.keys(params).forEach(key => {
|
|
urlObj.searchParams.set(key, params[key]);
|
|
});
|
|
return urlObj.toString();
|
|
}
|
|
function isString(value) {
|
|
return typeof value === 'string' || value instanceof String;
|
|
}
|
|
|
|
function parseRGB(colorStr) {
|
|
const match = colorStr.match(/^rgb\s*\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/);
|
|
if (!match) return null;
|
|
const [, r, g, b] = match.map(Number);
|
|
return { r, g, b };
|
|
}
|
|
|
|
function parseRGBA(colorStr) {
|
|
const match = colorStr.match(/^rgba?\s*\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)$/);
|
|
if (!match) return null;
|
|
const [, r, g, b, a] = match;
|
|
return { r: +r, g: +g, b: +b, a: a !== undefined ? +a : 1 };
|
|
}
|
|
|
|
async function streamResponseJson(response, onJson) {
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder("utf-8");
|
|
let buffer = "";
|
|
while (true) {
|
|
const { value, done } = await reader.read();
|
|
if (done) break;
|
|
buffer += decoder.decode(value, { stream: true });
|
|
// 按换行切分 NDJSON
|
|
let lines = buffer.split("\n");
|
|
buffer = lines.pop(); // 可能是不完整的一行,留到下一轮
|
|
for (const line of lines) {
|
|
if (line.trim()) {
|
|
try {
|
|
const json = JSON.parse(line);
|
|
onJson(json); // 👈 回调处理每个 JSON 对象
|
|
} catch (err) {
|
|
console.warn("Failed to parse JSON line:", line);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 处理最后残留的一行
|
|
if (buffer.trim()) {
|
|
try {
|
|
const json = JSON.parse(buffer);
|
|
onJson(json);
|
|
} catch (err) {
|
|
console.warn("Failed to parse trailing JSON line:", buffer);
|
|
}
|
|
}
|
|
}
|
|
|
|
function base64_to_url(base64, mimeType = "audio/wav") {
|
|
const binary = atob(base64); // 解码 Base64 成 binary 字符串
|
|
const len = binary.length;
|
|
const bytes = new Uint8Array(len);
|
|
|
|
for (let i = 0; i < len; i++) {
|
|
bytes[i] = binary.charCodeAt(i);
|
|
}
|
|
|
|
const blob = new Blob([bytes], { type: mimeType });
|
|
const url = URL.createObjectURL(blob);
|
|
return url;
|
|
}
|
|
|
|
function blobToBase64(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
|
|
reader.onloadend = () => {
|
|
resolve(reader.result); // This will be a base64 string prefixed with "data:*/*;base64,"
|
|
};
|
|
|
|
reader.onerror = reject;
|
|
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
|
|
bricks.formdata_copy = function(fd){
|
|
var cfd = new FormData();
|
|
|
|
// 遍历 originalFormData 中的所有条目并添加到 clonedFormData 中
|
|
for (var pair of fd.entries()) {
|
|
cfd.append(pair[0], pair[1]);
|
|
}
|
|
return cfd;
|
|
}
|
|
|
|
bricks.map = function(data_source, mapping, need_others){
|
|
ret = {};
|
|
Object.entries(data_source).forEach(([key, value]) => {
|
|
if (mapping.hasOwnProperty(key)){
|
|
ret[mapping[key]] = data_source[key];
|
|
} else if (need_others){
|
|
ret[key] = data_source[key];
|
|
}
|
|
});
|
|
return ret;
|
|
}
|
|
|
|
bricks.relocate_by_eventpos = function(event, widget){
|
|
var ex,ey;
|
|
var x,y;
|
|
var xsize = bricks.Body.dom_element.clientWidth;
|
|
var ysize = bricks.Body.dom_element.clientHeight;
|
|
ex = event.clientX;
|
|
ey = event.clientY;
|
|
var mxs = widget.dom_element.offsetWidth;
|
|
var mys = widget.dom_element.offsetHeight;
|
|
if (ex < (xsize / 2)) {
|
|
x = ex + bricks.app.charsize;
|
|
} else {
|
|
x = ex - mxs - bricks.app.charsize;
|
|
}
|
|
if (ey < (ysize / 2)) {
|
|
y = ey + bricks.app.charsize;
|
|
} else {
|
|
y = ey - mys - bricks.app.charsize;
|
|
}
|
|
widget.set_style('left', x + 'px');
|
|
widget.set_style('top', y + 'px');
|
|
}
|
|
|
|
var formdata2object = function(formdata){
|
|
let result = {};
|
|
formdata.forEach((value, key) => {
|
|
result[key] = value;
|
|
});
|
|
return result;
|
|
}
|
|
|
|
var inputdata2dic = function(data){
|
|
try {
|
|
var d = {}
|
|
for (let k of data.keys()){
|
|
var x = data.get(k);
|
|
if (k == 'prompt'){
|
|
x = bricks.escapeSpecialChars(x);
|
|
}
|
|
d[k] = x;
|
|
}
|
|
return d;
|
|
} catch (e){
|
|
return data;
|
|
}
|
|
}
|
|
bricks.delete_null_values = function(obj) {
|
|
for (let key in obj) {
|
|
if (obj[key] === null) {
|
|
delete obj[key];
|
|
}
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
bricks.is_empty = function(obj){
|
|
if (obj === null) return true;
|
|
return JSON.stringify(obj) === '{}';
|
|
}
|
|
bricks.serverdebug = async function(message){
|
|
var jc = new bricks.HttpJson();
|
|
await jc.post(url='/debug', {params:{
|
|
message:message
|
|
}});
|
|
return;
|
|
}
|
|
bricks.debug = function(...args){
|
|
if (! bricks.bug){
|
|
return;
|
|
}
|
|
if (bricks.bug == 'server'){
|
|
var message = args.join(" ");
|
|
f = bricks.serverdebug.bind(null, message);
|
|
schedule_once(f, 0.1);
|
|
return;
|
|
}
|
|
var callInfo;
|
|
try {
|
|
throw new Error();
|
|
} catch (e) {
|
|
try {
|
|
callInfo = e.stack.split('\n')[2].trim();
|
|
} catch (e1) {
|
|
callInfo = e.toString();
|
|
}
|
|
}
|
|
console.log(callInfo, ...args);
|
|
}
|
|
|
|
bricks.is_mobile = function(){
|
|
var userAgent = navigator.userAgent;
|
|
if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
|
|
return true;
|
|
}
|
|
if (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) {
|
|
return true;
|
|
}
|
|
if (window.innerWidth <= 768 && window.innerHeight <= 1024) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
class _TypeIcons {
|
|
constructor(){
|
|
this.kv = {}
|
|
}
|
|
get(n, defaultvalue){
|
|
return objget(this.kv, n, defaultvalue);
|
|
}
|
|
register(n, icon){
|
|
this.kv[n] = icon;
|
|
}
|
|
}
|
|
|
|
TypeIcons = new _TypeIcons();
|
|
|
|
/**
|
|
* Current Script Path
|
|
*
|
|
* Get the dir path to the currently executing script file
|
|
* which is always the last one in the scripts array with
|
|
* an [src] attr
|
|
*/
|
|
var currentScriptPath = function () {
|
|
var currentScript;
|
|
if (document.currentScript){
|
|
currentScript = document.currentScript.src;
|
|
} else {
|
|
bricks.debug('has not currentScriot');
|
|
var scripts = document.querySelectorAll( 'script[src]' );
|
|
if (scripts.length < 1){
|
|
return null;
|
|
}
|
|
currentScript = scripts[ scripts.length - 1 ].src;
|
|
}
|
|
var currentScriptChunks = currentScript.split( '/' );
|
|
var currentScriptFile = currentScriptChunks[ currentScriptChunks.length - 1 ];
|
|
return currentScript.replace( currentScriptFile, '' );
|
|
}
|
|
|
|
bricks.path = currentScriptPath();
|
|
|
|
var bricks_resource = function(name){
|
|
return bricks.path + name;
|
|
}
|
|
|
|
/**
|
|
* Finds all elements in the entire page matching `selector`, even if they are in shadowRoots.
|
|
* Just like `querySelectorAll`, but automatically expand on all child `shadowRoot` elements.
|
|
* @see https://stackoverflow.com/a/71692555/2228771
|
|
*/
|
|
function querySelectorAllShadows(selector, el = document.body) {
|
|
// recurse on childShadows
|
|
const childShadows = Array.from(el.querySelectorAll('*')).
|
|
map(el => el.shadowRoot).filter(Boolean);
|
|
|
|
bricks.debug('[querySelectorAllShadows]', selector, el, `(${childShadows.length} shadowRoots)`);
|
|
|
|
const childResults = childShadows.map(child => querySelectorAllShadows(selector, child));
|
|
|
|
// fuse all results into singular, flat array
|
|
const result = Array.from(el.querySelectorAll(selector));
|
|
return result.concat(childResults).flat();
|
|
}
|
|
|
|
var schedule_once = function(f, t){
|
|
/* f: function
|
|
t:time in second unit
|
|
*/
|
|
t = t * 1000
|
|
window.setTimeout(f, t);
|
|
}
|
|
|
|
var schedule_interval = function(f, t){
|
|
var mf = function(func, t){
|
|
console.log('arguments:', func, t);
|
|
func();
|
|
schedule_once(mf.bind(func, t), t);
|
|
}
|
|
schedule_once(mf.bind(f,t), t);
|
|
}
|
|
|
|
var debug = function(){
|
|
bricks.debug(...arguments);
|
|
}
|
|
|
|
var import_cache = new Map()
|
|
|
|
var import_css = async function(url){
|
|
if (objget(import_cache, url)===1) return;
|
|
var result = await (bricks.tget(url));
|
|
debug('import_css():tget() return', result);
|
|
var s = document.createElement('style');
|
|
s.setAttribute('type', 'text/javascript');
|
|
s.innerHTML = result;
|
|
document.getElementsByTagName("head")[0].appendChild(s);
|
|
import_cache.set(url, 1);
|
|
}
|
|
|
|
var import_js = async function(url){
|
|
if (objget(import_cache, url)===1) return;
|
|
// var result = await (bricks.tget(url));
|
|
// debug('import_js():tget() return', url, result);
|
|
var s = document.createElement('script');
|
|
s.setAttribute('type', 'text/javascript');
|
|
s.src=url;
|
|
// s.innerHTML = result;
|
|
document.body.appendChild(s);
|
|
import_cache.set(url, 1);
|
|
|
|
}
|
|
|
|
bricks.extend = function(d, s){
|
|
for (var p in s){
|
|
if (! s.hasOwnProperty(p)){
|
|
continue;
|
|
}
|
|
if (d[p] && (typeof(d[p]) == 'object')
|
|
&& (d[p].toString() == '[object Object]') && s[p]){
|
|
bricks.extend(d[p], s[p]);
|
|
} else {
|
|
d[p] = s[p];
|
|
}
|
|
}
|
|
return d;
|
|
}
|
|
|
|
var objget = function(obj, key, defval){
|
|
if (obj.hasOwnProperty(key)){
|
|
return obj[key];
|
|
}
|
|
return defval;
|
|
}
|
|
|
|
bricks.obj_fmtstr = function(obj, fmt){
|
|
/* fmt like
|
|
'my name is ${name}, ${age=}'
|
|
'${name:}, ${age=}'
|
|
*/
|
|
var s = fmt;
|
|
s = s.replace(/\${(\w+)([:=]*)}/g, (k, key, op) => {
|
|
if (obj.hasOwnProperty(key)){
|
|
if (op == ''){
|
|
return obj[key];
|
|
} else {
|
|
return key + op + obj[key];
|
|
}
|
|
}
|
|
return ''
|
|
})
|
|
return s;
|
|
}
|
|
|
|
var archor_at = function(archor){
|
|
/* archor maybe one of the:
|
|
"tl", "tc", "tr",
|
|
"cl", "cc", "cr",
|
|
"bl", "bc", "br"
|
|
*/
|
|
if (! archor)
|
|
archor = 'cc';
|
|
var v = archor[0];
|
|
var h = archor[1];
|
|
var y = "0%";
|
|
switch(v){
|
|
case 't':
|
|
y = "0%";
|
|
break;
|
|
case 'b':
|
|
y = '100%';
|
|
break;
|
|
case 'c':
|
|
y = '50%';
|
|
break;
|
|
default:
|
|
y = '50%';
|
|
break;
|
|
}
|
|
var x = "0%";
|
|
switch(h){
|
|
case 'l':
|
|
x = "0%";
|
|
break;
|
|
case 'r':
|
|
x = '100%';
|
|
break;
|
|
case 'c':
|
|
x = '50%';
|
|
break;
|
|
default:
|
|
x = '50%';
|
|
break;
|
|
}
|
|
return {
|
|
x:x,
|
|
y:y,
|
|
top:y,
|
|
left:x
|
|
}
|
|
}
|
|
|
|
var archorize = function(ele,archor){
|
|
var lt = archor_at(archor);
|
|
ele.style.top = lt.top;
|
|
ele.style.left = lt.left;
|
|
var o = {
|
|
'x':lt.x,
|
|
'y':lt.y
|
|
}
|
|
var tsf = bricks.obj_fmtstr(o, 'translateY(-${y}) translateX(-${x})');
|
|
ele.style.transform = tsf;
|
|
ele.style.position = "absolute";
|
|
}
|
|
|
|
Array.prototype.insert = function ( index, ...items ) {
|
|
this.splice( index, 0, ...items );
|
|
};
|
|
|
|
Array.prototype.remove = function(item){
|
|
var idx = this.indexOf(item);
|
|
if (idx >= 0){
|
|
this.splice(idx, 1);
|
|
}
|
|
return this;
|
|
}
|
|
|
|
function removeArrayItems(array, itemsToRemove) {
|
|
return array.filter(item => !itemsToRemove.includes(item));
|
|
}
|
|
|
|
bricks.absurl = function(url, widget){
|
|
if (url.startsWith('http://') || url.startsWith('https://')){
|
|
return url;
|
|
}
|
|
var base_uri = widget.baseURI;
|
|
if (!base_uri){
|
|
base_uri = bricks.Body.baseURI;
|
|
}
|
|
if (url.startsWith('/')){
|
|
base_uri = bricks.Body.baseURI;
|
|
url = url.substring(1);
|
|
}
|
|
paths = base_uri.split('/');
|
|
delete paths[paths.length - 1];
|
|
var ret_url = paths.join('/') + url;
|
|
return ret_url;
|
|
}
|
|
|
|
var debug = function(...args){
|
|
bricks.debug(...args);
|
|
}
|
|
|
|
var convert2int = function(s){
|
|
if (typeof(s) == 'number') return s;
|
|
var s1 = s.match(/\d+/);
|
|
return parseInt(s1[0]);
|
|
}
|
|
|
|
function setCookie(name,value,days) {
|
|
var expires = "";
|
|
if (days) {
|
|
var date = new Date();
|
|
date.setTime(date.getTime() + (days*24*60*60*1000));
|
|
expires = "; expires=" + date.toUTCString();
|
|
}
|
|
document.cookie = name + "=" + (value || "") + expires + "; path=/";
|
|
}
|
|
function getCookie(name) {
|
|
var nameEQ = name + "=";
|
|
var ca = document.cookie.split(';');
|
|
for(var i=0;i < ca.length;i++) {
|
|
var c = ca[i];
|
|
while (c.charAt(0)==' ') c = c.substring(1,c.length);
|
|
if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
|
|
}
|
|
return null;
|
|
}
|
|
function eraseCookie(name) {
|
|
document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
|
}
|
|
|
|
var set_max_height = function(w1, w2){
|
|
var v1 = w1.dom_element.offsetHeight;
|
|
var v2 = w2.dom_element.offsetHeight;
|
|
var v = v1 - v2;
|
|
if (v < 0){
|
|
w1.set_height(w2.dom_element.offsetHeight);
|
|
} else if (v > 0) {
|
|
w2.set_height(w1.dom_element.offsetHeight);
|
|
}
|
|
}
|
|
var objcopy = function(obj){
|
|
var s = JSON.stringify(obj);
|
|
return JSON.parse(s);
|
|
}
|
|
|
|
bricks.set_stream_source = async function(target, url, params){
|
|
var widget;
|
|
if (typeof target == typeof ''){
|
|
widget = bricks.getWidgetById(target);
|
|
} else if (target instanceof bricks.JsWidget){
|
|
widget = target;
|
|
} else {
|
|
widget = await bricks.widgetBuild(target);
|
|
}
|
|
if (! widget){
|
|
bricks.debug('playResponseAudio():', target, 'can not found or build a widget');
|
|
return;
|
|
}
|
|
const mediaSource = new MediaSource();
|
|
mediaSource.addEventListener('sourceopen', handleSourceOpen);
|
|
widget.set_url(URL.createObjectURL(mediaSource));
|
|
function handleSourceOpen(){
|
|
const sourceBuffer = mediaSource.addSourceBuffer('audio/wav; codecs=1');
|
|
var ht = new bricks.HttpText();
|
|
ht.bricks_fetch(url, {params:params})
|
|
.then(response => response.body)
|
|
.then(body => {
|
|
const reader = body.getReader();
|
|
const read = () => {
|
|
reader.read().then(({ done, value }) => {
|
|
if (done) {
|
|
mediaSource.endOfStream();
|
|
return;
|
|
}
|
|
sourceBuffer.appendBuffer(value);
|
|
read();
|
|
}).catch(error => {
|
|
console.error('Error reading audio stream:', error);
|
|
});
|
|
};
|
|
read();
|
|
});
|
|
}
|
|
}
|
|
|
|
bricks.playResponseAudio = async function(response, target){
|
|
var widget = null;
|
|
if (response.status != 200){
|
|
bricks.debug('playResponseAudio(): response.status != 200', response.status);
|
|
return;
|
|
}
|
|
if (typeof target == typeof ''){
|
|
widget = bricks.getWidgetById(target);
|
|
} else {
|
|
widget = bricks.widgetBuild(target);
|
|
}
|
|
if (! widget){
|
|
bricks.debug('playResponseAudio():', target, 'can not found or build a widget');
|
|
return;
|
|
}
|
|
const blob = await response.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
widget.set_url(url);
|
|
widget.play();
|
|
}
|
|
|
|
bricks.widgetBuildWithData = async function(desc_tmpl, from_widget, data){
|
|
if (!desc_tmpl){
|
|
bricks.debug('bricks.widgetBuildWithData():data=', data, 'desc_tmpl=', desc_tmpl);
|
|
}
|
|
var s = bricks.obj_fmtstr(data, desc_tmpl);
|
|
var desc = JSON.parse(s);
|
|
var w = await bricks.widgetBuild(desc, from_widget);
|
|
if (! w){
|
|
bricks.debug(desc, 'widgetBuild() failed...........');
|
|
return;
|
|
}
|
|
w.row_data = data;
|
|
return w;
|
|
}
|
|
|
|
bricks.Observable = class {
|
|
constructor(owner, name, v){
|
|
this.owner = owner;
|
|
this.name = name;
|
|
this.value = v;
|
|
}
|
|
set(v){
|
|
var ov = this.value;
|
|
this.value = v;
|
|
if (this.value != ov){
|
|
this.owner.dispatch(this.name, v);
|
|
}
|
|
}
|
|
get(){
|
|
return this.v;
|
|
}
|
|
}
|
|
|
|
bricks.Queue = class {
|
|
constructor() {
|
|
this.items = [];
|
|
this._done = false;
|
|
}
|
|
|
|
// 添加元素到队列尾部
|
|
enqueue(element) {
|
|
this.items.push(element);
|
|
}
|
|
done(){
|
|
this._done = true;
|
|
}
|
|
is_done(){
|
|
return this._done;
|
|
}
|
|
|
|
// 移除队列的第一个元素并返回
|
|
dequeue() {
|
|
if (this.isEmpty()) {
|
|
return null;
|
|
}
|
|
return this.items.shift();
|
|
}
|
|
|
|
// 查看队列的第一个元素
|
|
peek() {
|
|
if (this.isEmpty()) {
|
|
return null;
|
|
}
|
|
return this.items[0];
|
|
}
|
|
|
|
// 检查队列是否为空
|
|
isEmpty() {
|
|
return this.items.length === 0;
|
|
}
|
|
|
|
// 获取队列的大小
|
|
size() {
|
|
return this.items.length;
|
|
}
|
|
|
|
// 清空队列
|
|
clear() {
|
|
this.items = [];
|
|
}
|
|
|
|
// 打印队列元素
|
|
print() {
|
|
console.log(this.items.toString());
|
|
}
|
|
}
|
|
|
|
function blobToBase64(blob) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onloadend = function() {
|
|
resolve(reader.result);
|
|
};
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(blob);
|
|
});
|
|
}
|
|
/*
|
|
opts = {
|
|
css:
|
|
id
|
|
}
|
|
*/
|
|
bricks.dom_create = function(tag, opts){
|
|
var e = document.createElement(tag);
|
|
if (opts.css){
|
|
var arr = css.split(' ');
|
|
arr.forEach(c =>{
|
|
e.classList.add(c);
|
|
});
|
|
}
|
|
if (opts.id){
|
|
e.id = opts.id;
|
|
}
|
|
return e;
|
|
}
|
|
bricks.element_from_html = function(html){
|
|
var e = document.createElement('div');
|
|
e.outerHTML = html;
|
|
return e;
|
|
}
|
|
/*
|
|
// 使用队列
|
|
const queue = new Queue();
|
|
queue.enqueue(1);
|
|
queue.enqueue(2);
|
|
queue.enqueue(3);
|
|
queue.print(); // 输出: 1,2,3
|
|
console.log(queue.dequeue()); // 输出: 1
|
|
queue.print(); // 输出: 2,3
|
|
*/
|