Files
code-server/packages/vscode/src/element/augment.ts
2019-02-05 11:15:42 -06:00

278 lines
8.9 KiB
TypeScript

export function classSplice(element: HTMLElement, removeClasses: string, addClasses: string): HTMLElement {
if (removeClasses) { removeClasses.split(/\s+/g).forEach((className) => element.classList.remove(className)); }
if (addClasses) { addClasses.split(/\s+/g).forEach((className) => element.classList.add(className)); }
return element;
}
export type Side = "LEFT" | "RIGHT" | "TOP" | "BOTTOM";
export type BoundaryPos = [Side, Side];
export interface IBoundary {
top: number;
left: number;
right: number;
bottom: number;
}
export type PointPos = ["LEFT" | "CENTER" | "RIGHT", "TOP" | "CENTER" | "BOTTOM"];
export class FloaterPositioning {
private static positionClasses = [
"--boundary_top_left",
"--boundary_top_right",
"--boundary_left_top",
"--boundary_right_top",
"--boundary_left_bottom",
"--boundary_right_bottom",
"--boundary_bottom_left",
"--boundary_bottom_right",
"--point_top_left",
"--point_top_center",
"--point_top_right",
"--point_center_left",
"--point_center_center",
"--point_center_right",
"--point_bottom_left",
"--point_bottom_center",
"--point_bottom_right",
].join(" ");
public readonly target: HTMLElement;
constructor(target: HTMLElement) {
this.target = target;
}
// this function was surprisingly difficult
public moveToBoundary(boundary: IBoundary, pos: BoundaryPos, keepInBounds: boolean = true) {
if (keepInBounds) {
const height = this.target.offsetHeight;
const width = this.target.offsetWidth;
if (height === 0 && width === 0) {
throw new Error("target must be added to page before it can be in bounds positioned");
}
const flip = {
BOTTOM: "TOP",
LEFT: "RIGHT",
RIGHT: "LEFT",
TOP: "BOTTOM",
} as any;
const getOverlap = (side: string, strong: boolean) => {
switch (side) {
case "BOTTOM": return ((strong ? boundary.bottom : boundary.top) + height) - window.innerHeight;
case "TOP": return 0 - (strong ? boundary.top : boundary.bottom) - height;
case "RIGHT": return ((strong ? boundary.right : boundary.left) + width) - window.innerWidth;
case "LEFT": return 0 - (strong ? boundary.left : boundary.right) - width;
}
};
const firstA = getOverlap(pos[0], true);
if (firstA > 0) {
const firstB = getOverlap(flip[pos[0]], true);
if (firstB < firstA) {
pos[0] = flip[pos[0]];
}
}
const secA = getOverlap(pos[1], false);
if (secA > 0) {
const secB = getOverlap(flip[pos[1]], false);
if (secB < secA) {
pos[1] = flip[pos[1]];
}
}
}
classSplice(this.target, FloaterPositioning.positionClasses, undefined);
this.target.classList.add(`--boundary_${pos.map((val) => val.toLowerCase()).join("_")}`);
const displayPos: IBoundary = {} as any;
switch (pos[0]) {
case "BOTTOM": displayPos.top = boundary.bottom; break;
case "TOP": displayPos.bottom = window.innerHeight - boundary.top; break;
case "LEFT": displayPos.right = window.innerWidth - boundary.left; break;
case "RIGHT": displayPos.left = boundary.right; break;
}
switch (pos[1]) {
case "BOTTOM": displayPos.top = boundary.top; break;
case "TOP": displayPos.bottom = window.innerHeight - boundary.bottom; break;
case "LEFT": displayPos.right = window.innerWidth - boundary.right; break;
case "RIGHT": displayPos.left = boundary.left; break;
}
this.applyPos(displayPos);
}
public moveToPoint(point: { top: number, left: number }, pos: PointPos, keepInBounds: boolean = true): void {
if (keepInBounds) {
const height = this.target.offsetHeight;
const width = this.target.offsetWidth;
if (height === 0 && width === 0) {
throw new Error("target must be added to page before it can be in bounds positioned");
}
const flip = {
BOTTOM: "TOP",
LEFT: "RIGHT",
RIGHT: "LEFT",
TOP: "BOTTOM",
} as any;
const getOverlap = (side: string) => {
switch (side) {
case "BOTTOM": return (point.top + height) - window.innerHeight;
case "TOP": return -1 * (point.top - height);
case "RIGHT": return (point.left + width) - window.innerWidth;
case "LEFT": return -1 * (point.left - width);
default: return 0;
}
};
const xAlign = pos[0];
const normalXOffset = getOverlap(xAlign);
if (normalXOffset > 0 && normalXOffset > getOverlap(flip[xAlign])) {
pos[0] = flip[xAlign];
}
const yAlign = pos[1];
const normalYOffset = getOverlap(yAlign);
if (normalYOffset > 0 && normalYOffset > getOverlap(flip[yAlign])) {
pos[1] = flip[yAlign];
}
}
const displayPos: IBoundary = {} as any;
let centerX = false;
let centerY = false;
switch (pos[0]) {
case "CENTER": centerX = true;
case "RIGHT": displayPos.left = point.left; break;
case "LEFT": displayPos.right = window.innerWidth - point.left; break;
}
switch (pos[1]) {
case "CENTER": centerY = true;
case "BOTTOM": displayPos.top = point.top; break;
case "TOP": displayPos.bottom = window.innerHeight - point.top; break;
}
classSplice(this.target, FloaterPositioning.positionClasses, undefined);
this.target.classList.add(`--point_${pos.map((val) => val.toLowerCase()).reverse().join("_")}`);
this.applyPos(displayPos);
this.target.style.transform = `${centerX ? "translateX(-50)" : ""} ${centerY ? "translateY(-50)" : ""}`;
}
private applyPos(pos: IBoundary) {
this.target.style.top = pos.top !== undefined ? (pos.top + "px") : "";
this.target.style.bottom = pos.bottom !== undefined ? (pos.bottom + "px") : "";
this.target.style.left = pos.left !== undefined ? (pos.left + "px") : "";
this.target.style.right = pos.right !== undefined ? (pos.right + "px") : "";
}
}
export type Boolable = ((item: HTMLElement) => boolean) | boolean;
export interface IMakeChildrenSelectableArgs {
maxSelectable?: number;
selectOnKeyHover?: Boolable;
selectOnMouseHover?: Boolable;
onHover?: (selectedItem: HTMLElement) => void;
onSelect: (selectedItem: HTMLElement, wasAlreadySelected?: boolean) => void;
isItemSelectable?: (item: HTMLElement) => boolean;
}
export class SelectableChildren {
public readonly target: HTMLElement;
private keyHoveredItem: HTMLElement;
private _selectedItem: HTMLElement;
private selectOnMouseHover: Boolable;
private onHover: (selectedItem: HTMLElement) => void;
private onSelect: (selectedItem: HTMLElement) => void;
private isItemSelectable: (item: HTMLElement) => boolean;
constructor(target: HTMLElement, args: IMakeChildrenSelectableArgs) {
this.target = target;
this.onHover = args.onHover;
this.onSelect = args.onSelect;
this.selectOnMouseHover = args.selectOnMouseHover || false;
this.isItemSelectable = args.isItemSelectable;
// this.target.addEventListener("keydown", (event) => this.onTargetKeydown(event));
this.target.addEventListener("mousemove", (event) => this.onTargetMousemove(event));
Array.from(this.target.children).forEach((child: HTMLElement) => this.registerChild(child));
}
public registerChild(child: HTMLElement) {
child.addEventListener("mouseover", (event) => this.onItemHover(child, event));
child.addEventListener("mousedown", (event) => this.onItemMousedown(child, event));
}
public get selectedItem() { return this._selectedItem; }
public unsetSelection() {
if (this.selectedItem) { this.selectedItem.classList.remove("--is_selected"); }
this._selectedItem = undefined;
}
public trySelectItem(item: HTMLElement): boolean {
if (this.checkItemSelectable(item) === false) { return false; }
const alreadySelected = item === this.selectedItem;
if (!alreadySelected) {
this.unsetSelection();
this._selectedItem = item;
this.selectedItem.classList.add("--is_selected");
this.onSelect(this.selectedItem);
}
return true;
}
public updateAllItemIsSelectableStates() {
this.updateItemIsSelectableState(Array.from(this.target.childNodes) as any);
}
public updateItemIsSelectableState(itemOrItems?: HTMLElement | HTMLElement[]) {
const items: HTMLElement[] = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems];
items.forEach((item) => {
if (!this.isItemSelectable || this.isItemSelectable(item)) {
item.classList.remove("--not_selectable");
} else {
item.classList.add("--not_selectable");
}
});
}
private checkItemSelectable(item: HTMLElement): boolean {
this.updateItemIsSelectableState(item);
return item.classList.contains("--not_selectable") === false;
}
private onTargetMousemove(event: MouseEvent) {
classSplice(this.target, "--key_naving", "--mouse_naving");
if (this.keyHoveredItem) {
this.keyHoveredItem.classList.remove("--key_hovered");
this.keyHoveredItem = undefined;
}
}
private onItemHover(item: HTMLElement, event: Event) {
if (this.onHover) { this.onHover(item); }
if (
this.checkItemSelectable(item)
&& typeof this.selectOnMouseHover === "boolean"
? this.selectOnMouseHover
: (this.selectOnMouseHover as any)(item)
) {
this.trySelectItem(item);
}
}
private onItemMousedown(item: HTMLElement, event: Event) {
this.trySelectItem(item);
}
}