github.com/vugu/vugu@v0.3.5/domrender/renderer-js-script.js (about) 1 (function () { 2 3 if (window.vuguRender) { 4 return; 5 } // only once 6 7 const opcodeEnd = 0 // no more instructions in this buffer 8 // const opcodeClearRefmap = 1 // clear the reference map, all following instructions must not reference prior IDs 9 const opcodeClearEl = 1 // clear the currently selected element 10 // const opcodeSetHTMLRef = 2 // assign ref for html tag 11 // const opcodeSetHeadRef = 3 // assign ref for head tag 12 // const opcodeSetBodyRef = 4 // assign ref for body tag 13 // const opcodeSelectRef = 5 // select element by ref 14 const opcodeRemoveOtherAttrs = 5 // remove any elements for the current element that we didn't just set 15 const opcodeSetAttrStr = 6 // assign attribute string to the current selected element 16 const opcodeSelectMountPoint = 7 // selects the mount point element and pushes to the stack - the first time by selector but every subsequent time it will reuse the element from before (because the selector may not match after it's been synced over, it's id etc), also make sure it's of this element name and recreate if so 17 // const opcodePicardFirstChildElement = 8 // ensure an element first child and push onto element stack 18 // const opcodePicardFirstChildText = 9 // ensure a text first child and push onto element stack 19 // const opcodePicardFirstChildComment = 10 // ensure a comment first child and push onto element stack 20 // const opcodeSelectParent = 11 // pop from the element stack 21 // const opcodePicardFirstChild = 12 // ensure an element first child and push onto element stack 22 23 const opcodeMoveToFirstChild = 20 // move node selection to first child (doesn't have to exist) 24 const opcodeSetElement = 21 // assign current selected node as an element of the specified type 25 // const opcodeSetElementAttr = 22 // set attribute on current element 26 const opcodeSetText = 23 // assign current selected node as text with specified content 27 const opcodeSetComment = 24 // assign current selected node as comment with specified content 28 const opcodeMoveToParent = 25 // move node selection to parent 29 const opcodeMoveToNextSibling = 26 // move node selection to next sibling (doesn't have to exist) 30 const opcodeRemoveOtherEventListeners = 27 // remove all event listeners from currently selected element that were not just set 31 const opcodeSetEventListener = 28 // assign event listener to currently selected element 32 const opcodeSetInnerHTML = 29 // set the innerHTML for an element 33 34 const opcodeSetCSSTag = 30 // write a CSS (style or link) tag 35 const opcodeRemoveOtherCSSTags = 31 // remove any CSS tags that have not been written since the last call 36 const opcodeSetJSTag = 32 // write a JS (script) tag 37 const opcodeRemoveOtherJSTags = 33 // remove any JS tags that have not been written since the last call 38 39 const opcodeSetProperty = 35 // assign a JS property to the current element 40 const opcodeSelectQuery = 36 // select an element 41 const opcodeBufferInnerHTML = 37 // pass chunked text to set as inner html, complete with opcodeSetInnerHTML 42 43 const opcodeSetAttrNSStr = 38 // assign attribute string to the current selected namespaced element 44 const opcodeSetElementNS = 39 // assign current selected node as an element of the specified type in the specified namespace 45 46 const opcodeCallback = 40 // issue callback, sends just callbackID 47 const opcodeCallbackLastElement = 41 // issue callback with callbackID and most recent element reference 48 49 /*DEBUG OPCODE STRINGS*/ 50 51 // Decoder provides our binary decoding. 52 // Using a class because that's what all the cool JS kids are doing these days. 53 class Decoder { 54 55 constructor(dataView, offset) { 56 this.dataView = dataView; 57 this.offset = offset || 0; 58 return this; 59 } 60 61 // readUint8 reads a single byte, 0-255 62 readUint8() { 63 var ret = this.dataView.getUint8(this.offset); 64 this.offset++; 65 return ret; 66 } 67 68 // readRefToString reads a 64-bit unsigned int ref but returns it as a hex string 69 readRefToString() { 70 // read in two 32-bit parts, BigInt is not yet well supported 71 var ret = this.dataView.getUint32(this.offset).toString(16).padStart(8, "0") + 72 this.dataView.getUint32(this.offset + 4).toString(16).padStart(8, "0"); 73 this.offset += 8; 74 return ret; 75 } 76 77 // readUint32 reads a 32-bit unsigned int and returns it as a regular number 78 readUint32() { 79 // getUint32 returns a regular JS number 80 var ret = this.dataView.getUint32(this.offset); 81 this.offset += 4; 82 return ret; 83 } 84 85 // readString is 4 bytes length followed by utf-8 chars 86 readString() { 87 var len = this.dataView.getUint32(this.offset); 88 var ret = utf8decoder.decode(new DataView(this.dataView.buffer, this.dataView.byteOffset + this.offset + 4, len)); 89 this.offset += len + 4; 90 return ret; 91 } 92 93 } 94 95 let utf8decoder = new TextDecoder(); 96 97 window.vuguGetActiveEvent = function () { 98 let state = window.vuguState || {}; 99 window.vuguState = state; 100 return state.activeEvent; 101 } 102 window.vuguGetActiveEventTarget = function () { 103 let state = window.vuguState || {}; 104 window.vuguState = state; 105 return state.activeEvent && state.activeEvent.target; 106 } 107 window.vuguGetActiveEventCurrentTarget = function () { 108 let state = window.vuguState || {}; 109 window.vuguState = state; 110 return state.activeEvent && state.activeEvent.currentTarget; 111 } 112 window.vuguActiveEventPreventDefault = function () { 113 let state = window.vuguState || {}; 114 window.vuguState = state; 115 if (state.activeEvent && state.activeEvent.preventDefault) { 116 state.activeEvent.preventDefault(); 117 } 118 } 119 window.vuguActiveEventStopPropagation = function () { 120 let state = window.vuguState || {}; 121 window.vuguState = state; 122 if (state.activeEvent && state.activeEvent.stopPropagation) { 123 state.activeEvent.stopPropagation(); 124 } 125 } 126 127 // window.vuguSetEventHandlerAndBuffer = function(eventHandlerFunc, eventBuffer) { 128 // let state = window.vuguState || {}; 129 // window.vuguState = state; 130 // state.eventBuffer = eventBuffer; 131 // state.eventBufferView = new DataView(eventBuffer.buffer, eventBuffer.byteOffset, eventBuffer.byteLength); 132 // state.eventHandlerFunc = eventHandlerFunc; 133 // } 134 135 // function called when DOM events happen 136 window.vuguSetEventHandler = function (eventHandlerFunc) { 137 let state = window.vuguState || {}; 138 window.vuguState = state; 139 state.eventHandlerFunc = eventHandlerFunc; 140 } 141 142 // function called when callback instructions are encountered 143 window.vuguSetCallbackHandler = function (callbackHandlerFunc) { 144 let state = window.vuguState || {}; 145 window.vuguState = state; 146 state.callbackHandlerFunc = callbackHandlerFunc; 147 } 148 149 window.vuguGetRenderArray = function () { 150 if (!window.vuguRenderArray) { 151 window.vuguRenderArray = new Uint8Array(16384); 152 } 153 return window.vuguRenderArray; 154 } 155 156 window.vuguRender = function () { 157 158 let buffer = window.vuguRenderArray; 159 if (!window.vuguRenderArray) { 160 throw "window.vuguRenderArray is not set"; 161 } 162 163 // NOTE: vuguRender must not automatically reset anything between calls. 164 // Since a series of instructions might get cut off due to buffer end, we 165 // need to be able to just pick right up with the next call where we left off. 166 // The caller decides when to reset things by sending the appropriate 167 // instruction(s). 168 169 let state = window.vuguState || {}; 170 window.vuguState = state; 171 172 // console.log("vuguRender called"); 173 174 let textEncoder = new TextEncoder(); 175 176 let bufferView = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength); 177 178 var decoder = new Decoder(bufferView, 0); 179 180 // state.refMap = state.refMap || {}; 181 // state.curRef = state.curRef || ""; // current reference number (as a hex string) 182 // state.curRefEl = state.curRefEl || null; // current reference element 183 // state.elStack = state.elStack || []; // stack of elements as we traverse the DOM tree 184 185 // mount point element 186 state.mountPointEl = state.mountPointEl || null; 187 188 // currently selected element 189 state.el = state.el || null; 190 191 // buffered innerHTML currently inflight 192 state.bufferedInnerHTML = state.bufferedInnerHTML || null; 193 194 // specifies a "next" move for the current element, if used it must be followed by 195 // one of opcodeSetElement, opcodeSetText, opcodeSetComment, which will create/replace/use existing 196 // the element and put it in "el". The point is this allow us to select nodes that may 197 // not exist yet, knowing that the next call will specify what that node is. It's more complex here 198 // but makes it easier to generate instructions while walking a DOM tree. 199 // Value is one of "first_child", "next_sibling" 200 // (Parents always exist and so doesn't use this mechanism.) 201 state.nextElMove = state.nextElMove || null; 202 203 // keeps track of attributes that are being set on the current element, so we can remove any extras 204 state.elAttrNames = state.elAttrNames || {}; 205 206 // map of positionID -> array of listener spec and handler function, for all elements 207 state.eventHandlerMap = state.eventHandlerMap || {}; 208 209 // keeps track of event listeners that are being set on the current element, so we can remvoe any extras 210 state.elEventKeys = state.elEventKeys || {}; 211 212 instructionLoop: while (true) { 213 214 let opcode = decoder.readUint8(); 215 216 try { 217 218 /*DEBUG*/ console.log("processing opcode", opcode, "("+textOpcodes[opcode]+")"); 219 220 switch (opcode) { 221 222 case opcodeEnd: { 223 break instructionLoop; 224 } 225 226 case opcodeClearEl: { 227 state.el = null; 228 state.nextElMove = null; 229 break; 230 } 231 232 case opcodeSetProperty: { 233 let el = state.el; 234 if (!el) { 235 throw "opcodeSetProperty: no current reference"; 236 } 237 let propName = decoder.readString(); 238 let propValueJSON = decoder.readString(); 239 /*DEBUG*/ console.log("opcodeSetProperty", propName, propValueJSON); 240 el[propName] = JSON.parse(propValueJSON); 241 break; 242 } 243 244 case opcodeSelectQuery: { 245 let selector = decoder.readString(); 246 /*DEBUG*/ console.log("opcodeSelectQuery", selector); 247 state.el = document.querySelector(selector); 248 state.nextElMove = null; 249 break; 250 } 251 252 case opcodeSetAttrStr: { 253 let el = state.el; 254 if (!el) { 255 throw "opcodeSetAttrStr: no current reference"; 256 } 257 let attrName = decoder.readString(); 258 let attrValue = decoder.readString(); 259 /*DEBUG*/ console.log("opcodeSetAttrStr", attrName, attrValue); 260 el.setAttribute(attrName, attrValue); 261 state.elAttrNames[attrName] = true; 262 // console.log("setting attr", attrName, attrValue, el) 263 break; 264 } 265 266 case opcodeSetAttrNSStr: { 267 let el = state.el; 268 if (!el) { 269 throw "opcodeSetAttrNSStr: no current reference"; 270 } 271 let attrNamespace = decoder.readString(); 272 if (attrNamespace == "") { 273 attrNamespace = null 274 } 275 let attrName = decoder.readString(); 276 let attrValue = decoder.readString(); 277 /*DEBUG*/ console.log("opcodeSetAttrNSStr", attrNamespace, attrName, attrValue); 278 el.setAttributeNS(attrNamespace, attrName, attrValue); 279 state.elAttrNames[attrName] = true; 280 // console.log("setting attr", attrName, attrValue, el) 281 break; 282 } 283 284 case opcodeSelectMountPoint: { 285 286 state.elAttrNames = {}; // reset attribute list 287 state.elEventKeys = {}; 288 289 // select mount point using selector or if it was done earlier re-use the one from before 290 let selector = decoder.readString(); 291 let nodeName = decoder.readString(); 292 293 /*DEBUG*/ console.log("opcodeSelectMountPoint", selector, nodeName); 294 295 // console.log("GOT HERE selector,nodeName = ", selector, nodeName); 296 // console.log("state.mountPointEl", state.mountPointEl); 297 if (state.mountPointEl) { 298 // console.log("opcodeSelectMountPoint: state.mountPointEl already exists, using it", state.mountPointEl, "parent is", state.mountPointEl.parentNode); 299 state.el = state.mountPointEl; 300 // state.elStack.push(state.mountPointEl); 301 } else { 302 // console.log("opcodeSelectMountPoint: state.mountPointEl does not exist, using selector to find it", selector); 303 let el = document.querySelector(selector); 304 if (!el) { 305 throw "mount point selector not found: " + selector; 306 } 307 state.mountPointEl = el; 308 // state.elStack.push(el); 309 state.el = el; 310 } 311 312 let el = state.el; 313 314 // make sure it's the right element name and replace if not 315 if (el.nodeName.toUpperCase() != nodeName.toUpperCase()) { 316 317 let newEl = document.createElement(nodeName); 318 el.parentNode.replaceChild(newEl, el); 319 320 state.mountPointEl = newEl; 321 el = newEl; 322 323 } 324 325 state.el = el; 326 327 state.nextElMove = null; 328 329 break; 330 } 331 332 // remove any elements for the current element that we didn't just set 333 case opcodeRemoveOtherAttrs: { 334 335 if (!state.el) { 336 throw "no element selected"; 337 } 338 339 if (state.nextElMove) { 340 throw "cannot call opcodeRemoveOtherAttrs when nextElMove is set"; 341 } 342 343 // build a list of attribute names to remove 344 let rmAttrNames = []; 345 for (let i = 0; i < state.el.attributes.length; i++) { 346 if (!state.elAttrNames[state.el.attributes[i].name]) { 347 rmAttrNames.push(state.el.attributes[i].name); 348 } 349 } 350 351 // remove them 352 for (let i = 0; i < rmAttrNames.length; i++) { 353 state.el.attributes.removeNamedItem(rmAttrNames[i]); 354 } 355 356 break; 357 } 358 359 // move node selection to parent 360 case opcodeMoveToParent: { 361 362 // this.console.log("opcodeMoveToParent, state.nextElMove=", state.nextElMove); 363 364 // if first_child is next move then we just unset this 365 if (state.nextElMove == "first_child") { 366 state.nextElMove = null; 367 } else { 368 // otherwise we move all silbings after current one, move to parent and reset nextElMove 369 let p = state.el.parentNode; 370 let e = state.el; 371 while (e.nextSibling) { 372 p.removeChild(e.nextSibling); 373 } 374 375 state.el = p; 376 state.nextElMove = null; 377 378 // // otherwise we actually move and also reset nextElMove 379 // state.el = state.el.parentNode; 380 // state.nextElMove = null; 381 } 382 383 break; 384 } 385 386 // move node selection to first child (doesn't have to exist) 387 case opcodeMoveToFirstChild: { 388 389 // if a next move already set, then we need to execute it before we can do this 390 if (state.nextElMove) { 391 if (state.nextElMove == "first_child") { 392 state.el = state.el.firstChild; 393 if (!state.el) { 394 throw "unable to find state.el.firstChild"; 395 } 396 } else if (state.nextElMove == "next_sibling") { 397 state.el = state.el.nextSibling; 398 if (!state.el) { 399 throw "unable to find state.el.nextSibling"; 400 } 401 } 402 state.nextElMove = null; 403 } 404 405 if (!state.el) { 406 throw "must have current selection to use opcodeMoveToFirstChild"; 407 } 408 state.nextElMove = "first_child"; 409 410 break; 411 } 412 413 // move node selection to next sibling (doesn't have to exist) 414 case opcodeMoveToNextSibling: { 415 416 // if a next move already set, then we need to execute it before we can do this 417 if (state.nextElMove) { 418 if (state.nextElMove == "first_child") { 419 state.el = state.el.firstChild; 420 if (!state.el) { 421 throw "unable to find state.el.firstChild"; 422 } 423 } else if (state.nextElMove == "next_sibling") { 424 state.el = state.el.nextSibling; 425 if (!state.el) { 426 throw "unable to find state.el.nextSibling"; 427 } 428 } 429 state.nextElMove = null; 430 } 431 432 if (!state.el) { 433 throw "must have current selection to use opcodeMoveToNextSibling"; 434 } 435 state.nextElMove = "next_sibling"; 436 437 break; 438 } 439 440 // assign current selected node as an element of the specified type 441 case opcodeSetElement: { 442 443 let nodeName = decoder.readString(); 444 445 /*DEBUG*/ console.log("opcodeSetElement", nodeName); 446 447 state.elAttrNames = {}; 448 state.elEventKeys = {}; 449 450 // handle nextElMove cases 451 452 if (state.nextElMove == "first_child") { 453 state.nextElMove = null; 454 let newEl = state.el.firstChild; 455 if (newEl) { 456 state.el = newEl; 457 // fall through to verify state.el is correct below 458 } else { 459 newEl = document.createElement(nodeName); 460 state.el.appendChild(newEl); 461 state.el = newEl; 462 break; // we're done here, since we just created the right element 463 } 464 } else if (state.nextElMove == "next_sibling") { 465 state.nextElMove = null; 466 let newEl = state.el.nextSibling; 467 if (newEl) { 468 state.el = newEl; 469 // fall through to verify state.el is correct below 470 } else { 471 newEl = document.createElement(nodeName); 472 // console.log("HERE1", state.el); 473 // state.el.insertAdjacentElement(newEl, 'afterend'); 474 state.el.parentNode.appendChild(newEl); 475 state.el = newEl; 476 break; // we're done here, since we just created the right element 477 } 478 } else if (state.nextElMove) { 479 throw "bad state.nextElMove value: " + state.nextElMove; 480 } 481 482 // if we get here we need to verify that state.el is in fact an element of the right type 483 // and replace if not 484 485 if (state.el.nodeType != 1 || state.el.nodeName.toUpperCase() != nodeName.toUpperCase()) { 486 487 let newEl = document.createElement(nodeName); 488 // throw "stopping here"; 489 state.el.parentNode.replaceChild(newEl, state.el); 490 state.el = newEl; 491 492 } 493 494 break; 495 } 496 // assign current selected node as an element of the specified type 497 case opcodeSetElementNS: { 498 499 let nodeName = decoder.readString(); 500 let namespace = decoder.readString(); 501 502 /*DEBUG*/ console.log("opcodeSetElementNS", nodeName, namespace); 503 504 state.elAttrNames = {}; 505 state.elEventKeys = {}; 506 507 // handle nextElMove cases 508 509 if (state.nextElMove == "first_child") { 510 state.nextElMove = null; 511 let newEl = state.el.firstChild; 512 if (newEl) { 513 state.el = newEl; 514 // fall through to verify state.el is correct below 515 } else { 516 newEl = document.createElementNS(namespace, nodeName); 517 state.el.appendChild(newEl); 518 state.el = newEl; 519 break; // we're done here, since we just created the right element 520 } 521 } else if (state.nextElMove == "next_sibling") { 522 state.nextElMove = null; 523 let newEl = state.el.nextSibling; 524 if (newEl) { 525 state.el = newEl; 526 // fall through to verify state.el is correct below 527 } else { 528 newEl = document.createElementNS(namespace, nodeName); 529 // console.log("HERE1", state.el); 530 // state.el.insertAdjacentElement(newEl, 'afterend'); 531 state.el.parentNode.appendChild(newEl); 532 state.el = newEl; 533 break; // we're done here, since we just created the right element 534 } 535 } else if (state.nextElMove) { 536 throw "bad state.nextElMove value: " + state.nextElMove; 537 } 538 539 // if we get here we need to verify that state.el is in fact an element of the right type 540 // and replace if not 541 542 if (state.el.nodeType != 1 || state.el.nodeName.toUpperCase() != nodeName.toUpperCase()) { 543 544 let newEl = document.createElementNS(namespace, nodeName); 545 // throw "stopping here"; 546 state.el.parentNode.replaceChild(newEl, state.el); 547 state.el = newEl; 548 549 } 550 551 break; 552 } 553 554 // assign current selected node as text with specified content 555 case opcodeSetText: { 556 557 let content = decoder.readString(); 558 559 /*DEBUG*/ console.log("opcodeSetText", content); 560 561 // this.console.log("opcodeSetText:", content); 562 563 // handle nextElMove cases 564 565 if (state.nextElMove == "first_child") { 566 state.nextElMove = null; 567 let newEl = state.el.firstChild; 568 // console.log("in opcodeSetText 2"); 569 if (newEl) { 570 state.el = newEl; 571 // fall through to verify state.el is correct below 572 } else { 573 let newEl = document.createTextNode(content); 574 state.el.appendChild(newEl); 575 state.el = newEl; 576 // console.log("in opcodeSetText 3"); 577 break; // we're done here, since we just created the right element 578 } 579 } else if (state.nextElMove == "next_sibling") { 580 state.nextElMove = null; 581 let newEl = state.el.nextSibling; 582 // console.log("in opcodeSetText 4"); 583 if (newEl) { 584 state.el = newEl; 585 // fall through to verify state.el is correct below 586 } else { 587 let newEl = document.createTextNode(content); 588 // state.el.insertAdjacentElement(newEl, 'afterend'); 589 state.el.parentNode.appendChild(newEl); 590 state.el = newEl; 591 // console.log("in opcodeSetText 5"); 592 break; // we're done here, since we just created the right element 593 } 594 } else if (state.nextElMove) { 595 throw "bad state.nextElMove value: " + state.nextElMove; 596 } 597 598 // if we get here we need to verify that state.el is in fact a node of the right type 599 // and with right content and replace if not 600 // console.log("in opcodeSetText 6"); 601 602 if (state.el.nodeType != 3) { 603 604 let newEl = document.createTextNode(content); 605 state.el.parentNode.replaceChild(newEl, state.el); 606 state.el = newEl; 607 // console.log("in opcodeSetText 7"); 608 609 } else { 610 // console.log("in opcodeSetText 8"); 611 state.el.textContent = content; 612 } 613 // console.log("in opcodeSetText 9"); 614 615 break; 616 } 617 618 // assign current selected node as comment with specified content 619 case opcodeSetComment: { 620 621 let content = decoder.readString(); 622 623 /*DEBUG*/ console.log("opcodeSetComment", content); 624 625 // handle nextElMove cases 626 627 if (state.nextElMove == "first_child") { 628 state.nextElMove = null; 629 let newEl = state.el.firstChild; 630 if (newEl) { 631 state.el = newEl; 632 // fall through to verify state.el is correct below 633 } else { 634 let newEl = document.createComment(content); 635 state.el.appendChild(newEl); 636 state.el = newEl; 637 break; // we're done here, since we just created the right element 638 } 639 } else if (state.nextElMove == "next_sibling") { 640 state.nextElMove = null; 641 let newEl = state.el.nextSibling; 642 if (newEl) { 643 state.el = newEl; 644 // fall through to verify state.el is correct below 645 } else { 646 let newEl = document.createComment(content); 647 // state.el.insertAdjacentElement(newEl, 'afterend'); 648 state.el.parentNode.appendChild(newEl); 649 state.el = newEl; 650 break; // we're done here, since we just created the right element 651 } 652 } else if (state.nextElMove) { 653 throw "bad state.nextElMove value: " + state.nextElMove; 654 } 655 656 // if we get here we need to verify that state.el is in fact a node of the right type 657 // and with right content and replace if not 658 659 if (state.el.nodeType != 8) { 660 661 let newEl = document.createComment(content); 662 state.el.parentNode.replaceChild(newEl, state.el); 663 state.el = newEl; 664 665 } else { 666 state.el.textContent = content; 667 } 668 669 break; 670 } 671 672 case opcodeBufferInnerHTML: { 673 let htmlChunk = decoder.readString(); 674 state.bufferedInnerHTML = (state.bufferedInnerHTML || "") + htmlChunk 675 break 676 } 677 678 case opcodeSetInnerHTML: { 679 680 let html = decoder.readString(); 681 682 /*DEBUG*/ console.log("opcodeSetInnerHTML", html); 683 684 // this.console.log("opcodeSetInnerHTML:", html); 685 686 if (!state.el) { 687 throw "opcodeSetInnerHTML must have currently selected element"; 688 } 689 if (state.nextElMove) { 690 throw "opcodeSetInnerHTML nextElMove must not be set"; 691 } 692 if (state.el.nodeType != 1) { 693 throw "opcodeSetInnerHTML currently selected element expected nodeType 1 but has: " + state.el.nodeType; 694 } 695 696 state.el.innerHTML = (state.bufferedInnerHTML || "") + html; 697 state.bufferedInnerHTML = null 698 699 break; 700 } 701 702 // remove all event listeners from currently selected element that were not just set 703 case opcodeRemoveOtherEventListeners: { 704 705 let positionID = decoder.readString(); 706 707 /*DEBUG*/ console.log("opcodeRemoveOtherEventListeners", positionID); 708 709 // look at all registered events for this positionID 710 let emap = state.eventHandlerMap[positionID] || {}; 711 // for any that we didn't just set, remove them 712 let toBeRemoved = []; 713 for (let k in emap) { 714 if (!state.elEventKeys[k]) { 715 toBeRemoved.push(k); 716 } 717 } 718 719 // for each one that was missing, we remove from emap and call removeEventListener 720 for (let i = 0; i < toBeRemoved.length; i++) { 721 let k = toBeRemoved[i]; 722 let f = emap[k]; 723 let kparts = k.split("|"); 724 state.el.removeEventListener(kparts[0], f, {capture: +kparts[1], passive: +kparts[2]}); 725 delete emap[k]; 726 } 727 728 // if emap is empty now, remove the entry from eventHandlerMap altogether 729 if (Object.keys(emap).length == 0) { 730 delete state.eventHandlerMap[positionID]; 731 } else { 732 state.eventHandlerMap[positionID] = emap; 733 } 734 735 break; 736 } 737 738 // assign event listener to currently selected element 739 case opcodeSetEventListener: { 740 let positionID = decoder.readString(); 741 let eventType = decoder.readString(); 742 let capture = decoder.readUint8(); 743 let passive = decoder.readUint8(); 744 745 /*DEBUG*/ console.log("opcodeSetEventListener", positionID, eventType, capture, passive); 746 747 if (!state.el) { 748 throw "must have state.el set in order to call opcodeSetEventListener"; 749 } 750 751 var eventKey = eventType + "|" + (capture ? "1" : "0") + "|" + (passive ? "1" : "0"); 752 state.elEventKeys[eventKey] = true; 753 754 // map of positionID -> map of listener spec and handler function, for all elements 755 //state.eventHandlerMap 756 let emap = state.eventHandlerMap[positionID] || {}; 757 758 // register function if not done already 759 let f = emap[eventKey]; 760 if (!f) { 761 f = function (event) { 762 763 /*DEBUG*/ console.log("event listener called with event", event); 764 765 // set the active event, so the Go code and call back in and examine it if needed 766 state.activeEvent = event; 767 768 let eventObj = {}; 769 // console.log(event); 770 for (let i in event) { 771 try { 772 // accessing `selectionDirection`, `selectionStart`, or `selectionEnd` throws in WebKit-based browsers. 773 let itype = typeof (event[i]); 774 // copy primitive values directly 775 if (itype == "boolean" || itype == "number" || itype == "string") { 776 eventObj[i] = event[i]; 777 } 778 } catch {} 779 } 780 781 // also do the same for anything in "target" 782 if (event.target) { 783 eventObj.target = {}; 784 let et = event.target; 785 for (let i in et) { 786 try { 787 let itype = typeof (et[i]); 788 if (itype == "boolean" || itype == "number" || itype == "string") { 789 eventObj.target[i] = et[i]; 790 } 791 } catch {} 792 } 793 } 794 795 // console.log(eventObj); 796 // console.log(JSON.stringify(eventObj)); 797 798 let fullJSON = JSON.stringify({ 799 800 // include properties from event registration 801 position_id: positionID, 802 event_type: eventType, 803 capture: !!capture, 804 passive: !!passive, 805 806 // the event object data as extracted above 807 event_summary: eventObj, 808 809 }); 810 811 // console.log(state.eventBuffer); 812 813 // write JSON to state.eventBuffer with uint32 length prefix 814 815 let encodeResultBuffer = textEncoder.encode(fullJSON); 816 817 const dataSize = encodeResultBuffer.byteLength - encodeResultBuffer.byteOffset 818 // we need to allocate more bytes for storing data size in the beginning of the buffer 819 const requiredBufferSize = dataSize + 4 820 821 const computeEventBufferSize = (requiredBufferSize) => { 822 const sixteen_kb = 16384 823 const actualRequired = requiredBufferSize + 1 824 const remainder = actualRequired % sixteen_kb 825 826 // but for now this needs to be at least one byte shorter 827 // than Go's buffer 828 if (remainder === 0) { 829 return actualRequired - 1 830 } 831 832 return actualRequired + (sixteen_kb - remainder) - 1 833 } 834 835 // before eventHandlerFunc is called make sure eventBuffer and eventBufferView are setup, 836 // and allocateEventBuffer is called 837 let eventBuffer = state.eventBuffer; 838 if (!eventBuffer || eventBuffer.length < requiredBufferSize) { 839 const eventBufferSize = computeEventBufferSize(requiredBufferSize) 840 eventBuffer = new Uint8Array(eventBufferSize); 841 state.eventBuffer = eventBuffer; 842 state.eventBufferView = new DataView(eventBuffer.buffer, eventBuffer.byteOffset, eventBuffer.byteLength); 843 } 844 //console.log("encodeResult", encodeResult); 845 state.eventBuffer.set(encodeResultBuffer, 4); // copy encoded string to event buffer 846 // now write length using DataView as uint32 847 state.eventBufferView.setUint32(0, dataSize); 848 849 // let result = textEncoder.encodeInto(fullJSON, state.eventBuffer); 850 // let eventBufferDataView = new DataView(state.eventBuffer.buffer, state.eventBuffer.byteOffset, state.eventBuffer.byteLength); 851 // eventBufferDataView.setUint8(result.written, 0); 852 853 // write length after, since only now do we know the final length 854 // state.eventBufferView.setUint32(0, result.written); 855 856 // serialize event into the event buffer, somehow, 857 // and keep track of the target element, also consider grabbing 858 // the value or relevant properties as appropriate for form things 859 860 /*DEBUG*/ console.log("event handler calling state.eventHandlerFunc", eventBuffer); 861 state.eventHandlerFunc.call(null, eventBuffer); // call with null this avoids unnecessary js.Value reference 862 863 // unset the active event 864 state.activeEvent = null; 865 }; 866 emap[eventKey] = f; 867 868 // remove here if we noted it as added before 869 // NOTE: there are cases where this may have no effect, since it is possible for the 870 // element to have be removed and recreated. 871 state.el.removeEventListener(eventType, f, {capture: capture, passive: passive}); 872 873 } 874 875 // we always re-add the event listener, see note above 876 //this.console.log("addEventListener", eventType); 877 state.el.addEventListener(eventType, f, {capture: capture, passive: passive}); 878 879 state.eventHandlerMap[positionID] = emap; 880 881 // this.console.log("opcodeSetEventListener", positionID, eventType, capture, passive); 882 break; 883 } 884 885 case opcodeSetCSSTag: { 886 887 let elementName = decoder.readString(); 888 let textContent = decoder.readString(); 889 let attrPairsLen = decoder.readUint8(); 890 891 892 /*DEBUG*/ console.log("opcodeSetCSSTag", elementName, textContent, attrPairsLen); 893 894 if (attrPairsLen % 2 != 0) { 895 throw "attrPairsLen is odd number: " + attrPairsLen; 896 } 897 // loop over one key/value pair at a time and put them in a map 898 var attrMap = {}; 899 for (let i = 0; i < attrPairsLen; i += 2) { 900 let key = decoder.readString(); 901 let val = decoder.readString(); 902 /*DEBUG*/ console.log("opcodeSetCSSTag attr", key, val); 903 attrMap[key] = val; 904 } 905 906 // this.console.log("got opcodeSetCSSTag: elementName=", elementName, "textContent=", textContent, "attrMap=", attrMap) 907 908 state.elCSSTagsSet = state.elCSSTagsSet || []; // ensure state.elCSSTagsSet is set to empty array if not already set 909 910 // let elementNameUC = elementName.toUpperCase(); 911 let thisTagKey = textContent; 912 if (elementName == "link") { 913 thisTagKey = attrMap["href"]; 914 } 915 916 if (thisTagKey == "") { // nothing to do in this case 917 this.console.log("element", elementName, "ignored due to empty key"); 918 break; 919 } 920 921 // TODO: 922 // * find all tags that have the same element type (link or style) 923 // * for each one for style use textContent as key, for link use url 924 // * see if matching tag already exists 925 // * if it has vuguCreated==true on it, then add to map of css tags set, else ignore 926 // * if no matching tag then create and set vuguCreated=true, add to map of css tags set 927 928 let foundTag = null; 929 this.document.querySelectorAll(elementName).forEach(cssEl => { 930 let cssElKey; 931 if (elementName == "style") { 932 cssElKey = cssEl.textContent; 933 } else /* elementName == "link" */ { 934 cssElKey = cssEl.href; 935 } 936 937 if (thisTagKey == cssElKey) { // textContent or href as appropriate is used to determine "sameness" 938 foundTag = cssEl; 939 } 940 }); 941 942 // could not find it, create 943 if (!foundTag) { 944 let cTag = this.document.createElement(elementName); 945 for (let k in attrMap) { 946 cTag.setAttribute(k, attrMap[k]); 947 } 948 cTag.vuguCreated = true; // so we know that we created this, as opposed to it already having been on the page 949 // this.console.log("GOT TEXTCONTENT: ", textContent); 950 if (textContent) { 951 cTag.appendChild(document.createTextNode(textContent)) // set textContent if provided 952 // cTag.innerText = textContent; // set textContent if provided 953 } 954 this.document.head.appendChild(cTag); // add to end of head 955 // this.console.log("CREATED ctag: ", cTag); 956 state.elCSSTagsSet.push(cTag); // add to elCSSTagsSet for use in opcodeRemoveOtherCSSTags 957 } else { 958 // if we did find it, we need to push to state.elCSSTagsSet to tell opcodeRemoveOtherCSSTags not to remove it 959 state.elCSSTagsSet.push(foundTag); 960 } 961 962 break; 963 } 964 case opcodeRemoveOtherCSSTags: { 965 966 /*DEBUG*/ console.log("opcodeRemoveOtherCSSTags"); 967 968 // any link or style tag in doc that has vuguCreated==true and is not in css tags set map gets removed 969 970 state.elCSSTagsSet = state.elCSSTagsSet || []; 971 972 this.document.querySelectorAll('style,link').forEach(cssEl => { 973 974 // ignore any not created by vugu 975 if (!cssEl.vuguCreated) { 976 return; 977 } 978 979 // ignore if in elCSSTagsSet 980 if (state.elCSSTagsSet.findIndex(el => el == cssEl) >= 0) { 981 return; 982 } 983 984 // if we got here, we remove the tag 985 cssEl.parentNode.removeChild(cssEl); 986 }); 987 988 state.elCSSTagsSet = null; // clear this out so it gets reinitialized the next time opcodeSetCSSTag or this opcode is used 989 990 break; 991 } 992 993 case opcodeCallbackLastElement: { 994 let callbackID = decoder.readUint32(); 995 996 /*DEBUG*/ console.log("opcodeCallbackLastElement", callbackID); 997 998 let el = state.el; 999 if (!el) { 1000 throw "opcodeCallbackLastElement: no current reference"; 1001 } 1002 // this.console.log("got opcodeCallbackLastElement, ", callbackID); 1003 state.callbackHandlerFunc(callbackID, el); 1004 break; 1005 } 1006 1007 case opcodeCallback: { 1008 let callbackID = decoder.readUint32(); 1009 1010 /*DEBUG*/ console.log("opcodeCallback", callbackID); 1011 1012 state.callbackHandlerFunc(callbackID); 1013 break; 1014 } 1015 1016 default: { 1017 console.error("found invalid opcode", opcode); 1018 return; 1019 } 1020 } 1021 1022 } catch (e) { 1023 this.console.log("Error during instruction loop. Data opcode=", opcode, 1024 ", state.el=", state.el, 1025 ", state.nextElMove=", state.nextElMove, 1026 ", with error: ", e) 1027 throw e; 1028 } 1029 1030 1031 } 1032 1033 } 1034 1035 })()