@hydroperx/tiles
Version:
Metro tile layout
310 lines (309 loc) • 12.4 kB
JavaScript
// third-party imports
import assert from "assert";
// local imports
import { getWidth, getHeight, } from "./enum/TileSize";
import * as RandomUtils from "./utils/RandomUtils";
import { TileDraggableBehavior } from "./TileDraggableBehavior";
import * as Attributes from "./Attributes";
import { LayoutTile } from "./Layout";
/**
* Tile factory.
*/
export class TileFactory {
// Constructor
constructor($) {
Object.defineProperty(this, "$", {
enumerable: true,
configurable: true,
writable: true,
value: $
});
//
}
// Adds a tile.
add(params) {
// First assertions
if (params.group) {
assert(this.$._state.groups.has(params.group), `Group ${params.group} does not exist.`);
}
assert(!this.$._state.tiles.has(params.id), `Duplicate tile ID: ${params.id}.`);
// Group (create/reuse if unspecified)
let group = "";
if (params.group) {
group = params.group;
}
else {
const groups = Array.from(this.$._state.groups.entries());
groups.sort((a, b) => a[1].index - b[1].index);
const last_group = groups.length == 0 ? null : groups[groups.length - 1];
if (last_group && !last_group[1].label) {
// reuse
group = last_group[0];
}
else {
group = "__anonymous$" + RandomUtils.hexLarge();
this.$.addGroup({
id: group,
});
}
}
// Size
const size = params.size ?? "medium";
// button
const button = document.createElement("button");
button.classList.add(this.$._class_names.tile);
button.setAttribute(Attributes.ATTR_ID, params.id);
button.setAttribute(Attributes.ATTR_DRAGGING, "false");
button.setAttribute(Attributes.ATTR_SIZE, size);
button.style.position = "absolute";
button.style.fontSize = "inherit";
button.style.boxSizing = "border-box";
button.style.width = this.$._tile_em[size].w + "em";
button.style.height = this.$._tile_em[size].h + "em";
// Event handlers for selection/checking
if (this.$._selection_enabled) {
// Variables
let contextTimeout = -1, contextTimestamp = -1, justHeldLong = false, touchDragging = false, cursorDragging = false;
// mouse down
button.addEventListener("mousedown", (e) => {
cursorDragging = false;
contextTimeout = window.setTimeout(() => {
// do not start check toggle if dragging tile
if (button.getAttribute(Attributes.ATTR_DRAGGING) == "true")
return;
// holding long on a tile will check it
toggle_check();
justHeldLong = true;
contextTimestamp = Date.now();
}, 600);
});
// mouse move
button.addEventListener("mousemove", e => {
cursorDragging = button.getAttribute(Attributes.ATTR_DRAGGING) == "true";
});
// mouse up
button.addEventListener("mouseup", (e) => {
if (contextTimeout !== -1)
window.clearTimeout(contextTimeout), (contextTimeout = -1);
});
// mouse out
button.addEventListener("mouseout", (e) => {
if (contextTimeout !== -1)
window.clearTimeout(contextTimeout), (contextTimeout = -1);
if (justHeldLong) {
justHeldLong = false;
return;
}
});
// click
button.addEventListener("click", (e) => {
if (justHeldLong) {
justHeldLong = false;
return;
}
if (cursorDragging) {
cursorDragging = false;
return;
}
click(e);
});
// touch start
button.addEventListener("touchstart", (e) => {
touchDragging = false;
contextTimeout = window.setTimeout(() => {
// do not simulate context menu if dragging tile
if (button.getAttribute(Attributes.ATTR_DRAGGING) == "true")
return;
// holding long on a tile will check it
// (simulated context menu)
toggle_check();
justHeldLong = true;
contextTimestamp = Date.now();
}, 600);
});
// touch move
button.addEventListener("touchmove", (e) => {
touchDragging = button.getAttribute(Attributes.ATTR_DRAGGING) == "true";
});
// touch end
button.addEventListener("touchend", (e) => {
if (contextTimeout !== -1)
window.clearTimeout(contextTimeout), (contextTimeout = -1);
if (justHeldLong) {
justHeldLong = false;
return;
}
if (touchDragging) {
touchDragging = false;
return;
}
if (contextTimestamp === -1 || contextTimestamp < Date.now() - 100) {
click(e);
}
});
// touch cancel
button.addEventListener("touchcancel", (e) => {
if (contextTimeout !== -1)
window.clearTimeout(contextTimeout), (contextTimeout = -1);
touchDragging = false;
if (justHeldLong) {
justHeldLong = false;
return;
}
});
// click
const click = (e) => {
if (contextTimeout !== -1)
window.clearTimeout(contextTimeout), (contextTimeout = -1);
// a click in a tile
if (contextTimestamp === -1 || contextTimestamp < Date.now() - 100) {
// during selection mode a click is a check toggle
const selection_mode = [
...this.$._container.getElementsByClassName(this.$._class_names.tile),
].some(btn => btn.getAttribute(Attributes.ATTR_CHECKED) === "true");
if (selection_mode)
toggle_check();
else {
// click
this.$.dispatchEvent(new CustomEvent("click", {
detail: {
tile: params.id,
}
}));
}
contextTimestamp = -1;
}
};
// Toggle selection/checked-state of a tile.
const toggle_check = () => {
button.setAttribute(Attributes.ATTR_CHECKED, (button.getAttribute(Attributes.ATTR_CHECKED) != "true").toString());
const all_buttons = [
...this.$._container.getElementsByClassName(this.$._class_names.tile),
].map(btn => [
btn.getAttribute(Attributes.ATTR_ID),
btn.getAttribute(Attributes.ATTR_CHECKED) === "true",
]);
// Emit selectionchange event
this.$.dispatchEvent(new CustomEvent("selectionchange", {
detail: {
tiles: all_buttons.filter(([, y]) => y).map(([id]) => id),
}
}));
};
}
else {
button.addEventListener("click", e => {
// click
this.$.dispatchEvent(new CustomEvent("click", {
detail: {
tile: params.id,
}
}));
});
}
// Contribute to layout
const layout_group = this.$._layout.groups.find(g => g.id == group);
const layout_tile = new LayoutTile(params.id, // tile ID
button);
if (!layout_tile.addTo(layout_group, // LayoutGroup
params.x ?? null, params.y ?? null, getWidth(size), // width
getHeight(size) // height
)) {
return false;
}
// Group div
const group_div = Array.from(this.$._container.getElementsByClassName(this.$._class_names.group))
.find(div => div.getAttribute(Attributes.ATTR_ID) == group);
const group_tiles_div = group_div.getElementsByClassName(this.$._class_names.groupTiles)[0];
// Contribute button
group_tiles_div.appendChild(button);
this.$._buttons.set(params.id, button);
// Tile content div
const content_div = document.createElement("div");
content_div.classList.add(this.$._class_names.tileContent);
content_div.style.boxSizing = "border-box";
content_div.style.width = "100%";
content_div.style.height = "100%";
content_div.style.overflow = "hidden";
button.appendChild(content_div);
// Contribute to overall state
this.$._state.tiles.set(params.id, {
size,
x: params.x ?? 0,
y: params.y ?? 0,
group,
});
// Install Draggable behavior
if (this.$._drag_enabled) {
new TileDraggableBehavior(this.$, params.id).install();
}
// State update
this.$._deferred_state_update_signal();
// Rearrange
this.$._deferred_rearrange();
// addedtile signal
this.$.dispatchEvent(new CustomEvent("addedtile", {
detail: { tile: layout_tile, button, contentDiv: content_div },
}));
// Result
return true;
}
// Removes a tile.
remove(id) {
// Button
const button = this.$._buttons.get(id);
assert(!!button, "Tile '" + id + "' not found.");
// Set ATTR_REMOVING to true
button.setAttribute(Attributes.ATTR_REMOVING, "true");
// Disable pointer events
button.style.pointerEvents = "none";
// If the tile is being dragged, cancel the drag.
if (button.getAttribute(Attributes.ATTR_DRAGGING) == "true") {
// Nullify the LayoutTile's `button`
const layout_tile = this.$._layout.groups.find(group => group.hasTile(id))?.getTile(id);
if (layout_tile)
layout_tile.button = null;
// Trigger drag end with a fake parameter
this.$._tile_drag_end_handlers.get(button)(button, 0, 0, new Event("noevent"));
// Exit
return;
}
// Remove tile from the DOM
if (this.$._tile_removal_work) {
this.$._tile_removal_work(button).then(() => {
button.remove();
});
}
else {
button.remove();
}
// Uninstall draggable behavior
new TileDraggableBehavior(this.$, id).uninstall();
// Remove from state
this.$._state.tiles.delete(id);
// Remove from layout
this.$._layout.groups.find(group => group.hasTile(id))
?.getTile(id)?.remove();
// Remove from `Tiles#_buttons`
this.$._buttons.delete(id);
// If checked, trigger selection change event.
if (button.getAttribute(Attributes.ATTR_CHECKED)) {
const all_buttons = [
...this.$._container.getElementsByClassName(this.$._class_names.tile),
].map(btn => [
btn.getAttribute(Attributes.ATTR_ID),
btn.getAttribute(Attributes.ATTR_CHECKED) === "true",
]);
// Emit selectionchange event
this.$.dispatchEvent(new CustomEvent("selectionchange", {
detail: {
tiles: all_buttons.filter(([, y]) => y).map(([id]) => id),
}
}));
}
// Rearrange
this.$._deferred_rearrange();
// State update signal
this.$._deferred_state_update_signal();
}
}