1function COM() 2{ 3 // v1.06 - PANE namespace + floating find navigation focus fix 4 /* 5 this.debugMe = function() 6 { 7 var o = apple2plus.DiskObj(); 8 oCOM.Download('dump.nib',o.diskBytes[0]) 9 } 10 */ 11 12 13 const hexLookupTable = new Array(256); 14 for (let i = 0; i < 256; i++) { 15 hexLookupTable[i] = i.toString(16).padStart(2, '0'); 16 } 17 18 const hex2tab=[ 19 "00","01","02","03","04","05","06","07","08","09","0A","0B","0C","0D","0E","0F","10","11","12","13","14","15","16","17","18","19","1A","1B","1C","1D","1E","1F", 20 "20","21","22","23","24","25","26","27","28","29","2A","2B","2C","2D","2E","2F","30","31","32","33","34","35","36","37","38","39","3A","3B","3C","3D","3E","3F", 21 "40","41","42","43","44","45","46","47","48","49","4A","4B","4C","4D","4E","4F","50","51","52","53","54","55","56","57","58","59","5A","5B","5C","5D","5E","5F", 22 "60","61","62","63","64","65","66","67","68","69","6A","6B","6C","6D","6E","6F","70","71","72","73","74","75","76","77","78","79","7A","7B","7C","7D","7E","7F", 23 "80","81","82","83","84","85","86","87","88","89","8A","8B","8C","8D","8E","8F","90","91","92","93","94","95","96","97","98","99","9A","9B","9C","9D","9E","9F", 24 "A0","A1","A2","A3","A4","A5","A6","A7","A8","A9","AA","AB","AC","AD","AE","AF","B0","B1","B2","B3","B4","B5","B6","B7","B8","B9","BA","BB","BC","BD","BE","BF", 25 "C0","C1","C2","C3","C4","C5","C6","C7","C8","C9","CA","CB","CC","CD","CE","CF","D0","D1","D2","D3","D4","D5","D6","D7","D8","D9","DA","DB","DC","DD","DE","DF", 26 "E0","E1","E2","E3","E4","E5","E6","E7","E8","E9","EA","EB","EC","ED","EE","EF","F0","F1","F2","F3","F4","F5","F6","F7","F8","F9","FA","FB","FC","FD","FE","FF"]; 27 28 this.hextab= ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F']; // TODO: remove from entire codebase 29 this.getHexByte = function(v) { return hex2tab[v&0xFF] } 30 this.getHexWord = function(v) { return hex2tab[v>>8] + hex2tab[v&0xFF] } 31 this.getHexMulti = function(v,m) 32 { 33 if(m === 2) return hex2tab[v & 0xFF]; 34 if(m === 4) return hex2tab[(v >> 8) & 0xFF] + hex2tab[v & 0xFF]; 35 return v.toString(16).padStart(m,"0").slice(-m).toUpperCase(); 36 } 37 this.getBinMulti = function(v,m) { return ("0".repeat(m)+v.toString(2)).slice(-m).toUpperCase() } 38 39 // Shared 6502 register/status formatter used by debugger views. 40 // Returns HTML because the status-register flag names are rendered as subscripts. 41 this.formatCpuStatusHTML = function(p) 42 { 43 p = Number(p) & 0xFF; 44 const names = ["n","v","-","b","d","i","z","c"]; 45 let out = "SR="; 46 for(let bit=7; bit>=0; bit--) 47 out += "<sub>"+names[7-bit]+"</sub>"+((p>>bit)&1); 48 return out; 49 } 50 51 this.formatCpuRegistersHTML = function(state,options) 52 { 53 state = state || {}; 54 options = options || {}; 55 const fields = []; 56 const hexByte = this.getHexByte.bind(this); 57 const hexWord = this.getHexWord.bind(this); 58 const dollar = options.dollarPrefix ? "$" : ""; 59 60 if(options.includePC && state.pc !== undefined) 61 fields.push("PC="+dollar+hexWord(Number(state.pc)&0xFFFF)); 62 if(state.a !== undefined) fields.push("A="+dollar+hexByte(Number(state.a)&0xFF)); 63 if(state.x !== undefined) fields.push("X="+dollar+hexByte(Number(state.x)&0xFF)); 64 if(state.y !== undefined) fields.push("Y="+dollar+hexByte(Number(state.y)&0xFF)); 65 if(state.sp !== undefined) fields.push("SP="+dollar+hexByte(Number(state.sp)&0xFF)); 66 if(state.p !== undefined) fields.push(this.formatCpuStatusHTML(state.p)); 67 return fields.join(" "); 68 } 69 //this.getNumByteArr = function(v) { let y= Math.floor(v/2**32); return [y,(y<<8),(y<<16),(y<<24), v,(v<<8),(v<<16),(v<<24)].map(z=> z>>>24) } // convert JS number to byte array 70 this.getNumByteArr = function(v) { let y= Math.floor(v/2**32); return [(v<<24),(v<<16),(v<<8),v,(y<<24),(y<<16),(y<<8),y].map(z=> z>>>24) } // convert JS number to byte array 71 this.getByteArrNum = function(arr) { return arr.reduce((a,c,i)=> a+c*2**(56-i*8),0) } // convert byte array to JS number 72 73 this.HEX2RGB = function(hex) { var n=parseInt(hex.slice(1),16); return [(n>>16)&0xFF,(n>>
738)&0xFF,n&0xFF] } 74 this.RGB2HEX = function(color) { return [hex2tab[color[0]&0xFF],hex2tab[color[1]&0xFF],hex2tab[color[2]&0xFF]] } 75 76 this.RGB2IDX = function(color,sig_bits) 77 { 78 const msk = (1<<sig_bits)-1, scl = (8-sig_bits); 79 return (((color[2]>>scl)&msk)<<sig_bits<<sig_bits) | (((color[1]>>scl)&msk)<<sig_bits) | ((color[0]>>scl)&msk) 80 } 81 82 this.IDX2RGB = function(idx,sig_bits) 83 { 84 const msk = (1<<sig_bits)-1, scl = (8-sig_bits); 85 return [(idx & msk)<<scl,((idx>>sig_bits) & msk)<<scl,((idx>>sig_bits>>sig_bits) & msk)<<scl]; 86 } 87 88 this.expandRngExpr = function(spec, modifiers = {}) 89 { 90 return this.expandRng(this.parseRngExpr(spec, modifiers)); 91 }; 92 93 this.parseRngExpr = function(spec, modifiers = {}) 94 { 95 const locate = modifiers._locate_ || ["<sub>", "</sub>"]; 96 const vars = { ...modifiers }; 97 delete vars._locate_; 98 99 const [rangePart, stepPart] = splitOutsideTags(spec, "/", locate); 100 const [fromPart, toPart] = splitOutsideTags(rangePart, "-", locate); 101 const base = vars.base || 0; 102 103 var ret = {}; 104 ret.from = parseRngToken(fromPart.trim(), vars, locate) - base; 105 if (toPart == null) return ret; // single value expression 106 ret.to = parseRngToken(toPart.trim(), vars, locate) - base; 107 if(stepPart != null) ret.step = parseRngToken(stepPart.trim(), vars, locate); 108 return ret; 109 }; 110 111 this.expandRng = function(a, b, c) 112 { 113 let from, to, step; 114 115 if (typeof a === "object" && a !== null) { 116 from = a.from; 117 to = a.to; 118 step = (a.step === undefined) ? 1 : a.step; 119 } else { 120 from = a; 121 to = b; 122 step = (c === undefined) ? 1 : c; 123 } 124 125 if (!Number.isInteger(from) || !Number.isInteger(to) || !Number.isInteger(step) || step === 0) { 126 throw new Error("expandRng expects integer from/to and a non-zero integer step"); 127 } 128 129 if (from > to && step > 0) step = -step; 130 if (from < to && step < 0) step = -step; 131 132 const out = []; 133 if (step > 0) { 134 for (let n = from; n <= to; n += step) out.push(n); 135 } else { 136 for (let n = from; n >= to; n += step) out.push(n); 137 } 138 return out; 139 }; 140 141 this.arrayRng = (from, to) => Array.from({ length: to - from + 1 }, (_, i) => from + i); 142 143 this.asHex = function(arr) 144 { 145 if (typeof arr === "number") arr = [arr]; 146 return arr.map(function(n) { 147 return "0x" + n.toString(16).toUpperCase(); 148 }); 149 }; 150 151 function parseRngToken(token, vars = {}, locate = ["<sub>", "</sub>"]) 152 { 153 let s = String(token).replace(/\s+/g, ""); 154 const isHex = s.startsWith("$"); 155 if (isHex) s = s.slice(1); 156 157 const base = isHex ? 16 : 10; 158 const [openTag, closeTag] = locate; 159 const digitRe = isHex ? /[0-9A-Fa-f]/ : /[0-9]/; 160 161 let skeleton = ""; 162 const terms = []; 163 164 for (let i = 0; i < s.length;) { 165 if (s.startsWith(openTag, i)) { 166 const end = s.indexOf(closeTag, i + openTag.length); 167 if (end < 0) throw new Error("Missing closing tag"); 168 169 const expr = s.slice(i + openTag.length, end).trim(); 170 if (!expr) throw new Error("Empty tagged expression"); 171 172 terms.push({ expr: expr, pos: skeleton.length }); 173 skeleton += "0"; // one digit slot placeholder 174 i = end + closeTag.length; 175 continue; 176 } 177 178 if (!digitRe.test(s[i])) { 179 throw new Error("Invalid digit '" + s[i] + "' in token: " + token); 180 } 181 182 skeleton += s[i]; 183 i++; 184 } 185 186 if (!skeleton.length) throw new Error("Empty numeric token"); 187 188 const baseValue = parseInt(skeleton, base); 189 let value = baseValue; 190 191 for (let i = 0; i < terms.length; i++) { 192 const digitsRight = skeleton.length - terms[i].pos - 1; 193 value += evalExpr(terms[i].expr, vars) * (base ** digitsRight); 194 } 195 196 return value; 197 } 198 199 function evalExpr(expr, vars) 200 { 201 const jsExpr = expr.replace(/\$/g, "0x"); 202 const names = Object.keys(vars); 203 const vals = names.map(function(k) { return vars[k]; }); 204 205 const ret = Function(...names, `"use strict"; return (${jsExpr});`)(...vals); 206 207 if (!Number.isFinite(ret) || !Number.isInteger(ret)) 208 throw new Error("Expression must evaluate to an integer: " + expr); 209 210 return ret; 211 } 212 213 function splitOutsideTags(str, sep, locate) 214 { 215 const [openTag, closeTag] = locate; 216 let depth = 0; 217 218 for (let i = 0; i < str.length; i++) { 219 if (str.startsWith(openTag, i)) { 220 depth++;
221 i += openTag.length - 1; 222 continue; 223 } 224 if (depth > 0 && str.startsWith(closeTag, i)) { 225 depth--; 226 i += closeTag.length - 1; 227 continue; 228 } 229 if (depth === 0 && str[i] === sep) { 230 return [str.slice(0, i), str.slice(i + 1)]; 231 } 232 } 233 234 return [str, null]; 235 } 236 237 this.base64ToArray = function(base64) 238 { 239 try{ var binary_string = window.atob(base64); } catch(e) { return null } 240 var len = binary_string.length; 241 var bytes = new Uint8Array(len); 242 for (var i = 0; i < len; i++) { 243 bytes[i] = binary_string.charCodeAt(i); 244 } 245 return bytes; 246 } 247 248 /* 249 * Shared embedded-data decoder. 250 * 251 * Offline catalogs use the same record shape for binary and text assets: 252 * {encoding:"zlib-base64",data:"..."} 253 * {encoding:"base64",data:"..."} 254 * 255 * A bare string remains a shorthand for zlib-base64. 256 * output is "bytes" (default) or "text". 257 */ 258 this.decodeDataRecord = function(rec,output) 259 { 260 if(rec==null) return null; 261 if(typeof rec=="string") 262 rec = {encoding:"zlib-base64",data:rec}; 263 264 var encoding = String(rec.encoding || "base64").toLowerCase(); 265 var bytes = this.base64ToArray(String(rec.data || "")); 266 267 if(bytes==null) 268 throw new Error("Invalid base64 payload"); 269 270 if(encoding=="zlib-base64") 271 { 272 if(typeof pako=="undefined" || typeof pako.inflate!="function") 273 throw new Error("zlib-base64 payload requires pako.inflate"); 274 bytes = pako.inflate(bytes); 275 } 276 else if(encoding!="base64") 277 throw new Error("Unsupported data encoding: " + encoding); 278 279 if(output=="text") 280 { 281 if(typeof TextDecoder!="function") 282 throw new Error("TextDecoder is not available"); 283 return new TextDecoder(rec.charset || "utf-8").decode(bytes); 284 } 285 286 return bytes; 287 } 288 289 this.ArrayBufferTobase64 = function(buffer) 290 { 291 //return btoa(String.fromCharCode.apply(null, new Uint8Array(bytes))); 292 293 var binary = ''; 294 var bytes = new Uint8Array( buffer ); 295 var len = buffer.byteLength; 296 for (var i = 0; i < len; i++) { 297 binary += String.fromCharCode( bytes[ i ] ); 298 } 299 return window.btoa( binary ); 300 } 301 302 this.b64EncodeUnicode = function(str) 303 { 304 // first we use encodeURIComponent to get percent-encoded Unicode, 305 // then we convert the percent encodings into raw bytes which 306 // can be fed into btoa. 307 return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, 308 function toSolidBytes(match, p1) { 309 return String.fromCharCode('0x' + p1); 310 })) 311 } 312 313 this.b64DecodeUnicode = function(str) 314 { 315 // Going backwards: from bytestream, to percent-encoding, to original string. 316 return decodeURIComponent(atob(str).split('').map(function(c) { 317 return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); 318 }).join('')) 319 } 320 321 this.Uint8ArrayFromBase64 = function(base64) 322 { 323 return Uint8Array.from(window.atob(base64), (v) => v.charCodeAt(0)); 324 } 325 326 this.Uint8ArrayToBase64 = function(a) 327 { 328 // 1. Preprocess Uint8Array into String 329 // (TODO: fix RAM usage from intermediate array creation) 330 var a_s = Array.prototype.map.call(a, c => String.fromCharCode(c)).join(String()); 331 // 2. Call btoa() 332 return btoa(a_s); 333 } 334 335 this.DumpBase64 = function(b64body,maxlen,lf) 336 { 337 var lf = lf===undefined ? "\n" : lf; 338 var maxlen = maxlen===undefined ? 1024 : maxlen; 339 var re = RegExp(".{1,"+maxlen+"}","g"); 340 return "var b64 ="+lf+" \""+b64body.match(re).join('"'+lf+'+"')+"\";"+lf 341 } 342 343 this.DumpTxt = function(txt,maxlen,lf) 344 { 345 var lf = lf===undefined ? "\n" : lf; 346 var maxlen = maxlen===undefined ? 1024 : maxlen; 347 var re = RegExp(".{1,"+maxlen+"}","g"); 348 return txt.match(re).join(lf); 349 } 350 351 this.base_convert = function (str, fromBase, toBase) 352 { 353 if(typeof(fromBase)=='object') { this.fromSymbols = fromBase[0] }
354 if(typeof(toBase) =='object') { this.toSymbols = toBase[0] } 355 fromBase = this.fromSymbols.length; toBase = this.toSymbols.length; 356 357 // PARSE INPUT DIGITS ARRAY 358 for(var _a = [0], str = str.split(''); str.length > 0 && _a[_a.push(this.fromSymbols.indexOf(str.pop())) - 1] >= 0;); 359 var _d = _a.shift() + _a[_a.length-1]>=0 ? _a : null; if (_d === null) return null; 360 361 // BASE CONVERSION 362 for (var _n = 0,_a = [],_p = [1]; _n < _d.length; _n++) { _a = add(_a, mul(_d[_n], _p, toBase), toBase); _p = mul(fromBase, _p, toBase) } 363 364 // PARSE OUTPUT DIGITS ARRAY 365 for (var _n = _a.length - 1, _o = ''; _n >= 0; _o += this.toSymbols[_a[_n--]]); 366 367 function add(x, y, base) { 368 var _m = Math.max(x.length, y.length); 369 for(var _c = 0,_n = 0,_r = [],_z; _n < _m || _c; _c = Math.floor(_z / base)) { 370 var _z = _c + (_n < x.length ? x[_n] : 0) + (_n < y.length ? y[_n] : 0); 371 var _n = _r.push(_z % base); 372 } 373 return _r; 374 } 375 376 function mul (x, pow, base) { 377 for(var _r = x < 0 ? null : []; x > 0; x = x >> 1) { 378 if(x & 1) _r = add(_r, pow, base); 379 pow = add(pow, pow, base); 380 } 381 return _r; 382 } 383
384 return _o.length==0?this.toSymbols[0]:_o; 385 } 386 387 this.symbols = 388 { 389 32:function(){return this.base32hex}, 390 45:function(){return this["qr-alnum"]}, 391 58:function(){return this.Bitcoin}, 392 60:function(){return this.appleII}, 393 "low hex": ["[0-9][a-f]"], // base 16 394 "qr-alnum": ["[0-9][A-Z] $%*+-./:"], // base 45 395 "Bitcoin": ["[1-9][A-H]JKLMN[P-Z][a-k][m-z]"], // base 58 396 "appleII": ["[0-9][A-Z]![#-/][:-@]]^ "], // base 60 397 "Base64": ["[A-Z][a-z][0-9]+/"], // base 64 398 "GUID64": ["[A-Z][a-z][0-9]-_"] // base 64 399 } 400 401 this.getsymbols = function(base) { 402 if(typeof(base)=="undefined") return ""; 403 if(typeof(this.symbols[base])=="undefined") 404 this.symbols[base] = base<95? 405 (this.rng(base<64?"[0-9][A-Z][a-z]+":"[A-Z][a-z][0-9][!-/][:-@][[-`][{-~]").substring(0,base)) 406 :(this.symbols[base] = base<216? 407 this.rng("[\x20-\x7e]\x80[\x82-\x8c]\x8e[\x91-\x9c]\x9e\x9f[\xa1-\xac][\xae-\xff]").substring(0,base) 408 :this.rng("[\x00-\xff]").substring(256-base,256)); 409 if(typeof(this.symbols[base])=="function") this.symbols[base] = this.symbols[base](); // process references 410 if(typeof(this.symbols[base])=="object") this.symbols[base] = this.rng(this.symbols[base][0]); // process range_replace 411 return this.symbols[base]; 412 } 413 414 this.rng = function(recipe) { 415 var _a = recipe.match(/\[.-.\]/); if(_a==null) return recipe; else { _a=[_a[0].charCodeAt(1),_a[0].charCodeAt(3)]; 416 return this.rng(recipe.replace(RegExp("\\[(\\x"+("0"+_a[0].toString(16)).slice(-2)+"-\\x"+_a[1].toString(16)+")\\]","g"), 417 String.fromCharCode(..." ".repeat(_a[1]-_a[0]+1).split("").map((_e,_i)=>_i+_a[0])) )) } 418 } 419 420 this.ltrim = function(s) { return s.replace(/^ */,"") } 421 this.rtrim = function(s) { return s.replace(/ *$/,"") } 422 this.trim = function(s) { return this.rtrim(this.ltrim(s)) } 423 this.stripHTML = function(s) { return s.replace(/( |<([^>]+)>)/ig,"") } 424 425 this.padding = function(word_arr, padding_arr) 426 { 427 let result = [], lim_word; 428 for (let i = 0; i < word_arr.length; i++) 429 { 430 word_arr[i] = this.unescapeHTML(word_arr[i]); 431 lim_word = word_arr[i].length > padding_arr[i] ? word_arr[i].substring(0, padding_arr[i]) : word_arr[i]; // Limit the word to its padding width if it exceeds the available space 432 if (i > 0) result.push(' '.repeat(Math.max(0, padding_arr[i-1] - result[result.length-1].length ))); // Calculate padding and ensure it's non-negative (no negative space) 433 result.push(lim_word); // Add the word to the result array 434 } 435 return result.join(''); // Join the array into a single string 436 } 437 438 /* 439 //+ Carlos R. L. Rodrigues 440 //@ http://jsfromhell.com/classes/math-parser [rev. #3] 441 442 this.MathParser = function() 443 { 444 var o = this, p = o.operator = {}; 445 p["+"] = function(n, m){return n + m;} 446 p["-"] = function(n, m){return n - m;} 447 p["*"] = function(n, m){return n * m;} 448 p["/"] = function(m, n){return n / m;} 449 p["%"] = function(m, n){return n % m;} 450 p["^"] = function(m, n){return Math.pow(n, m);} 451 p["~"] = function(m, n){return Math.sqrt(n, m);} 452 o.custom = {} 453 p.f = function(s, n) 454 { 455 if(Math[s]) return Math[s](n); 456 else if(o.custom[s]) return o.custom[s].apply(o, n); 457 else throw new Error("Function \"" + s + "\" not defined."); 458 } 459 o.add = function(n, f){this.custom[n] = f;} 460 } 461 462 this.MathParser.prototype.eval = function(e, ig) 463 { 464 var v = [], p = [], i, _, a, c = 0, s = 0, x, t = !ig ? e.indexOf("^") : -1, d = null; 465 var cp = e, e = e.split(""), n = "0123456789.", o = "+-/*^%~", f = this.operator; 466 if(t + 1) 467 do{ 468 for(a = "", _ = t - 1; _ && o.indexOf(e[_]) < 0; a += e[_], e[_--] = ""); a += "^"; 469 for(_ = t + 1, i = e.length; _ < i && o.indexOf(e[_]) < 0; a += e[_], e[_++] = ""); 470 e = e.slice(0, t).concat((this.eval(a, 1) + "").split("")).concat(e.slice(t + 1)); 471 } 472 while(t = cp.indexOf("^", ++t) + 1); 473 for(i = 0, l = e.length; i < l; i++){ 474 if(o.indexOf(e[i]) > -1) 475 e[i] == "-" && (s > 1 || d === null) && ++s, !s && d !== null && (p.push(e[i]), s = 2), "+-".indexOf(e[i]) < (d = null) && (c = 1); 476 else if(a = n.indexOf(e[i]) + 1 ? e[i++] : "") 477 { 478 while(n.indexOf(e[i]) + 1) a += e[i++]; 479 v.push(d = (s & 1 ? -1 : 1) * a), c && v.push(f[p.pop()](v.pop(), v.pop())
479) && (c = 0), --i, s = 0; 480 } 481 } 482 for(c = v[0], i = 0, l = p.length; l--; c = f[p[i]](c, v[++i])); 483 return c; 484 } 485 486 this.MathParser.prototype.parse = function(e) 487 { 488 var p = [], f = [], ag, n, c, a, o = this, v = "0123456789.+-*"+"/^%~(, )"; 489 for(var x, i = 0, l = e.length; i < l; i++){ 490 if(v.indexOf(c = e.charAt(i)) < 0) { for(a = c; v.indexOf(c = e.charAt(++i)) < 0; a += c); f.push((--i, a)) } 491 else if(!(c == "(" && p.push(i)) && c == ")") 492 { 493 if(a = e.slice(0, (n = p.pop()) - (x = v.indexOf(e.charAt(n - 1)) < 0 ? y = (c = f.pop()).length : 0)), x) 494 for(var j = (ag = e.slice(n, ++i).split(",")).length; j--; ag[j] = o.eval(ag[j])); 495 l = (e = a + (x ? o.operator.f(c, ag) : o.eval(e.slice(n, ++i))) + e.slice(i)).length, i -= i - n + c.length; 496 } 497 } 498 return o.eval(e); 499 } 500 */ 501 502 503 this.MathParser = function() 504 { 505 p = this.operator = {}; 506 p["+"] = function(n, m){return n + m} 507 p["-"] = function(n, m){return n - m} 508 p["*"] = function(n, m){return n * m} 509 p["/"] = function(m, n){return n / m} 510 p["%"] = function(m, n){return n % m} 511 p["^"] = function(m, n){return Math.pow(n, m)} 512 p["~"] = function(m, n){return Math.sqrt(n, m)} 513 p["&"] = function(m, n){return n & m} 514 p["|"] = function(m, n){return n | m} 515 p["x"] = function(m, n){return Number("0x"+m)} 516 this.custom = {} 517 p.f = function(s, n) 518 { 519 if(Math[s]) return Math[s](n); 520 else if(this.custom[s]) return this.custom[s].apply(this, n); 521 else throw new Error("Function \"" + s + "\" not defined."); 522 } 523 this.add = function(n, f){this.custom[n] = f;} 524 525 this.eval = function(e, ig) 526 { 527 var v = [], p = [], i, _, a, c = 0, s = 0, x, t = !ig ? e.indexOf("^") : -1, d = null; 528 var cp = e, e = e.split(""), n = "0123456789ABCDEF.", o = "+-/*^%~&|x", f = this.operator; 529 if(t + 1) 530 do 531 { 532 for(a = "", _ = t - 1; _ && o.indexOf(e[_]) < 0; a += e[_], e[_--] = ""); a += "^"; 533 for(_ = t + 1, i = e.length; _ < i && o.indexOf(e[_]) < 0; a += e[_], e[_++] = ""); 534 e = e.slice(0, t).concat((this.eval(a, 1) + "").split("")).concat(e.slice(t + 1)); 535 } 536 while(t = cp.indexOf("^", ++t) + 1); 537 for(i = 0, l = e.length; i < l; i++) 538 { 539 if(o.indexOf(e[i]) > -1) 540 e[i] == "-" && (s > 1 || d === null) && ++s, !s && d !== null && (p.push(e[i]), s = 2), "+-".indexOf(e[i]) < (d = null) && (c = 1); 541 else if(a = n.indexOf(e[i]) + 1 ? e[i++] : "") 542 { 543 while(n.indexOf(e[i]) + 1) a += e[i++]; 544 v.push(d = (s & 1 ? -1 : 1) * a), c && v.push(f[p.pop()](v.pop(), v.pop())) && (c = 0), --i, s = 0; 545 } 546 } 547 for(c = v[0], i = 0, l = p.length; l--; c = f[p[i]](c, v[++i])); 548 return c; 549 } 550 551 this.parse = function(e) 552 { 553 var p = [], f = [], ag, n, c, a, o = this, v = "0123456789ABCDEF.+-*/^%~&|x(, )"; 554 for(var x, i = 0, l = e.length; i < l; i++) 555 { 556 if(v.indexOf(c = e.charAt(i)) < 0) { for(a = c; v.indexOf(c = e.charAt(++i)) < 0; a += c); f.push((--i, a)) } 557 else if(!(c == "(" && p.push(i)) && c == ")") 558 { 559 if(a = e.slice(0, (n = p.pop()) - (x = v.indexOf(e.charAt(n - 1)) < 0 ? y = (c = f.pop()).length : 0)), x) 560 for(var j = (ag = e.slice(n, ++i).split(",")).length; j--; ag[j] = this.eval(ag[j])); 561 l = (e = a + (x ? o.operator.f(c, ag) : this.eval(e.slice(n, ++i))) + e.slice(i)).length, i -= i - n + c.length; 562 } 563 } 564 return this.eval(e); 565 } 566 567 this.format = function(str,substitutes) 568 { 569 var pos = str.lastIndexOf(","); 570 var out = str.substring(pos+1,str.length); 571 var str2 = str.substring(0,pos); 572 573 if(substitutes) 574 for(var ii in substitutes) 575 out = out.replace(RegExp(ii,"g"),substitutes[ii]); // substitute all variables 576 577 var arr = String(this.parse(out)).split(); 578 var i = -1; 579 function callback(exp, p0, p1, p2, p3, p4) 580 { 581 if (exp=='%%') return '%';
582 if (arr[++i]===undefined) return undefined; 583 exp = p2 ? parseInt(p2.substr(1)) : undefined; 584 var base = p3 ? parseInt(p3.substr(1)) : undefined; 585 var val; 586 switch (p4) 587 { 588 case 's': val = arr[i]; break; 589 case 'c': val = arr[i][0]; break; 590 case 'f': val = parseFloat(arr[i]).toFixed(exp); break; 591 case 'p': val = parseFloat(arr[i]).toPrecision(exp); break; 592 case 'e': val = parseFloat(arr[i]).toExponential(exp); break; 593 case 'x': val = parseInt(arr[i]).toString(base?base:16); break; 594 case 'X': val = parseInt(arr[i]).toString(base?base:16).toUpperCase(); break; 595 case 'd': val = parseFloat(parseInt(arr[i], base?base:10).toPrecision(exp)).toFixed(0); break; 596 } 597 val = typeof(val)=='object' ? JSON.stringify(val) : val.toString(base); 598 var sz = parseInt(p1); // padding size 599 var ch = p1 && p1[0]=='0' ? '0' : ' '; // isnull? 600 while (val.length<sz) val = p0 !== undefined ? val+ch : ch+val; // isminus? 601 return val; 602 } 603 var regex = /%(-)?(0?[0-9]+)?([.][0-9]+)?([#][0-9]+)?([scfpexXd%])/g; 604 return str2.replace(regex, callback); 605 } 606 607 } 608 609 // https://stackoverflow.com/questions/1293147/how-to-parse-csv-data 610 this.CSVParser = 611 { 612 parse: 613 function(csv) 614 { 615 var arr = csv.match( /\s*(\"[^"]*\"|'[^']*'|[^,]*)\s*(,|$)/g ).map( 616 function (csv) 617 { 618 let m; 619 if (m = csv.match(/^\s*,?$/)) return null; // null value 620 if (m = csv.match(/^\s*\"([^"]*)\"\s*,?$/)) return "\""+m[1]+"\""; // Double Quoted Text 621 if (m = csv.match(/^\s*'([^']*)'\s*,?$/)) return "'"+m[1]+"'"; // Single Quoted Text 622 if (m = csv.match(/^\s*(true|false)\s*,?$/)) return m[1] === "true"; // Boolean 623 if (m = csv.match(/^\s*((?:\+|\-)?\d+)\s*,?$/)) return parseInt(m[1]); // Integer Number 624 if (m = csv.match(/^\s*((?:\+|\-)?\d*\.\d*)\s*,?$/)) return parseFloat(m[1]); // Floating Number 625 if (m = csv.match(/^\s*(.*?)\s*,?$/)) return m[1]; // Unquoted Text 626 } 627 ) 628 if(arr[arr.length-1]==null) arr.pop(); 629 return arr; 630 } 631 , 632 stringify: 633 function(table, replacer) 634 { 635 replacer = replacer || function(r, c, v) { return v; }; 636 var csv = '', c, cc, r, rr = table.length, cell; 637 for (r = 0; r < rr; ++r) 638 { 639 if (r) csv += '\r\n'; 640 for (c = 0, cc = table[r].length; c < cc; ++c) 641 { 642 if (c) csv += ','; 643 cell = replacer(r, c, table[r][c]); 644 if (/[,\r\n"]/.test(cell)) 645 cell = '"' + cell.replace(/"/g, '""') + '"'; 646 csv += (cell || 0 === cell) ? cell : ''; 647 } 648 } 649 return csv; 650 } 651 } 652 653 this.escapeHTML = function(str) 654 { 655 return String(str) 656 .replaceAll("&", "&") 657 .replaceAll("<", "<") 658 .replaceAll(">", ">") 659 .replaceAll("<b>", "<b>") 660 .replaceAll("</b>", "</b>") 661 .replaceAll("<u>", "<u>") 662 .replaceAll("</u>", "</u>") 663 .replaceAll("<i>", "<i>") 664 .replaceAll("</i>", "</i>") 665 .replaceAll('"', """) 666 .replaceAll("'", "'") 667 .replaceAll("\n","<br>") 668 .replaceAll(" ", " "); 669 } 670 671 this.unescapeHTML = function(str) 672 { 673 return String(str) 674 .replace(/ /g, " ") 675 .replace(/<br>/g, "\n") 676 .replace(/'/g, "'") 677 .replace(/"/g, '"') 678 .replace(/>/g, ">") 679 .replace(/</g, "<") 680 .replace(/&/g, "&"); 681 } 682/* 683 this.unescapeHTML = function(str) 684 { 685 return str 686 .replace(/"/g, '"') 687 .replace(/</g, '<') 688 .replace(/>/g, '>') 689 .replace(/ /g, '\u00A0') 690 .replace(/&/g, '&'); 691 } 692*/ 693 this.escapeREGEXP = function(str) 694 { 695 return str 696 .replace(/([\.\^\$\*\+\-\?\(\)\[\]\{\}\\\|])/g, "\\$1") 697 } 698 699 this.uuid = function() // UUID v4 700 { 701 return "10000000-1000-4000-8000-100000000000".replace(/[018]/g, c => 702 (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 703 ); 704 } 705 706 this.default = function(src_obj,default_obj,message) // create default object when main object is missing 707 { 708 try 709 { 710 var cn = this.default.caller.name; 711 //if(cn!="EMU_init") 712 //oCOM.POPUP.html("default["+cn+"] typeof(src_obj)="+typeof(src_obj)+" Object.keys(src_obj).length="+Object.keys(src_obj).length+" = "+(typeof(src_obj)=="undefined" || Ob
712ject.keys(src_obj).length==0 ? "default override" : "main")); 713 714 if(typeof(src_obj)=="undefined" || Object.keys(src_obj).length==0) { console.warn(cn+" : proceeding without "+message); return default_obj } 715 return src_obj; 716 } 717 catch({ name, message }) 718 { 719 //oCOM.POPUP.html("error in oCOM.default ["+cn+"]: "+name+" "+message); 720 } 721 } 722 723 this.instance_of = function(V, F) 724 { 725 var O = F.prototype; 726 V = V.__proto__; 727 while (true) 728 { 729 if (V === null) 730 return false; 731 if (O === V) 732 return true; 733 V = V.__proto__; 734 } 735 } 736 737 this.toASCIIarr = function(str){for(var a=[],i=0;i<str.length;i++)a.push(str.charCodeAt(i));return a;} // convert string into array of ascii codes (numbers) 738 739 this.crc16 = function(r){var crc=0xFFFF;var odd; for(var i=0;i<r.length;i++) { crc = crc ^ r[i]; for (var j = 0; j < 8; j++) { odd = crc & 0x0001; crc = crc >> 1; if (odd) { crc = crc ^ 0xA001 }}} return crc }; 740 this.crc32 = function(r){for(var a,o=[],c=0;c<256;c++){a=c;for(var f=0;f<8;f++)a=1&a?3988292384^a>>>1:a>>>1;o[c]=a}for(var n=-1,t=0;t<r.length;t++)n=n>>>8^o[255&(n^r[t])];return(-1^n)>>>0}; 741 742 /* 743 * General-purpose fixed-capacity ring buffer. 744 * 745 * _type is optional: 746 * undefined/null : normal Array, accepts objects 747 * 8 : Uint8Array 748 * 16 : Uint16Array 749 * 32 : Uint32Array 750 * 751 * This replaces the older RingBuffer implementation while keeping the same 752 * high-level API: push/pop/shift/unshift/front/back/clear/slice/debug plus resize/fromArray/forEach. 753 */ 754 var _COM_this = this; 755 756 this.buffer = function(_capacity,_type) 757 { 758 _capacity = Number(_capacity) | 0; 759 if(_capacity < 1) _capacity = 1; 760 761 var _typed = false; 762 var _buffer; 763 764 function _newBuffer(capacity) 765 { 766 switch(Number(_type)) 767 { 768 case 8: _typed = true; return new Uint8Array(capacity); 769 case 16: _typed = true; return new Uint16Array(capacity); 770 case 32: _typed = true; return new Uint32Array(capacity); 771 default: _typed = false; return new Array(capacity); 772 } 773 } 774 775 _buffer = _newBuffer(_capacity); 776 777 var _first = 0; 778 var _length = 0; 779 780 function _wrap(index) 781 { 782 index = index % _capacity; 783 return index < 0 ? index + _capacity : index; 784 } 785 786 function _last() 787 { 788 return _wrap(_first + _length - 1); 789 } 790 791 function _clearIndex(index) 792 { 793 _buffer[index] = _typed ? 0 : undefined; 794 } 795 796 function _toArray() 797 { 798 var out = []; 799 for(var i = 0; i < _length; i++) 800 out.push(_buffer[_wrap(_first + i)]); 801 return out; 802 } 803 804 var api = 805 { 806 get length() { return _length; }, 807 get capacity() { return _capacity; }, 808 get first() { return _first; }, 809 810 // Backward-compatible debug fields. Code should prefer length, capacity and first, but these keep the old RingBuffer shape readable. 811 get _first() { return _first; }, 812 set _first(v) { _first = _wrap(Number(v) | 0); }, 813 get _length() { return _length; }, 814 set _length(v) { _length = Math.max(0,Math.min(_capacity,Number(v) | 0)); }, 815 get _capacity(){ return _capacity; }, 816 817 debug: function() 818 { 819 return JSON.stringify({ 820 buffer: _typed ? Array.from(_buffer) : _buffer.slice(), 821 first: _first, 822 length: _length, 823 capacity: _capacity, 824 typed: _typed, 825 type: _type || 0 826 }); 827 }, 828 829 clear: function() 830 { 831 for(var i = 0; i < _capacity; i++) _clearIndex(i); 832 _first = 0; 833 _length = 0; 834 return this; 835 }, 836 empty: function() { return _length === 0; }, 837 full: function() { return _length === _capacity; }, 838 front: function() { if(_length === 0) return undefined; else return _buffer[_first]; }, 839 back: function() { if(_length === 0) return undefined; else return _buffer[_last()]; }, 840 at: function(index) { index = Number(index) | 0; if(index < 0) index = _length + index; if(index < 0 || index >= _length) return undefined; else return _buffer[_wrap(_first + index)]; }, 841 push: function(value) { if(_length === _capacity) this.shift(); _buffer[_wrap(_first + _length)] = value; _length++; return _length; }, 842 pop: function() { if(_length === 0) return undefined; else { var index = _last(); var value = _buffer[index]; _clearIndex(index); _length--; return value; } },
843 shift: function() { if(_length === 0) return undefined; else { var value = _buffer[_first]; _clearIndex(_first); _first = _wrap(_first + 1); _length--; return value;} }, 844 unshift: function(value) { if(_length === _capacity) this.pop(); _first = _wrap(_first - 1); _buffer[_first] = value; _length++; return _length; }, 845 846 // Historical helper used as a delay line: insert newest value at the front and discard the oldest value when the buffer is full. 847 delay: function(value) { return this.unshift(value); }, 848 resize: function(newCapacity) 849 { 850 newCapacity = Number(newCapacity) | 0; 851 if(newCapacity < 1) newCapacity = 1; 852 if(newCapacity === _capacity) return this; 853 854 var values = _toArray(); 855 856 _capacity = newCapacity; 857 _buffer = _newBuffer(_capacity); 858 _first = 0; 859 _length = 0; 860 861 return this.fromArray(values); 862 }, 863 fromArray: function(records) 864 { 865 this.clear(); 866 867 if(!records) return this; 868 if(typeof records.length != "number") return this; 869 870 var start = Math.max(0,records.length - _capacity); 871 for(var i = start; i < records.length; i++) 872 this.push(records[i]); 873 874 return this; 875 }, 876 forEach: function(callback,thisArg) 877 { 878 if(typeof callback != "function") return this; 879 880 for(var i = 0; i < _length; i++) 881 callback.call(thisArg,_buffer[_wrap(_first + i)],i,this); 882 883 return this; 884 }, 885 slice: function(pos1,pos2) { return _toArray().slice(pos1,pos2); }, 886 toArray: function() { return _toArray(); }, 887 _last: function() { if(_length === 0) return undefined; else return _last(); }, 888 _right: function() { _first = _wrap(_first + 1); return _first; }, 889 _left: function() { _first = _wrap(_first - 1); return _first; } 890 }; 891 892 return api; 893 } 894 895 this.FIFO = function(length) 896 { 897 var pointer = 0, buffer = []; 898 899 return { 900 get : function(key) 901 { 902 if (key < 0) return buffer[pointer+key]; 903 else if (key === false) return buffer[pointer - 1]; 904 else return buffer[key]; 905 }, 906 push : function(item) 907 { 908 buffer[pointer] = item; 909 pointer = (pointer + 1) % length; 910 return item; 911 }, 912 prev : function() 913 { 914 var tmp_pointer = (pointer - 1) % length; 915 if (buffer[tmp_pointer]) { pointer = tmp_pointer; return buffer[pointer]; } 916 }, 917 next : function() 918 { 919 if (buffer[pointer]) { pointer = (pointer + 1) % length; return buffer[pointer]; } 920 } 921 }; 922 }; 923 924 this.cloneJSON = function(obj) 925 { 926 return JSON.parse(JSON.stringify(obj)); 927 } 928 929 930// ââââââ ââ ââ ââ âââââââ ââ ââ âââ ââ ââââââ ââââââââ ââ ââââââ âââ ââ âââââââ 931// ââ ââ ââ ââ ââ ââ ââ ââââ ââ ââ ââ ââ ââ ââ ââââ ââ ââ 932// ââ âââ ââ ââ ââ âââââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ âââââââ 933// ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ ââ 934// ââââââ ââââââ ââ ââ ââââââ ââ ââââ ââââââ ââ ââ ââââââ ââ ââââ âââââââ 935 936 ///////////////////////////////// 937 // FIXED-COLUMN TEXT RENDERER // 938 ///////////////////////////////// 939 940 this.parseColumnSpec = function(spec, defaults) 941 { 942 defaults = defaults || {}; 943 var out = {}; 944 945 function put(k, v) 946 { 947 v = Number(v); 948 if (!Number.isFinite(v)) return; 949 out[String(k)] = v | 0; 950 } 951 952 for (var k in defaults) put(k, defaults[k]); 953 954 if (spec == null || spec === "") return out; 955 956 if (typeof spec === "object") 957 { 958 for (var kk in spec) put(kk, spec[kk]); 959 return out; 960 } 961 962 var text = String(spec).trim(); 963 if (!text) return out; 964 965 try 966 { 967 var jsonish = text; 968 // Accept both JSON and compact object notation like {adr:0,code:6,lbl:16}. 969 if (jsonish.charAt(0) === "{" && jsonish.indexOf('"') < 0) 970 jsonish = jsonish.replace(/([A-Za-z_$][A-Za-z0-9_$-]*)\s*:/g, '"$1":'); 971 var obj = JSON.parse(jsonish); 972 for (var j in obj) put(j, obj[j]); 973 return out; 974 } 975 catch (_ignore) {} 976 977 var re = /([A-Za-z_$][A-Za-z0-9_$-]*)\s*[:=]\s*(-?\d+)/g, m; 978 while ((m = re.exec(text))) put(m[1], m[2]); 979 return out; 980 }; 981 982 this.textTableClip = function(text, width, ellipsis) 983 { 984 text = String(text == null ? "" : text); 985 ellipsis = ellipsis || "â¦"; 986 if (width == null || !Number.isFinite(Number(width)) || Number(width) < 0) return text; 987 width = Number(width) | 0; 988 if (width === 0) return "";
989 if (text.length <= width) return text; 990 if (width <= ellipsis.length) return ellipsis.substring(0, width); 991 return text.substring(0, width - ellipsis.length) + ellipsis; 992 }; 993 994 this.renderTextTableRows = function(rows, columnSpec, options) 995 { 996 options = options || {}; 997 rows = rows || []; 998 var ellipsis = options.ellipsis || "â¦"; 999 var trimRight = options.trimRight !== false; 1000 var singleColumnRaw = options.singleColumnRaw !== false; 1001 var defaults = options.defaults || {}; 1002 var spec = this.parseColumnSpec(columnSpec, defaults); 1003 var order = (options.order || Object.keys(spec)).slice(); 1004 1005 // Keep only configured keys and sort them by their start column. Ties keep order[] priority. 1006 order = order.filter(function(k) { return spec[k] != null; }); 1007 order.sort(function(a, b) { 1008 var d = spec[a] - spec[b]; 1009 return d || ((options.order || []).indexOf(a) - (options.order || []).indexOf(b)); 1010 }); 1011 1012 function rowToLine(row) 1013 { 1014 row = row || {}; 1015 var nonEmpty = []; 1016 for (var i = 0; i < order.length; i++) 1017 { 1018 var key = order[i]; 1019 var val = row[key]; 1020 if (val != null && String(val) !== "") nonEmpty.push(key); 1021 } 1022 1023 // UX rule for comment-only or otherwise single-field rows: print at column 0. 1024 if (singleColumnRaw && nonEmpty.length === 1) 1025 return String(row[nonEmpty[0]]); 1026 1027 var line = ""; 1028 for (var c = 0; c < order.length; c++) 1029 { 1030 var key = order[c]; 1031 var text = row[key]; 1032 if (text == null || String(text) === "") continue; 1033 var col = Number(spec[key]) | 0; 1034 var nextKey = null; 1035 for (var n = c + 1; n < order.length; n++) 1036 { 1037 if (spec[order[n]] > col) { nextKey = order[n]; break; } 1038 } 1039 var width = nextKey == null ? undefined : (Number(spec[nextKey]) - col); 1040 text = this.textTableClip(text, width, ellipsis); 1041 if (line.length < col) line = line + " ".repeat(col - line.length); 1042 if (line.length > col) 1043 { 1044 // Preserve an already-written overflowing field; move this field just after it. 1045 // Properly configured columns should not hit this path because clipping happens above. 1046 line += " "; 1047 } 1048 line += text; 1049 } 1050 return trimRight ? line.replace(/\s+$/, "") : line; 1051 } 1052 1053 var out = []; 1054 for (var r = 0; r < rows.length; r++) out.push(rowToLine.call(this, rows[r])); 1055 return out; 1056 }; 1057 1058 this.renderTextTable = function(rows, columnSpec, options) 1059 { 1060 return this.renderTextTableRows(rows, columnSpec, options).join((options && options.lineBreak) || "\n"); 1061 }; 1062 1063 1064// Interactive JSON pretty printer, adapted from the JSON.prettify pattern used in AsciiCAD_CMD.js. 1065this.JSONprettify = function makeInteractiveJson(mountEl, boundaryStyles, indent) 1066{ 1067 if (!mountEl) throw new Error("mountEl is required"); 1068 indent = indent || 2; 1069 var jsonStr = mountEl.textContent; 1070 1071 var styleMap = Array.isArray(boundaryStyles) 1072 ? Object.fromEntries(boundaryStyles) 1073 : (boundaryStyles || {}); 1074 1075 var openToKey = {}; 1076 var closeToKey = {}; 1077 Object.keys(styleMap).forEach(function (pairKey) { 1078 if (typeof pairKey !== "string" || pairKey.length !== 2) return; 1079 openToKey[pairKey.charAt(0)] = pairKey; 1080 closeToKey[pairKey.charAt(1)] = pairKey; 1081 }); 1082 1083 var pretty; 1084 try { 1085 pretty = prettyJsonAllman(JSON.parse(jsonStr), indent); 1086 } catch (e) { 1087 var fallback = document.createElement("pre"); 1088 fallback.className = "json-view"; 1089 fallback.textContent = jsonStr; 1090 mountEl.replaceChildren(fallback); 1091 return fallback; 1092 } 1093 1094 var root = document.createElement("pre"); 1095 root.className = "json-view"; 1096 var stacks = {}; 1097 var nextPairId = 1; 1098 var frag = document.createDocumentFragment(); 1099 1100 function appendText(text) { 1101 if (text) frag.appendChild(document.createTextNode(text)); 1102 } 1103 1104 function appendBoundarySpan(ch, pairKey, pairId) { 1105 var sp = document.createElement("span"); 1106 sp.className = "json-boundary"; 1107 sp.textContent = ch; 1108 sp.dataset.pair = String(pairId); 1109 if (styleMap[pairKey]) sp.setAttribute("style", styleMap[pairKey]); 1110 frag.appendChild(sp); 1111 return sp; 1112 } 1113 1114 var i = 0; 1115 while (i < pretty.length) { 1116 var ch = pretty.charAt(i); 1117 1118 if (openToKey[ch]) { 1119 var openPairKey = openToKey[ch]; 1120 var pairId = nextPairId++; 1121 appendBoundarySpan(ch, openPairKey, pairId); 1122 if (!stacks[openPairKey]) stacks[openPairKey] = []; 1123 stacks[openPairKey].push(pairId); 1124 i++; 1125 continue; 1126 } 1127 1128 if (closeToKey[ch]) { 1129 var closePairKey = closeToKey[ch]; 1130 var stack = stacks[closePairKey]; 1131 if (stack && stack.length) appendBoundarySpan(ch, closePairKey, stack.pop()); 1132 else appendText(ch); 1133 i++; 1134 continue; 1135 } 1136 1137 // Light token coloring on top of the boundary-pair highlighting. 1138 var rest = pretty.substring(i); 1139 var m; 1140 if ((m = rest.match(/^\s+|^[:,]/))) { 1141 appendText(m[0]); 1142 i += m[0].length; 1143 continue; 1144 } 1145 if ((m = rest.match(/^"(?:\\.|[^"\\])*"(?=\s*:)/))) { 1146 appendClassed(m[0], "json-key"); 1147 i += m[0].length; 1148 continue; 1149 } 1150 if ((m = rest.match(/^"(?:\\.|[^"\\])*"/))) { 1151 appendClassed(m[0], "json-string"); 1152 i += m[0].length; 1153 continue; 1154 } 1155 if ((m = rest.match(/^-?\d+(?:\.\d+)?(?:[eE][+\-]?\d+)?/))
1155) { 1156 appendClassed(m[0], "json-number"); 1157 i += m[0].length; 1158 continue; 1159 } 1160 if ((m = rest.match(/^(true|false)\b/))) { 1161 appendClassed(m[0], "json-boolean"); 1162 i += m[0].length; 1163 continue; 1164 } 1165 if ((m = rest.match(/^null\b/))) { 1166 appendClassed(m[0], "json-null"); 1167 i += m[0].length; 1168 continue; 1169 } 1170 1171 appendText(ch); 1172 i++; 1173 } 1174 1175 function appendClassed(text, className) { 1176 var sp = document.createElement("span"); 1177 sp.className = className; 1178 sp.textContent = text; 1179 frag.appendChild(sp); 1180 } 1181 1182 root.appendChild(frag); 1183 mountEl.replaceChildren(root); 1184 1185 var activePair = null; 1186 function cssEscape(s) { 1187 if (window.CSS && typeof window.CSS.escape === "function") return window.CSS.escape(String(s)); 1188 return String(s).replace(/[^a-zA-Z0-9_-]/g, "\\$&"); 1189 } 1190 function setActive(pairId) { 1191 if (activePair === pairId) return; 1192 clearActive(); 1193 activePair = pairId; 1194 root.querySelectorAll('.json-boundary[data-pair="' + cssEscape(pairId) + '"]').forEach(function (el) { 1195 el.classList.add("active"); 1196 }); 1197 } 1198 function clearActive() { 1199 if (activePair == null) return; 1200 root.querySelectorAll('.json-boundary[data-pair="' + cssEscape(activePair) + '"]').forEach(function (el) { 1201 el.classList.remove("active"); 1202 }); 1203 activePair = null; 1204 } 1205 1206 root.addEventListener("mousemove", function (e) { 1207 var b = e.target.closest(".json-boundary"); 1208 if (!b || !root.contains(b)) { clearActive(); return; } 1209 setActive(b.dataset.pair); 1210 }); 1211 root.addEventListener("mouseleave", clearActive); 1212 return root; 1213}; 1214 1215function prettyJsonAllman(value, indent) { 1216 indent = indent || 2; 1217 var obj = (typeof value === "string") ? JSON.parse(value) : value; 1218 var s = JSON.stringify(obj, null, indent); 1219 s = s.replace(/^(\s*)"([^"]+)"\s*:\s*([\{\[])([\}\]])?\s*(,?)\s*$/gm, 1220 function (_m, ws, key, open, maybeClose, comma) { 1221 // Keep empty arrays/objects compact: "tag": [] and "sym": {}. 1222 if (maybeClose) return ws + '"' + key + '": ' + open + maybeClose + comma; 1223 // For non-empty object/array values, keep the existing Allman-style key break. 1224 return ws + '"' + key + '":\n' + ws + open; 1225 } 1226 ); 1227 return s; 1228} 1229 1230 /////////////////////////////////// 1231 // ASCII / UTF-8 TEXT GRID // 1232 /////////////////////////////////// 1233 1234 this.ASCII_GRID = function(cols,rows,fill) 1235 { 1236 const COLS = Math.max(1,Number(cols) || 1); 1237 const ROWS = Math.max(1,Number(rows) || 1); 1238 const FILL = (fill == null ? " " : String(fill)).charAt(0) || " "; 1239 const ascii = []; 1240 1241 this.cols = COLS; 1242 this.rows = ROWS; 1243 1244 this.BOX_SINGLE = { tl:"â", tr:"â", bl:"â", br:"â", h:"â", v:"â" }; 1245 this.BOX_FAT = { tl:"â", tr:"â", bl:"â", br:"â", h:"â", v:"â" }; 1246 this.BOX_DOUBLE = { tl:"â", tr:"â", bl:"â", br:"â", h:"â", v:"â" }; 1247 this._currentStroke = []; 1248 this.undoStack = []; 1249 1250 for(var r=0;r<ROWS;r++) 1251 { 1252 var row = []; 1253 for(var c=0;c<COLS;c++) row.push(FILL); 1254 ascii.push(row); 1255 } 1256 1257 this.clear = function(ch) 1258 { 1259 ch = (ch == null ? FILL : String(ch).charAt(0)) || " "; 1260 1261 this._currentStroke = []; 1262 for(var c=0;c<COLS;c++) 1263 for(var r=0;r<ROWS;r++) 1264 { 1265 if(ascii[r][c]!=ch) 1266 { 1267 const cell = {"r":r,"c":c}; 1268 const op = {"ch":ch,"type":"place"}; 1269 this.applyOpAtCell?.( cell , op ) ?? {}; // display character on grid & build undo buffer 1270 } 1271 } 1272 this.pushStrokeIfNonEmpty?.(this._currentStroke) ?? {}; // commit undo buffer 1273 1274 1275 return this; 1276 }; 1277 1278 this.getCharAtCell = function(r, c) { return ascii[r][c] } 1279 this.drawCharAtCell = function(r, c, ch) { ascii[r][c] = ch; } 1280 1281 /* 1282 * Visual-layer adapter for RetroAppleJS. 1283 * This is the hook AsciiCAD uses to update the grid and build undo data. 1284 */ 1285 this.applyOpAtCell = function(cell,op) 1286 { 1287 if(!cell || cell.r < 0 || cell.r >= ROWS || cell.c < 0 || cell.c >= COLS) return; 1288 const prev = ascii[cell.r][cell.c]; 1289 const next = (op.type === "place") ? op.ch : FILL; 1290 if (prev === next) return; 1291 1292 this._currentStroke.push({ r: cell.r, c: cell.c, prev, next }); 1293 ascii[cell.r][cell.c] = next; 1294 } 1295 1296 this.pushStrokeIfNonEmpty = function(stroke) 1297 { 1298 if(stroke && stroke.length) this.undoStack.push(stroke.slice()); 1299 } 1300 1301 this.draw = function() 1302 { 1303 /* no-op: RetroAppleJS renders this text grid into a readonly textarea */ 1304 } 1305 1306 /* 1307 * AsciiCAD-compatible text placement. 1308 * Original API: cell(<c>,<r>,<string>) 1309 */ 1310 this.cell = function(c, r, str) 1311 { 1312 if(r===undefined || c===undefined || str===undefined) return; // safe escape if no arguments provided 1313 if (r < 0 || r >= ROWS || c < 0 || c >= COLS) 1314 throw new Error("Position out of bounds. Valid: col[0-" + (COLS - 1) + "], row[0-" + (ROWS - 1) + "]"); 1315 1316 this._currentStroke = []; 1317 for(var i=0,dx=0;i<str.length;i++,dx++) 1318 { 1319 var op = {}; 1320 op.ch = str.charAt(i); if(op.ch=="\n") { dx=-1; r++; continue; } 1321 op.type = "place"; 1322 var cell = {"r":r,"c":c+dx} 1323 if(cell.c < COLS) 1324 this.applyOpAtCell(cell,op); // display character on grid & push coordinate to currentStroke 1325 } 1326 this.pushStrokeIfNonEmpty(this._currentStroke); // feed undo buffer 1327 return this; 1328 } 1329 1330 /* 1331 * AsciiCAD-compatible box. 1332 * Original API: box(<c0>,<r0>,<c1>,<r1>, {kind:[boxStyle]}) 1333 */ 1334 this.box = function(c0,r0,c1,r1, modifiers) 1335 { 1336 //console.log("this.box("+c0+","+r0+","+c1+","+r1+","+JSON.stringify(modifiers)+")"); 1337 1338 if(c0===undefined || r0===undefined || c1===undefined || r1===undefined) return; // safe escape if no arguments provided 1339 if(modifiers===undefined) var modifiers = {kind:{ h:'â', v:'â', tl:'â', tr:'â', bl:'â', br:'â' }}; 1340 const path = this.buildBoxPath( {"c":c0,"r":r0} , {"c":c1,"r":r1} , modifiers); 1341 1342 // De-dup (corners overwrite edges) 1343 const m = new Map(); 1344 for (const p of path) 1345 { 1346 if (p.r < 0 || p.r >= ROWS || p.c < 0 || p.c >= COLS) continue; 1347 m.set(p.r + ',' + p.c, p.ch); 1348 } 1349 1350 this._currentStroke = []; 1351 for (const [key, ch] of m) 1352 { 1353 const [r, c] = key.split(',').map(Number); 1354 const cell = {"r":r,"c":c}; 1355 const op = {"ch":ch,"type":"place"}; 1356 this.applyOpAtCell( cell , op ); // display character on grid & build undo buffer 1357 } 1358 this.pushStrokeIfNonEmpty(this._currentStroke); // commit undo buffer 1359 } 1360 1361 this.buildBoxPath = function(start, end, modifiers) 1362 { 1363 const style = modifiers.kind; 1364 const r0 = start.r, c0 = start.c; 1365 const r1 = end.r, c1 = end.c; 1366 1367 const top = Math.min(r0, r1); 1368 const bot = Math.max(r0, r1); 1369 const left = Math.min(c0, c1); 1370 const right = Math.max(c0, c1); 1371 1372 const out = []; 1373 1374 // Degenerate: a line or a point â we still draw something useful 1375 if (top === bot && left === right) { 1376 out.push({ r: top, c: left, ch: style.tl }); // or '+'; your call 1377 return out; 1378 } 1379 1380 // Top edge 1381 for (let c = left + 1; c <= right - 1; c++) out.push({ r: top, c, ch: style.h }); 1382 // Bottom edge 1383 if (bot !== top) { 1384 for (let c = left + 1; c <= right - 1; c++) out.push({ r: bot, c, ch: style.h }); 1385 } 1386 1387 // Left edge 1388 for (let r = top + 1; r <= bot - 1; r++) out.push({ r, c: left, ch: style.v }); 1389 // Right edge 1390 if (right !== left) { 1391 for (let r = top + 1; r <= bot - 1; r++) out.push({ r, c: right, ch: style.v }); 1392 } 1393 1394 // Corners (only if box has width/height) 1395 out.push({ r: top, c: left, ch: style.tl }); 1396 if (right !== left) out.push({ r: top, c: right, ch: style.tr }); 1397 if (bot !== top) out.push({ r: bot, c: left, ch: style.bl }); 1398 if (bot !== top && right !== left) out.push({ r: bot, c: right, ch: style.br }); 1399 1400 return out; 1401 } 1402 1403 this.line = function(arg) 1404 { 1405 arg = arg || {}; 1406 var path = arg.path || []; 1407 var k = arg.wire || this.BOX_SINGLE; 1408 1409 function step(a,b) { return a < b ? 1 : (a > b ? -1 : 0); } 1410 1411 var stroke = []; 1412 for(var i=1;i<path.length;i++) 1413 { 1414 var c0 = Number(path[i-1][0]), r0 = Number(path[i-1][1]); 1415 var c1 = Number(path[i][0]), r1 = Number(path[i][1]); 1416 1417 var dc = step(c0,c1); 1418 while(c0 != c1) 1419 { 1420 this.applyOpAtCell({c:c0,r:r0},{ch:k.h,type:"place"}); 1421 c0 += dc; 1422 } 1423 1424 var dr = step(r0,r1); 1425 while(r0 != r1) 1426 { 1427 this.applyOpAtCell({c:c0,r:r0},{ch:k.v,type:"place"}); 1428 r0 += dr; 1429 } 1430 1431 this.applyOpAtCell({c:c1,r:r1},{ch:(c0 == c1 ? k.v : k.h),type:"place"}); 1432 } 1433 this.pushStrokeIfNonEmpty(this._currentStroke); 1434 1435 return this; 1436 }; 1437 1438 this.toString = function() 1439 { 1440 return ascii.map(function(row){ return row.join("").replace(/\s+$/,""); }).join("\n"); 1441 }; 1442 }; 1443 1444 this.textGrid = function(cols,rows,fill) 1445 { 1446 return new this.ASCII_GRID(cols,rows,fill); 1447 }; 1448 1449 1450 ///////////////////////////////// 1451 // TAB SWITCHING / ANIMATION // 1452 ///////////////////////////////// 1453 1454 this.TAB_TWEEN = function() 1455 { 1456 this.jobs = new WeakMap(); 1457 1458 this.ease = function(t) 1459 { 1460 /* 1461 * Small approximation of GSAP's default power1.out easing: 1462 * fast at the beginning, smooth near the end. 1463 */ 1464 return 1 - Math.pow(1 - t,2); 1465 }; 1466 1467 this.to = function(el,duration,vars) 1468 { 1469 if(!el) return; 1470 1471 duration = Math.max(0,Number(duration) || 0); 1472 vars = vars || {}; 1473 1474 var previous = this.jobs.get(el); 1475 if(previous) cancelAnimationFrame(previous); 1476 1477 var start = performance.now(); 1478 var from = {}; 1479 var to = {}; 1480 1481 for(var prop in vars) 1482 { 1483 if(prop == "duration") continue; 1484 from[prop] = parseFloat(getComputedStyle(el)[prop]) || 0; 1485 to[prop] = parseFloat(vars[prop]) || 0; 1486 } 1487 1488 var self = this; 1489 1490 function step(now) 1491 { 1492 var t = duration <= 0 ? 1 : Math.min(1,(now - start) / (duration * 1000)); 1493 var e = self.ease(t); 1494 1495 for(var prop in to) 1496 el.style[prop] = (from[prop] + (to[prop] - from[prop]) * e) + "px"; 1497 1498 if(t < 1) self.jobs.set(el,requestAnimationFrame(step)); 1499 else self.jobs.delete(el); 1500 } 1501 1502 step(start); 1503 }; 1504 } 1505 1506 this.TAB_SWITCHER = function(arg) 1507 { 1508 arg = arg || {}; 1509 1510 this.menuID = arg.menuID || "#app ul"; 1511 this.contentCL = arg.contentCL || ".tabs"; 1512 this.defID = arg.defID || ""; 1513 this.animDuration = arg.animDuration == null ? 0.1 : arg.animDuration; 1514 this.groups = []; 1515 this.animator = arg.animator || new _COM_this.TAB_TWEEN(); 1516 this.readhash = arg.readhash || function(event) { 1517 if(typeof event == "string") return event; 1518 if(event && typeof event.node == "string") return event.node; 1519 return location.hash.slice(1); 1520 }; 1521 1522 this.init = function() 1523 { 1524 this.initGroupsOnly(); 1525 this.bindClicks(); 1526 this.switchTab(); 1527 return this; 1528 }; 1529 1530 this.initGroupsOnly = function() 1531 { 1532 var menus = document.querySelectorAll(this.menuID); 1533 this.groups = []; 1534 1535 for(var i = 0; i < menus.length; i++) 1536 { 1537 var group = this.createGroup(menus[i],i); 1538 if(group) this.groups.push(group); 1539 } 1540 }; 1541 1542 this.createGroup = function(menuEl,index) 1543 { 1544 var links = []; 1545 1546 for(var i = 0; i < menuEl.childNodes.length; i++) 1547 if(menuEl.childNodes[i].tagName == "A" && menuEl.childNodes[i].href !== undefined) 1548 links.push(menuEl.childNodes[i]); 1549 1550 if(!links.length) return null; 1551 1552 var focusEl = menuEl.querySelector(".tab_div_end") || menuEl.querySelector(".focus-el"); 1553 var contentEl = this.findContentForMenu(menuEl); 1554 1555 if(!focusEl || !contentEl) return null; 1556 1557 menuEl.style.setProperty("--tab-count",String(links.length)); 1558 1559 /* 1560 * Keep the old second-slider markup inert. The v42 implementation uses 1561 * one focus element per menu and moves it directly to the active link. 1562 */ 1563 var focusEls = menuEl.querySelectorAll(".tab_div_end,.focus-el"); 1564 for(var f = 0; f < focusEls.length; f++) 1565 if(focusEls[f] !== focusEl) 1566 focusEls[f].style.display = "none"; 1567 1568 var defaultLink = null; 1569 for(var d = 0; d < links.length; d++) 1570 if(this.defID && links[d].id == this.defID) 1571 defaultLink = links[d]; 1572 1573 if(!defaultLink) defaultLink = links[0]; 1574 1575 return { 1576 index: index, 1577 menuEl: menuEl, 1578 focusEl: focusEl, 1579 contentEl: contentEl, 1580 links: links, 1581 defaultLink: defaultLink 1582 }; 1583 }; 1584 1585 this.findContentForMenu = function(menuEl) 1586 { 1587 var wrapper = menuEl.parentElement; 1588 var node = wrapper ? wrapper.nextElementSibling : null; 1589 1590 while(node) 1591 { 1592 if(node.classList && node.classList.contains("tabs")) 1593 return node; 1594 node = node.nextElementSibling; 1595 } 1596 1597 var parent = wrapper ? wrapper.parentElement : null; 1598 while(parent) 1599 { 1600 for(var i = 0; i < parent.children.length; i++) 1601 if(parent.children[i].classList && parent.children[i].classList.contains("tabs")) 1602 return parent.children[i]; 1603 1604 parent = parent.parentElement; 1605 } 1606 1607 return null; 1608 }; 1609 1610 this.bindClicks = function() 1611 { 1612 var self = this; 1613 1614 for(var i = 0; i < this.groups.length; i++) 1615 { 1616 for(var j = 0; j < this.groups[i].links.length; j++) 1617 { 1618 this.groups[i].links[j].onclick = function() 1619 { 1620 /* 1621 * The browser updates location.hash after this click. 1622 * setTimeout also handles clicks on the already-selected hash. 1623 */ 1624 window.setTimeout(function(){ self.switchTab(); },0); 1625 }; 1626 } 1627 } 1628 }; 1629 1630 this.cleanTabId = function(id) 1631 { 1632 id = String(id || "").replace(/^#/,""); 1633 if(id.indexOf("_") > 0) id = id.split("_")[0]; 1634 return id; 1635 }; 1636 1637 this.linkTarget = function(link) 1638 { 1639 return link.hash ? link.hash.split("#")[1] : ""; 1640 }; 1641 1642 this.findTargetLink = function(group,target) 1643 { 1644 if(target) 1645 { 1646 for(var i = 0; i < group.links.length; i++) 1647 if(this.linkTarget(group.links[i]) == target) 1648 return group.links[i]; 1649 1650 /* 1651 * Prefix match handles parent tabs when the hash points deeper: 1652 * #tab2-1 activates #tab2 in the parent group. 1653 */ 1654 for(var j = 0; j < group.links.length; j++) 1655 { 1656 var linkId = this.linkTarget(group.links[j]); 1657 if(target.indexOf(linkId + "-") == 0) 1658 return group.links[j]; 1659 } 1660 } 1661 1662 return group.defaultLink; 1663 }; 1664 1665 this.switchTab = function(event) 1666 { 1667 if(!this.groups.length) this.initGroupsOnly(); 1668 1669 var target = this.cleanTabId(this.readhash(event)); 1670 1671 for(var i = 0; i < this.groups.length; i++) 1672 { 1673 var group = this.groups[i]; 1674 var link = this.findTargetLink(group,target); 1675 1676 if(link) 1677 { 1678 this.switchTabHeader(group,link); 1679 this.switchTabContent(group,this.linkTarget(link)); 1680 } 1681 } 1682 if(_COM_this.POPUP && typeof(_COM_this.POPUP.syncScopes) == "function") 1683 _COM_this.POPUP.syncScopes(); 1684 1685 return true; 1686 }; 1687 1688 this.switchTabHeader = function(group,link) 1689 { 1690 var menuRect = group.menuEl.getBoundingClientRect(); 1691 var linkRect = link.getBoundingClientRect(); 1692 1693 this.animator.to(group.focusEl,this.animDuration,{ 1694 left: linkRect.left - menuRect.left, 1695 width: linkRect.width 1696 }); 1697 }; 1698 1699 this.switchTabContent = function(group,sel_id) 1700 { 1701 var node = group.contentEl.childNodes; 1702 1703 for(var i
1703= 0; i < node.length; i++) 1704 { 1705 if(!node[i].id || !node[i].classList) continue; 1706 if(node[i].id == sel_id) node[i].classList.add("active"); 1707 else node[i].classList.remove("active"); 1708 } 1709 }; 1710 } 1711 1712 this.tabSwitch = function(arg) 1713 { 1714 return new this.TAB_SWITCHER(arg || {}); 1715 } 1716 1717 1718 /////// GUI FUNCTIONS //////////////////////////////////////////////////////////////////////////////////////// 1719 1720 1721 ///////////////////////////////////// 1722 // WRITE VALUES TO ANY TAG ELEMENT // 1723 ///////////////////////////////////// 1724 1725 this.writeDisplay = function(n,v,f) 1726 { 1727 // n = el, v = value, f = extra HTML 1728 var obj,tagname,bAppend = typeof(f)!="undefined"; 1729 if(document.getElementById) { obj=document.getElementById(n); tagname = obj.tagName.toUpperCase() } 1730 else if (document.all) { obj=document.all[n] } 1731 if(!obj) return; 1732 1733 switch(tagname) 1734 { 1735 case "INPUT": 1736 switch(f) 1737 { 1738 case "beforebegin":obj.value += v; break; // less common 1739 case "afterbegin": obj.value = v + obj.value; break; // less common 1740 case "afterend": obj.value = v + obj.value; break; 1741 case "beforeend": obj.value += v; break; 1742 default: obj.value = v; 1743 } 1744 break; 1745 default: 1746 if(bAppend) obj.insertAdjacentHTML(f,v); 1747 else obj.innerHTML=v; 1748 } 1749 } 1750 1751 this.POPUP = { 1752 create: function(target_id) 1753 { 1754 var obj = Object.create(this); 1755 obj.target_id = target_id; 1756 obj.id=""; 1757 obj.states={}; 1758 obj.rules={}; 1759 obj.scopes={}; 1760 this.el(obj.target_id.dest).innerHTML = obj.target_html(); 1761 return obj 1762 }, 1763 id:"", 1764 states:{}, 1765 rules:{}, 1766 scopes:{}, 1767 target_id:{"html":"COM_popup","html_txt":"COM_popup_text","class":"appbut","float":false}, 1768 target_html:function(){return "<div class=appbox id=\""+this.target_id.html+"\" style=\""+(this.target_id.float?"position: absolute; z-index: 3; left: 0px; top: 120px;":"")+"\" hidden=\"\"><div id=\""+this.target_id.html_txt+"\"></div></div>"}, 1769 target_btn:function(arg){return "<div class=\"appbut\" onclick=\"oCOM.POPUP.toggle('"+this.target_id.html+"');document.getElementById('"+this.target_id.html_txt+"').innerHTML='';"+(arg===undefined?"":arg.onclick)+"\" style=\"text-align:center;float:right;\">x</div>"}, 1770 el: function(id) { return document.getElementById(id) }, 1771 addRule: function(id,function_obj) { this.rules[id] = function_obj }, 1772 runRule: function(id) { this.id = id; this.rules[id](this) }, 1773 set_state: function(id,val) { this.states[id] = val; this.syncScope(id); }, 1774 get_state: function(id){ return this.states[id]===undefined?this.el(id).classList.item(1):this.states[id]}, 1775 update_state: function(id,el){ this.states[id] = el.hidden }, 1776 1777 addScope: function(id,owner_id) 1778 { 1779 this.scopes[id] = owner_id; 1780 1781 var el = this.el(id); 1782 if(el && this.states[id]===undefined) 1783 this.states[id] = !!el.hidden; 1784 1785 this.syncScope(id); 1786 }, 1787 scopeActive: function(owner_id) 1788 { 1789 var owner = this.el(owner_id); 1790 if(!owner || owner.hidden) return false; 1791 1792 /* 1793 * A popupHost can be visible itself while its top-level tab is inactive. 1794 * Find the direct child of a .tabs container and require its active class. 1795 */ 1796 var node = owner; 1797 while(node && node.parentElement) 1798 { 1799 if(node.parentElement.classList && 1800 node.parentElement.classList.contains("tabs")) 1801 return node.classList.contains("active"); 1802 1803 node = node.parentElement; 1804 } 1805 1806 return true; 1807 }, 1808 syncScope: function(id) 1809 { 1810 var owner_id = this.scopes[id]; 1811 if(owner_id===undefined) return this.el(id); 1812 1813 var el = this.el(id); 1814 if(!el) return null; 1815 1816 if(this.states[id]===undefined) 1817 this.states[id] = !!el.hidden; 1818 1819 /* 1820 * states[id] is the user's requested state. 1821 * el.hidden may additionally be forced while the owner toolbox/tab is 1822 * inactive, without overwriting that requested state. 1823 */ 1824 el.hidden = !!this.states[id] || !this.scopeActive(owner_id); 1825 return el; 1826 }, 1827 syncScopes: function() 1828 { 1829 for(var id in this.scopes) 1830 if(Object.prototype.hasOwnProperty.call(this.scopes,id)) 1831 this.syncScope(id); 1832 }, 1833 on: function(id) 1834 { var el = this.el(id); 1835 if(!el) return null; 1836 this.states[id] = false; el.hidden = false; this.syncScopes(); return el; 1837 }, 1838 off: function(id) 1839 { var el = this.el(id); 1840 if(!el) return null; 1841 this.states[id] = true; el.hidden = true; this.syncScopes(); return el; 1842 }, 1843 toggle: function(id) 1844 { 1845 var el = this.el(id); 1846 if(!el) return null; 1847 1848 /* 1849 * A scoped popup may be physically hidden because its owner is inactive 1850 * while remaining logically open. Toggle that logical state. 1851 */ 1852 var hidden = this.scopes[id]!==undefined && this.states[id]!==undefined 1853 ? !!this.states[id] 1854 : !!el.hidden; 1855 1856 this.states[id] = !hidden; 1857 el.hidden = this.states[id]; 1858 this.syncScopes(); 1859 return el; 1860 }, 1861 1862 title_body_html: function(title_html, body_html, body_id, body_class) 1863 { 1864 return "" 1865 +"<div class='com_popup_title'>" 1866 +title_html 1867 +"</div>" 1868 +"<div" 1869 +(body_id ? " id='"+body_id+"'" : "") 1870 +" class='"+(body_class || "com_popup_body com_scroll_xy")+"'" 1871 +">" 1872 +body_html 1873 +"</div>"; 1874 }, 1875 get_class: function(el,idx) { return el.classList.item(idx===un
1875defined?0:idx) }, 1876 set_class: function(el,class1,class2,bool) 1877 { 1878 el.hidden = bool; 1879 if(bool) {el.classList.replace(class2,class1);this.states[el.id]=class1} 1880 else {el.classList.replace(class1,class2);this.states[el.id]=class2} 1881 //if(bool) {el.classList.remove(class2);el.classList.add(class1);this.states[el.id]=class1} 1882 //else {el.classList.remove(class1);el.classList.add(class2);this.states[el.id]=class2} 1883 }, 1884 toggle_class: function(el,class1,class2) 1885 { 1886 this.toggle(el.id); 1887 this.set_class(el,class1,class2,el.hidden); 1888 }, 1889 wipe: function() 1890 { 1891 document.getElementById(this.target_id.html_txt).innerHTML = ""; 1892 }, 1893 html: function(html,ttl) 1894 { 1895 var e = oCOM.POPUP.toggle(this.target_id.html); 1896 if(e==null) return null; 1897 if(html==null) e.hidden = true; 1898 if(ttl) { setTimeout( COM_PopupHTML,ttl*1000 ); e.hidden = false }; 1899 document.getElementById(this.target_id.html_txt).innerHTML += !e.hidden?(html+"<br>"):""; 1900 } 1901 } 1902 1903 this.GetHTTP = function(url,responsetype,callback_function,arg,error_callback) 1904 { 1905 // random value (workaround to avoid caching) 1906 var r = ""; //"?"+btoa(Math.round(Math.random(1)*6*6*6)+"").replace(new RegExp("=","g"),""); 1907 const xhttp = new XMLHttpRequest(); 1908 xhttp.responseType = responsetype; // check: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType 1909 xhttp["arg"] = arg; 1910 xhttp.onload = callback_function; 1911 if(typeof error_callback=="function") 1912 { 1913 xhttp.onerror = error_callback; 1914 xhttp.ontimeout = error_callback; 1915 } 1916 xhttp.open("GET", url+r); 1917 xhttp.send(); 1918 } 1919 1920 1921 /* 1922 * Shared GitHub-backed catalog browser. 1923 * 1924 * Consumers provide only the repository/path plus adapter callbacks. The 1925 * directory request/cache, parent navigation and Name/Size rendering live 1926 * here so Disk II, Assembler and future catalog users do not duplicate it. 1927 */ 1928 this.CATALOG = { 1929 cache:{}, 1930 instances:{}, 1931 1932 create:function(cfg) 1933 { 1934 cfg = Object.assign({ 1935 id:"catalog" 1936 ,popupID:null 1937 ,title:"CATALOG" 1938 ,owner:"" 1939 ,repo:"" 1940 ,ref:"main" 1941 ,basepath:"" 1942 ,path:"" 1943 ,fileIcon:"fa fa-file" 1944 ,popupStyle:"position:absolute;z-index:3;left:800px;top:32px;width:450px;height:450px;text-align:left;padding:0px;margin:0px" 1945 },cfg || {}); 1946 1947 cfg.popupID = cfg.popupID || (cfg.id + "_popup"); 1948 1949 var inst = { 1950 id:cfg.id 1951 ,cfg:cfg 1952 ,arg:{ 1953 id:cfg.id 1954 ,owner:cfg.owner 1955 ,repo:cfg.repo 1956 ,ref:cfg.ref 1957 ,basepath:cfg.basepath 1958 ,path:cfg.path || cfg.basepath 1959 } 1960 }; 1961 1962 inst.open = function(arg) { return _COM_this.CATALOG.open(inst.id,arg); }; 1963 inst.render = function(arg) { return _COM_this.CATALOG.render(inst.id,arg); }; 1964 inst.close = function() { return _COM_this.CATALOG.close(inst.id); }; 1965 1966 this.instances[inst.id] = inst; 1967 return inst; 1968 }, 1969 1970 ensurePopup:function(popupID,cssText) 1971 { 1972 var popup = document.getElementById(popupID); 1973 if(popup) return popup; 1974 1975 popup = document.createElement("div"); 1976 popup.id = popupID; 1977 popup.hidden = true; 1978 popup.className = "appbox com_popup_frame"; 1979 popup.style.cssText = cssText || 1980 "position:absolute;z-index:3;left:800px;top:32px;width:450px;height:450px;text-align:left;padding:0px;margin:0px"; 1981 document.body.appendChild(popup); 1982 return popup; 1983 }, 1984 1985 githubContentsURL:function(arg) 1986 { 1987 arg = arg || {}; 1988 var ref = arg.ref || "main"; 1989 var path = arg.path || ""; 1990 return "https://api.github.com/repos/" 1991 + arg.owner + "/" + arg.repo + "/contents/" + path 1992 + "?ref=" + encodeURIComponent(ref); 1993 }, 1994 1995 githubRawURL:function(arg) 1996 { 1997 arg = arg || {}; 1998 var ref = arg.ref || "main"; 1999 var rawPath = String(arg.path || "") 2000 .split("/") 2001 .map(function(p){ return encodeURIComponent(p); }) 2002 .join("/"); 2003 2004 return "https://raw.githubusercontent.com/"
2005 + arg.owner + "/" + arg.repo + "/" 2006 + encodeURIComponent(ref) + "/" + rawPath; 2007 }, 2008 2009 isRateLimitResponse:function(xhr) 2010 { 2011 if(!xhr) return false; 2012 2013 var status = Number(xhr.status); 2014 var remaining = "", retryAfter = ""; 2015 try 2016 { 2017 remaining = xhr.getResponseHeader("x-ratelimit-remaining") || ""; 2018 retryAfter = xhr.getResponseHeader("retry-after") || ""; 2019 } 2020 catch(e) {} 2021 2022 if(remaining == "0") return true; 2023 if(status != 403 && status != 429) return false; 2024 2025 var message = String(xhr.responseText || xhr.response || ""); 2026 try 2027 { 2028 var json = JSON.parse(message); 2029 if(json && json.message) message = json.message; 2030 } 2031 catch(e) {} 2032 2033 return retryAfter != "" || /rate\s+limit/i.test(message); 2034 }, 2035 2036 isOffline:function() 2037 { 2038 return typeof navigator != "undefined" && navigator.onLine === false; 2039 }, 2040 2041 offlineDirectory:function(table,url) 2042 { 2043 if(!table) return null; 2044 2045 var entry = table[url]; 2046 if(entry === undefined) return null; 2047 2048 if(Array.isArray(entry)) 2049 return {list:_COM_this.cloneJSON(entry)}; 2050 2051 return { 2052 list:_COM_this.cloneJSON(entry.list || []) 2053 ,arg:entry.arg === undefined ? undefined : _COM_this.cloneJSON(entry.arg) 2054 }; 2055 }, 2056 2057 offlineData:function(table,arg,fullPath,output) 2058 { 2059 if(!table) return null; 2060 2061 arg = arg || {}; 2062 var rec = table[arg.path] || table[arg.name] || table[fullPath]; 2063 if(rec === undefined) return null; 2064 2065 return _COM_this.decodeDataRecord(rec,output); 2066 }, 2067 2068 pack:function(arg) 2069 { 2070 return encodeURIComponent(JSON.stringify(arg || {})); 2071 }, 2072 2073 unpack:function(txt) 2074 { 2075 try { return JSON.parse(decodeURIComponent(String(txt || ""))); } 2076 catch(e) { return {}; } 2077 }, 2078 2079 open:function(id,arg) 2080 { 2081 var inst = this.instances[id]; 2082 if(!inst) return false; 2083 this.ensurePopup(inst.cfg.popupID,inst.cfg.popupStyle); 2084 oCOM.POPUP.on(inst.cfg.popupID); 2085 return this.render(id,arg); 2086 }, 2087 2088 close:function(id) 2089 { 2090 var inst = this.instances[id]; 2091 if(!inst) return false; 2092 oCOM.POPUP.off(inst.cfg.popupID); 2093 return true; 2094 }, 2095 2096 navigate:function(id,packedArg) 2097 { 2098 return this.render(id,this.unpack(packedArg)); 2099 }, 2100 2101 select:function(id,packedArg) 2102 { 2103 var inst = this.instances[id]; 2104 if(!inst) return false; 2105 var arg = this.unpack(packedArg); 2106 if(typeof inst.cfg.onFile == "function") return inst.cfg.onFile(arg,inst); 2107 return false; 2108 }, 2109 2110 render:function(id,arg) 2111 { 2112 var inst = this.instances[id]; 2113 if(!inst) return false; 2114 2115 arg = Object.assign({},inst.arg,arg || {}); 2116 arg.id = inst.id; 2117 arg.owner = arg.owner || inst.cfg.owner; 2118 arg.repo = arg.repo || inst.cfg.repo; 2119 arg.ref = arg.ref || inst.cfg.ref || "main"; 2120 arg.basepath = arg.basepath || inst.cfg.basepath || ""; 2121 arg.path = arg.path || arg.basepath; 2122 inst.arg = Object.assign({},arg); 2123 2124 this.ensurePopup(inst.cfg.popupID,inst.cfg.popupStyle); 2125 if(typeof inst.cfg.onNavigate == "function") inst.cfg.onNavigate(Object.assign({},arg),inst); 2126 2127 var self = this; 2128 return this.list(inst,arg,function(err,list,resolvedArg) 2129 { 2130 if(err) 2131 { 2132 if(typeof inst.cfg.onError == "function") inst.cfg.onError(err,inst); 2133 else console.warn("Catalog directory listing failed",err); 2134 return false; 2135 } 2136 self.renderList(inst,list,resolvedArg); 2137 }); 2138 }, 2139 2140 list:function(inst,arg,callback) 2141 { 2142 var self = this; 2143 var url = this.githubContentsURL(arg); 2144 2145 function useFallback(reason) 2146 { 2147 if(typeof inst.cfg.offlineProvider != "function") return false; 2148 delete self.cache[url]; 2149 var offline = inst.cfg.offlineProvider(url,Object.assign({},arg),reason); 2150 if(!offline) return false; 2151 callback(null,offline.list || offline,offline.arg || arg); 2152 return true; 2153 } 2154 2155 if(this.isOffline()) 2156 { 2157 if(useFallback("navigator.onLine=false")) return true; 2158 callback({ 2159 status:0 2160 ,message:"Offline: no embedded catalog data for " + String(arg.path || "") 2161 ,url:url 2162 }); 2163 return false;
2164 } 2165 2166 if(this.cache[url] !== undefined) 2167 { 2168 var cached = this.cache[url]; 2169 callback(null,cached.list,Object.assign({},arg)); 2170 return true; 2171 } 2172 2173 oCOM.GetHTTP(url,"text",function() 2174 { 2175 if(this.status != 200) 2176 { 2177 if(Number(this.status)==0 && useFallback("network error (HTTP 0)")) return; 2178 if(self.isRateLimitResponse(this) && useFallback("GitHub API rate limit (HTTP " + this.status + ")")) return; 2179 callback({status:this.status,message:this.responseText || this.response,url:url}); 2180 return; 2181 } 2182 2183 try 2184 { 2185 var json = JSON.parse(this.responseText || this.response); 2186 if(!Array.isArray(json)) throw new Error("GitHub contents response is not a directory listing"); 2187 2188 var list = json.map(function(e) 2189 { 2190 return { 2191 name:e.name 2192 ,path:e.path 2193 ,type:e.type 2194 ,size:e.size 2195 ,download_url:e.download_url 2196 ,html_url:e.html_url 2197 }; 2198 }); 2199 2200 self.cache[url] = {list:list}; 2201 callback(null,list,Object.assign({},arg)); 2202 } 2203 catch(e) 2204 { 2205 callback({status:this.status,message:e.message,url:url}); 2206 } 2207 },null,function() 2208 { 2209 if(useFallback("network error")) return; 2210 callback({ 2211 status:this.status || 0 2212 ,message:"Network error" 2213 ,url:url 2214 }); 2215 2216 2217 }); 2218 2219 return true; 2220 }, 2221 2222 renderList:function(inst,list,arg) 2223 { 2224 var cfg = inst.cfg; 2225 if(typeof cfg.filter == "function") list = list.filter(cfg.filter); 2226 2227 var dirs = list.filter(function(e){ return e.type == "dir"; }); 2228 var files = list.filter(function(e){ return e.type == "file"; }); 2229 var rows = dirs.concat(files); 2230 var html = ""; 2231 2232 if(arg.path != arg.basepath) 2233 { 2234 var parentArg = Object.assign({},arg); 2235 var parent = String(arg.path || "").split("/"); 2236 parent.pop(); 2237 parentArg.path = parent.join("/") || arg.basepath; 2238 if(arg.basepath && parentArg.path.indexOf(arg.basepath) !== 0) parentArg.path = arg.basepath; 2239 2240 html += '<div class="appbut" style="width:25px;cursor:pointer" title="Parent directory"' 2241 + ' data-cat-action="navigate" data-cat-arg="' + this.pack(parentArg) + '">' 2242 + '<i class="fa fa-arrow-alt-circle-up"></i></div>'; 2243 } 2244 2245 html += '<div style="margin-top:6px;"><table style="width:100%;border-collapse:collapse;font-size:11px;">' 2246 + '<tr><th style="text-align:left;padding:2px 4px;">Name</th>' 2247 + '<th style="text-align:left;padding:2px 4px;">Size</th></tr>'; 2248 2249 for(var i=0;i<rows.length;i++) 2250 { 2251 var row = rows[i]; 2252 var isDir = row.type == "dir"; 2253 var nextArg = Object.assign({},arg,row,{path:row.path}); 2254 var packed = this.pack(nextArg); 2255 var icon = isDir ? "fa fa-folder" : cfg.fileIcon; 2256 var action = isDir ? "navigate" : "select"; 2257 2258 html += '<tr>' 2259 + '<td style="text-align:left;vertical-align:top;border-top:1px solid #888;padding:2px 4px;">' 2260 + '<div style="cursor:pointer" data-cat-action="' + action + '" data-cat-arg="' + packed + '">' 2261 + '<i class="' + icon + '"></i> ' + _COM_this.escapeHTML(row.name) + '</div></td>' 2262 + '<td style="text-align:left;vertical-align:top;border-top:1px solid #888;padding:2px 4px;">' 2263 + _COM_this.escapeHTML(isDir ? "-" : row.size) + '</td></tr>'; 2264 } 2265 html += '</table></div>'; 2266 2267 var title = typeof cfg.titleHTML == "function" 2268 ? cfg.titleHTML(inst,arg) 2269 : "<span>" + _COM_this.escapeHTML(cfg.title || "CATALOG") + "</span>"; 2270 2271 title = '<div style="display:flex;align-items:center;gap:6px;width:100%;min-width:0;">' 2272 + title 2273 + '<span style="flex:1 1 auto;"></span>' 2274 + '<div class="appbut" data-cat-close="1"' 2275 + ' style="text-align:center;flex:0 0 auto;">x</div>' 2276 + '</div>'; 2277 2278 var popup = this.ensurePopup(cfg.popupID,cfg.popupStyle); 2279 popup.innerHTML = oCOM.POPUP.title_body_html( 2280 title 2281 ,html 2282 ,inst.id + "_body" 2283 ,"com_popup_body com_scroll_xy" 2284 ); 2285 2286 var body = document.getElementById(inst.id + "_body"); 2287 if(body) 2288 { 2289 body.onclick = function(ev) 2290 { 2291 var el = ev.target && ev.target.closest ? ev.target.closest("[data-cat-action]") : null; 2292 if(!el || !body.contains(el)) return; 2293 var action = el.getAttribute("data-cat-action"); 2294 var packedArg = el.getAttribute("data-cat-arg") || ""; 2295 if(action == "navigate") oCOM.CATALOG.navigate(inst.id,packedArg); 2296 else if(action == "select") oCOM.CATALOG.select(inst.id,packedArg); 2297 }; 2298 } 2299 2300 var close = popup.querySelector("[data-cat-close='1']"); 2301 if(close) close.onclick = function(ev){ ev.stopPropagation(); oCOM.CATALOG.close(inst.id); }; 2302 return true; 2303 } 2304 }; 2305 2306 this.Download = function(fileName, data) 2307 { 2308 if(data.length===undefined) return; 2309 2310 var ui8 = new Uint8Array(data.length); 2311 for(var i=0;i<data.length;i++) 2312 ui8[i] = data[i]; 2313 var url, mimeType = 'application/octet-stream'; 2314 var blob = new Blob([ui8], {type: mimeType}); 2315 var url = window.URL.createObjectURL(blob); 2316 2317 console.log("downloadURL('"+url+"', '"+fileName+"')") 2318 downloadURL(url, fileName); 2319 setTimeout(function() { 2320 return window.URL.revokeObjectURL(url); 2321 }, 2000); 2322 } 2323 2324 function downloadURL(data, fileName) { 2325 var a; 2326 a = document.createElement('a'); 2327 a.href = data; 2328 a.download = fileName; 2329 document.body.appendChild(a); 2330 a.style = 'display: none'; 2331 a.click(); 2332 a.remove(); 2333 }; 2334 2335 this.UploadData = new Array(); 2336 this.Upload = function(elementId,callback) 2337 { 2338 this.UploadData = []; 2339 var file = document.getElementById(elementId).files[0]; 2340 if (!file) return; 2341 oCOM.UploadName = file.name; 2342 2343 this.fread = new FileReader(); 2344 this.fread.readAsArrayBuffer(file); 2345 if(typeof(callback)!="function") callback = function() { alert("test") }; // override if needed 2346 else this.callback = callback; 2347 2348 function handleEvent(obj) 2349 { 2350 return function(levent) 2351 { 2352 var data = new DataView(levent.target.result); 2353 var size = levent.target.result.byteLength; 2354 this.UploadData = Array(size); 2355 for (var i = 0; i < size; i++) 2356 obj.UploadData[i] = data.getUint8(i); 2357 obj.callback(obj.UploadData); 2358 } 2359 } 2360 this.fread.addEventListener('load',handleEvent(this)); 2361 2362 } 2363 2364 2365 2366 this.roughSizeOfObject = function(object) 2367 { 2368 const stack = [object]; 2369 const seen = new WeakSet(); 2370 let bytes = 0; 2371 2372 while (stack.length) 2373 { 2374 const value = stack.pop(); 2375 switch (typeof value) 2376 { 2377 case 'boolean': bytes += 4; break; 2378 case 'string': bytes += value.length * 2; break; // UTF-16 assumption 2379 case 'number': bytes += 8; break; // IEEE-754 double 2380 case 'bigint': bytes += 8; break; // very rough 2381 case 'symbol': 2382 case 'function': 2383 case 'undefined': break; 2384 case 'object': 2385 if (value === null) break; 2386 if (seen.has(value)) break; 2387 seen.add(value); 2388 bytes += 16; // Count object overhead â completely engine-dependent 2389 2390 // Handle Maps, Sets, etc. 2391 if (value instanceof Map) { stack.push(...value.keys(), ...value.values()); break; } 2392 if (value instanceof Set) { stack.push(...value.values()); break; } 2393 if (ArrayBuffer.isView(value)) { bytes += value.byteLength; break; } 2394 if (value instanceof ArrayBuffer) { bytes += value.byteLength; break; } 2395 2396 // Walk own keys (including non-enumerable & symbols) 2397 for (const key of Reflect.ownKeys(value)) { bytes += key.toString().length * 2; stack.push(value[key]);} 2398 break; 2399 } 2400 } 2401 return bytes; 2402 } 2403 2404 2405 this.SystemSelect = function(id,tab) 2406 { 2407 var def = _TABS[tab]["SYS"]; 2408 var s = "<select onchange=\"document.getElementById('"+id+"').innerHTML=oCOM.onSystemSelect(this.value,'"+tab+"')\">"; 2409 for(var i in _CFG_SYSCODE) 2410 s+= "<option value='"+i+"' "+(i==def?"selected":"")+">"+_CFG_SYSCODE[i].Model+"</option>"; 2411 return(s+"</select>"); 2412 } 2413 2414 this.onSystemSelect = function(val,tab) 2415 { 2416 _TABS[tab].SYS = val; 2417 2418 var s = ""; 2419 for(var i in _CFG_SYSCODE[val]) 2420 { 2421 if(i!="Model" && i!="ROMS") 2422 { 2423 if(_CFG_SYSCODE[val][i] && _CFG_SYSCODE[val][i].split(",").length>1) 2424 { 2425 this.onSysOptionSelect(i,tab,0); 2426 var name = tab+"_"+i 2427 s+="<div style=display:flex;>" 2428 +i+":" 2429 +"<button onclick=oCOM.onSysOptionSelect('"+i+"','"+tab+"',-1)><i class='fa fa-arrow-alt-circle-left'></i></button>" 2430 +"<input name='"+name+"' value='"+_CFG_SYSCODE[val][i].split(",")[0]+"' size=7>" // TODO NAVIGATE THROUGH OPTIONS 2431 +"<button onclick=oCOM.onSysOptionSelect('"+i+"','"+tab+"',1)><i class='fa fa-arrow-alt-circle-right'></i></button>" 2432 +"</div>" 2433 } 2434 else 2435 s+= i+":"+_CFG_SYSCODE[val][i]+"<br>"; 2436 } 2437 } 2438 return s; 2439 } 2440 2441 this.onSysOptionSelect = function(option,tab,up_dn) 2442 { 2443 var sys = _TABS[tab]["SYS"]; 2444 var arr = _CFG_SYSCODE[sys][option].split(","); 2445 var idx = 0; 2446 if(typeof(_TABS[tab][option])!="undefined") 2447 { 2448 for(var i=0;i<arr.length;i++) 2449 if(arr[i]==_TABS[tab][option]) idx=i; 2450 idx += up_dn; 2451 if(idx<0) idx=arr.length-1; 2452 if(idx==arr.length) idx=0; 2453 } 2454 _TABS[tab][option] = arr[idx]; 2455 var name = tab+"_"+option; 2456 var el = document.getElementsByName(name); 2457 if(el.length>0) 2458 el[0].value = arr[idx]; 2459 2460 //var el = el[0]; 2461 //el.value = arr[idx] 2462 //.val.value = arr[idx];
2463 //alert("_TABS['"+tab+"']['"+option+"'] = '"+arr[idx]+"'"); 2464 } 2465 2466 ///////////////////// 2467 // URL PARSER // 2468 ///////////////////// 2469 2470 this.URL = 2471 { 2472 url:"", 2473 uri:{}, 2474 hash:"", 2475 merge: function (_uri1,_uri2) 2476 { 2477 var _o = new Array(); 2478 for(_i in _uri1) 2479 _o[_i] = _uri1[_i]; 2480 for(_i in _uri2) 2481 _o[_i] = _uri2[_i]; 2482 return _o[_i]; 2483 }, 2484 addURI: function (_uri2) 2485 { 2486 for(_i in _uri2) 2487 if(_uri2[_i]) 2488 this.uri[_i] = _uri2[_i]; // add or override 2489 else 2490 delete this.uri[_i]; 2491 }, 2492 getURI: function () 2493 { 2494 var _str = "" 2495 for(_i in this.uri) 2496 { 2497 if(this.uri[_i]) 2498 _str += (_str?"&":"?") + _i + "=" + this.uri[_i]; 2499 } 2500 return _str; 2501 }, 2502 parse: function (_url) 2503 { 2504 var _ppos = _url.lastIndexOf("#"); 2505 if(_ppos>0) { _url = _url.substring(0,_ppos); this.hash = _url.hash } 2506 var _urlarr = _url?_url.split("?"):new Array("",""); 2507 var _urlargs = _urlarr[1]?_urlarr[1].split("&"):new Array(_urlarr[0],""); 2508 this.url = _urlarr[0]; 2509 2510 this.uri = {}; 2511 for(var _i=0;_i<_urlargs.length;_i++) 2512 { 2513 if(!(_urlargs[_i]===undefined)) 2514 { 2515 var a = _urlargs[_i].split("="); 2516 this.uri[ a[0] ] = a[1]; 2517 } 2518 } 2519 } 2520 } 2521 2522 ///////////////////// 2523 // EVENT SEQUENCER // 2524 ///////////////////// 2525 2526 this.addToEventStack = function(event,func) 2527 { 2528 console.log("addToEventStack("+event+" "+func.name+")"); 2529 if(o_event==null) var o_event = {}; 2530 o_event[event] = window[event]; 2531 if (typeof window[event] != "function") window[event] = func; 2532 else { window[event] = function() { if (o_event[event]) o_event[event](); func() } }; 2533 } 2534 2535 2536 ///////////////////////////////// 2537 // DASHBOARD REFRESH SEQUENCER // 2538 ///////////////////////////////// 2539 2540 // array of functions is called at the pace of EMU_DashboardRefresh_s = Dashboard updates per second 2541 this.RefreshEvent_arr = {}; 2542 this.bRefreshEvent = false; 2543 2544 this.addRefreshEvent = function(func,name,active) 2545 { 2546 if(!this.RefreshEvent_arr[ name ]) console.log("addRefreshEvent("+name+")"); 2547 else console.log("addRefreshEvent("+name+") -> already exists (no problem)"); 2548 this.RefreshEvent_arr[ name ] = {"func":func,"active":active} 2549 this.checkActiveRefreshEvents(); 2550 } 2551 2552 this.toggleRefreshEvent = function(name) 2553 { 2554 if(!this.RefreshEvent_arr[name]) return false; 2555 this.RefreshEvent_arr[ name ].active = !this.RefreshEvent_arr[ name ].active; 2556 this.checkActiveRefreshEvents(); 2557 return this.RefreshEvent_arr[ name ].active; 2558 } 2559 2560 this.checkActiveRefreshEvents = function() 2561 { 2562 var bActive = false; // if any of the events is active, keep bRefreshEvent = true 2563 for(var _o in this.RefreshEvent_arr) 2564 if(this.RefreshEvent_arr[_o].active) bActive = true; 2565 this.bRefreshEvent = bActive; 2566 } 2567 2568 this.enableRefreshEvent = function(name, active) 2569 { 2570 if(!this.RefreshEvent_arr[name]) return false; 2571 this.RefreshEvent_arr[name].active = !!active; 2572 this.checkActiveRefreshEvents(); 2573 return this.RefreshEvent_arr[name].active; 2574 } 2575} 2576 2577oCOM = new COM(); 2578 2579var oMEMGRID = new function() 2580{ 2581 if(typeof(this.bDebug)=="undefined") this.bDebug = false; 2582 this.oCOM = new COM(); 2583 2584 const mem_gran = 8; // block granularity in bits 2585 this.mem_gran = mem_gran; 2586 2587 this.build_mem_map = function(layout) 2588 { 2589 for(var i in layout) 2590 { 2591 var a = i.split("-"); var b = [parseInt(a[0],16),parseInt(a[1],16),a[0].length]; 2592 for(var addr=b[0],s="";addr<b[1];addr+=1<<mem_gran) 2593 { 2594 this.mem_pg[addr>>mem_gran] = layout[i][2]; 2595 if(this.bDebug) console.log("this.mem_pg["+(addr>>mem_gran)+"] = "+layout[i][2]); 2596 s += (addr>>mem_gran)+"="+layout[i][2]+" "; 2597 } 2598 //console.log(a[0]+"-"+a[1]+" ("+s+")"); 2599 } 2600 } 2601 2602 this.mem_pg = new Array(0x10000>>mem_gran); 2603 2604 this.line = function(start,len,step,digits) 2605 { 2606 var end = start+len*step; 2607 for(var j=start,a=[],ii=0;j<end;j+=step) a[ii++] = this.oCOM.getHexMulti(j,digits); 2608 return a; 2609 } 2610 2611 this.conf_grid = {id_prefix:"m",digits:4}; 2612 this.build_grid = function(start,len,step,cnf) 2613 { 2614 if(cnf===undefined) var cnf = this.conf_grid; 2615 var table_id = cnf.table_id===undefined ? cnf.id_prefix : cnf.table_id; 2616 var s = "<table class=gtable style='display:inline-block;' id='gtable_"+table_id+"'>\n"; 2617 var end = start+len*step; 2618 for(var i=start;i!=end;i+=step) 2619 s += "<tr><td>"+this.oCOM.getHexMulti(i,cnf.digits)+"</td>" 2620 +"<td id='"+cnf.id_prefix+this.line(i,16,256,cnf.digits).join("'></td><td id='"+cnf.id_prefix)+"'></td></tr>\n"; 2621 return s+"</table>" 2622 } 2623 2624 this.relabel_grid_rows = function(table_id, labels) 2625 { 2626 var tbl = document.getElementById("gtable_"+table_id); 2627 if(tbl==null) return; 2628 var rows = tbl.getElementsByTagName("tr"); 2629 for(var i=0;i<rows.length && i<labels.length;i++) 2630 if(rows[i].cells.length>0) 2631 rows[i].cells[0].textContent = labels[i]; 2632 } 2633 2634 this.paint_grid = function(layout,cnf) 2635 { 2636 var fix = this.conf_grid.id_prefix; 2637 if(cnf===undefined) cnf = this.conf_grid; 2638 fix = cnf.id_prefix; 2639 this.build_mem_map(layout); // populate this.mem_pg array (not for display, but later lookup) 2640 2641 // PREPARE DISPLAY DATA 2642 var blk_col = {}, blk_txt = {} //, c = "" 2643 for(var i in layout) 2644 { 2645 var a = i.split("-"); var b = [parseInt(a[0],16),parseInt(a[1],16),a[0].length]; 2646 for(var j=b[0];j<b[1];j+=parseInt("0100",16)) // iterate through layout address range 2647 { //c += ("0000"+j.toString(16)).slice(-4).toUpperCase()+" "+blk[i][0]+"<br>"; 2648 //var idx = ("0000"+j.toString(16)).slice(-4).toUpperCase(); 2649 var idx = this.oCOM.getHexMulti(j,b[2]); // address index 2650 blk_col[idx] = layout[i][0]; // populate background colors (indexed by address) 2651 blk_txt[idx] = "$"+idx+" "+layout[i][1]; // populate popup texts (indexed by address)
2652 if(this.bDebug) console.log("blk_col["+idx+"] = "+layout[i][0]); 2653 if(this.bDebug) console.log("blk_txt["+idx+"] = $"+idx+" "+layout[i][1]); 2654 } 2655 } 2656 2657 // PAINT GRID 2658 for(var i in layout) 2659 { 2660 var a = i.split("-"); 2661 if(a.length==2) 2662 { 2663 var b = [parseInt(a[0],16),parseInt(a[1],16),a[0].length]; 2664 for(var j=b[0];j<b[1];j+=parseInt("0100",16)) 2665 { 2666 //var idx = ("000"+j.toString(16)).slice(-4).toUpperCase(); 2667 var idx = this.oCOM.getHexMulti(j,b[2]); 2668 var el = document.getElementById(fix+idx); 2669 if(el!=null) 2670 { 2671 if(blk_col[idx].charAt(0)=="#" || blk_col[idx].substring(0,5)=="rgba(") 2672 el.style.backgroundColor = blk_col[idx]; 2673 el.innerHTML = "<span class=gt>"+blk_txt[idx]+"</span>" 2674 } 2675 } 2676 } 2677 } 2678 } 2679 2680 this.update_grid = function(mem_mon,cnf,bgcolor) 2681 { 2682 if(mem_mon===undefined || mem_mon==null) return; 2683 if(cnf===undefined) cnf = this.conf_grid; 2684 if(bgcolor===undefined) bgcolor = "#FFFFFF"; 2685 2686 var fix = cnf.id_prefix; 2687 var digits = cnf.digits===undefined ? this.conf_grid.digits : cnf.digits; 2688 2689 for(var i in mem_mon) 2690 { 2691 if(!mem_mon[i]) continue; 2692 2693 var addr = Number(i) << mem_gran; 2694 var idx = this.oCOM.getHexMulti(addr,digits); 2695 var el = document.getElementById(fix + idx); 2696 if(el!=null) el.style.backgroundColor = bgcolor; 2697 } 2698 } 2699 2700}(COM) 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713// _______ 2714// |_ __ \ 2715// | |__) |,--. _ .--. .---. 2716// | ___/`'_\ : [ `.-. |/ /__\\ 2717// _| |_ // | |, | | | || \__., 2718// |_____| \'-;__/[___||__]'.__.' 2719 2720 2721function PANE() 2722{ 2723 this.rootClass = "com-pane"; 2724 this.titleClass = "com-pane-title"; 2725 this.titleEqualizedClass = "com-pane-title-equalized"; 2726 2727 /* 2728 * Common pane skin owned by PANE(). 2729 * Page files should not define pane/header/body fonts, colors, dark-mode 2730 * foreground/background, or pane background patterns. They should only 2731 * provide layout constraints, content, ids, and callbacks. 2732 */ 2733 this.dotMatrixImage = 'url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAkkAAAA0AgMAAADjzUSRAAAAAXNSR0IB2cksfwAAAAlwSFlzAAAewgAAHsIBbtB1PgAAAAxQTFRF9PT04PDr2NrX4uXjoOJ1gwAAARpJREFUeJzt2CEOwjAUxvGG59cLETgFdr3KLvE8egqN2Y6CxyAJIZR9jGWtgNS9T/SvOvcLJS97c25KepLcmgxDZOiVmDZj5wjyTfJQTT+rprLoTTJ0Vo40n84C6TsrR1qTzky+u9sdiUzb+RjvPCY5jzgJk8nHE04SI5PpghOVaR9vOHGanjzzyc8mN5lo5vhi8iPP3Ukcvw88prVq+lk1lZW/01nvmkuZabBeN9Ejf6fTA0GhqaaSqqksepMwmnq19qA2/06n1h4U8u90au1BId+D1dqDYLrOJuycau1BAftd9/kzcZnW/U6tPShgv7sQmpbfiWcWpHuwWntQm+7BPHe37JzYg9XagzCfIu8cr6Y/VVNZgXgPfgPwisbf2JoIlwAAAABJRU5ErkJggg==")'; 2734 2735 this.paneCSS = function() 2736 { 2737 return '' 2738 + '.com-pane-host{display:grid;grid-template-columns:repeat(var(--com-pane-visible-count,1),minmax(0,1fr));gap:10px;width:100%;align-items:start;}' 2739 + '.com-pane-host>.com-pane[hidden]{display:none!important;}' 2740 + '.com-pane{min-width:0;max-width:100%;display:flex;flex-direction:column;gap:2px;border-radius:10px;overflow:visible;' 2741 + '--com-pane-light-header-bg:rgba(244,244,244,.88);--com-pane-light-header-fg:#000;--com-pane-light-header-border:rgba(0,0,0,.18);' 2742 + '--com-pane-light-body-bg:#f4f4f4;--com-pane-light-body-fg:#000;--com-pane-light-body-border:rgba(0,0,0,.12);' 2743 + '--com-pane-dark-header-bg:#111820;--com-pane-dark-header-fg:#e6edf3;--com-pane-dark-header-border:#30363d;' 2744 + '--com-pane-dark-body-bg:#000;--com-pane-dark-body-fg:#e6edf3;--com-pane-dark-body-border:#30363d;' 2745 + '--com-pane-body-font:500 12px/21px courier,monospace;--com-pane-title-font:12px/20px arial,sans-serif;' 2746 + '--com-pane-pattern-image:' + this.dotMatrixImage
2746+ ';--com-pane-pattern-size:585px 42px;}' 2747 + '.com-pane-title{display:flex;align-items:center;justify-content:space-between;gap:10px;min-height:34px;margin:0 0 2px 0;padding:4px 8px;' 2748 + 'border-radius:10px;border:1px solid var(--com-pane-light-header-border);background:var(--com-pane-light-header-bg);color:var(--com-pane-light-header-fg);' 2749 + 'font:var(--com-pane-title-font);box-sizing:border-box;overflow:visible;position:relative;}' 2750 + '.com-pane-title-left{display:flex;align-items:center;gap:8px;min-width:0;white-space:nowrap;}' 2751 + '.com-pane-title-right{display:inline-flex;align-items:center;justify-content:flex-end;gap:4px;flex-wrap:nowrap;margin-left:auto;}' 2752 + '.com-pane-title strong{color:inherit;font-size:13px;font-weight:700;}' 2753 + '.com-pane-info{color:rgba(0,0,0,.70);font:11px/1.2 arial,sans-serif;white-space:nowrap;}' 2754 + '.com-pane-action{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:2px;margin:0;border:0;border-radius:3px;' 2755 + 'background:rgba(255,255,255,.4);color:inherit;cursor:pointer;font:13px/1 arial,sans-serif;}' 2756 + '.com-pane-action:hover{background:rgba(255,255,255,.75);}' 2757 + '.com-pane-body{width:100%;max-width:100%;min-height:var(--com-pane-body-min-height,420px);height:var(--com-pane-body-height,auto);' 2758 + 'padding:0 30px;border:1px solid var(--com-pane-light-body-border);border-radius:10px;box-sizing:border-box;' 2759 + 'background-color:var(--com-pane-light-body-bg);color:var(--com-pane-light-body-fg);font:var(--com-pane-body-font);tab-size:4;white-space:pre;' 2760 + 'overflow-x:auto;overflow-y:var(--com-pane-body-overflow-y,hidden);resize:none;}' 2761 + '.com-pane-body-pattern{background-image:var(--com-pane-pattern-image);background-size:var(--com-pane-pattern-size);background-attachment:local;background-repeat:repeat;}' 2762 + '.com-pane-body-plain{background-image:none;}' 2763 + '.com-pane-body-auto-grow{height:auto;overflow-y:hidden;}' 2764 + '.com-pane-body-scroll{overflow-y:auto;}' 2765 + '.com-pane-theme-dark .com-pane-title{border-color:var(--com-pane-dark-header-border);background:var(--com-pane-dark-header-bg);color:var(--com-pane-dark-header-fg);}' 2766 + '.com-pane-theme-dark .com-pane-info{color:rgba(230,237,243,.72);}' 2767 + '.com-pane-theme-dark .com-pane-action{background:rgba(255,255,255,.08);color:var(--com-pane-dark-header-fg);}' 2768 + '.com-pane-theme-dark .com-pane-action:hover{background:rgba(255,255,255,.16);}' 2769 + '.com-pane-theme-dark .com-pane-body{border-color:var(--com-pane-dark-body-border);background-color:var(--com-pane-dark-body-bg);background-image:var(--com-pane-dark-body-image,none);color:var(--com-pane-dark-body-fg);caret-color:var(--com-pane-dark-body-fg);}' 2770 + '.com-pane-theme-dark .com-pane-body *{color:var(--com-pane-dark-body-fg);}' 2771 + '.com-pane-theme-dark .com-pane-highlight-current{background:rgba(255,214,64,.62)!important;color:#000!important;}' 2772 + '.com-pane-theme-dark .com-pane-highlight-current *{color:#000!important;}'; 2773 }; 2774 2775 this.injectPaneCSS = function() 2776 { 2777 if(document.getElementById("com_pane_core_css")) return; 2778 var style = document.createElement("style"); 2779 style.id = "com_pane_core_css"; 2780 style.textContent = this.paneCSS(); 2781 document.head.appendChild(style); 2782 }; 2783 2784 this.setTheme = function(root, theme) 2785 { 2786 if(typeof root == "string") root = document.getElementById(root); 2787 if(!root) return false; 2788 var dark = (theme === true || theme === "dark"); 2789 root.classList.toggle("com-pane-theme-dark", dark); 2790 root.classList.toggle("com-pane-theme-light", !dark); 2791 return dark; 2792 }; 2793 2794 this.toggleTheme = function(root) 2795 { 2796 if(typeof root == "string") root = document.getElementById(root); 2797 if(!root) return false; 2798 return this.setTheme(root, !root.classList.contains("com-pane-theme-dark")); 2799 }; 2800 2801 this.syncVisiblePanes = function(host) 2802 { 2803 if(typeof host == "string") host = document.getElementById(host); 2804 if(!host) return 0; 2805 var panes = Array.prototype.slice.call(host.children).filter(function(el){ 2806 return el.classList && el.classList.contains("com-pane") && !el.hidden && getComputedStyle(el).display !== "none"; 2807 }); 2808 host.style.setProperty("--com-pane-visible-count", Math.max(1, panes.length)); 2809 return panes.length; 2810 }; 2811 2812 this.actionButtonHTML = function(action) 2813 { 2814 action = action || {}; 2815 return '<button type="button" class="com-pane-action ' + this.escapeHTML(action.class || "") + '"' 2816 + (action.id ? ' id="' + this.escapeHTML(action.id) + '"' : '') 2817 + (action.act ? ' data-pane-default-act="' + this.escapeHTML(action.act) + '"' : '') 2818 + (action.onclick ? ' onclick="' + this.escapeHTML(action.onclick) + '"' : '') 2819 + (action.title ? ' title="' + this.escapeHTML(action.title) + '"' : '') 2820 + '>' + (action.html || (action.icon ? '<i class="' + this.escapeHTML(action.icon) + '"></i>' : this.escapeHTML(action.text || ""))) + '</button>'; 2821 }; 2822 2823 this.headerHTML = function(header) 2824 { 2825 header = header || {}; 2826 var left = header.leftHTML || ('<strong>' + this.escapeHTML(header.title || "") + '</strong>' 2827 + (header.info ? '<span class="com-pane-info"' + (header.infoId ? ' id="' + this.escapeHTML(header.infoId) + '"' : '') + '>
2827' + this.escapeHTML(header.info) + '</span>' : '') 2828 + (header.inputHTML || "")); 2829 var actions = (header.actions || []).map(this.actionButtonHTML.bind(this)).join(""); 2830 var right = header.rightHTML || actions; 2831 return '<div class="com-pane-title ' + this.escapeHTML(header.class || "") + '">' 2832 + '<div class="com-pane-title-left">' + left + '</div>' 2833 + '<div class="com-pane-title-right">' + right + '</div>' 2834 + '</div>'; 2835 }; 2836 2837 this.bodyClass = function(body) 2838 { 2839 body = body || {}; 2840 return this.classHTML( 2841 "com-pane-body", 2842 body.pattern === false ? "com-pane-body-plain" : "com-pane-body-pattern", 2843 body.grow ? "com-pane-body-auto-grow" : "", 2844 body.scroll ? "com-pane-body-scroll" : "", 2845 body.class || "" 2846 ); 2847 }; 2848 2849 this.getHTML = function(txt, cfg) 2850 { 2851 this.injectPaneCSS(); 2852 cfg = cfg || {}; 2853 var header = cfg.header || {}; 2854 var body = cfg.body || {}; 2855 2856 var sectionClass = this.classHTML(this.rootClass, cfg.class || ""); 2857 var bodyClass = this.bodyClass(body); 2858 2859 return '' 2860 + '<section' 2861 + (cfg.id ? ' id="' + this.escapeHTML(cfg.id) + '"' : '') 2862 + ' class="' + this.escapeHTML(sectionClass) + '">' 2863 + (header.html || this.headerHTML(header)) 2864 + '<' + btag 2865 + (body.id ? ' id="' + this.escapeHTML(body.id) + '"' : '') 2866 + ' class="' + this.escapeHTML(bodyClass) + '"' 2867 + (body.style ? ' style="' + this.escapeHTML(this.styleHTML(body.style)) + '"' : '') 2868 + this.attrHTML(body.attr) 2869 + '>' 2870 + this.escapeHTML(content || "") 2871 + '</' + btag + '>' 2872 + '</section>'; 2873 }; 2874 2875 /* 2876 * Visual-Studio-style Find / Replace for textarea panes. 2877 * 2878 * Usage: 2879 * oPANE.bindFindReplace("sourcePane", { 2880 * barWidth:"400px", 2881 * debounce_ms:500, 2882 * onChange:function(target){ ... } 2883 * }); 2884 * 2885 * Notes: 2886 * - This replaces the older pane-searchbar/bindSearch UI. 2887 * - The target textarea is wrapped in an overlay container so matches and 2888 * persistent "find in selection" can remain visible while focus is in 2889 * the Find input. 2890 * - Regex replacement supports \n, \r, \t, \\, $&, $$, $1..$99. 2891 */ 2892 this.findReplaceCSS = function() 2893 { 2894 return '' 2895 + '.com-pane-fr-wrap{position:relative;width:100%;height:100%;min-height:0;overflow:hidden;}' 2896 + '.com-pane-fr-wrap>textarea,.com-pane-fr-overlay{position:absolute;inset:0;width:100%;height:100%;margin:0;border:0;outline:0;' 2897 + 'padding:14px;font:13px/1.45 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono",monospace;' 2898 + 'tab-size:4;white-space:pre;overflow:auto;}' 2899 2900 + '.com-pane-fr-overlay{pointer-events:none;color:inherit;background:transparent;}' 2901 + '.com-pane-fr-wrap>textarea{resize:none;background:transparent!important;color:transparent!important;caret-color:currentColor;}' 2902 + '.com-pane-fr-wrap>textarea::selection{background:rgba(197,139,27,.65);color:transparent;}' 2903 + '.com-pane-fr-selection{background:rgba(190,190,190,.35);}' 2904 + '.com-pane-fr-match{background:rgba(255,210,70,.28);}' 2905 + '.com-pane-fr-current{background:rgba(255,185,45,.85);color:#000;}' 2906 + '.pane-title.com-pane-title-equalized{height:var(--com-pane-title-height,auto);min-height:var(--com-pane-title-height,auto);}' 2907 + '.com-pane-findreplace{z-index:2000;display:none;' 2908 + 'grid-template-columns:auto minmax(150px,392px) auto auto auto auto auto;grid-template-rows:24px;' 2909 + 'align-items:center;gap:4px;padding:2px 4px;border:1px solid var(--com-pane-fr-border,#d2d2d2);' 2910 + 'border-radius:4px;background:var(--com-pane-fr-bg,#f3f3f3);box-shadow:0 2px 12px rgba(0,0,0,.18);' 2911 + 'color:var(--com-pane-fr-text,#5f5f5f);}' 2912 + '.com-pane-findreplace.com-pane-fr-inline{position:relative;top:auto;left:auto;right:auto;box-shadow:none;}' 2913 + '.com-pane-findreplace.com-pane-fr-overlay-expand.expanded{position:absolute;right:8px;top:2px;transform:none;box-shadow:0 2px 12px rgba(0,0,0,.35);}' 2914 + '.com-pane-findreplace.open{display:grid;}' 2915 + '.com-pane-findreplace.expanded{grid-template-rows:24px 24px;}' 2916 + '.com-pane-header-widget-host{display:flex;align-items:center;justify-content:flex-end;margin-left:auto;min-width:0;position:relative;}' 2917 + '.com-pane-header-widget{display:none;}' 2918 + '.com-pane-header-widget.active{display:flex;}' 2919 + '.com-pane-findreplace.com-pane-header-widget.active{display:grid;}' 2920 + '.com-pane-header-default-widget{align-items:center;gap:4px;}' 2921 + '.com-pane-header-default-widget .com-pane-fr-btn{display:inline-flex;}' 2922 + '.com-pane-fr-expander,.com-pane-fr-btn{display:inline-flex;align-items:center;justify-content:center;flex:0 0 20px;' 2923 + 'width:20px;min-width:20px;height:20px;padding:0;margin:0;border:1px solid transparent;border-radius:3px;' 2924 + 'background:transparent;color:var(--com-pane-fr-muted,#9a9a9a);
2924cursor:pointer;font:14px/1 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;' 2925 + 'text-align:center;vertical-align:middle;}' 2926 + '.com-pane-fr-expander{color:var(--com-pane-fr-expander,#333);}' 2927 + '.com-pane-fr-expander:hover,.com-pane-fr-expander.active,.com-pane-fr-btn.active{border-color:var(--com-pane-fr-active,#0078d4);background:var(--com-pane-fr-active-bg,#e9f4ff);color:var(--com-pane-fr-active-text,#111);}' 2928 + '.com-pane-fr-btn:hover{color:var(--com-pane-fr-hover-text,#555);background:var(--com-pane-fr-hover-bg,#ececec);}' 2929 + '.com-pane-fr-btn:disabled{opacity:.35;cursor:default;pointer-events:none;}' 2930 + '.com-pane-fr-expander .chev{display:inline-flex;align-items:center;justify-content:center;line-height:1;transform:rotate(0deg);}' 2931 + '.com-pane-findreplace.expanded .com-pane-fr-expander .chev{transform:rotate(90deg);}' 2932 + '.com-pane-fr-field{display:flex;height:20px;border-radius:3px;background:var(--com-pane-fr-field-bg,#fff);overflow:hidden;}' 2933 + '.com-pane-fr-field input{min-width:0;flex:1;padding:0 2px;border:0;outline:0;font:10px/1 sans-serif;overflow:hidden;color:var(--com-pane-fr-field-text,#5f5f5f);background:var(--com-pane-fr-field-bg,#fff);}' 2934 + '.com-pane-fr-field-buttons{display:flex;height:100%;align-items:center;padding-right:8px;gap:2px;}' 2935 + '.com-pane-fr-under{display:inline-flex;align-items:center;justify-content:center;line-height:1;text-decoration:underline;text-underline-offset:3px;}' 2936 + '.com-pane-fr-results{font-size:10px;min-width:70px;white-space:nowrap;}' 2937 + '.com-pane-fr-nav,.com-pane-fr-close,.com-pane-fr-selection-btn{font-size:14px;}' 2938 + '.com-pane-fr-replace-spacer{grid-column:1;}' 2939 + '.com-pane-fr-replace-field{grid-column:2;grid-row:2;}' 2940 + '.com-pane-fr-actions{grid-column:3 / span 3;grid-row:2;display:flex;align-items:center;gap:10px;}' 2941 + '.com-pane-fr-floating{position:fixed;left:10px;top:10px;z-index:3000;display:none;align-items:center;gap:4px;' 2942 + 'padding:4px;border:1px solid var(--com-pane-fr-border,#d2d2d2);border-radius:6px;' 2943 + 'background:var(--com-pane-fr-bg,#f3f3f3);box-shadow:0 2px 12px rgba(0,0,0,.22);color:var(--com-pane-fr-text,#5f5f5f);}' 2944 + '.com-pane-fr-floating.open{display:flex;}' 2945 + '.com-pane-fr-floating .com-pane-fr-btn{flex:0 0 24px;width:24px;min-width:24px;height:24px;}' 2946 + '.com-pane-fr-hidden-row{display:none;}' 2947 + '.com-pane-findreplace.expanded .com-pane-fr-hidden-row{display:flex;}'; 2948 }; 2949 2950 this.injectFindReplaceCSS = function() 2951 { 2952 this.injectPaneCSS(); 2953 if(document.getElementById("com_pane_findreplace_css")) return; 2954 var style = document.createElement("style"); 2955 style.id = "com_pane_findreplace_css"; 2956 style.textContent = this.findReplaceCSS(); 2957 document.head.appendChild(style); 2958 }; 2959 2960 this.findReplaceHTML = function(id, cfg) 2961 { 2962 cfg = cfg || {}; 2963 var w = cfg.barWidth || "400px"; 2964 return '' 2965 + '<div id="' + this.escapeHTML(id) + '" style="width:' + this.escapeHTML(w) + '" class="com-pane-findreplace" aria-label="Find and replace">' 2966 + '<button type="button" class="com-pane-fr-expander" data-fr-act="expand" title="Expand / collapse replace"><span class="chev">âº</span></button>' 2967 + '<div class="com-pane-fr-field">' 2968 + '<input class="com-pane-fr-find" placeholder="Find" autocomplete="off" spellcheck="false">' 2969 + '<span class="com-pane-fr-field-buttons">' 2970 + '<button type="button" class="com-pane-fr-btn com-pane-fr-case active" title="Match case">Aa</button>' 2971 + '<button type="button" class="com-pane-fr-btn com-pane-fr-word" title="Match whole word"><span class="com-pane-fr-under">ab</span></button>' 2972 + '<button type="button" class="com-pane-fr-btn com-pane-fr-regex" title="Use regular expression">.*</button>' 2973 + '</span>' 2974 + '</div>' 2975 + '<div class="com-pane-fr-results">No results</div>' 2976 + '<button type="button" class="com-pane-fr-btn com-pane-fr-nav com-pane-fr-prev" title="Previous match">â</button>' 2977 + '<button type="button" class="com-pane-fr-btn com-pane-fr-nav com-pane-fr-next" title="Next match">â</button>' 2978 + '<button type="button" class="com-pane-fr-btn com-pane-fr-selection-btn" title="Find in selection">â°</button>' 2979 + '<button type="button" class="com-pane-fr-btn com-pane-fr-close" title="Close">Ã</button>' 2980 + '<div class="com-pane-fr-replace-spacer com-pane-fr-hidden-row"></div>' 2981 + '<div class="com-pane-fr-field com-pane-fr-replace-field com-pane-fr-hidden-row">' 2982 + '<input class="com-pane-fr-replace" placeholder="Replace" autocomplete="off" spellcheck="false">' 2983 + '<span class="com-pane-fr-field-buttons"><button type="button" class="com-pane-fr-btn com-pane-fr-preserve" title="Preserve case">AB</button></span>' 2984 + '</div>' 2985 + '<div class="com-pane-fr-actions com-pane-fr-hidden-row">' 2986 + '<button type="button" class="com-pane-fr-btn com-pane-fr-replace-one" title="Replace selected match">c</button>' 2987 + '<button type="button" class="com-pane-fr-btn com-pane-fr-replace-all" title="Replace all">ac</button>' 2988 + '</div>' 2989 + '</div>'; 2990 }; 2991 2992 this.findReplaceFloatingNavHTML = function(id) 2993 { 2994 return '' 2995 + '<div id="' + this.escapeHTML(id) + '" class="com-pane-fr-floating" aria-label="Find navigation">' 2996 + '<button type="button" class="com-pane-fr-btn com-pane-fr-nav com-pane-fr-float-prev" title="Previous match">â</button>' 2997 + '<button type="button" class="com-pane-fr-btn com-pane-fr-nav com-pane-fr-float-next" title="Next match">â</button>' 2998 + '<button type="button" class="com-pane-fr-btn com-pane-fr-float-top" title="Back to top"><i class="fa fa-angle-double-up"></i></button>' 2999 + '<button type="button" class="com-pane-fr-btn com-pane-fr-close com-pane-fr-float-close" title="Close">Ã</button>' 3000 + '</div>'; 3001 }; 3002 3003 this.bindFindReplace = function(target, cfg) 3004 { 3005 cfg = cfg || {}; 3006 this.injectFindReplaceCSS(); 3007 3008 if(typeof target == "string") target = document.getElementById(target); 3009 if(!target || target.tagName.toUpperCase() != "TEXTAREA") return null; 3010 3011 var uid = cfg.id || ("com_pane_findreplace_" + (target.id || Math.random().toString(36).slice(2))); 3012 var bar = document.getElementById(uid); 3013 3014 if(!bar) { 3015 var mount = cfg.mount; 3016 if(typeof mount == "string") mount = document.getElementById(mount); 3017 if(mount) mount.insertAdjacentHTML("beforeend", this.findReplaceHTML(uid, cfg)); 3018 else document.body.insertAdjacentHTML("beforeend", this.findReplaceHTML(uid, cfg)); 3019 bar = document.getElementById(uid); 3020 } 3021 3022 if(cfg.inline !== false) bar.classList.add("com-pane-fr-inline"); 3023 if(cfg.overlayExpand) bar.classList.add("com-pane-fr-overlay-expand"); 3024 3025 var floatingNav = null; 3026 if(cfg.floatingNav) { 3027 var floatingId = uid + "_floating"; 3028 floatingNav = document.getElementById(floatingId); 3029 if(!floatingNav) { 3030 document.body.insertAdjacentHTML("beforeend", this.findReplaceFloatingNavHTML(floatingId)); 3031 floatingNav = document.getElementById(floatingId); 3032 } 3033 } 3034 3035 if(cfg.scheme) { 3036 for(var k in cfg.scheme) { 3037 bar.style.setProperty("--com-pane-fr-" + k, cfg.scheme[k]); 3038 if(floatingNav) floatingNav.style.setProperty("--com-pane-fr-" + k, cfg.scheme[k]); 3039 } 3040 } 3041 if(cfg.manager && typeof cfg.manager.add == "function") 3042 cfg.manager.add(uid, bar, false); 3043 3044 var wrap = target.parentNode; 3045 var overlay; 3046 if(!wrap.classList || !wrap.classList.contains("com-pane-fr-wrap")) { 3047 wrap = document.createElement("div"); 3048 wrap.className = "com-pane-fr-wrap"; 3049 target.parentNode.insertBefore(wrap, target);
3050 overlay = document.createElement("pre"); 3051 overlay.className = "com-pane-fr-overlay"; 3052 overlay.setAttribute("aria-hidden", "true"); 3053 wrap.appendChild(overlay); 3054 wrap.appendChild(target); 3055 } else { 3056 overlay = wrap.querySelector(".com-pane-fr-overlay"); 3057 } 3058 3059 var findInput = bar.querySelector(".com-pane-fr-find"); 3060 var replaceInput = bar.querySelector(".com-pane-fr-replace"); 3061 var resultInfo = bar.querySelector(".com-pane-fr-results"); 3062 var prevBtn = bar.querySelector(".com-pane-fr-prev"); 3063 var nextBtn = bar.querySelector(".com-pane-fr-next"); 3064 var caseBtn = bar.querySelector(".com-pane-fr-case"); 3065 var wordBtn = bar.querySelector(".com-pane-fr-word"); 3066 var regexBtn = bar.querySelector(".com-pane-fr-regex"); 3067 var preserveBtn = bar.querySelector(".com-pane-fr-preserve"); 3068 var selectionBtn = bar.querySelector(".com-pane-fr-selection-btn"); 3069 var floatPrevBtn = floatingNav ? floatingNav.querySelector(".com-pane-fr-float-prev") : null; 3070 var floatNextBtn = floatingNav ? floatingNav.querySelector(".com-pane-fr-float-next") : null; 3071 var floatTopBtn = floatingNav ? floatingNav.querySelector(".com-pane-fr-float-top") : null; 3072 var floatCloseBtn = floatingNav ? floatingNav.querySelector(".com-pane-fr-float-close") : null; 3073 3074 var state = { 3075 matches: [], 3076 current: -1, 3077 selectionRange: null, 3078 lastEditorSelection: null, 3079 timer: null, 3080 debounce_ms: cfg.debounce_ms == null ? 500 : cfg.debounce_ms 3081 }; 3082 3083 function esc(s) { 3084 return String(s).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">"); 3085 } 3086 3087 function renderOverlay() { 3088 var text = target.value; 3089 var ranges = []; 3090 var visibleSelection = state.selectionRange || state.lastEditorSelection; 3091 if(visibleSelection && visibleSelection.end > visibleSelection.start) 3092 ranges.push({start:visibleSelection.start,end:visibleSelection.end,cls:"com-pane-fr-selection",priority:1}); 3093 3094 for(var i=0;i<state.matches.length;i++) 3095 ranges.push({start:state.matches[i].start,end:state.matches[i].end,cls:i==state.current?"com-pane-fr-current":"com-pane-fr-match",priority:i==state.current?3:2}); 3096 3097 ranges = ranges.filter(function(r){return r.end>r.start}).sort(function(a,b){return a.start-b.start || b.priority-a.priority}); 3098 var out = "", pos = 0; 3099 for(var r=0;r<ranges.length;r++) { 3100 if(ranges[r].end <= pos) continue; 3101 var start = Math.max(ranges[r].start, pos); 3102 if(start > pos) out += esc(text.slice(pos,start)); 3103 out += '<span class="'+ranges[r].cls+'">'+esc(text.slice(start,ranges[r].end))+'</span>'; 3104 pos = ranges[r].end; 3105 } 3106 if(pos < text.length) out += esc(text.slice(pos)); 3107 overlay.innerHTML = out || " "; 3108 syncOverlayScroll(); 3109 } 3110 3111 function syncOverlayScroll() { 3112 overlay.scrollTop = target.scrollTop; 3113 overlay.scrollLeft = target.scrollLeft; 3114 } 3115 3116 function rememberEditorSelection() { 3117 if(target.selectionStart !== target.selectionEnd) 3118 state.lastEditorSelection = {start:target.selectionStart,end:target.selectionEnd}; 3119 updateSelectionButtonState(); 3120 } 3121 3122 function hasUsableEditorSelection() { 3123 return !!(state.lastEditorSelection && state.lastEditorSelection.end > state.lastEditorSelection.start); 3124 } 3125 3126 function updateSelectionButtonState() { 3127 var active = selectionBtn.classList.contains("active"); 3128 selectionBtn.disabled = !active && !hasUsableEditorSelection(); 3129 } 3130 3131 3132 function updateNavButtons() 3133 { 3134 var enabled = state.matches.length > 0; 3135 prevBtn.disabled = !enabled; 3136 nextBtn.disabled = !enabled; 3137 if(floatPrevBtn) floatPrevBtn.disabled = !enabled; 3138 if(floatNextBtn) floatNextBtn.disabled = !enabled; 3139 } 3140 3141 function openSearch(expanded) { 3142 var expandedState = !!expanded; 3143 rememberEditorSelection(); 3144 if(cfg.manager && typeof cfg.manager.activate == "function") cfg.manager.activate(uid); 3145 bar.classList.add("open"); 3146 bar.classList.toggle("expanded", expandedState); 3147 if(floatingNav) floatingNav.classList.add("open"); 3148 setTimeout(function(){ findInput.focus(); findInput.select(); }, 0); 3149 scheduleFind(); 3150 } 3151 3152 function closeSearch() 3153 { 3154 bar.classList.remove("open"); 3155 bar.classList.remove("expanded"); 3156 if(floatingNav) floatingNav.classList.remove("open"); 3157 if(cfg.manager && typeof cfg.manager.activateDefault == "function") cfg.manager.activateDefault();
3158 target.focus(); 3159 } 3160 3161 function toggleSearch(expanded) { 3162 if(bar.classList.contains("open")) closeSearch(); 3163 else openSearch(expanded); 3164 } 3165 3166 function escapeRegex(s) { 3167 return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 3168 } 3169 3170 function flags(global) { 3171 return (global ? "g" : "") + (caseBtn.classList.contains("active") ? "" : "i"); 3172 } 3173 3174 function buildRegex(global) { 3175 var q = findInput.value; 3176 if(!q) return null; 3177 if(!regexBtn.classList.contains("active")) q = escapeRegex(q); 3178 if(wordBtn.classList.contains("active")) q = "\\b(?:" + q + ")\\b"; 3179 try { return new RegExp(q, flags(global)); } 3180 catch(e) { return null; } 3181 } 3182 3183 function findScope() { 3184 var txt = target.value; 3185 if(selectionBtn.classList.contains("active") && state.selectionRange) 3186 return {text:txt.slice(state.selectionRange.start,state.selectionRange.end),offset:state.selectionRange.start}; 3187 return {text:txt,offset:0}; 3188 } 3189 3190 function scheduleFind() { 3191 clearTimeout(state.timer); 3192 state.matches = []; 3193 state.current = -1; 3194 resultInfo.textContent = findInput.value ? "..." : "No results"; 3195 updateNavButtons(); 3196 renderOverlay(); 3197 state.timer = setTimeout(function(){ updateMatches({selectFirst:true, keepFindFocus:true}); }, state.debounce_ms); 3198 } 3199 3200 function updateMatches(options) { 3201 options = options || {}; 3202 var re = buildRegex(true), scope = findScope(); 3203 state.matches = []; 3204 state.current = -1; 3205 3206 if(re) { 3207 var m; 3208 while((m = re.exec(scope.text)) !== null) { 3209 state.matches.push({start:scope.offset + m.index,end:scope.offset + m.index + m[0].length,text:m[0]}); 3210 if(m[0].length === 0) re.lastIndex++; 3211 } 3212 } 3213 3214 if(state.matches.length) state.current = 0; 3215 resultInfo.textContent = state.matches.length ? "1 of " + state.matches.length : "No results"; 3216 updateNavButtons(); 3217 3218 if(state.matches.length && options.selectFirst) selectCurrent({keepFindFocus:!!options.keepFindFocus}); 3219 else renderOverlay(); 3220 } 3221 3222 function scrollCurrentMatchIntoPage() { 3223 if(!cfg.pageScrollOnMatch) return; 3224 3225 requestAnimationFrame(function() { 3226 var current = overlay ? overlay.querySelector(".com-pane-fr-current") : null; 3227 if(current && typeof current.getBoundingClientRect == "function") { 3228 var rect = current.getBoundingClientRect(); 3229 var currentY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 3230 var targetY = Math.max(0, currentY + rect.top - (cfg.pageScrollOffset == null ? 72 : cfg.pageScrollOffset)); 3231 try { window.scrollTo({ top: targetY, left: 0, behavior: cfg.pageScrollBehavior || "smooth" }); } 3232 catch (_ignore) { window.scrollTo(0, targetY); } 3233 } 3234 }); 3235 } 3236 3237 function selectCurrent(mode) { 3238 if(state.current < 0 || !state.matches[state.current]) { renderOverlay(); return; } 3239 mode = mode || {}; 3240 var m = state.matches[state.current]; 3241 target.setSelectionRange(m.start, m.end); 3242 resultInfo.textContent = (state.current + 1) + " of " + state.matches.length; 3243 renderOverlay(); 3244 scrollCurrentMatchIntoPage(); 3245 if(mode.focusEditor) target.focus(); 3246 else if(mode.keepFindFocus) findInput.focus(); 3247 } 3248 3249 function nextMatch(dir, selectMode) { 3250 clearTimeout(state.timer); 3251 if(!state.matches.length) { updateNavButtons(); renderOverlay(); return; } 3252 state.current = (state.current + dir + state.matches.length) % state.matches.length; 3253 selectCurrent(selectMode || {keepFindFocus:true}); 3254 } 3255 3256 function preserveCase(replacement, found) { 3257 if(!preserveBtn.classList.contains("active")) return replacement;
3258 if(found.toUpperCase() === found) return replacement.toUpperCase(); 3259 if(found.toLowerCase() === found) return replacement.toLowerCase(); 3260 if(found[0] && found[0].toUpperCase() === found[0]) 3261 return replacement.charAt(0).toUpperCase() + replacement.slice(1).toLowerCase(); 3262 return replacement; 3263 } 3264 3265 function regexReplacementText(template, found, groups) { 3266 if(!regexBtn.classList.contains("active")) return template; 3267 return String(template).replace(/\\(n|r|t|\\)|\$(\$|&|`|'|\d{1,2})/g, function(token, esc, dollar) { 3268 if(esc === "n") return "\n"; 3269 if(esc === "r") return "\r"; 3270 if(esc === "t") return "\t"; 3271 if(esc === "\\") return "\\"; 3272 if(dollar === "$") return "$"; 3273 if(dollar === "&") return found; 3274 if(/^\d+$/.test(dollar)) return groups[Number(dollar)-1] || ""; 3275 if(dollar === "`" || dollar === "'") return ""; 3276 return token; 3277 }); 3278 } 3279 3280 function replacementForMatch(template, found, groups) { 3281 return preserveCase(regexReplacementText(template, found, groups || []), found); 3282 } 3283 3284 function replaceCurrent() { 3285 clearTimeout(state.timer); 3286 if(state.current < 0 || !state.matches[state.current]) return; 3287 var m = state.matches[state.current]; 3288 var singleRe = buildRegex(false); 3289 var singleMatch = singleRe ? singleRe.exec(m.text) : null; 3290 var groups = singleMatch ? Array.prototype.slice.call(singleMatch, 1) : []; 3291 var rep = replacementForMatch(replaceInput.value, m.text, groups); 3292 target.value = target.value.slice(0,m.start) + rep + target.value.slice(m.end); 3293 3294 if(state.selectionRange && m.start >= state.selectionRange.start && m.end <= state.selectionRange.end) { 3295 state.selectionRange.end += rep.length - (m.end - m.start); 3296 state.lastEditorSelection = {start:state.selectionRange.start,end:state.selectionRange.end}; 3297 } 3298 if(typeof cfg.onChange == "function") cfg.onChange(target); 3299 updateMatches({selectFirst:true, keepFindFocus:true}); 3300 } 3301 3302 function replaceAll() { 3303 clearTimeout(state.timer); 3304 var re = buildRegex(true); 3305 if(!re) return; 3306 var scope = findScope(); 3307 var replaced = scope.text.replace(re, function() { 3308 var found = arguments[0]; 3309 var groups = Array.prototype.slice.call(arguments, 1, -2); 3310 return replacementForMatch(replaceInput.value, found, groups); 3311 }); 3312 target.value = target.value.slice(0,scope.offset) + replaced + target.value.slice(scope.offset + scope.text.length); 3313 if(state.selectionRange) { 3314 state.selectionRange.end = state.selectionRange.start + replaced.length; 3315 state.lastEditorSelection = {start:state.selectionRange.start,end:state.selectionRange.end}; 3316 } 3317 if(typeof cfg.onChange == "function") cfg.onChange(target); 3318 updateMatches({selectFirst:true, keepFindFocus:true}); 3319 } 3320 3321 document.addEventListener("keydown", function(ev) 3322 { 3323 var mod = ev.ctrlKey || ev.metaKey; 3324 if((document.activeElement === target || document.activeElement === findInput || document.activeElement === replaceInput) && mod && ev.key.toLowerCase() === "f") { 3325 ev.preventDefault(); toggleSearch(false); 3326 } 3327 if((document.activeElement === target || document.activeElement === findInput || document.activeElement === replaceInput) && mod && ev.key.toLowerCase() === "h") { 3328 ev.preventDefault(); toggleSearch(true); 3329 } 3330 if(ev.key === "Escape" && bar.classList.contains("open")) { 3331 ev.preventDefault(); closeSearch(); 3332 } 3333 }); 3334 3335 target.addEventListener("mousedown", function() 3336 { 3337 if(!selectionBtn.classList.contains("active")) { 3338 state.lastEditorSelection = null; 3339 renderOverlay(); 3340 updateSelectionButtonState(); 3341 } 3342 }); 3343 3344 ["mouseup","keyup","select"].forEach(function(name) 3345 { 3346 target.addEventListener(name, function(){ rememberEditorSelection(); renderOverlay(); }); 3347 }); 3348 3349 target.addEventListener("input", function() 3350 { 3351 rememberEditorSelection(); 3352 if(selectionBtn.classList.contains("active")) { 3353 selectionBtn.classList.remove("active"); 3354 state.selectionRange = null; 3355 } 3356 if(typeof cfg.onChange == "function") cfg.onChange(target); 3357 scheduleFind(); 3358 }); 3359 3360 target.addEventListener("scroll", syncOverlayScroll); 3361 findInput.addEventListener("input", scheduleFind); 3362 findInput.addEventListener("keydown", function(ev) 3363 { 3364 if(ev.key === "Enter") { 3365 ev.preventDefault(); 3366 clearTimeout(state.timer); 3367 if(!state.matches.length) updateMatches({selectFirst:true, keepFindFocus:true}); 3368 else nextMatch(ev.shiftKey ? -1 : 1); 3369 } 3370 }); 3371 replaceInput.addEventListener("keydown", function(ev) { 3372 if(ev.key === "Enter") { ev.preventDefault(); replaceCurrent(); } 3373 }); 3374 3375 bar.querySelector('[data-fr-act="expand"]').onclick = function(){ bar.classList.toggle("expanded"); }; 3376 prevBtn.onclick = function(){ nextMatch(-1); }; 3377 nextBtn.onclick = function(){ nextMatch(1); }; 3378 bar.querySelector(".com-pane-fr-replace-one").onclick = replaceCurrent; 3379 bar.querySelector(".com-pane-fr-replace-all").onclick = replaceAll; 3380 bar.querySelector(".com-pane-fr-close").onclick = closeSearch; 3381 if(floatPrevBtn) floatPrevBtn.onclick = function(ev) 3382 { 3383 if(ev) ev.preventDefault(); 3384 nextMatch(-1, {}); 3385 }; 3386 if(floatNextBtn) floatNextBtn.onclick = function(ev){ 3387 if(ev) ev.preventDefault(); 3388 nextMatch(1, {}); 3389 }; 3390 if(floatTopBtn) floatTopBtn.onclick = function(ev){ 3391 if(ev) ev.preventDefault(); 3392 try { window.scrollTo({ top: 0, left: 0, behavior: "smooth" }); } 3393 catch (_ignore) { window.scrollTo(0, 0); } 3394 }; 3395 if(floatCloseBtn) floatCloseBtn.onclick = function(ev){ 3396 if(ev) ev.preventDefault(); 3397 closeSearch(); 3398 }; 3399 caseBtn.onclick = function(){ caseBtn.classList.toggle("active"); scheduleFind(); }; 3400 wordBtn.onclick = function(){ wordBtn.classList.toggle("active"); scheduleFind(); }; 3401 regexBtn.onclick = function(){ regexBtn.classList.toggle("active"); scheduleFind(); }; 3402 preserveBtn.onclick = function(){ preserveBtn.classList.toggle("active"); }; 3403 selectionBtn.onclick = function() 3404 { 3405 if(selectionBtn.disabled) return; 3406 if(selectionBtn.classList.contains("active")) { 3407 selectionBtn.classList.remove("active"); 3408 state.selectionRange = null; 3409 scheduleFind(); 3410 return; 3411 } 3412 if(!hasUsableEditorSelection()) return; 3413 state.selectionRange = {start:state.lastEditorSelection.start,end:state.lastEditorSelection.end}; 3414 selectionBtn.classList.add("active"); 3415 updateSelectionButtonState(); 3416 scheduleFind(); 3417 findInput.focus(); 3418 }; 3419 3420 renderOverlay(); 3421 updateSelectionButtonState(); 3422 updateNavButtons(); 3423 return { open:openSearch, close:closeSearch, toggle:toggleSearch, update:updateMatches, render:renderOverlay, floatingNav:floatingNav }; 3424 }; 3425 3426 this.createPaneHeaderWidgetManager = function(host, cfg) 3427 { 3428 cfg = cfg || {}; 3429 if(typeof host == "string") host = document.getElementById(host); 3430 if(!host) return null; 3431 3432 host.classList.add("com-pane-header-widget-host"); 3433 3434 var widgets = {}; 3435 var activeId = ""; 3436 var defaultId = cfg.defaultId || "default"; 3437 3438 function setVisible(id) 3439 { 3440 activeId = id; 3441 for(var k in widgets) { 3442 if(widgets[k]) 3443 widgets[k].classList.toggle("active", k == id); 3444 } 3445 if(typeof cfg.onChange == "function") cfg.onChange(id); 3446 } 3447 3448 function add(id, el, isDefault) 3449 { 3450 if(typeof el == "string") { 3451 host.insertAdjacentHTML("beforeend", el); 3452 el = host.lastElementChild; 3453 } 3454 if(!el) return null; 3455 3456 el.classList.add("com-pane-header-widget"); 3457 if(isDefault) { 3458 defaultId = id; 3459 el.classList.add("com-pane-header-default-widget"); 3460 } 3461 widgets[id] = el; 3462 if(!el.parentNode) host.appendChild(el); 3463 3464 /* 3465 * If a non-default widget is registered after the default widget 3466 * has already been activated, keep it hidden until activate(id) 3467 * is called. This is the one-line header toggle contract: 3468 * exactly one registered widget is visible at a time. 3469 */ 3470 el.classList.toggle("active", activeId == id); 3471 3472 return el; 3473 } 3474 3475 function activate(id) 3476 { 3477 if(!widgets[id]) return; 3478 setVisible(id); 3479 } 3480 3481 function activateDefault() 3482 { 3483 if(widgets[defaultId]) setVisible(defaultId); 3484 } 3485 3486 function current() 3487 { 3488 return activeId; 3489 } 3490 3491 var api = { 3492 host:host, 3493 widgets:widgets, 3494 add:add, 3495 activate:activate, 3496 activateDefault:activateDefault, 3497 current:current 3498 }; 3499 3500 if(cfg.defaultHTML) add(defaultId, cfg.defaultHTML, true); 3501 activateDefault(); 3502 return api; 3503 }; 3504 3505 this.equalizePaneHeaderHeights = function(selector) 3506 { 3507 selector = selector || ".com-pane-title"; 3508 requestAnimationFrame(function() { 3509 var headers = Array.prototype.slice.call(document.querySelectorAll(selector)); 3510 var h = 0;
3511 headers.forEach(function(el) { 3512 el.classList.remove("com-pane-title-equalized"); 3513 el.style.removeProperty("height"); 3514 h = Math.max(h, Math.ceil(el.getBoundingClientRect().height)); 3515 }); 3516 if(h <= 0) return; 3517 document.documentElement.style.setProperty("--com-pane-title-height", h + "px"); 3518 headers.forEach(function(el) { el.classList.add("com-pane-title-equalized"); }); 3519 }); 3520 }; 3521 3522 3523 this.bindSearch = function(wrapper, target, callback) 3524 { 3525 return this.bindFindReplace(target, { onChange:callback }); 3526 }; 3527 3528 this.escapeHTML = function(str) 3529 { 3530 return String(str) 3531 .replaceAll("&", "&") 3532 .replaceAll("<", "<") 3533 .replaceAll(">", ">") 3534 .replaceAll("<b>", "<b>") 3535 .replaceAll("</b>", "</b>") 3536 .replaceAll("<u>", "<u>") 3537 .replaceAll("</u>", "</u>") 3538 .replaceAll("<i>", "<i>") 3539 .replaceAll("</i>", "</i>") 3540 .replaceAll('"', """) 3541 .replaceAll("'", "'") 3542 .replaceAll("\n","<br>") 3543 .replaceAll(" ", " "); 3544 } 3545}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.