attached-windows
Version:
Using Chrome extension API attach multiple normal and/or popup windows together that behave like a single window.
173 lines (172 loc) • 8.51 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const minimumWidths = {
'normal': 525,
'popup': 200
};
const onBoundsChanged = ({ id: currentId, height, top, state, left, width }) => {
if (AttachedWindows.isBusy)
return false;
const { windows, container } = AttachedWindows;
const currentIndex = windows.findIndex(({ id }) => id === currentId);
if (currentIndex !== -1) {
const { bounds } = windows[currentIndex];
// Update container
if (state !== bounds.state)
container.state = (state === 'normal' || state === 'minimized') ? state : 'normal';
if (height !== bounds.height)
container.height = height;
if (top !== bounds.top)
container.top = top;
if ((left !== bounds.left) && (width === bounds.width || currentIndex === 0))
container.left += left - bounds.left;
if (width !== bounds.width) {
// Condition: (RESIZED FROM THE LEFT SIDE OF THE FIRST WINDOW) || (RESIZED FROM THE RIGHT SIDE OF THE LAST WINDOW)
if ((currentIndex === 0 && left !== bounds.left) || (left === bounds.left && currentIndex === windows.length - 1)) {
windows[currentIndex].widthFraction = windows[currentIndex].widthFraction / bounds.width * width;
container.width += width - bounds.width;
}
else {
const { widthFraction: widthFractionPrevious } = windows[currentIndex];
windows[currentIndex].widthFraction = widthFractionPrevious / bounds.width * width;
if (left === bounds.left) {
windows[currentIndex + 1].widthFraction += (widthFractionPrevious - windows[currentIndex].widthFraction);
}
else {
windows[currentIndex - 1].widthFraction += (widthFractionPrevious - windows[currentIndex].widthFraction);
}
}
}
// Apply changes
AttachedWindows.calculateBounds();
AttachedWindows.syncPositions();
}
};
const onRemoved = (closedWindowId) => __awaiter(void 0, void 0, void 0, function* () {
if (AttachedWindows.isBusy)
return false;
const { windows } = AttachedWindows;
const currentIndex = windows.findIndex(({ id }) => id === closedWindowId);
if (currentIndex !== -1) {
// Terminate if primary window removed
const window = windows[currentIndex];
if (window.isPrimary)
yield AttachedWindows.terminate();
else {
// Remove closed window
windows.splice(currentIndex, 1);
// Apply changes
AttachedWindows.calculateBounds();
AttachedWindows.syncPositions();
}
// Trigger listener if available
if (AttachedWindows.onRemoveAttachedWindow)
AttachedWindows.onRemoveAttachedWindow(window);
}
});
const onBeforeUnload = () => __awaiter(void 0, void 0, void 0, function* () {
// Terminate on before unload
yield AttachedWindows.terminate();
});
class AttachedWindows {
static terminate({ closeWindows = true, closePrimary = false } = {}) {
return __awaiter(this, void 0, void 0, function* () {
// Remove event listeners
if (chrome.windows.onBoundsChanged.hasListener(onBoundsChanged))
chrome.windows.onBoundsChanged.removeListener(onBoundsChanged);
if (chrome.windows.onRemoved.hasListener(onRemoved))
chrome.windows.onRemoved.removeListener(onRemoved);
removeEventListener('beforeunload', onBeforeUnload, false);
// Close windows
let availableWindows = [];
yield Promise.all(this.windows.map(({ id, isPrimary }) => __awaiter(this, void 0, void 0, function* () {
if ((isPrimary && closePrimary) || (!isPrimary && closeWindows))
yield chrome.windows.remove(id).catch(() => { });
else
availableWindows.push(Object.assign({ id }, (isPrimary ? { isPrimary } : {})));
})));
// Restore primary window if not closing
if (!closePrimary) {
const foundPrimaryWindow = this.windows.find(({ isPrimary }) => isPrimary);
if (foundPrimaryWindow) {
this.windows = [foundPrimaryWindow];
this.calculateBounds();
yield this.syncPositions(true);
}
}
// Empty windows array
this.windows = [];
this.container = {};
// Return available / not closed windows
return availableWindows;
});
}
static calculateBounds() {
// Calculate window bounds
const { container: { top, height, state, left: containerLeft, width: containerWidth }, windows } = this;
const containerWidthFraction = windows.reduce((total, { widthFraction, isHidden }) => total + (isHidden ? 0 : widthFraction), 0);
windows.reduce(([left, availableWidth], window) => {
if (window.isHidden) {
window.bounds = { state: 'minimized' };
return [left, availableWidth];
}
else {
let width = Math.round(containerWidth * window.widthFraction / containerWidthFraction);
if (width < minimumWidths[window.type])
width = minimumWidths[window.type];
if (width > availableWidth)
width = availableWidth;
window.bounds = { top, height, state, left, width: Math.round(width) };
return [left + width, availableWidth - width];
}
}, [containerLeft, containerWidth]);
}
static syncPositions(ignoreCheck = false) {
return __awaiter(this, void 0, void 0, function* () {
// Apply changes to the windows
if (this.isBusy)
return false;
this.isBusy = true;
yield Promise.all(this.windows.map(({ id, bounds }) => chrome.windows.update(id, bounds.state === 'minimized' ? { state: 'minimized' } : bounds).catch(() => { }).catch(() => { })));
this.isBusy = false;
// Terminate if total number of window is 0 or 1
if (!ignoreCheck) {
if (this.windows.length === 0)
yield this.terminate();
else if (this.windows.length === 1)
yield this.terminate({ closePrimary: false });
}
// Return true if succeed
return true;
});
}
static initialize({ container, windows }) {
return __awaiter(this, void 0, void 0, function* () {
if (!windows.find(({ isPrimary }) => isPrimary))
throw new Error(`No primary window. 'windows' must have one primary window.`);
if (this.windows.length)
yield this.terminate();
this.container = container;
this.windows = windows;
this.calculateBounds();
yield this.syncPositions();
// Add event listeners
if (!chrome.windows.onBoundsChanged.hasListener(onBoundsChanged))
chrome.windows.onBoundsChanged.addListener(onBoundsChanged);
if (!chrome.windows.onRemoved.hasListener(onRemoved))
chrome.windows.onRemoved.addListener(onRemoved);
addEventListener('beforeunload', onBeforeUnload, false);
});
}
}
AttachedWindows.isBusy = false;
AttachedWindows.container = {};
AttachedWindows.windows = [];
export default AttachedWindows;