constructor(element) {
      this.treeBuilt = Mavo.promise();
      this.dataLoaded = Mavo.promise();
      this.deleted = [];
      this.element = element;
      this.inProgress = false; // Index among other mavos in the page, 1 is first

      this.index = Object.keys(_.all).length + 1;
      Object.defineProperty(_.all, this.index - 1, {
        value: this,
        configurable: true
      }); // Assign a unique (for the page) id to this mavo instance

      this.id = Mavo.getAttribute(this.element, "mv-app", "id") || "mavo".concat(this.index);

      if (this.id in _.all) {
        // Duplicate app name
        for (var i = 2; (this.id + i in _.all); i++) {}

        this.id = this.id + i;
      }

      _.all[this.id] = this;
      this.element.setAttribute("mv-app", this.id);
      this.observe({
        attribute: "lang",
        deep: false
      }, () => {
        var lang = Mavo.getClosestAttribute(this.element, "lang") || Mavo.locale;
        this.locale = Mavo.Locale.get(lang);
      })(); // Should we start in edit mode?

      this.autoEdit = this.element.classList.contains("mv-autoedit"); // Should we save automatically?

      this.autoSave = this.element.hasAttribute("mv-autosave");
      this.autoSaveDelay = (this.element.getAttribute("mv-autosave") || 0) * 1000;
      Mavo.setAttributeShy(this.element, "typeof", "");
      Mavo.hooks.run("init-start", this); // ----- Heuristic for groups ------
      // Now, turn properties that contain other properties into groups

      $$(_.selectors.primitive, this.element).forEach(element => {
        if ($(_.selectors.property, element)) {
          // contains other properties
          let config = Mavo.Primitive.getConfig(element);

          if (!config.attribute && !config.hasChildren || element.hasAttribute("mv-list-item")) {
            element.setAttribute("mv-group", "");
          }
        }
      });
      this.expressions = new Mavo.Expressions(this);
      _.observers = _.observers || new Mavo.Observers();

      _.observers.observer.observe(this.element, {
        // Observe everything
        characterData: true,
        childList: true,
        subtree: true,
        attributes: true
      }); // Build mavo objects


      Mavo.hooks.run("init-tree-before", this);
      this.root = new Mavo.Group(this.element, this);
      this.treeBuilt.resolve();
      Mavo.hooks.run("init-tree-after", this);
      this.permissions = new Mavo.Permissions();
      var backendTypes = ["source", "storage", "init", "uploads"]; // order is significant!
      // Figure out backends for storage, data reads, and initialization respectively

      backendTypes.forEach(role => this.updateBackend(role));
      this.observe({
        deep: false,
        attribute: true
      }, ({
        attribute
      }) => {
        if (attribute.indexOf("mv-") === 0) {
          var _attribute$replace, _attribute$replace$sp;

          // We want to observe changes both in a backend (the mv-role attribute)
          // and its metadata (provided via the mv-role-* family of attributes)
          let role = attribute === null || attribute === void 0 ? void 0 : (_attribute$replace = attribute.replace(/^mv-/, "")) === null || _attribute$replace === void 0 ? void 0 : (_attribute$replace$sp = _attribute$replace.split("-")) === null || _attribute$replace$sp === void 0 ? void 0 : _attribute$replace$sp[0];

          if (backendTypes.includes(role)) {
            this.updateBackend(role); // Do we need to re-load data?

            if (role === "source" || !this.source && (role === "storage" || role === "init" && !this.root.data)) {
              this.load();
            }
          }
        }
      });
      this.permissions.can("login", () => {
        // We also support a URL param to trigger login, in case the user doesn't want visible login UI
        let loginUrlParam;

        if (Mavo.Functions.url("login") !== null && this.index === 1) {
          loginUrlParam = "login";
        } else if (Mavo.Functions.url(this.id + "-login") !== null) {
          loginUrlParam = this.id + "-login";
        }

        if (loginUrlParam !== undefined) {
          // Remove param from url
          const currentURL = new URL(location.href);
          currentURL.searchParams.delete(loginUrlParam);
          history.replaceState(null, "", currentURL);
          this.primaryBackend.login();
        }
      }); // Update login status

      $.bind(this.element, "mv-login.mavo", evt => {
        if (evt.backend == (this.source || this.storage)) {
          // If last time we rendered we got nothing, maybe now we'll have better luck?
          if (!this.root.data && !this.unsavedChanges) {
            this.load();
          }
        }
      });
      this.bar = new Mavo.UI.Bar(this); // Is there any control that requires an edit button?

      this.needsEdit = this.calculateNeedsEdit();
      this.setUnsavedChanges(false);
      this.permissions.onchange(({
        action,
        value
      }) => {
        var permissions = this.element.getAttribute("mv-permissions") || "";
        permissions = permissions.trim().split(/\s+/).filter(a => a != action);

        if (value) {
          permissions.push(action);
        }

        this.element.setAttribute("mv-permissions", permissions.join(" "));
      });
      this.permissions.can(["edit", "add", "delete"], () => {
        if (this.autoEdit) {
          this.edit();
        }
      }); // Observe entire tree for mv-mode changes

      this.observe({
        attribute: "mv-mode"
      }, ({
        element
      }) => {
        if (!this.permissions.edit && !this.permissions.add && !this.permissions.delete) {
          return;
        }

        let nodes = _.Node.children(element);

        nodeloop: for (let i = 0; i < nodes.length; i++) {
          let node = nodes[i];
          let previousMode = node.mode,
              mode;

          if (node.element == element) {
            // If attribute set directly on a Mavo node, then it forces it into that mode
            // otherwise, descendant nodes still inherit, unless they are also mode-restricted
            mode = node.element.getAttribute("mv-mode");
            node.modes = mode;
          } else {
            // Inherited
            if (node.modes) {
              // Mode-restricted, we cannot change to the other mode
              continue nodeloop;
            }

            mode = _.getStyle(node.element.parentNode, "--mv-mode");
          }

          node.mode = mode;

          if (previousMode != node.mode) {
            node[node.mode == "edit" ? "edit" : "done"]();
          }
        }
      });

      if (this.primaryBackend) {
        // Fetch existing data
        this.permissions.can("read", () => this.load());
      } else {
        // No storage or source
        requestAnimationFrame(() => {
          this.dataLoaded.resolve();
          this.expressions.update();
          this.element.dispatchEvent(new Event("mv-load", {
            bubbles: true
          }));
        });
      } // Dynamic ids


      $.bind(this.element, "mv-load.mavo", evt => {
        if (location.hash) {
          var callback = () => {
            var target = document.getElementById(location.hash.slice(1));

            if (target || !location.hash) {
              if (this.element.contains(target)) {
                requestAnimationFrame(() => {
                  // Give the browser a chance to render
                  Mavo.scrollIntoViewIfNeeded(target);
                });
              }
            }

            return target;
          };

          if (!callback()) {
            // No target, perhaps not yet?
            this.observe({
              attribute: "id",
              once: true
            }, callback); // FIXME if expressions take multiple cycles to resolve, this will not scroll to the proper id
            // FIXME also, if the user has started interacting with the document, we shouldn't scroll
          }
        }

        requestAnimationFrame(() => Stretchy.resizeAll());
      });
      this.dataLoaded.then(async evt => {
        await Mavo.defer();
        this.permissions.can("save", () => {
          if (this.autoSave) {
            let debouncedSave = _.debounce(() => {
              this.save();
            }, this.autoSaveDelay);

            $.bind(this.element, "mv-change.mavo:autosave", evt => {
              if (evt.node.saved && this.autoSave) {
                debouncedSave();
              }
            });
          }
        }, () => {
          $.unbind(this.element, "mv-change.mavo:autosave");
        });
      }); // Keyboard navigation

      this.element.addEventListener("keydown", evt => {
        var element = evt.target; // Ctrl + S or Cmd + S to save

        if (this.permissions.save && evt.key == "S" && evt[_.superKey] && !evt.altKey) {
          evt.preventDefault();
          this.save();
        } else if (evt.key === "ArrowUp" || evt.key === "ArrowDown") {
          if (element.matches("textarea, input[type=range], input[type=number]")) {
            // Up/down arrow keys are meaningful here
            return;
          }

          if (element.matches(".mv-editor")) {
            var editor = true;
            element = element.parentNode;
          }

          var node = Mavo.Node.get(element);

          if (node !== null && node !== void 0 && node.closestCollection) {
            var nextNode = node.getCousin(evt.key === "ArrowUp" ? -1 : 1, {
              wrap: true
            });

            if (nextNode) {
              if (editor && nextNode.editing) {
                nextNode.edit();
                nextNode.editor.focus();
              } else {
                nextNode.element.focus();
              }

              evt.preventDefault();
            }
          }
        }
      });
      $.bind(this.element, "click submit", _.Actions.listener);
      Mavo.hooks.run("init-end", this);
    }
Mavo: Create web applications by writing HTML and CSS!
@author Lea Verou and contributors
@version v0.3.2