import e = require("dojo"); import declare = require("dojo/_base/declare"); interface XYWH { x: number; y: number; w: number; h: number } /** * The Zone component is meant to organise items of the same type inside a predefined space. * * Zones are not great at responsive design and are intended for fixed-size spaces. If you want a responsive zone-like structure, you should use the {@link Stock} component instead. * @example * // Zone's always target an existing HTML element. In your template file, define the area you want to use as a zone. This should be styled to have a fixed size, or us should use the 'SetFluidWidth' method to make it responsive. *
* * // Then create the zone and set the pattern you want to use to organize the items. * this.my_zone = new ebg.zone(); * this.my_zone.create(this, $('my_zone'), , ); * this.my_zone.setPattern( ); // See 'setPattern' for available modes. * * // Then you can add and remove items using their DOM ids: * this.my_zone.placeInZone( ); * this.my_zone.removeFromZone( ); * The interface for the `ebg/zone` module. * Partial: This has been partially typed based on a subset of the BGA source code. */ class Zone_Template { page: InstanceType | null = null; container_div: HTMLElement | null = null; item_height: number | null = null; item_width: number | null = null; instantaneous: boolean = true; items: { id: string; weight: number }[] = []; control_name: string | null = null; item_margin: number = 5; autowidth: boolean = false; autoheight: boolean = true; item_pattern: 'grid' | 'diagonal' | 'verticalfit' | 'horizontalfit' | 'ellipticalfit' | 'custom' = 'grid'; /** * Initializes the field values for this zone, and updates the container_div position type if needed. * @param page The game which this zone is a part of. * @param container_div The div that will contain the items in this zone. * @param item_width An integer for the width of the objects you want to organize in this zone. * @param item_height An integer for the height of the objects you want to organize in this zone. */ create(page: InstanceType, container_div: HTMLElement, item_width: number, item_height: number): void { if (container_div === null) { console.error("Null container in zone::create"); } this.page = page; this.container_div = container_div; this.item_width = item_width; this.item_height = item_height; this.control_name = container_div.id; if (e.style(this.container_div, "position") !== "absolute") { e.style(this.container_div, "position", "relative"); } } /** Connects an `onresize` event to the window which will update this zone's display. */ setFluidWidth(): void { e.connect(window, "onresize", this as Zone_Template, "updateDisplay"); } /** * Sets what pattern the zone uses to position and arrange elements. The zone package comes with many positioning patterns pre-coded; these allow your items to take on a variety of arrangements. * @param pattern The pattern to use for this zone. The following patterns are available: * - 'grid' (which is the default, if you never actually call setPattern) * - 'diagonal' * - 'verticalfit' * - 'horizontalfit' * - 'ellipticalfit' * - 'custom' */ setPattern(pattern: typeof this.item_pattern): void { switch (pattern) { case 'grid': case 'diagonal': this.autoheight = true; this.item_pattern = pattern; break; case 'verticalfit': case 'horizontalfit': case 'ellipticalfit': this.autoheight = false; this.item_pattern = pattern; break; case 'custom': break; default: console.error("zone::setPattern: unknow pattern: " + e); } } /** Checks if this zone contains an item with the matching DOM id. */ isInZone(id: string): boolean { for (const item of this.items) { if (item.id === id) return true; } return false; } /** * After creating an object that you want to add to the zone as a classic HTML template (dojo.place / this.format_block), this is used to add and position the object in the zone. * @param target_id The DOM id of the object to add to the zone. * @param weight The weight of the object to add to the zone. This is used to determine the order of the items in the zone. Whenever a new item is added, all elements in the items array is sorted by weight, in ascending order with ties broken by the order they were added. */ placeInZone(target_id: string, weight?: number): void { if (weight === undefined) weight = 0; if (!this.isInZone(target_id)) { this.items.push({ id: target_id, weight }); this.page!.attachToNewParent($(target_id)!, this.container_div!); this.items.sort((a, b) => a.weight - b.weight); this.updateDisplay(); } } /** * Removes the object with the matching DOM id from the zone. * @param target The DOM id of the object to remove from the zone. * @param destroy If true, the object will be removed from the DOM entirely. If false, the object will be removed from the zone but remain in the DOM. * @param animateTo If set, the object will animate to the specified DOM element (using {@link Gamegui.slideToObject}). This happens before the object is destroyed if destroy is true. */ removeFromZone(target: string | HTMLElement, destroy: boolean, animateTo: string | HTMLElement): void { const destroyFunc = (target: Element) => e.destroy(target); for (const key in this.items) { const item = this.items[key]!; if (item.id === target) { let anim: InstanceType | null = null; if (animateTo) { let duration = 500; if (this.instantaneous) duration = 1; anim = this.page!.slideToObject($(item.id)!, animateTo, duration).play(); if (destroy) e.connect(anim, "onEnd", destroyFunc); anim.play(); } else if (destroy) { let duration = 500; if (this.page!.instantaneousMode || this.instantaneous) duration = 1; anim = e.fadeOut({ node: $(item.id)!, duration, onEnd: destroyFunc }); anim.play(); } this.items.splice(Number(key), 1); this.updateDisplay(); return; } } } /** * Removes and destroys all objects from the zone. */ removeAll(): void { const destroyFunc = (target: HTMLElement) => e.destroy(target); for (const key in this.items) { const item = this.items[key]!; const anim = e.fadeOut({ node: $(item.id)!, onEnd: destroyFunc }); anim.play(); } this.items = []; this.updateDisplay(); } /** * Repositions all objects in the zone. This is useful if the zone's size has changed, or if the pattern has changed. */ updateDisplay(): void { const containerId = this.container_div!.id; const containerPos = e.position(this.container_div!); let pos_width = containerPos.w!; if (this.autowidth) { pos_width = e.position($("page-content")!).w!; } let height = 0; let width = 0; let count = 0; for (const key in this.items) { const item = this.items[key]!; const itemDiv = $(item.id); if (itemDiv) { const coords = this.itemIdToCoords(count, pos_width, containerPos.h!, this.items.length); count++; width = Math.max(width, coords.x + coords.w); height = Math.max(height, coords.y + coords.h); let duration = 1000; if (this.page!.instantaneousMode || this.instantaneous) duration = 2; let anim = e.fx.slideTo({ node: itemDiv, top: coords.y, left: coords.x, duration, // @ts-ignore - unit is not a valid property in this version of dojo unit: "px" }); anim = this.page!.transformSlideAnimTo3d(anim, itemDiv, duration, Number(null)); anim.play(); } } if (this.autoheight) e.style(this.container_div!, "height", height + "px"); if (this.autowidth) e.style(this.container_div!, "width", pos_width + "px"); } /** * Determines the position of an item based on the zone's pattern. * @param index The index of the item in the zone. * @param width The width of the zone. * @param height The height of the zone. * @param count The number of items in the zone. */ itemIdToCoords(index: number, width: number, height: number, count: number): XYWH { switch (this.item_pattern) { case 'grid': return this.itemIdToCoordsGrid(index, width); case 'diagonal': return this.itemIdToCoordsDiagonal(index, width); case 'verticalfit': return this.itemIdToCoordsVerticalFit(index, width, height, count); case 'horizontalfit': return this.itemIdToCoordsHorizontalFit(index, width, height, count); case 'ellipticalfit': return this.itemIdToCoordsEllipticalFit(index, width, height, count); } // @ts-ignore this function must be overridden by the user for custom patterns, but somehow the error is not thrown here but when using the result of this function return undefined; } itemIdToCoordsGrid(index: number, width: number): XYWH { const cols = Math.max(1, Math.floor(width / (this.item_width! + this.item_margin))); const row = Math.floor(index / cols); return { x: (index - row * cols) * (this.item_width! + this.item_margin), y: row * (this.item_height! + this.item_margin), w: this.item_width!, h: this.item_height! }; } itemIdToCoordsDiagonal(index: number, width: number): XYWH { return { x: index * this.item_margin, y: index * this.item_margin, w: this.item_width!, h: this.item_height! }; } itemIdToCoordsVerticalFit(index: number, width: number, height: number, count: number): XYWH { const totalHeight = count * this.item_height!; let step: number; let offset: number; if (totalHeight <= height) { step = this.item_height!; offset = (height - totalHeight) / 2; } else { step = (height - this.item_height!) / (count - 1); offset = 0; } return { x: 0, y: Math.round(index * step + offset), w: this.item_width!, h: this.item_height! }; } itemIdToCoordsHorizontalFit(index: number, width: number, height: number, count: number): XYWH { const totalWidth = count * this.item_width!; let step: number; let offset: number; if (totalWidth <= width) { step = this.item_width!; offset = (width - totalWidth) / 2; } else { step = (width - this.item_width!) / (count - 1); offset = 0; } return { x: Math.round(index * step + offset), y: 0, w: this.item_width!, h: this.item_height! }; } itemIdToCoordsEllipticalFit(index: number, width: number, height: number, count: number): XYWH { const halfWidth = width / 2; const halfHeight = height / 2; const pi = 3.1415927; let result = { w: this.item_width!, h: this.item_height! } as XYWH; const l = count - (index + 1); if (l <= 4) { let w = result.w; let h = (result.h * halfHeight) / halfWidth; let angle = pi + l * ((2 * pi) / 5); result.x = halfWidth + w * Math.cos(angle) - result.w / 2; result.y = halfHeight + h * Math.sin(angle) - result.h / 2; } else if (l > 4) { let w = 2 * result.w; let h = (2 * result.h * halfHeight) / halfWidth; let angle = pi - pi / 2 + (l - 4) * ((2 * pi) / Math.max(10, count - 5)); result.x = halfWidth + w * Math.cos(angle) - result.w / 2; result.y = halfHeight + h * Math.sin(angle) - result.h / 2; } return result; } /** Returns the count of items within this zone. */ getItemNumber(): number { return this.items.length; } /** * Returns the DOM id for all elements in the zone, in order of how they are displayed (weight and order added). */ getItems(): string[] { let result: string[] = []; for (const item of this.items) { result.push(item.id); } return result; } } let Zone = declare("ebg.zone", Zone_Template); export = Zone; declare global { namespace BGA { type Zone = typeof Zone; interface EBG { zone: Zone; } } var ebg: BGA.EBG; }