github.com/vugu/vugu@v0.3.6-0.20240430171613-3f6f402e014b/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  })()