mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-18 22:42:25 +00:00
Six more findings from CodeRabbit on #928. Three are real bugs. 1. THE CHIP HID DOCKED PANES. `_onOpened` decided "did the pane take my element?" from `ownerDocument !== document`. That is true for a pane in a pop-out window — and false for a pane moved into the DOCK, which lives in this very document. So docking a pane stamped `.fb-pane-detached` (display:none !important) onto the panel the user was looking at, and put the stub next to it instead of at its home. The element cannot answer this question — `isConnected` is true in a pane window, `ownerDocument` is this one in the dock. Both were live bugs. Ask the manager, which knows exactly what it handed to the host: `panes.elementOf(id)`. That holds for every host, and for reconciling after the fact (detail == null), which is what a plugin rebuilding its panel mid-pop-out triggers. 2. `.fb-paned` FORCED `display: block !important`. A panel that is `display:flex` or `grid` would be silently re-laid-out while detached — the exact opposite of "placement only", and precisely the kind of surprise this feature exists to avoid. Removed. Making a hidden panel visible is a separate job, and it now belongs to the manager, which does it without touching the panel's display MODE: clear `hidden`, and clear an inline `display:none` if that is how the panel hides. 3. VISIBILITY IS NOW RESTORED. The hosts used to set `el.hidden = false` and never put it back, so the docs' "core only changes placement" was a lie and a panel's hidden state was quietly lost. The manager stashes both `hidden` and the inline `display` on open and restores them on dock: a panel that was closed when you opened its pane from the tray goes back to being closed; one that was open stays open. Plus: - The launcher rebuilt its whole list on every panes:opened/closed — including the one fired by clicking a button in that list — destroying the button under the user's finger and dropping focus to <body>. It now restores focus to the toggled pane's button. - `_copyStyles` cloned every stylesheet link, including the panes.css that pane.html already loads. Skip sheets the pane document already has. - Docs: the chip may route to the DOCK, not always a window (it goes through detach() → the host router). `header` precedence was documented backwards — an explicit `header` wins. And the visibility contract above is now written down rather than being a surprise. Signed-off-by: topkoa <topkoa@gmail.com>
184 lines
8.0 KiB
JavaScript
184 lines
8.0 KiB
JavaScript
/*
|
|
* fee[dB]ack — the pop-out chip.
|
|
*
|
|
* One affordance, core-owned, identical everywhere: the small ⇱ button a plugin
|
|
* drops into the panel it already has.
|
|
*
|
|
* feedBack.panes.register({ id: 'camera_director', title: 'Camera', element: () => panelEl });
|
|
* feedBack.panes.attachChip(panelEl, 'camera_director');
|
|
*
|
|
* That is the entire adoption cost. Clicking the chip pops the panel out; a stub
|
|
* takes its place so the user can find it again; closing the pane brings the panel
|
|
* home and restores the chip. The plugin writes no show/hide logic — if it did,
|
|
* every plugin would invent a slightly different one, which is exactly the
|
|
* inconsistency this exists to prevent.
|
|
*
|
|
* The panel a chip is attached to is USUALLY the very element the pane moves into
|
|
* the pop-out window — so most of the time there is nothing here left to hide, and
|
|
* the job is simply to mark the hole it left. Hiding it would in fact be actively
|
|
* harmful: `.fb-pane-detached` is `display:none !important`, and it would travel
|
|
* with the node straight into the pane window and blank it.
|
|
*
|
|
* When the chip IS attached to something the pane didn't take (a wrapper, a
|
|
* launcher row), that element stays put and is hidden with `.fb-pane-detached` —
|
|
* a dedicated class, not `.hidden`/[hidden], because the panels we attach to
|
|
* already toggle those themselves.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
const panes = window.feedBack && window.feedBack.panes;
|
|
if (!panes || typeof panes.register !== 'function') {
|
|
console.error('[panes] pane-manager.js must load before pane-chip.js');
|
|
return;
|
|
}
|
|
|
|
// paneId -> { el, chip, stub, spec }
|
|
const attached = new Map();
|
|
|
|
function _makeChip(spec) {
|
|
const b = document.createElement('button');
|
|
b.type = 'button';
|
|
b.className = 'fb-pane-chip';
|
|
b.title = 'Pop out';
|
|
b.setAttribute('aria-label', 'Pop out ' + spec.title);
|
|
b.textContent = '⇱';
|
|
b.addEventListener('click', (e) => {
|
|
// Rail popovers close on any document click that lands outside them
|
|
// (player-chrome.js). Without this the popover would close under the
|
|
// chip mid-click, which reads as the button not working.
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
panes.detach(spec.id);
|
|
});
|
|
return b;
|
|
}
|
|
|
|
function _makeStub(spec) {
|
|
const s = document.createElement('button');
|
|
s.type = 'button';
|
|
s.className = 'fb-pane-stub';
|
|
s.setAttribute('aria-label', 'Bring ' + spec.title + ' back');
|
|
s.title = 'Bring it back';
|
|
const glyph = document.createElement('span');
|
|
glyph.className = 'fb-pane-stub-glyph';
|
|
glyph.textContent = '⇲';
|
|
const label = document.createElement('span');
|
|
label.textContent = spec.title + ' is popped out';
|
|
s.appendChild(glyph);
|
|
s.appendChild(label);
|
|
s.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
panes.close(spec.id);
|
|
});
|
|
return s;
|
|
}
|
|
|
|
// The pane is out. Leave a stub where its panel used to be.
|
|
//
|
|
// The subtlety: the panel a chip is attached to is USUALLY the very element the
|
|
// pane moved into the pop-out window. It is no longer in this document at all —
|
|
// so hiding it would be worse than pointless (the `display:none` travels with
|
|
// the node and blanks the pane window, which is exactly the bug this fixes), and
|
|
// the stub cannot be inserted "before it", because it is not here to be before.
|
|
//
|
|
// Hence `home`: the manager tells us where the element used to live, and the
|
|
// stub goes there. If the chip is attached to something the pane did NOT take —
|
|
// a wrapper, a launcher row — that element is still here, and we hide it as
|
|
// before.
|
|
function _onOpened(rec, detail) {
|
|
// Did the pane take MY element?
|
|
//
|
|
// Ask the manager, which knows exactly what it handed to the host. Do not
|
|
// try to infer it from the element:
|
|
//
|
|
// - `isConnected` says "still here" for a panel sitting in a pane window.
|
|
// It IS connected — to that window.
|
|
// - `ownerDocument` says "still here" for a panel moved into the DOCK,
|
|
// which is in this very document. Hiding it there would blank a pane the
|
|
// user is looking at.
|
|
//
|
|
// Both were live bugs. The manager's answer is the only one that holds for
|
|
// every host, and it works when reconciling after the fact (detail == null),
|
|
// which is what a plugin rebuilding its panel mid-pop-out triggers.
|
|
const takenEl = (detail && detail.el) || panes.elementOf(rec.spec.id);
|
|
const moved = takenEl === rec.el;
|
|
|
|
if (!moved && rec.el.isConnected) {
|
|
rec.el.classList.add('fb-pane-detached');
|
|
if (!rec.stub.isConnected && rec.el.parentNode) rec.el.parentNode.insertBefore(rec.stub, rec.el);
|
|
return;
|
|
}
|
|
|
|
// Mark the hole the element left. `home` comes with the event, or from the
|
|
// manager when we are reconciling after the fact.
|
|
const home = (detail && detail.home) || panes.homeOf(rec.spec.id);
|
|
if (!rec.stub.isConnected && home && home.parent && home.parent.isConnected) {
|
|
const next = (home.next && home.next.parentNode === home.parent) ? home.next : null;
|
|
home.parent.insertBefore(rec.stub, next);
|
|
}
|
|
}
|
|
|
|
function _onClosed(rec) {
|
|
// The element is back. Whatever we did to hide it, undo — including a class
|
|
// it might have carried out of the document and back.
|
|
rec.el.classList.remove('fb-pane-detached');
|
|
rec.stub.remove();
|
|
}
|
|
|
|
/**
|
|
* attachChip(el, paneId, opts)
|
|
*
|
|
* `el` — the dialog to hide when the pane pops out. The chip is injected
|
|
* into `el.querySelector('[data-pane-header]')` when present, else
|
|
* prepended to `el` itself.
|
|
* `opts` — { header: Element } to place the chip somewhere specific.
|
|
*
|
|
* Returns a detach function that removes the chip and stub and restores the
|
|
* dialog — call it if your plugin tears its dialog down.
|
|
*/
|
|
function attachChip(el, paneId, opts) {
|
|
opts = opts || {};
|
|
if (!(el instanceof Element)) throw new TypeError('panes.attachChip: el must be an Element');
|
|
const spec = panes.get(paneId);
|
|
if (!spec) { console.warn('[panes] attachChip: register the pane first:', paneId); return () => {}; }
|
|
if (attached.has(paneId)) { console.warn('[panes] attachChip: already attached:', paneId); return () => {}; }
|
|
|
|
const chip = _makeChip(spec);
|
|
const stub = _makeStub(spec);
|
|
const host = opts.header || el.querySelector('[data-pane-header]') || el;
|
|
if (host === el) host.insertBefore(chip, host.firstChild);
|
|
else host.appendChild(chip);
|
|
|
|
const rec = { el, chip, stub, spec };
|
|
attached.set(paneId, rec);
|
|
|
|
// Reconcile immediately: register() reopens a pane the user left open at
|
|
// last unload, and that can land before (or after) attachChip runs.
|
|
if (panes.isOpen(paneId)) _onOpened(rec, null);
|
|
|
|
return () => {
|
|
if (attached.get(paneId) !== rec) return;
|
|
attached.delete(paneId);
|
|
chip.remove();
|
|
_onClosed(rec);
|
|
};
|
|
}
|
|
|
|
// One pair of bus listeners for every chip, rather than one pair per chip.
|
|
const bus = window.feedBack;
|
|
if (bus && typeof bus.on === 'function') {
|
|
bus.on('panes:opened', (e) => {
|
|
const rec = attached.get(e.detail && e.detail.id);
|
|
if (rec) _onOpened(rec, e.detail);
|
|
});
|
|
bus.on('panes:closed', (e) => {
|
|
const rec = attached.get(e.detail && e.detail.id);
|
|
if (rec) _onClosed(rec);
|
|
});
|
|
}
|
|
|
|
window.feedBack.panes.attachChip = attachChip;
|
|
})();
|