Changeset - 91ae65157e5f
[Not reviewed]
default
0 5 0
drewp@bigasterisk.com - 20 months ago 2023-05-25 18:46:08
drewp@bigasterisk.com
logging and comments
5 files changed with 13 insertions and 8 deletions:
0 comments (0 inline, 0 general)
light9/live/Effect.ts
Show inline comments
 
import debug from "debug";
 
import { Literal, NamedNode, Quad_Object, Quad_Predicate, Quad_Subject, Term } from "n3";
 
import { some } from "underscore";
 
import { Patch, patchContainsPreds, patchUpdate } from "../web/patch";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { shortShow } from "../web/show_specific";
 

	
 
// todo: Align these names with newtypes.py, which uses HexColor and VTUnion.
 
type Color = string;
 
export type ControlValue = number | Color | NamedNode;
 

	
 
const log = debug("effect");
 

	
 
function isUri(x: Term | number | string): x is NamedNode {
 
  return typeof x == "object" && x.termType == "NamedNode";
 
}
 

	
 
function valuePred(graph: SyncedGraph, attr: NamedNode): NamedNode {
 
  const U = graph.U();
 
  const scaledAttributeTypes = [U(":color"), U(":brightness"), U(":uv")];
 
  if (some(scaledAttributeTypes, (x: NamedNode) => attr.equals(x))) {
 
    return U(":scaledValue");
 
  } else {
 
    return U(":value");
 
  }
 
}
 

	
 
// effect settings data; r/w sync with the graph
 
export class Effect {
 
  private settings: Array<{ device: NamedNode; deviceAttr: NamedNode; setting: NamedNode; value: ControlValue }> = [];
 
  private ctxForEffect: NamedNode
 
  constructor(
 
    public graph: SyncedGraph,
 
    public uri: NamedNode,
 
    // called if the graph changes our values and not when the caller uses edit()
 
    private onValuesChanged: (values: void) => void
 
  ) {
 
    this.ctxForEffect = this.graph.Uri(this.uri.value.replace("light9.bigasterisk.com/effect", `light9.bigasterisk.com/show/${shortShow}/effect`));
 
    graph.runHandler(this.rebuildSettingsFromGraph.bind(this), `effect sync ${uri.value}`);
 
  }
 

	
 
  addNewEffectToGraph() {
 
    const U = this.graph.U();
 
    const quad = (s: Quad_Subject, p: Quad_Predicate, o: Quad_Object) => this.graph.Quad(s, p, o, this.ctxForEffect);
 

	
 
    const addQuads = [
 
      quad(this.uri, U("rdf:type"), U(":Effect")),
 
      quad(this.uri, U("rdfs:label"), this.graph.Literal(this.uri.value.replace(/.*\//, ""))),
 
      quad(this.uri, U(":publishAttr"), U(":strength")),
 
    ];
 
    const patch = { adds: addQuads, dels: [] } as Patch;
 
    log("init new effect", patch);
 
    this.settings = [];
 
    this.graph.applyAndSendPatch(patch);
 
  }
 

	
 
  rebuildSettingsFromGraph(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch && !patchContainsPreds(patch, [U(":setting"), U(":device"), U(":deviceAttr")])) {
 
      // that's an approx list of preds , but it just means we'll miss some pathological settings edits
 
      //   return;
 
    }
 

	
 
    // log("syncFromGraph", this.uri);
 

	
 
    // this repeats work- it gathers all settings when really some values changed (and we might even know about them). maybe push the value-fetching into a secnod phase of the run, and have the 1st phase drop out early
 
    const newSettings = [];
 

	
 
    const seenDevAttrPairs: Set<string> = new Set();
 

	
 
    for (let setting of Array.from(this.graph.objects(this.uri, U(":setting")))) {
 
      //   log(`  setting ${setting.value}`);
 
      if (!isUri(setting)) throw new Error();
 
      let value: ControlValue;
 
      const device = this.graph.uriValue(setting, U(":device"));
 
      const deviceAttr = this.graph.uriValue(setting, U(":deviceAttr"));
 

	
 
      const pred = valuePred(this.graph, deviceAttr);
 
      try {
 
        value = this.graph.uriValue(setting, pred);
 
        if (!(value as NamedNode).id.match(/^http/)) {
 
          throw new Error("not uri");
 
        }
 
      } catch (error) {
 
        try {
 
          value = this.graph.floatValue(setting, pred);
 
        } catch (error1) {
 
          value = this.graph.stringValue(setting, pred); // this may find multi values and throw
 
        }
 
      }
 
      //   log(`change: graph contains ${deviceAttr.value} ${value}`);
 

	
 
      newSettings.push({ device, deviceAttr, setting, value });
 
    }
 
    this.settings = newSettings;
 
    log(`rebuild to ${this.settings.length}`);
 
    this.onValuesChanged();
 
  }
 

	
 
  currentValue(device: NamedNode, deviceAttr: NamedNode): ControlValue | null {
 
    for (let s of this.settings) {
 
      if (device.equals(s.device) && deviceAttr.equals(s.deviceAttr)) {
 
        return s.value;
 
      }
light9/live/Light9DeviceControl.ts
Show inline comments
 
import debug from "debug";
 
import { css, html, LitElement } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { unique } from "underscore";
 
import { Patch, patchContainsPreds } from "../web/patch";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { GraphToControls } from "./GraphToControls";
 
import { Choice } from "./Light9Listbox";
 
import { Light9LiveControl } from "./Light9LiveControl";
 
export { ResourceDisplay } from "../web/ResourceDisplay";
 
export { Light9LiveControl };
 
const log = debug("devcontrol");
 

	
 
export interface DeviceAttrRow {
 
  uri: NamedNode; //devattr
 
  attrClasses: string; // the css kind
 
  dataType: NamedNode;
 
  showColorPicker: boolean;
 
  useColor: boolean;
 
  useChoice: boolean;
 
  choices: Choice[];
 
  choiceSize: number;
 
  useSlider: boolean;
 
  max: number;
 
}
 

	
 
@customElement("light9-device-control")
 
export class Light9DeviceControl extends LitElement {
 
  graph!: SyncedGraph;
 
  static styles = [
 
    css`
 
      :host {
 
        display: inline-block;
 
      }
 
      .device {
 
        border: 2px solid #151e2d;
 
        margin: 4px;
 
        padding: 1px;
 
        background: #171717; /* deviceClass gradient added later */
 
        break-inside: avoid-column;
 
        width: 335px;
 
      }
 
      .deviceAttr {
 
        border-top: 1px solid #272727;
 
        padding-bottom: 2px;
 
        display: flex;
 
      }
 
      .deviceAttr > span {
 
      }
 
      .deviceAttr > light9-live-control {
 
        flex-grow: 1;
 
      }
 
      h2 {
 
        font-size: 110%;
 
        padding: 4px;
 
        margin-top: 0;
 
        margin-bottom: 0;
 
      }
 
      .device,
 
      h2 {
 
        border-top-right-radius: 15px;
 
      }
 

	
 
      #mainLabel {
 
        font-size: 120%;
 
        color: #9ab8fd;
 
        text-decoration: initial;
 
      }
 
      .device.selected h2 {
 
        outline: 3px solid #ffff0047;
 
      }
 
      .deviceAttr.selected {
 
        background: #cada1829;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <div class="device ${this.devClasses}">
 
        <h2 style="${this._bgStyle(this.deviceClass)}" xon-click="onClick">
 
          <resource-display id="mainLabel" .uri="${this.uri}"></resource-display>
 
          a <resource-display minor .uri="${this.deviceClass}"></resource-display>
 
        </h2>
 

	
 
        ${this.deviceAttrs.map(
 
          (dattr: DeviceAttrRow) => html`
 
            <div xon-click="onAttrClick" class="deviceAttr ${dattr.attrClasses}">
 
            <div @click="onAttrClick" class="deviceAttr ${dattr.attrClasses}">
 
              <span>attr <resource-display minor .uri="${dattr.uri}"></resource-display></span>
 
              <light9-live-control
 
                .device="${this.uri}"
 
                .deviceAttrRow="${dattr}"
 
                .effect="${this.effect}"
 
                .graphToControls="${this.graphToControls}"
 
              ></light9-live-control>
 
            </div>
 
          `
 
        )}
 
      </div>
 
    `;
 
  }
 

	
 
  @property() uri!: NamedNode;
 
  @property() effect!: NamedNode;
 
  @property() graphToControls!: GraphToControls;
 

	
 
  @property() devClasses: string = ""; // the css kind
 
  @property() deviceAttrs: DeviceAttrRow[] = [];
 
  @property() deviceClass: NamedNode | null = null;
 
  @property() selectedAttrs: Set<NamedNode> = new Set();
 

	
 
  constructor() {
 
    super();
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.graph.runHandler(this.syncDeviceAttrsFromGraph.bind(this), `${this.uri.value} update`);
 
    });
 
    this.selectedAttrs = new Set();
 
  }
 

	
 
  _bgStyle(deviceClass: NamedNode | null): string {
 
    if (!deviceClass) return "";
 
    let hash = 0;
 
    const u = deviceClass.value;
 
    for (let i = u.length - 10; i < u.length; i++) {
 
      hash += u.charCodeAt(i);
 
    }
 
    const hue = (hash * 8) % 360;
 
    const accent = `hsl(${hue}, 49%, 22%)`;
 
    return `background: linear-gradient(to right, rgba(31,31,31,0) 50%, ${accent} 100%);`;
 
  }
 

	
 
  setDeviceSelected(isSel: any) {
 
    this.devClasses = isSel ? "selected" : "";
 
  }
 

	
 
  setAttrSelected(devAttr: NamedNode, isSel: boolean) {
 
    if (isSel) {
 
      this.selectedAttrs.add(devAttr);
 
    } else {
 
      this.selectedAttrs.delete(devAttr);
 
    }
 
    // this.syncDeviceAttrsFromGraph();
 
  }
 

	
 
  syncDeviceAttrsFromGraph(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch && !patchContainsPreds(patch, [U("rdf:type"), U(":deviceAttr"), U(":dataType"), U(":choice")])) {
 
      return;
 
    }
 
    try {
 
    this.deviceClass = this.graph.uriValue(this.uri, U("rdf:type"));
 
    } catch(e) {
 
      // what's likely is we're going through a graph reload and the graph 
 
      // is gone but the controls remain
 
    }
 
    this.deviceAttrs = [];
 
    Array.from(unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(":deviceAttr"))))).map((da: NamedNode) =>
 
      this.deviceAttrs.push(this.attrRow(da))
 
    );
 
    this.requestUpdate();
 
  }
 

	
 
  attrRow(devAttr: NamedNode): DeviceAttrRow {
 
    let x: NamedNode;
 
    const U = (x: string) => this.graph.Uri(x);
 
    const dataType = this.graph.uriValue(devAttr, U(":dataType"));
 
    const daRow = {
 
      uri: devAttr,
 
      dataType,
 
      showColorPicker: dataType.equals(U(":color")),
 
      attrClasses: this.selectedAttrs.has(devAttr) ? "selected" : "",
 
      useColor: false,
 
      useChoice: false,
 
      choices: [] as Choice[],
 
      choiceSize: 0,
 
      useSlider: false,
 
      max: 1,
 
    };
 
    if (dataType.equals(U(":color"))) {
 
      daRow.useColor = true;
 
    } else if (dataType.equals(U(":choice"))) {
 
      daRow.useChoice = true;
 
      const choiceUris = this.graph.sortedUris(this.graph.objects(devAttr, U(":choice")));
light9/live/Light9Listbox.ts
Show inline comments
 
import debug from "debug";
 
const log = debug("listbox");
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
export type Choice = {uri:string,label:string}
 
const log = debug("listbox");
 
export type Choice = { uri: string; label: string };
 

	
 
@customElement("light9-listbox")
 
export class Light9Listbox extends LitElement {
 
  static styles = [
 
    css`
 
      paper-listbox {
 
        --paper-listbox-background-color: none;
 
        --paper-listbox-color: white;
 
        --paper-listbox: {
 
          /* measure biggest item? use flex for columns? */
 
          column-width: 9em;
 
        }
 
      }
 
      paper-item {
 
        --paper-item-min-height: 0;
 
        --paper-item: {
 
          display: block;
 
          border: 1px outset #0f440f;
 
          margin: 0 1px 5px 0;
 
          background: #0b1d0b;
 
        }
 
      }
 
      paper-item.iron-selected {
 
        background: #7b7b4a;
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <paper-listbox id="list" selected="{{value}}" attr-for-selected="uri" on-focus-changed="selectOnFocus">
 
        <paper-item on-focus="selectOnFocus">None</paper-item>
 
        <template is="dom-repeat" items="{{choices}}">
 
          <paper-item on-focus="selectOnFocus" uri="{{item.uri}}">{{item.label}}</paper-item>
 
        </template>
 
      </paper-listbox>
 
    `;
 
  }
 
  @property() choices: Array<Choice> = [];
 
  @property() value: String | null = null;
 

	
 
  constructor() {
 
    super();
 
  }
 
  selectOnFocus(ev) {
 
    if (ev.target.uri === undefined) {
 
      // *don't* clear for this, or we can't cycle through all choices (including none) with up/down keys
 
      //this.clear();
 
      //return;
 
    }
 
    this.value = ev.target.uri;
 
  }
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("value")) {
 
      if (this.value === null) {
 
        this.clear();
 
      }
 
    }
 
  }
 
  onValue(value: String | null) {
 
    if (value === null) {
 
      this.clear();
 
    }
 
  }
 
  clear() {
 
    this.querySelectorAll("paper-item").forEach(function (item) {
 
      item.blur();
 
    });
 
    this.value = null;
 
  }
 
}
light9/live/Light9LiveControl.ts
Show inline comments
 
import debug from "debug";
 
const log = debug("control");
 
import { css, html, LitElement, PropertyPart, PropertyValues } from "lit";
 
import { css, html, LitElement, PropertyValues } from "lit";
 
import { customElement, property } from "lit/decorators.js";
 
import { NamedNode } from "n3";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { Literal, NamedNode } from "n3";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { ControlValue } from "./Effect";
 
import { GraphToControls } from "./GraphToControls";
 
import { DeviceAttrRow } from "./Light9DeviceControl";
 
import { Choice } from "./Light9Listbox";
 

	
 
const log = debug("control");
 

	
 
export { Slider } from "@material/mwc-slider";
 
// UI for one device attr (of any type).
 
@customElement("light9-live-control")
 
export class Light9LiveControl extends LitElement {
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      #colorControls {
 
        display: flex;
 
        align-items: center;
 
      }
 
      #colorControls > * {
 
        margin: 0 3px;
 
      }
 
      #colorControls paper-slider {
 
      }
 
      paper-slider {
 
        width: 100%;
 
        height: 25px;
 
      }
 

	
 
      paper-slider {
 
        --paper-slider-knob-color: var(--paper-red-500);
 
        --paper-slider-active-color: var(--paper-red-500);
 

	
 
        --paper-slider-font-color: white;
 
        --paper-slider-input: {
 
          width: 75px;
 

	
 
          background: black;
 
          display: inline-block;
 
        }
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    if (this.dataType.value === "http://light9.bigasterisk.com/scalar") {
 
      return html`<mwc-slider .value=${this.sliderValue} step=${1 / 255} min="0" max="1" @input=${this.onSliderInput}></mwc-slider> `;
 
    } else if (this.dataType.value === "http://light9.bigasterisk.com/color") {
 
      return html`
 
        <div id="colorControls">
 
          <button on-click="goBlack">0.0</button>
 
          <light9-color-picker color="{{value}}"></light9-color-picker>
 
        </div>
 
      `;
 
    } else if (this.dataType.value === "http://light9.bigasterisk.com/choice") {
 
      return html` <light9-listbox choices="{{deviceAttrRow.choices}}" value="{{choiceValue}}"> </light9-listbox> `;
 
    } else {
 
      throw new Error(`${this.dataType} unknown`);
 
    }
 
  }
 

	
 
  // passed from parent
 
  @property() device!: NamedNode;
 
  @property() dataType: NamedNode;
 
  @property() deviceAttrRow!: DeviceAttrRow;
 
  // we'll connect to this and receive graphValueChanged and send uiValueChanged
 
  @property() graphToControls!: GraphToControls;
 

	
 
  @property() enableChange: boolean = false;
 
  @property() value: ControlValue | null = null;
 

	
 
  // slider mode
 
  @property() sliderValue: number = 0;
 

	
 
  // color mode
 

	
 
  // choice mode
 
  @property() pickedChoice: Choice | null = null;
 
  @property() choiceValue: Choice | null = null;
 

	
 
  constructor() {
 
    super();
 
    this.dataType = new NamedNode("http://light9.bigasterisk.com/scalar");
 
    // getTopGraph().then((g) => {
 
    //   this.graph = g;
 
    //   // this.graph.runHandler(this.graphReads.bind(this), `${this.device} ${this.deviceAttrRow.uri} reads`);
 
    // });
 
  }
 

	
 
  // graphReads() {
 
  //   const U = this.graph.U();
 
  // }
 

	
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("graphToControls")) {
 
      this.graphToControls.register(this.device, this.deviceAttrRow.uri, this.onGraphValueChanged.bind(this));
 
      this.enableChange = true;
 
    }
 
  }
 

	
 
  onSliderInput(ev: CustomEvent) {
 
    if (ev.detail === undefined) {
 
      // not sure what this is, but it seems to be followed by good events
 
      return;
 
    }
light9/live/Light9LiveControls.ts
Show inline comments
 
@@ -6,190 +6,190 @@ import { sortBy, uniq } from "underscore
 
import { Patch, patchContainsPreds } from "../web/patch";
 
import { getTopGraph } from "../web/RdfdbSyncedGraph";
 
import { SyncedGraph } from "../web/SyncedGraph";
 
import { GraphToControls } from "./GraphToControls";
 
export { EditChoice } from "../web/EditChoice";
 
export { Light9DeviceControl as Light9LiveDeviceControl } from "./Light9DeviceControl";
 
const log = debug("controls");
 

	
 
@customElement("light9-live-controls")
 
export class Light9LiveControls extends LitElement {
 
  graph!: SyncedGraph;
 

	
 
  static styles = [
 
    css`
 
      :host {
 
        display: flex;
 
        flex-direction: column;
 
      }
 
      #preview {
 
        width: 100%;
 
      }
 
      #deviceControls {
 
        flex-grow: 1;
 
        position: relative;
 
        width: 100%;
 
        overflow-y: auto;
 
      }
 

	
 
      light9-live-device-control > div {
 
        break-inside: avoid-column;
 
      }
 
      light9-live-device-control {
 
      }
 
    `,
 
  ];
 

	
 
  render() {
 
    return html`
 
      <rdfdb-synced-graph></rdfdb-synced-graph>
 

	
 
      <h1>device control</h1>
 

	
 
      <div id="save">
 
        <div>
 
          <button @click=${this.newEffect}>New effect</button>
 
          <edit-choice .uri=${this.effectChoice}></edit-choice>
 
          <button @click=${this.clearAll}>clear settings in this effect</button>
 
        </div>
 
      </div>
 
      <div id="deviceControls">
 
        ${this.devices.map(
 
          (device: NamedNode) => html`
 
            <light9-device-control .uri=${device} .effect=${this.effectChoice} .graphToControls=${this.graphToControls}></light9-device-control>
 
          `
 
        )}
 
      </div>
 
    `;
 
  }
 

	
 
  devices: Array<NamedNode> = [];
 
  // uri of the effect being edited, or null. This is the
 
  // master value; GraphToControls follows.
 
  @property() effectChoice: NamedNode | null = null;
 
  graphToControls!: GraphToControls;
 
  okToWriteUrl: boolean = false;
 

	
 
  constructor() {
 
    super();
 

	
 
    getTopGraph().then((g) => {
 
      this.graph = g;
 
      this.graph.runHandler(this.findDevices.bind(this), "findDevices");
 
      this.graphToControls = new GraphToControls(this.graph);
 
      // this.graph.runHandler(this.xupdate.bind(this), "Light9LiveControls update");
 
      this.setEffectFromUrl();
 
    });
 
  }
 

	
 
  updated(changedProperties: PropertyValues) {
 
    if (changedProperties.has("effectChoice")) {
 
      log(`effectChoice to ${this.effectChoice?.value}`);
 
      this.onEffectChoice();
 
    }
 
  }
 

	
 
  findDevices(patch?: Patch) {
 
    const U = this.graph.U();
 
    if (patch && !patchContainsPreds(patch, [U("rdf:type")])) {
 
      return;
 
    }
 

	
 
    this.devices = [];
 
    let classes = this.graph.subjects(U("rdf:type"), U(":DeviceClass"));
 
    log(`found ${classes.length} device classes`);
 
    uniq(sortBy(classes, "value"), true).forEach((dc) => {
 
      sortBy(this.graph.subjects(U("rdf:type"), dc), "value").forEach((dev) => {
 
        log(`found dev ${dev.value}`)
 
        log(`found dev ${dev.value}`);
 
        this.devices.push(dev as NamedNode);
 
      });
 
    });
 
    this.requestUpdate();
 
  }
 

	
 
  setEffectFromUrl() {
 
    // not a continuous bidi link between url and effect; it only reads
 
    // the url when the page loads.
 
    const effect = new URL(window.location.href).searchParams.get("effect");
 
    if (effect != null) {
 
      log(`found effect in url ${effect}`);
 
      this.effectChoice = this.graph.Uri(effect);
 
    }
 
    this.okToWriteUrl = true;
 
  }
 

	
 
  writeToUrl(effect: NamedNode | null) {
 
    const effectStr = effect ? this.graph.shorten(effect) : "";
 
    if (!this.okToWriteUrl) {
 
      return;
 
    }
 
    const u = new URL(window.location.href);
 
    if ((u.searchParams.get("effect") || "") === effectStr) {
 
      return;
 
    }
 
    u.searchParams.set("effect", effectStr); // this escapes : and / and i wish it didn't
 
    window.history.replaceState({}, "", u.href);
 
    log("wrote new url", u.href);
 
  }
 

	
 
  newEffect() {
 
    this.effectChoice = this.graphToControls.newEffect();
 
  }
 

	
 
  onEffectChoice() {
 
    const U = (x: any) => this.graph.Uri(x);
 
    if (this.effectChoice == null) {
 
      // unlink
 
      log("onEffectChoice unlink");
 
      if (this.graphToControls != null) {
 
        this.graphToControls.setEffect(null);
 
      }
 
    } else {
 
      if (this.graphToControls != null) {
 
        this.graphToControls.setEffect(this.effectChoice);
 
      } else {
 
        throw new Error("graphToControls not set");
 
      }
 
    }
 
    this.writeToUrl(this.effectChoice);
 
  }
 

	
 
  clearAll() {
 
    // clears the effect!
 
    return this.graphToControls.emptyEffect();
 
  }
 

	
 
  configureFromGraph() {
 
    const U = (x: string) => this.graph.Uri(x);
 

	
 
    const newDevs: NamedNode[] = [];
 
    for (let dc of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), U(":DeviceClass"))))) {
 
      for (let dev of Array.from(this.graph.sortedUris(this.graph.subjects(U("rdf:type"), dc)))) {
 
        if (this.graph.contains(dev, U(":hideInLiveUi"), null)) {
 
          continue;
 
        }
 
        if (newDevs.length == 0) newDevs.push(dev);
 
      }
 
    }
 
    log("is this called?");
 
    log(`controls update now has ${newDevs.length} devices`);
 
    this.devices = newDevs;
 
    this.requestUpdate();
 

	
 
    return;
 

	
 
    // Tried css columns- big slowdown from relayout as I'm scrolling.
 
    // Tried isotope- seems to only scroll to the right.
 
    // Tried columnize- fails in jquery maybe from weird elements.
 

	
 
    // not sure how to get this run after the children are created
 
    return setTimeout(
 
      () =>
 
        $("#deviceControls").isotope({
 
          // fitColumns would be nice, but it doesn't scroll vertically
 
          layoutMode: "masonry",
 
          containerStyle: null,
 
        }),
 
      2000
 
    );
 
  }
 
}
0 comments (0 inline, 0 general)