diff --git a/light9/web/live/ActiveSettings.ts b/light9/web/live/ActiveSettings.ts
new file mode 100644
--- /dev/null
+++ b/light9/web/live/ActiveSettings.ts
@@ -0,0 +1,98 @@
+class ActiveSettings {
+ graph: any;
+ settings: any;
+ keyForSetting: any;
+ onChanged: any;
+ constructor(graph: any) {
+ // The settings we're showing (or would like to but the widget
+ // isn't registered yet):
+ // dev+attr : {setting: Uri, onChangeFunc: f, jsValue: str_or_float}
+ this.graph = graph;
+ this.settings = new Map();
+ this.keyForSetting = new Map(); // setting uri str -> dev+attr
+
+
+
+ // Registered graphValueChanged funcs, by dev+attr. Kept even when
+ // settings are deleted.
+ this.onChanged = new Map();
+ }
+
+ addSettingsRow(device: { value: string; }, deviceAttr: { value: string; }, setting: { value: string; }, value: any) {
+ const key = device.value + " " + deviceAttr.value;
+ if (this.settings.has(key)) {
+ throw new Error("repeated setting on " + key);
+ }
+ if (this.keyForSetting.has(setting.value)) {
+ throw new Error("repeated keyForSetting on " + setting.value);
+ }
+ this.settings.set(key, {
+ setting,
+ onChangeFunc: this.onChanged[key],
+ jsValue: value
+ });
+ this.keyForSetting.set(setting.value, key);
+ if (this.onChanged[key] != null) {
+ return this.onChanged[key](value);
+ }
+ }
+
+ has(setting: { value: any; }) {
+ return this.keyForSetting.has(setting.value);
+ }
+
+ setValue(setting: { value: any; }, value: any) {
+ const row = this.settings.get(this.keyForSetting.get(setting.value));
+ row.jsValue = value;
+ if (row.onChangeFunc != null) { return row.onChangeFunc(value); }
+ }
+
+ registerWidget(device: { value: string; }, deviceAttr: { value: string; }, graphValueChanged: any) {
+ const key = device.value + " " + deviceAttr.value;
+ this.onChanged[key] = graphValueChanged;
+
+ if (this.settings.has(key)) {
+ const row = this.settings.get(key);
+ row.onChangeFunc = graphValueChanged;
+ return row.onChangeFunc(row.jsValue);
+ }
+ }
+
+ effectSettingLookup(device: { value: string; }, attr: { value: string; }) {
+ const key = device.value + " " + attr.value;
+ if (this.settings.has(key)) {
+ return this.settings.get(key).setting;
+ }
+
+ return null;
+ }
+
+ deleteSetting(setting: { value: string; }) {
+ log('deleteSetting ' + setting.value);
+ const key = this.keyForSetting.get(setting.value);
+ const row = this.settings.get(key);
+ if ((row != null) && !row.setting.equals(setting)) {
+ throw new Error('corrupt row for ' + setting.value);
+ }
+ if ((row != null ? row.onChangeFunc : undefined) != null) { row.onChangeFunc(null); }
+ this.settings.delete(key);
+ return this.keyForSetting.delete(setting);
+ }
+
+ clear() {
+ new Map(this.settings).forEach(function (row: { onChangeFunc: (arg0: any) => any; }, key: any) {
+ if (row.onChangeFunc != null) { return row.onChangeFunc(null); }
+ });
+ this.settings.clear();
+ return this.keyForSetting.clear();
+ }
+
+ forAll(cb: (arg0: any) => any) {
+ const all = Array.from(this.keyForSetting.keys());
+ return Array.from(all).map((s: any) => cb(this.graph.Uri(s)));
+ }
+
+ allSettingsStr() {
+ return this.keyForSetting.keys();
+ }
+}
diff --git a/light9/web/live/GraphToControls.ts b/light9/web/live/GraphToControls.ts
new file mode 100644
--- /dev/null
+++ b/light9/web/live/GraphToControls.ts
@@ -0,0 +1,207 @@
+
+const valuePred = function(graph: { Uri: (arg0: any) => any; }, attr: { equals: (arg0: any) => any; }) {
+ const U = (x: string) => graph.Uri(x);
+ const scaledAttributeTypes = [U(':color'), U(':brightness'), U(':uv')];
+ if (_.some(scaledAttributeTypes,
+ ( x: any) => attr.equals(x))) { return U(':scaledValue'); } else { return U(':value'); }
+};
+
+const log = debug('live');
+
+// Like element.set(path, newArray), but minimizes splices.
+// Dotted paths don't work yet.
+const syncArray = function(element: this, path: string, newArray: { length?: any; }, isElementEqual: { (a: any, b: any): boolean; (arg0: any, arg1: any): any; }) {
+ let pos = 0;
+ let newPos = 0;
+
+ while (newPos < newArray.length) {
+ if (pos < element[path].length) {
+ if (isElementEqual(element[path][pos], newArray[newPos])) {
+ pos += 1;
+ newPos += 1;
+ } else {
+ element.splice('devices', pos, 1);
+ }
+ } else {
+ element.push('devices', newArray[newPos]);
+ pos += 1;
+ newPos += 1;
+ }
+ }
+
+ if (pos < element[path].length) {
+ return element.splice('devices', pos, element[path].length - pos);
+ }
+};
+
+
+class GraphToControls {
+ graph: any;
+ activeSettings: ActiveSettings;
+ effect: any;
+ ctx: any;
+ // More efficient bridge between liveControl widgets and graph edits,
+ // as opposed to letting each widget scan the graph and push lots of
+ // tiny patches to it.
+ constructor(graph: any) {
+ this.graph = graph;
+ this.activeSettings = new ActiveSettings(this.graph);
+ this.effect = null;
+ }
+
+ ctxForEffect(effect: { value: { replace: (arg0: string, arg1: string) => any; }; }) {
+ return this.graph.Uri(effect.value.replace(
+ "light9.bigasterisk.com/effect",
+ "light9.bigasterisk.com/show/dance2019/effect"));
+ }
+
+ setEffect(effect: any) {
+ this.clearSettings();
+ this.effect = effect;
+ this.ctx = this.ctxForEffect(this.effect);
+ // are these going to pile up? consider @graph.triggerHandler('GTC sync')
+ return this.graph.runHandler(this.syncFromGraph.bind(this), 'GraphToControls sync');
+ }
+
+ newEffect() {
+ // wrong- this should be our editor's scratch effect, promoted to a
+ // real one when you name it.
+ const U = (x: string) => this.graph.Uri(x);
+ const effect = this.graph.nextNumberedResource(U('http://light9.bigasterisk.com/effect/effect'));
+ const ctx = this.ctxForEffect(effect);
+ const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, ctx);
+
+ const addQuads = [
+ quad(effect, U('rdf:type'), U(':Effect')),
+ quad(effect, U('rdfs:label'), this.graph.Literal(effect.value.replace(/.*\//, ""))),
+ quad(effect, U(':publishAttr'), U(':strength'))
+ ];
+ const patch = { addQuads, delQuads: [] };
+ log('init new effect', patch);
+ this.graph.applyAndSendPatch(patch);
+ return effect;
+ }
+
+ syncFromGraph() {
+ const U = (x: string) => this.graph.Uri(x);
+ if (!this.effect) { return; }
+ log('syncFromGraph', this.effect);
+
+ const toClear = new Set(this.activeSettings.allSettingsStr());
+
+ for (let setting of Array.from(this.graph.objects(this.effect, U(':setting')))) {
+ var value: { id: { match: (arg0: {}) => any; }; };
+ const dev = this.graph.uriValue(setting, U(':device'));
+ const devAttr = this.graph.uriValue(setting, U(':deviceAttr'));
+
+ const pred = valuePred(this.graph, devAttr);
+ try {
+ value = this.graph.uriValue(setting, pred);
+ if (!value.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);
+ }
+ }
+ //log('change: graph contains', devAttr, value)
+ if (this.activeSettings.has(setting)) {
+ this.activeSettings.setValue(setting, value);
+ toClear.delete(setting.value);
+ } else {
+ this.activeSettings.addSettingsRow(dev, devAttr, setting, value);
+ }
+ }
+
+ return Array.from(Array.from(toClear)).map((settingStr: any) => this.activeSettings.deleteSetting(U(settingStr)));
+ }
+
+ clearSettings() {
+ return this.activeSettings.clear();
+ }
+
+ register(device: any, deviceAttr: any, graphValueChanged: any) {
+ return this.activeSettings.registerWidget(device, deviceAttr, graphValueChanged);
+ }
+
+ shouldBeStored(deviceAttr: any, value: string | number) {
+ // this is a bug for zoom=0, since collector will default it to
+ // stick at the last setting if we don't explicitly send the
+ // 0. rx/ry similar though not the exact same deal because of
+ // their remap.
+ return (value != null) && (value !== 0) && (value !== '#000000');
+ }
+
+ emptyEffect() {
+ return this.activeSettings.forAll(this._removeEffectSetting.bind(this));
+ }
+
+ controlChanged(device: any, deviceAttr: any, value: string) {
+ // todo: controls should be disabled if there's no effect and they won't do anything.
+ if (!this.effect) { return; }
+
+ // value is float or #color or (Uri or null)
+ if ((value === undefined) || ((typeof value === "number") && isNaN(value)) || ((typeof value === "object") && (value !== null) && !value.id)) {
+ throw new Error("controlChanged sent bad value " + value);
+ }
+ const effectSetting = this.activeSettings.effectSettingLookup(device, deviceAttr);
+
+ // sometimes this misses an existing setting, which leads to a mess
+ if (this.shouldBeStored(deviceAttr, value)) {
+ if ((effectSetting == null)) {
+ return this._addEffectSetting(device, deviceAttr, value);
+ } else {
+ return this._patchExistingEffectSetting(effectSetting, deviceAttr, value);
+ }
+ } else {
+ return this._removeEffectSetting(effectSetting);
+ }
+ }
+
+ _nodeForValue(value: { id: any; }) {
+ if (value.id != null) {
+ return value;
+ }
+ return this.graph.prettyLiteral(value);
+ }
+
+ _addEffectSetting(device: any, deviceAttr: { value: any; }, value: any) {
+ log('change: _addEffectSetting', deviceAttr.value, value);
+ const U = (x: string) => this.graph.Uri(x);
+ const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, this.ctx);
+ const effectSetting = this.graph.nextNumberedResource(this.effect.value + '_set');
+ this.activeSettings.addSettingsRow(device, deviceAttr, effectSetting, value);
+ const addQuads = [
+ quad(this.effect, U(':setting'), effectSetting),
+ quad(effectSetting, U(':device'), device),
+ quad(effectSetting, U(':deviceAttr'), deviceAttr),
+ quad(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value))
+ ];
+ const patch = { addQuads, delQuads: [] };
+ log('save', patch);
+ return this.graph.applyAndSendPatch(patch);
+ }
+
+ _patchExistingEffectSetting(effectSetting: { value: any; }, deviceAttr: any, value: any) {
+ log('change: patch existing', effectSetting.value);
+ this.activeSettings.setValue(effectSetting, value);
+ return this.graph.patchObject(effectSetting, valuePred(this.graph, deviceAttr), this._nodeForValue(value), this.ctx);
+ }
+
+ _removeEffectSetting(effectSetting: { value: any; }) {
+ const U = (x: string) => this.graph.Uri(x);
+ const quad = (s: any, p: any, o: any) => this.graph.Quad(s, p, o, this.ctx);
+ if (effectSetting != null) {
+ log('change: _removeEffectSetting', effectSetting.value);
+ const toDel = [quad(this.effect, U(':setting'), effectSetting, this.ctx)];
+ for (let q of Array.from(this.graph.graph.getQuads(effectSetting))) {
+ toDel.push(q);
+ }
+ this.graph.applyAndSendPatch({ delQuads: toDel, addQuads: [] });
+ return this.activeSettings.deleteSetting(effectSetting);
+ }
+ }
+}
diff --git a/light9/web/live/Light9Listbox.ts b/light9/web/live/Light9Listbox.ts
new file mode 100644
--- /dev/null
+++ b/light9/web/live/Light9Listbox.ts
@@ -0,0 +1,72 @@
+
+
+
+
+ None
+
+ {{item.label}}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/light9/web/live/Light9LiveControl.ts b/light9/web/live/Light9LiveControl.ts
new file mode 100644
--- /dev/null
+++ b/light9/web/live/Light9LiveControl.ts
@@ -0,0 +1,161 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+const coffeeElementSetupLight9LiveControl = (function() {
+ class Light9LiveControl extends Polymer.Element {
+ static is: string;
+ static getter_properties: {
+ graph: { type: any; notify: boolean; }; device: { type: any; }; deviceAttrRow: { type: any; }; // object returned from attrRow, below
+ value: { type: any; notify: boolean; }; // null, Uri, float, str
+ choiceValue: { type: any; }; immediateSlider: { notify: boolean; observer: string; }; sliderWriteValue: { ...; }; pickedChoice: { ...; }; graphToControls: { ...; };
+ };
+ static getter_observers: {};
+ enableChange: boolean;
+ value: any;
+ immediateSlider: any;
+ deviceAttrRow: any;
+ sliderWriteValue: { value: any; };
+ choiceValue: any;
+ graphToControls: any;
+ graph: any;
+ pickedChoice: any;
+ static initClass() {
+ this.is = 'light9-live-control';
+ this.getter_properties = {
+ graph: { type: Object, notify: true },
+ device: { type: Object },
+ deviceAttrRow: { type: Object }, // object returned from attrRow, below
+ value: { type: Object, notify: true }, // null, Uri, float, str
+ choiceValue: { type: Object },
+
+ immediateSlider: { notify: true, observer: 'onSlider' },
+ sliderWriteValue: { type: Number },
+
+ pickedChoice: { observer: 'onChange' },
+ graphToControls: { type: Object }
+ };
+ this.getter_observers = [
+ 'onChange(value)',
+ 'onGraphToControls(graphToControls)',
+ 'onChoice(choiceValue)'
+ ];
+ }
+ constructor() {
+ super();
+ this.enableChange = false; // until 1st graph read
+ }
+ onSlider() { return this.value = this.immediateSlider; }
+ goBlack() { return this.value = "#000000"; }
+ onGraphToControls(gtc: { register: (arg0: any, arg1: any, arg2: any) => void; }) {
+ gtc.register(this.device, this.deviceAttrRow.uri, this.graphValueChanged.bind(this));
+ return this.enableChange = true;
+ }
+ device(device: any, uri: any, arg2: any) {
+ throw new Error("Method not implemented.");
+ }
+
+ graphValueChanged(v: { value: any, }) {
+ log('change: control gets', v);
+ this.enableChange = false;
+ if (v === null) {
+ this.clear();
+ } else {
+ this.value = v;
+ }
+ if (this.deviceAttrRow.useSlider) { this.sliderWriteValue = v; }
+ if (this.deviceAttrRow.useChoice) { this.choiceValue = (v === null ? v : v.value); }
+ return this.enableChange = true;
+ }
+
+ onChoice(value: any) {
+ if ((this.graphToControls == null) || !this.enableChange) { return; }
+ if (value != null) {
+ value = this.graph.Uri(value);
+ } else {
+ value = null;
+ }
+ return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
+ }
+
+ onChange(value: any) {
+ if ((this.graphToControls == null) || !this.enableChange) { return; }
+ if ((typeof value === "number") && isNaN(value)) { return; } // let onChoice do it
+ //log('change: control tells graph', @deviceAttrRow.uri.value, value)
+ if (value === undefined) {
+ value = null;
+ }
+ return this.graphToControls.controlChanged(this.device, this.deviceAttrRow.uri, value);
+ }
+
+ clear() {
+ this.pickedChoice = null;
+ this.sliderWriteValue = 0;
+ if (this.deviceAttrRow.useColor) {
+ return this.value = '#000000';
+ } else if (this.deviceAttrRow.useChoice) {
+ return this.value = (this.pickedChoice = null);
+ } else {
+ return this.value = (this.immediateSlider = 0);
+ }
+ }
+ }
+
+ Light9LiveControl.initClass();
+ return Light9LiveControl;
+ })();
+
\ No newline at end of file
diff --git a/light9/web/live/Light9LiveControls.ts b/light9/web/live/Light9LiveControls.ts
new file mode 100644
--- /dev/null
+++ b/light9/web/live/Light9LiveControls.ts
@@ -0,0 +1,178 @@
+
+
+
+
+
+
+ device control
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+const coffeeElementSetupLight9LiveControls = (function() {
+ class Light9LiveControls extends Polymer.Element {
+ static is: string;
+ static getter_properties: {
+ graph: { type: any; notify: boolean; }; devices: { type: any; notify: boolean; value: {}; };
+ // string uri of the effect being edited, or null. This is the
+ // master value; GraphToControls follows.
+ effectChoice: { type: any; notify: boolean; value: any; }; graphToControls: { type: any; };
+ };
+ static getter_observers: {};
+ graphToControls: any;
+ okToWriteUrl: boolean;
+ currentSettings: {};
+ graph: any;
+ effectChoice: any;
+ static initClass() {
+ this.is = "light9-live-controls";
+ this.getter_properties = {
+ graph: { type: Object, notify: true },
+ devices: { type: Array, notify: true, value: [] },
+ // string uri of the effect being edited, or null. This is the
+ // master value; GraphToControls follows.
+ effectChoice: { type: String, notify: true, value: null },
+ graphToControls: { type: Object }
+ };
+ this.getter_observers = [
+ 'onGraph(graph)',
+ 'onEffectChoice(effectChoice)'
+ ];
+ }
+
+ constructor() {
+ super();
+ this.graphToControls = null;
+ this.okToWriteUrl = false;
+ }
+
+ ready() {
+ super.ready(...arguments).ready();
+ return this.currentSettings = {};
+ }
+
+ onGraph() {
+ this.graphToControls = new GraphToControls(this.graph);
+ this.graph.runHandler(this.update.bind(this), 'Light9LiveControls update');
+
+ // need graph to be loaded, so we don't make double settings? not sure.
+ return setTimeout(this.setFromUrl.bind(this), 1);
+ }
+
+ setFromUrl() {
+ // 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 url', effect);
+ this.effectChoice = effect;
+ }
+ return this.okToWriteUrl = true;
+ }
+
+ writeToUrl(effectStr: any) {
+ if (!this.okToWriteUrl) { return; }
+ const u = new URL(window.location.href);
+ if (u.searchParams.get('effect') === effectStr) {
+ return;
+ }
+ u.searchParams.set('effect', effectStr);
+ window.history.replaceState({}, "", u.href);
+ return log('wrote new url', u.href);
+ }
+
+ newEffect() {
+ return this.effectChoice = this.graphToControls.newEffect().value;
+ }
+
+ onEffectChoice() {
+ const U = (x: any) => this.graph.Uri(x);
+ if ((this.effectChoice == null)) {
+ // unlink
+ if (this.graphToControls != null) { this.graphToControls.setEffect(null); }
+ } else {
+ log('load', this.effectChoice);
+ if (this.graphToControls != null) { this.graphToControls.setEffect(this.graph.Uri(this.effectChoice)); }
+ }
+ return this.writeToUrl(this.effectChoice);
+ }
+
+ clearAll() {
+ // clears the effect!
+ return this.graphToControls.emptyEffect();
+ }
+
+ update() {
+ const U = (x: string) => this.graph.Uri(x);
+
+ const newDevs = [];
+ 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;
+ }
+ newDevs.push({uri: dev});
+ }
+ }
+
+ //log("controls update now has #{newDevs.length} devices")
+ syncArray(this, 'devices', newDevs, (a: { uri: { value: any; }; }, b: { uri: { value: any; }; }) => a.uri.value === b.uri.value);
+
+ 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);
+ }
+ }
+
+ Light9LiveControls.initClass();
+ return Light9LiveControls;
+ })();
\ No newline at end of file
diff --git a/light9/web/live/Light9LiveDeviceControl.ts b/light9/web/live/Light9LiveDeviceControl.ts
new file mode 100644
--- /dev/null
+++ b/light9/web/live/Light9LiveDeviceControl.ts
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+ a
+
+
+
+ attr
+
+
+
+
+
+
+
+const coffeeElementSetupLight9LiveDeviceControl = (function() {
+ class Light9LiveDeviceControl extends Polymer.Element {
+ static is: string;
+ static getter_properties: {
+ graph: { type: any; notify: boolean; }; uri: { type: any; notify: boolean; }; effect: { type: any; }; deviceClass: { type: any; notify: boolean; }; // the uri str
+ deviceAttrs: { type: any; notify: boolean; }; graphToControls: { ...; }; bgStyle: { ...; }; devClasses: { ...; }; // the css kind
+ };
+ static getter_observers: {};
+ selectedAttrs: any;
+ graph: any;
+ uri: any;
+ devClasses: string;
+ deviceClass: any;
+ deviceAttrs: {};
+ shadowRoot: any;
+ static initClass() {
+ this.is = "light9-live-device-control";
+ this.getter_properties = {
+ graph: { type: Object, notify: true },
+ uri: { type: String, notify: true },
+ effect: { type: String },
+ deviceClass: { type: String, notify: true }, // the uri str
+ deviceAttrs: { type: Array, notify: true },
+ graphToControls: { type: Object },
+ bgStyle: { type: String, computed: '_bgStyle(deviceClass)' },
+ devClasses: { type: String, value: '' } // the css kind
+ };
+ this.getter_observers = [
+ 'onGraph(graph)'
+ ];
+ }
+ constructor() {
+ super();
+ this.selectedAttrs = new Set(); // uri strings
+ }
+ _bgStyle(deviceClass: { value: any, length: number, charCodeAt: (arg0: number) => number, }) {
+ let hash = 0;
+ deviceClass = deviceClass.value;
+ for (let start = deviceClass.length-10, i = start, end = deviceClass.length, asc = start <= end; asc ? i < end : i > end; asc ? i++ : i--) {
+ hash += deviceClass.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%);`;
+ }
+
+ onGraph() {
+ return this.graph.runHandler(this.update.bind(this), `${this.uri.value} update`);
+ }
+
+ setDeviceSelected(isSel: any) {
+ return this.devClasses = isSel ? 'selected' : '';
+ }
+
+ setAttrSelected(devAttr: { value: any, }, isSel: any) {
+ if (isSel) {
+ this.selectedAttrs.add(devAttr.value);
+ } else {
+ this.selectedAttrs.delete(devAttr.value);
+ }
+ return this.update();
+ }
+
+ update(patch: null) {
+ const U = (x: string) => this.graph.Uri(x);
+ if ((patch != null) && !SyncedGraph.patchContainsPreds(
+ patch, [U('rdf:type'), U(':deviceAttr'), U(':dataType'), U(':choice')])) { return; }
+ this.deviceClass = this.graph.uriValue(this.uri, U('rdf:type'));
+ this.deviceAttrs = [];
+ return Array.from(_.unique(this.graph.sortedUris(this.graph.objects(this.deviceClass, U(':deviceAttr'))))).map((da: any) =>
+ this.push('deviceAttrs', this.attrRow(da)));
+ }
+ push(arg0: string, arg1: { uri: { value: any, }, dataType: any, showColorPicker: any, attrClasses: string, }) {
+ throw new Error("Method not implemented.");
+ }
+
+ attrRow(devAttr: { value: any, }) {
+ let x: { value: any; };
+ 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.value) ? 'selected' : ''
+ };
+ 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')));
+ daRow.choices = ((() => {
+ const result = [];
+ for (x of Array.from(choiceUris)) { result.push({uri: x.value, label: this.graph.labelOrTail(x)});
+ }
+ return result;
+ })());
+ daRow.choiceSize = Math.min(choiceUris.length + 1, 10);
+ } else {
+ daRow.useSlider = true;
+ daRow.max = 1;
+ if (dataType.equals(U(':angle'))) {
+ // varies
+ daRow.max = 1;
+ }
+ }
+ return daRow;
+ }
+
+ clear() {
+ return Array.from(this.shadowRoot.querySelectorAll("light9-live-control")).map((lc: { clear: () => any; }) =>
+ lc.clear());
+ }
+
+ onClick(ev: any) {
+ return log('click', this.uri);
+ }
+ // select, etc
+
+ onAttrClick(ev: { model: { dattr: { uri: any, }, }, }) {
+ return log('attr click', this.uri, ev.model.dattr.uri);
+ }
+ }
+ // select
+
+ Light9LiveDeviceControl.initClass();
+ return Light9LiveDeviceControl;
+ })();
+
+
\ No newline at end of file
diff --git a/light9/web/live/elements.html b/light9/web/live/elements.html
deleted file mode 100644
--- a/light9/web/live/elements.html
+++ /dev/null
@@ -1,279 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- None
-
- {{item.label}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- a
-
-
-
- attr
-
-
-
-
-
-
-
-
-
-
-
-
- device control
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/light9/web/live/index.html b/light9/web/live/index.html
--- a/light9/web/live/index.html
+++ b/light9/web/live/index.html
@@ -4,8 +4,6 @@
device control
-
-