janusweb 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1387,7 +1387,9 @@ elation.elements.define('janus-voip-client-callbutton', class extends elation.el
1387
1387
  this.detached = true;
1388
1388
  this.button = elation.elements.create('ui-button', {
1389
1389
  append: this,
1390
- label: '📞',
1390
+ // buttonlabel lets a host (e.g. the invite menu) relabel the action;
1391
+ // set as an attribute so it survives the deferred create() lifecycle.
1392
+ label: this.getAttribute('buttonlabel') || '📞',
1391
1393
  });
1392
1394
  this.button.addEventListener('click', ev => this.handleClick(ev));
1393
1395
  super.create();
@@ -1416,6 +1418,114 @@ super.create();
1416
1418
  }
1417
1419
  }
1418
1420
  });
1421
+ /**
1422
+ * The invite hub in the comms bar. A popup button that gathers the ways to
1423
+ * bring people together: invite people already here into voice, copy a link
1424
+ * that pulls in someone who isn't here, or schedule a meetup at a set time.
1425
+ * Replaces the bare 📞 call button, which it reuses internally for the voice
1426
+ * path so the existing "ring" chat-signaling flow is unchanged.
1427
+ */
1428
+ elation.elements.define('janus-comms-invite', class extends elation.elements.ui.popupbutton {
1429
+ create() {
1430
+ this.label = '➕ Invite';
1431
+ this.title = 'Invite people / meet up';
1432
+ super.create();
1433
+ this.popupcontent = this.buildMenu();
1434
+ }
1435
+ createPopup() {
1436
+ super.createPopup();
1437
+ // Class hook for the opaque, high-z popup styling in comms.css. Without an
1438
+ // opaque backdrop the popup (rendered at document root) lets the scene and
1439
+ // the chat transcript bleed through. Repositioning as the async rows lay
1440
+ // out is handled by ui.popupbutton's ResizeObserver.
1441
+ if (this.popup) this.popup.classList.add('janus-comms-invite-popup');
1442
+ }
1443
+ buildMenu() {
1444
+ let menu = elation.elements.create('ui-content');
1445
+ menu.classList.add('janus-comms-invite-menu');
1446
+
1447
+ // Invite people already in the room into voice chat. Reuses the existing
1448
+ // call button element, so the mic picker + "ring ring" chat signaling and
1449
+ // the accept/decline card all keep working exactly as before.
1450
+ this.callbutton = elation.elements.create('janus-voip-client-callbutton', {
1451
+ append: menu,
1452
+ buttonlabel: '🎙 Invite to voice',
1453
+ });
1454
+
1455
+ // Copy a link that brings someone who isn't here into this room.
1456
+ this.copybutton = elation.elements.create('ui-button', { append: menu, label: '🔗 Copy room link' });
1457
+ this.copybutton.addEventListener('click', (ev) => this.handleCopyLink(ev));
1458
+
1459
+ // "Schedule a meetup" is hidden for now: without a backend that persists the
1460
+ // invite and emails/notifies the invitee it can't actually deliver anything.
1461
+ // The seam (handleSchedule + buildRoomLink(time) + comms-schedule-request)
1462
+ // stays below so it can be re-enabled once that backend lands.
1463
+ // this.appendSchedule(menu);
1464
+
1465
+ return menu;
1466
+ }
1467
+ appendSchedule(menu) {
1468
+ let schedule = document.createElement('div');
1469
+ schedule.className = 'janus-comms-schedule';
1470
+ let label = document.createElement('label');
1471
+ label.innerHTML = '📅 Schedule a meetup';
1472
+ this.timeinput = document.createElement('input');
1473
+ this.timeinput.type = 'datetime-local';
1474
+ this.schedulebutton = elation.elements.create('ui-button', { label: 'Copy invite link' });
1475
+ this.schedulebutton.addEventListener('click', (ev) => this.handleSchedule(ev));
1476
+ schedule.appendChild(label);
1477
+ schedule.appendChild(this.timeinput);
1478
+ schedule.appendChild(this.schedulebutton);
1479
+ menu.appendChild(schedule);
1480
+ }
1481
+ buildRoomLink(opts) {
1482
+ opts = opts || {};
1483
+ let base = (typeof janus != 'undefined' && janus.currentroom && janus.currentroom.url)
1484
+ || location.href.replace(/#.*/, '');
1485
+ let url = new URL(base, location.href);
1486
+ if (opts.time) url.searchParams.set('t', Math.floor(opts.time / 1000));
1487
+ // Position anchoring is object-based: a #<js_id> fragment teleports the
1488
+ // arriving player to a named room object. Only meaningful when one exists.
1489
+ if (opts.anchor) url.hash = opts.anchor;
1490
+ return url.toString();
1491
+ }
1492
+ copyToClipboard(text) {
1493
+ if (navigator.clipboard && navigator.clipboard.writeText) {
1494
+ return navigator.clipboard.writeText(text).catch(() => this.copyFallback(text));
1495
+ }
1496
+ this.copyFallback(text);
1497
+ return Promise.resolve();
1498
+ }
1499
+ copyFallback(text) {
1500
+ let ta = document.createElement('textarea');
1501
+ ta.value = text;
1502
+ ta.style.position = 'fixed';
1503
+ ta.style.opacity = '0';
1504
+ document.body.appendChild(ta);
1505
+ ta.select();
1506
+ try { document.execCommand('copy'); } catch (e) {}
1507
+ document.body.removeChild(ta);
1508
+ }
1509
+ flashLabel(button, text, revert) {
1510
+ button.setLabel(text);
1511
+ setTimeout(() => button.setLabel(revert), 1500);
1512
+ }
1513
+ handleCopyLink(ev) {
1514
+ let link = this.buildRoomLink();
1515
+ this.copyToClipboard(link);
1516
+ this.flashLabel(this.copybutton, '✓ Link copied', '🔗 Copy room link');
1517
+ }
1518
+ handleSchedule(ev) {
1519
+ let val = this.timeinput.value;
1520
+ let time = val ? new Date(val).getTime() : null;
1521
+ let link = this.buildRoomLink({ time: time });
1522
+ this.copyToClipboard(link);
1523
+ this.flashLabel(this.schedulebutton, '✓ Invite copied', 'Copy invite link');
1524
+ document.dispatchEvent(new CustomEvent('comms-schedule-request', {
1525
+ detail: { time: time, url: link },
1526
+ }));
1527
+ }
1528
+ });
1419
1529
  elation.elements.define('janus-voip-client-incomingcall', class extends elation.elements.base {
1420
1530
  create() {
1421
1531
  this.actions = elation.elements.create('ui-flexpanel', { append: this });
@@ -75,10 +75,41 @@ elation.elements.define('janus-ui-editor-property', class extends elation.elemen
75
75
  });
76
76
 
77
77
  if (this.label) {
78
- elation.elements.create('ui-text', {
78
+ let labelobj = elation.elements.create('ui-text', {
79
79
  text: this.label,
80
80
  append: this
81
81
  });
82
+ // Clicking the label activates the editor's control, like a standard <label>.
83
+ elation.events.add(labelobj, 'click', (ev) => this.activateControl(ev));
84
+ }
85
+ }
86
+ mousedown(ev) {
87
+ // Always (re)fire selection on click, even when this property is already the
88
+ // selected one — the base ui.item suppresses re-selection, which made
89
+ // re-clicking a property (to re-assert its edit mode) a no-op.
90
+ if (this.selectable) {
91
+ this.select(ev);
92
+ ev.stopPropagation();
93
+ }
94
+ }
95
+ activateControl(ev) {
96
+ // Mirror a native <label>: clicking the label activates the editor's control
97
+ // (the first non-label child; the label is a ui-text we create). Prefer the
98
+ // element's own focus() — ui-input.focus() focuses its field, ui-toggle.focus()
99
+ // flips it — and fall back to forwarding the click for controls without focus().
100
+ let control = null;
101
+ for (let i = 0; i < this.children.length; i++) {
102
+ let child = this.children[i];
103
+ if (child.tagName && child.tagName.toLowerCase() != 'ui-text') { control = child; break; }
104
+ }
105
+ if (!control) return;
106
+ if (typeof control.focus == 'function') {
107
+ control.focus();
108
+ } else {
109
+ control.dispatchEvent(new MouseEvent(ev.type, {
110
+ bubbles: true, cancelable: true, view: window,
111
+ clientX: ev.clientX, clientY: ev.clientY, button: ev.button
112
+ }));
82
113
  }
83
114
  }
84
115
  updateValue(value) {
@@ -381,10 +412,11 @@ elation.elements.define('janus-ui-editor-property-boolean', class extends elatio
381
412
  value: { type: 'boolean' },
382
413
  });
383
414
  if (this.label) {
384
- elation.elements.create('ui-text', {
415
+ let labelobj = elation.elements.create('ui-text', {
385
416
  text: this.label,
386
417
  append: this
387
418
  });
419
+ elation.events.add(labelobj, 'click', (ev) => this.activateControl(ev));
388
420
  }
389
421
  this.elements = elation.elements.fromString(`
390
422
  <ui-toggle name="toggle"></ui-toggle>
@@ -169,3 +169,30 @@ janus-ui-editor-source ui-button[name="refresh"] {
169
169
  top: 0;
170
170
  right: 0;
171
171
  }
172
+
173
+ /* Scene-tree type icons (Blender-style): ui.treeviewitem renders an
174
+ <span class="st-typeicon st-type-<tag>"> in each row's label (via attrs.icon),
175
+ drawn from the engine object-icon sprite (20px cells). All primitives share the
176
+ 'object' icon; unknown tags (custom elements) fall back to the generic icon.
177
+ Negative top/bottom margins keep the 20px glyph from inflating the row, and
178
+ vertical-align centres it with the label text. */
179
+ .st-typeicon {
180
+ display: inline-block;
181
+ width: 21px;
182
+ height: 21px;
183
+ margin: -5px 2px -5px 0;
184
+ vertical-align: middle;
185
+ background-image: url(./objecticons-small.png);
186
+ background-repeat: no-repeat;
187
+ background-position: -21px -40px; /* default / custom element: generic geometry */
188
+ opacity: .85;
189
+ }
190
+ .st-type-object { background-position: -21px -20px; } /* cube */
191
+ .st-type-light { background-position: -251px -20px; } /* point light */
192
+ .st-type-text,
193
+ .st-type-paragraph { background-position: -168px -40px; } /* text */
194
+ .st-type-image { background-position: -105px -20px; } /* grid/plane */
195
+ .st-type-video { background-position: -126px -40px; } /* camera */
196
+ .st-type-sound { background-position: -210px -40px; } /* speaker */
197
+ .st-type-link { background-position: -189px -60px; } /* portal */
198
+ .st-type-particle { background-position: -84px -40px; } /* particles */