seat tray: tray.js, SCSS, FTs, Jasmine specs
- new apps.epic.static tray.js: IIFE with drag-open/click-close/wobble behaviour; document-level pointermove+mouseup listeners; reset() for Jasmine afterEach; try/catch around setPointerCapture for synthetic events - _room.scss: #id_tray_wrap fixed-right flex container; #id_tray_handle + #id_tray_grip (box-shadow frame, transparent inner window, border-radius clip); #id_tray_btn grab cursor; #id_tray bevel box-shadows, margin-left gap, height removed (align-items:stretch handles it); tray-wobble keyframes - _applets.scss + _game-kit.scss: z-index raised (312-318) for primacy over tray (310) - room.html: #id_tray_wrap + children markup; tray.js script tag - FTs test_room_tray: 5 tests (T1-T5); _simulate_drag via execute_script pointer events (replaces unreliable ActionChains drag); wobble asserts on #id_tray_wrap not btn - static_src/tests/TraySpec.js + SpecRunner.html: Jasmine unit tests for all tray.js branches Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
194
src/apps/epic/static/apps/epic/tray.js
Normal file
194
src/apps/epic/static/apps/epic/tray.js
Normal file
@@ -0,0 +1,194 @@
|
||||
var Tray = (function () {
|
||||
var _open = false;
|
||||
var _dragStartX = null;
|
||||
var _dragStartLeft = null;
|
||||
var _dragHandled = false;
|
||||
|
||||
var _wrap = null;
|
||||
var _btn = null;
|
||||
var _tray = null;
|
||||
var _minLeft = 0;
|
||||
var _maxLeft = 0;
|
||||
|
||||
// Stored so reset() can remove them from document
|
||||
var _onDocMove = null;
|
||||
var _onDocUp = null;
|
||||
|
||||
function _computeBounds() {
|
||||
var rightPx = parseInt(getComputedStyle(_wrap).right, 10);
|
||||
if (isNaN(rightPx)) rightPx = 0;
|
||||
var handleW = _btn.offsetWidth || 48;
|
||||
_minLeft = 0;
|
||||
_maxLeft = window.innerWidth - rightPx - handleW;
|
||||
}
|
||||
|
||||
function _applyVerticalBounds() {
|
||||
var nav = document.querySelector('nav');
|
||||
var footer = document.querySelector('footer');
|
||||
var rem = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
var inset = Math.round(rem * 0.125);
|
||||
if (nav) {
|
||||
var nb = nav.getBoundingClientRect();
|
||||
if (nb.width > nb.height && nb.bottom < window.innerHeight * 0.4) {
|
||||
_wrap.style.top = (Math.round(nb.bottom) + inset) + 'px';
|
||||
}
|
||||
}
|
||||
if (footer) {
|
||||
var fb = footer.getBoundingClientRect();
|
||||
if (fb.width > fb.height && fb.top > window.innerHeight * 0.6) {
|
||||
_wrap.style.bottom = (window.innerHeight - Math.round(fb.top) + inset) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function open() {
|
||||
if (_open) return;
|
||||
_open = true;
|
||||
if (_tray) _tray.style.display = 'block';
|
||||
if (_btn) _btn.classList.add('open');
|
||||
if (_wrap) {
|
||||
_wrap.classList.remove('tray-dragging');
|
||||
_wrap.style.left = _minLeft + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (!_open) return;
|
||||
_open = false;
|
||||
if (_tray) _tray.style.display = 'none';
|
||||
if (_btn) _btn.classList.remove('open');
|
||||
if (_wrap) {
|
||||
_wrap.classList.remove('tray-dragging');
|
||||
_wrap.style.left = _maxLeft + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function isOpen() { return _open; }
|
||||
|
||||
function _wobble() {
|
||||
if (!_wrap) return;
|
||||
_wrap.classList.add('wobble');
|
||||
_wrap.addEventListener('animationend', function handler() {
|
||||
_wrap.classList.remove('wobble');
|
||||
_wrap.removeEventListener('animationend', handler);
|
||||
});
|
||||
}
|
||||
|
||||
function _startDrag(clientX) {
|
||||
_dragStartX = clientX;
|
||||
_dragStartLeft = _wrap ? (parseInt(_wrap.style.left, 10) || _maxLeft) : _maxLeft;
|
||||
_dragHandled = false;
|
||||
if (_wrap) _wrap.classList.add('tray-dragging');
|
||||
}
|
||||
|
||||
function init() {
|
||||
_wrap = document.getElementById('id_tray_wrap');
|
||||
_btn = document.getElementById('id_tray_btn');
|
||||
_tray = document.getElementById('id_tray');
|
||||
if (!_btn) return;
|
||||
|
||||
_computeBounds();
|
||||
_applyVerticalBounds();
|
||||
if (_wrap) _wrap.style.left = _maxLeft + 'px';
|
||||
|
||||
// Drag start — pointer and mouse variants so Selenium W3C actions
|
||||
// and synthetic Jasmine PointerEvents both work.
|
||||
_btn.addEventListener('pointerdown', function (e) {
|
||||
_startDrag(e.clientX);
|
||||
try { _btn.setPointerCapture(e.pointerId); } catch (_) {}
|
||||
});
|
||||
_btn.addEventListener('mousedown', function (e) {
|
||||
if (e.button !== 0) return;
|
||||
if (_dragStartX !== null) return; // pointerdown already handled it
|
||||
_startDrag(e.clientX);
|
||||
});
|
||||
|
||||
// Drag move / end — on document so events that land elsewhere during
|
||||
// the drag (no capture, or Selenium pointer quirks) still bubble here.
|
||||
_onDocMove = function (e) {
|
||||
if (_dragStartX === null || !_wrap) return;
|
||||
var newLeft = _dragStartLeft + (e.clientX - _dragStartX);
|
||||
newLeft = Math.max(_minLeft, Math.min(_maxLeft, newLeft));
|
||||
_wrap.style.left = newLeft + 'px';
|
||||
if (newLeft < _maxLeft) {
|
||||
if (!_open) {
|
||||
_open = true;
|
||||
if (_tray) _tray.style.display = 'block';
|
||||
if (_btn) _btn.classList.add('open');
|
||||
}
|
||||
} else {
|
||||
if (_open) {
|
||||
_open = false;
|
||||
if (_tray) _tray.style.display = 'none';
|
||||
if (_btn) _btn.classList.remove('open');
|
||||
}
|
||||
}
|
||||
};
|
||||
document.addEventListener('pointermove', _onDocMove);
|
||||
document.addEventListener('mousemove', _onDocMove);
|
||||
|
||||
_onDocUp = function (e) {
|
||||
if (_dragStartX !== null && Math.abs(e.clientX - _dragStartX) > 10) {
|
||||
_dragHandled = true;
|
||||
}
|
||||
_dragStartX = null;
|
||||
_dragStartLeft = null;
|
||||
if (_wrap) _wrap.classList.remove('tray-dragging');
|
||||
};
|
||||
document.addEventListener('pointerup', _onDocUp);
|
||||
document.addEventListener('mouseup', _onDocUp);
|
||||
|
||||
_btn.addEventListener('click', function () {
|
||||
if (_dragHandled) {
|
||||
_dragHandled = false;
|
||||
return;
|
||||
}
|
||||
if (_open) {
|
||||
close();
|
||||
} else {
|
||||
_wobble();
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
_computeBounds();
|
||||
_applyVerticalBounds();
|
||||
if (!_open && _wrap) _wrap.style.left = _maxLeft + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
// reset() — restores module state; used by Jasmine afterEach
|
||||
function reset() {
|
||||
_open = false;
|
||||
_dragStartX = null;
|
||||
_dragStartLeft = null;
|
||||
_dragHandled = false;
|
||||
if (_tray) _tray.style.display = 'none';
|
||||
if (_btn) _btn.classList.remove('open');
|
||||
if (_wrap) {
|
||||
_wrap.classList.remove('wobble', 'tray-dragging');
|
||||
_wrap.style.left = '';
|
||||
}
|
||||
if (_onDocMove) {
|
||||
document.removeEventListener('pointermove', _onDocMove);
|
||||
document.removeEventListener('mousemove', _onDocMove);
|
||||
_onDocMove = null;
|
||||
}
|
||||
if (_onDocUp) {
|
||||
document.removeEventListener('pointerup', _onDocUp);
|
||||
document.removeEventListener('mouseup', _onDocUp);
|
||||
_onDocUp = null;
|
||||
}
|
||||
_wrap = null;
|
||||
_btn = null;
|
||||
_tray = null;
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
|
||||
return { init: init, open: open, close: close, isOpen: isOpen, reset: reset };
|
||||
}());
|
||||
Reference in New Issue
Block a user