From d3ce39ae687681503bbcffb2f199d1511064c5db Mon Sep 17 00:00:00 2001 From: barsdeveloper Date: Wed, 29 Sep 2021 22:39:41 +0200 Subject: [PATCH] Selection model added --- css/ueblueprint-style.css | 19 ++- js/SelectionModel.js | 231 +++++++++++++++++++++++++ js/UEBlueprint.js | 105 +++++++----- js/UEBlueprintObject.js | 5 + js/UEBlueprintSelect.js | 1 + ueblueprint.js | 342 +++++++++++++++++++++++++++++++++----- 6 files changed, 622 insertions(+), 81 deletions(-) create mode 100644 js/SelectionModel.js diff --git a/css/ueblueprint-style.css b/css/ueblueprint-style.css index 621a8b4..7aeb16d 100644 --- a/css/ueblueprint-style.css +++ b/css/ueblueprint-style.css @@ -290,11 +290,12 @@ u-blueprint { } .ueb-selector { - position: absolute; - top : min(var(--ueb-select-from-y) * 1px, var(--ueb-select-to-y) * 1px); - left : min(var(--ueb-select-from-x) * 1px, var(--ueb-select-to-x) * 1px); - width : calc(max(var(--ueb-select-from-x) - var(--ueb-select-to-x), var(--ueb-select-to-x) - var(--ueb-select-from-x)) * 1px); - height : calc(max(var(--ueb-select-from-y) - var(--ueb-select-to-y), var(--ueb-select-to-y) - var(--ueb-select-from-y)) * 1px); + position : absolute; + visibility: hidden; + top : min(var(--ueb-select-from-y) * 1px, var(--ueb-select-to-y) * 1px); + left : min(var(--ueb-select-from-x) * 1px, var(--ueb-select-to-x) * 1px); + width : calc(max(var(--ueb-select-from-x) - var(--ueb-select-to-x), var(--ueb-select-to-x) - var(--ueb-select-from-x)) * 1px); + height : calc(max(var(--ueb-select-from-y) - var(--ueb-select-to-y), var(--ueb-select-to-y) - var(--ueb-select-from-y)) * 1px); background-image: /* Top */ repeating-linear-gradient(90deg, transparent, transparent 1px, white 2px, white 7px, transparent 7px, transparent 11px), @@ -335,4 +336,12 @@ u-blueprint { calc(100% - 1px) 0, 100% 0; background-repeat: no-repeat; +} + +.ueb-selector>* { + visibility: visible; +} + +.ueb-selector[data-selecting="true"] { + visibility: visible; } \ No newline at end of file diff --git a/js/SelectionModel.js b/js/SelectionModel.js new file mode 100644 index 0000000..5cf4d82 --- /dev/null +++ b/js/SelectionModel.js @@ -0,0 +1,231 @@ +class OrderedIndexArray { + + /** + * @param {(arrayElement: number) => number} compareFunction A function that, given acouple of elements of the array telles what order are they on. + * @param {(number|array)} value Initial length or array to copy from + */ + constructor(comparisonValueSupplier = (a) => a, value = null) { + this.array = new Uint32Array(value) + this.comparisonValueSupplier = comparisonValueSupplier + this.length = 0 + } + + /** + * + * @param {number} index The index of the value to return + * @returns The element of the array + */ + get(index) { + return this.array[index] + } + + /** + * Returns the array used by this object. + * @returns The array. + */ + getArray() { + return this.array + } + + /** + * Get the position that the value supplied should (or does) occupy in the aray. + * @param {number} value The value to look for (it doesn't have to be part of the array). + * @returns The position index. + */ + getPosition(value) { + let l = 0 + let r = this.array.length + while (l < r) { + let m = Math.floor((l + r) / 2) + if (this.comparisonValueSupplier(this.array[m]) < value) { + l = m + 1 + } else { + r = m + } + } + return l + } + + /** + * Inserts the element in the array. + * @param array {number[]} The array to insert into. + * @param value {number} The value to insert into the array. + * @returns {number} The position into occupied by value into the array. + */ + insert(value) { + let position = this.getPosition(value) + let newArray = new Uint32Array(this.array.length + 1) + newArray.set(this.array.subarray(0, position), 0) + newArray[position] = value + newArray.set(this.array.subarray(position), position + 1) + this.array = newArray + this.length = this.array.length + return position + } + + /** + * Removes the element from the array. + * @param {number} value The value of the element to be remove. + */ + remove(value) { + let position = this.getPosition(value) + if (this.array[position] == value) { + this.removeAt(position) + } + } + + /** + * Removes the element into the specified position from the array. + * @param {number} position The index of the element to be remove. + */ + removeAt(position) { + let newArray = new Uint32Array(this.array.length - 1) + newArray.set(this.array.subarray(0, position), 0) + newArray.set(this.array.subarray(position + 1), position) + this.array = newArray + this.length = this.array.length + } +} + +export default class SelectionModel { + + /** + * @typedef {{ + * primaryInf: number, + * primarySup: number, + * secondaryInf: number, + * secondarySup: number + * }} BoundariesInfo + * @typedef {{ + * primaryBoundary: number, + * secondaryBoundary: number, + * insertionPosition: number, + * rectangle: number + * }} Metadata + * @typedef {numeric} Rectangle + * @param {number[]} initialPosition Coordinates of the starting point of selection [primaryAxisValue, secondaryAxisValue]. + * @param {Rectangle[]} rectangles Rectangles that can be selected by this object. + * @param {(rect: Rectangle) => BoundariesInfo} boundariesFunc A function that, given a rectangle, it provides the boundaries of such rectangle. + * @param {(rect: Rectangle, selected: bool) => void} selectToggleFunction A function that selects or deselects individual rectangles. + */ + constructor(initialPosition, rectangles, boundariesFunc, selectToggleFunction) { + this.initialPosition = initialPosition + this.finalPosition = initialPosition + /** @type Metadata[] */ + this.metadata = new Array(rectangles.length) + this.primaryOrder = new OrderedIndexArray((element) => this.metadata[element].primaryBoundary) + this.secondaryOrder = new OrderedIndexArray((element) => this.metadata[element].secondaryBoundary) + this.selectToggleFunction = selectToggleFunction + this.rectangles = rectangles + rectangles.forEach((rect, index) => { + /** @type Metadata */ + let rectangleMetadata = { + primaryBoundary: this.initialPosition, + secondaryBoundary: this.initialPosition, + rectangle: index // used to move both directions inside the this.metadata array + } + const rectangleBoundaries = boundariesFunc(rect) + if (rectangleBoundaries.primarySup < this.initialPosition[0]) { // rectangle is before position on the main axis + rectangleMetadata.primaryBoundary = rectangleBoundaries.primarySup + } else if (position < rectangleBoundaries.primaryInf) { // rectangle is after position on the main axis + rectangleMetadata.primaryBoundary = rectangleBoundaries.primaryInf + } else { + // Secondary order depends on primary order, if primary boundaries are not satisfied, the element is not watched for secondary ones + if (rectangleBoundaries.secondarySup < this.initialPosition[1] || this.initialPosition[1] < rectangleBoundaries.secondaryInf) { + this.secondaryOrder.insert(index) + } else { + selectToggleFunction(rect) + } + } + if (rectangleBoundaries.secondarySup < this.initialPosition[1]) { + rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondarySup + } else if (this.initialPosition[1] < rectangleBoundaries.secondaryInf) { + rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondaryInf + } + this.primaryOrder.insert(index) + this.metadata[index] = rectangleMetadata + }) + this.computeBoundaries(this.initialPosition) + } + + computeBoundaries(position) { + const primaryPosition = this.primaryOrder.getPosition(position[0]) + const secondaryPosition = this.secondaryOrder.getPosition(position[1]) + this.boundaries = { + // Primary axis negative direction + primaryN: primaryPosition > 0 + ? { + 'value': this.metadata[this.primaryOrder.get(primaryPosition - 1)].primaryBoundary, + 'index': primaryPosition - 1 + } + : { + 'value': Number.MIN_SAFE_INTEGER, + 'index': null + }, + // Primary axis positive direction + primaryP: primaryPosition < this.primaryOrder.length + ? { + 'value': this.metadata[this.primaryOrder.get(primaryPosition)].primaryBoundary, + 'index': primaryPosition + } + : { + 'value': Number.MAX_SAFE_INTEGER, + 'index': null + }, + // Secondary axis negative direction + secondaryN: secondaryPosition > 0 + ? { + 'value': this.metadata[this.secondaryOrder.get(secondaryPosition - 1)].secondaryBoundary, + 'index': secondaryPosition - 1 + } + : { + 'value': Number.MIN_SAFE_INTEGER, + 'index': null + }, + // Secondary axis positive direction + secondaryP: secondaryPosition < this.secondaryOrder.length + ? { + 'value': this.metadata[this.secondaryOrder.get(secondaryPosition)].secondaryBoundary, + 'index': secondaryPosition + } + : { + 'value': Number.MAX_SAFE_INTEGER, + 'index': null + } + } + } + + selectTo(finalPosition) { + const primaryBoundaryCrossed = (index, extended) => { + if (extended) { + this.secondaryOrder.insert(index) + } else { + this.secondaryOrder.remove(index) + this.selectToggleFunction(this.rectangles[index], false) + } + this.computeBoundaries(finalPosition) + this.selectTo(finalPosition) + } + + if (finalPosition[0] < this.boundaries.primaryN.value) { + primaryBoundaryCrossed(this.boundaries.primaryN.index, finalPosition[0] < this.initialPosition[0]) + } else if (finalPosition[0] > this.boundaries.primaryP.value) { + primaryBoundaryCrossed(this.boundaries.primaryP.index, this.initialPosition[0] < finalPosition[0]) + } + + + const secondaryBoundaryCrossed = (index, extended) => { + this.selectToggleFunction(this.rectangles[index], extended) + this.computeBoundaries(finalPosition) + this.selectTo(finalPosition) + } + + if (finalPosition[1] < this.boundaries.secondaryN.value) { + secondaryBoundaryCrossed(this.boundaries.secondaryN.index, finalPosition[1] < this.initialPosition[1]); + } else if (finalPosition[1] > this.boundaries.secondaryP.value) { + secondaryBoundaryCrossed(this.boundaries.secondaryP.index, this.initialPosition[1] < finalPosition[1]); + } + this.finalPosition = finalPosition + } + +} \ No newline at end of file diff --git a/js/UEBlueprint.js b/js/UEBlueprint.js index 9338603..3c4a842 100644 --- a/js/UEBlueprint.js +++ b/js/UEBlueprint.js @@ -1,6 +1,10 @@ import UEBlueprintDragScroll from "./UEBlueprintDragScroll.js" import UEBlueprintSelect from "./UEBlueprintSelect.js" +import SelectionModel from "./SelectionModel.js" +/** + * @typedef {import("./UEBlueprintObject.js").default} UEBlueprintObject + */ export default class UEBlueprint extends HTMLElement { headerTemplate() { @@ -23,16 +27,13 @@ export default class UEBlueprint extends HTMLElement {
+
` } - selectorTemplate() { - return `
` - } - static getElement(template) { let div = document.createElement('div'); div.innerHTML = template @@ -49,22 +50,49 @@ export default class UEBlueprint extends HTMLElement { constructor() { super() - /** @type {Set}" */ - this.nodes = new Set() + /** @type {UEBlueprintObject[]}" */ + this.nodes = new Array() this.expandGridSize = 400 + /** @type {HTMLElement} */ this.gridElement = null + /** @type {HTMLElement} */ this.viewportElement = null + /** @type {HTMLElement} */ this.overlayElement = null + /** @type {HTMLElement} */ this.selectorElement = null + /** @type {HTMLElement} */ + this.nodesContainerElement = null + /** @type {IntersectionObserver} */ this.selectorObserver = null this.dragObject = null this.selectObject = null + /** @type {Array} */ this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0] + /** @type {Array} */ this.translateValue = /*[this.expandGridSize, this.expandGridSize]*/[0, 0] + /** @type {number} */ this.zoom = 0 + /** @type {HTMLElement} */ this.headerElement = null - this.selectFrom = null - this.selectTo = null + /** @type {SelectionModel} */ + this.selectionModel = null + /** @type {(node: UEBlueprintObject) => BoundariesInfo} */ + this.nodeBoundariesSupplier = (node) => { + let rect = node.getBoundingClientRect() + let gridRect = this.nodesContainerElement.getBoundingClientRect() + return { + primaryInf: rect.left - gridRect.left, + primarySup: rect.right - gridRect.right, + // Counter intuitive here: the y (secondary axis is positive towards the bottom, therefore upper bound "sup" is bottom) + secondaryInf: rect.top - gridRect.top, + secondarySup: rect.bottom - gridRect.bottom + } + } + /** @type {(node: UEBlueprintObject, selected: bool) => void}} */ + this.nodeSelectToggleFunction = (node, selected) => { + node.setSelected(selected) + } } connectedCallback() { @@ -72,16 +100,28 @@ export default class UEBlueprint extends HTMLElement { this.headerElement = this.constructor.getElement(this.headerTemplate()) this.appendChild(this.headerElement) - this.overlayElement = this.constructor.getElement(this.overlayTemplate()) this.appendChild(this.overlayElement) - this.viewportElement = this.constructor.getElement(this.viewportTemplate()) this.appendChild(this.viewportElement) - this.gridElement = this.viewportElement.querySelector('.ueb-grid') + this.selectorElement = this.viewportElement.querySelector('.ueb-selector') + this.nodesContainerElement = this.querySelector('[data-nodes]') this.insertChildren() + this.selectorObserver = new IntersectionObserver( + (entries, observer) => { + entries.map(entry => { + /** @type {import("./UEBlueprintObject.js").default;}" */ + let target = entry.target + target.setSelected(entry.isIntersecting) + }) + }, { + threshold: [0.01], + root: this.selectorElement + }) + this.nodes.forEach(element => this.selectorObserver.observe(element)) + this.dragObject = new UEBlueprintDragScroll(this, { 'clickButton': 2, 'stepSize': 1, @@ -291,35 +331,20 @@ export default class UEBlueprint extends HTMLElement { * @param {number[]} initialPosition - Selection rectangle initial position (relative to the .ueb-grid element) */ startSelecting(initialPosition) { - if (this.selectorElement) { - this.finishSelecting() - } initialPosition = this.compensateTranslation(initialPosition) - this.selectorElement = this.constructor.getElement(this.selectorTemplate()) - this.querySelector('[data-nodes]').appendChild(this.selectorElement) + // Set initial position this.selectorElement.style.setProperty('--ueb-select-from-x', initialPosition[0]) this.selectorElement.style.setProperty('--ueb-select-from-y', initialPosition[1]) - this.selectorObserver = new IntersectionObserver( - (entries, observer) => { - entries.map(entry => { - /** @type {import("./UEBlueprintObject.js").default;}" */ - let target = entry.target - target.setSelected(entry.isIntersecting) - }) - }, { - threshold: [0.01], - root: this.selectorElement - }) - this.nodes.forEach(element => this.selectorObserver.observe(element)) + // Final position coincide with the initial position, at the beginning of selection + this.selectorElement.style.setProperty('--ueb-select-to-x', initialPosition[0]) + this.selectorElement.style.setProperty('--ueb-select-to-y', initialPosition[1]) + this.selectorElement.dataset.selecting = "true" + this.selectionModel = new SelectionModel(initialPosition, this.nodes, this.nodeBoundariesSupplier, this.nodeSelectToggleFunction) } finishSelecting() { - if (this.selectorElement) { - this.selectorElement.remove() - this.selectorElement = null - this.selectorObserver.disconnect() - this.selectorObserver = null - } + this.selectorElement.dataset.selecting = "false" + this.selectionModel = null } /** @@ -330,13 +355,17 @@ export default class UEBlueprint extends HTMLElement { finalPosition = this.compensateTranslation(finalPosition) this.selectorElement.style.setProperty('--ueb-select-to-x', finalPosition[0]) this.selectorElement.style.setProperty('--ueb-select-to-y', finalPosition[1]) + this.selectionModel.selectTo(finalPosition) } + /** + * + * @param {...UEBlueprintObject} blueprintNodes + */ addNode(...blueprintNodes) { - [...blueprintNodes].reduce((s, e) => s.add(e), this.nodes) - let nodesDestination = this.querySelector('[data-nodes]') - if (nodesDestination) { - nodesDestination.append(...blueprintNodes) + [...blueprintNodes].reduce((s, e) => s.push(e), this.nodes) + if (this.nodesContainerElement) { + this.nodesContainerElement.append(...blueprintNodes) } } } diff --git a/js/UEBlueprintObject.js b/js/UEBlueprintObject.js index cd16b03..36d28a3 100644 --- a/js/UEBlueprintObject.js +++ b/js/UEBlueprintObject.js @@ -97,6 +97,11 @@ export default class UEBlueprintObject extends UEBlueprintDraggableObject { setSelected(value = true) { this.selected = value + if (value) { + this.classList.add('ueb-selected') + } else { + this.classList.remove('ueb-selected') + } } } diff --git a/js/UEBlueprintSelect.js b/js/UEBlueprintSelect.js index 3f12f02..1b2a555 100644 --- a/js/UEBlueprintSelect.js +++ b/js/UEBlueprintSelect.js @@ -36,6 +36,7 @@ export default class UEBlueprintSelect { if (!self.exitSelectAnyButton || e.button == self.clickButton) { // Remove the handlers of `mousemove` and `mouseup` self.blueprintNode.getGridDOMElement().removeEventListener('mousemove', self.mouseMoveHandler) + self.blueprintNode.finishSelecting() document.removeEventListener('mouseup', self.mouseUpHandler) } } diff --git a/ueblueprint.js b/ueblueprint.js index ddb4f9c..df1900b 100644 --- a/ueblueprint.js +++ b/ueblueprint.js @@ -142,6 +142,7 @@ class UEBlueprintSelect { if (!self.exitSelectAnyButton || e.button == self.clickButton) { // Remove the handlers of `mousemove` and `mouseup` self.blueprintNode.getGridDOMElement().removeEventListener('mousemove', self.mouseMoveHandler); + self.blueprintNode.finishSelecting(); document.removeEventListener('mouseup', self.mouseUpHandler); } }; @@ -164,6 +165,241 @@ class UEBlueprintSelect { } } +class OrderedIndexArray { + + /** + * @param {(arrayElement: number) => number} compareFunction A function that, given acouple of elements of the array telles what order are they on. + * @param {(number|array)} value Initial length or array to copy from + */ + constructor(comparisonValueSupplier = (a) => a, value = null) { + this.array = new Uint32Array(value); + this.comparisonValueSupplier = comparisonValueSupplier; + this.length = 0; + } + + /** + * + * @param {number} index The index of the value to return + * @returns The element of the array + */ + get(index) { + return this.array[index] + } + + /** + * Returns the array used by this object. + * @returns The array. + */ + getArray() { + return this.array + } + + /** + * Get the position that the value supplied should (or does) occupy in the aray. + * @param {number} value The value to look for (it doesn't have to be part of the array). + * @returns The position index. + */ + getPosition(value) { + let l = 0; + let r = this.array.length; + while (l < r) { + let m = Math.floor((l + r) / 2); + if (this.comparisonValueSupplier(this.array[m]) < value) { + l = m + 1; + } else { + r = m; + } + } + return l + } + + /** + * Inserts the element in the array. + * @param array {number[]} The array to insert into. + * @param value {number} The value to insert into the array. + * @returns {number} The position into occupied by value into the array. + */ + insert(value) { + let position = this.getPosition(value); + let newArray = new Uint32Array(this.array.length + 1); + newArray.set(this.array.subarray(0, position), 0); + newArray[position] = value; + newArray.set(this.array.subarray(position), position + 1); + this.array = newArray; + this.length = this.array.length; + return position + } + + /** + * Removes the element from the array. + * @param {number} value The value of the element to be remove. + */ + remove(value) { + let position = this.getPosition(value); + if (this.array[position] == value) { + this.removeAt(position); + } + } + + /** + * Removes the element into the specified position from the array. + * @param {number} position The index of the element to be remove. + */ + removeAt(position) { + let newArray = new Uint32Array(this.array.length - 1); + newArray.set(this.array.subarray(0, position), 0); + newArray.set(this.array.subarray(position + 1), position); + this.array = newArray; + this.length = this.array.length; + } +} + +class SelectionModel { + + /** + * @typedef {{ + * primaryInf: number, + * primarySup: number, + * secondaryInf: number, + * secondarySup: number + * }} BoundariesInfo + * @typedef {{ + * primaryBoundary: number, + * secondaryBoundary: number, + * insertionPosition: number, + * rectangle: number + * }} Metadata + * @typedef {numeric} Rectangle + * @param {number[]} initialPosition Coordinates of the starting point of selection [primaryAxisValue, secondaryAxisValue]. + * @param {Rectangle[]} rectangles Rectangles that can be selected by this object. + * @param {(rect: Rectangle) => BoundariesInfo} boundariesFunc A function that, given a rectangle, it provides the boundaries of such rectangle. + * @param {(rect: Rectangle, selected: bool) => void} selectToggleFunction A function that selects or deselects individual rectangles. + */ + constructor(initialPosition, rectangles, boundariesFunc, selectToggleFunction) { + this.initialPosition = initialPosition; + this.finalPosition = initialPosition; + /** @type Metadata[] */ + this.metadata = new Array(rectangles.length); + this.primaryOrder = new OrderedIndexArray((element) => this.metadata[element].primaryBoundary); + this.secondaryOrder = new OrderedIndexArray((element) => this.metadata[element].secondaryBoundary); + this.selectToggleFunction = selectToggleFunction; + this.rectangles = rectangles; + rectangles.forEach((rect, index) => { + /** @type Metadata */ + let rectangleMetadata = { + primaryBoundary: this.initialPosition, + secondaryBoundary: this.initialPosition, + rectangle: index // used to move both directions inside the this.metadata array + }; + const rectangleBoundaries = boundariesFunc(rect); + if (rectangleBoundaries.primarySup < this.initialPosition[0]) { // rectangle is before position on the main axis + rectangleMetadata.primaryBoundary = rectangleBoundaries.primarySup; + } else if (position < rectangleBoundaries.primaryInf) { // rectangle is after position on the main axis + rectangleMetadata.primaryBoundary = rectangleBoundaries.primaryInf; + } else { + // Secondary order depends on primary order, if primary boundaries are not satisfied, the element is not watched for secondary ones + if (rectangleBoundaries.secondarySup < this.initialPosition[1] || this.initialPosition[1] < rectangleBoundaries.secondaryInf) { + this.secondaryOrder.insert(index); + } else { + selectToggleFunction(rect); + } + } + if (rectangleBoundaries.secondarySup < this.initialPosition[1]) { + rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondarySup; + } else if (this.initialPosition[1] < rectangleBoundaries.secondaryInf) { + rectangleMetadata.secondaryBoundary = rectangleBoundaries.secondaryInf; + } + this.primaryOrder.insert(index); + this.metadata[index] = rectangleMetadata; + }); + this.computeBoundaries(this.initialPosition); + } + + computeBoundaries(position) { + const primaryPosition = this.primaryOrder.getPosition(position[0]); + const secondaryPosition = this.secondaryOrder.getPosition(position[1]); + this.boundaries = { + // Primary axis negative direction + primaryN: primaryPosition > 0 + ? { + 'value': this.metadata[this.primaryOrder.get(primaryPosition - 1)].primaryBoundary, + 'index': primaryPosition - 1 + } + : { + 'value': Number.MIN_SAFE_INTEGER, + 'index': null + }, + // Primary axis positive direction + primaryP: primaryPosition < this.primaryOrder.length + ? { + 'value': this.metadata[this.primaryOrder.get(primaryPosition)].primaryBoundary, + 'index': primaryPosition + } + : { + 'value': Number.MAX_SAFE_INTEGER, + 'index': null + }, + // Secondary axis negative direction + secondaryN: secondaryPosition > 0 + ? { + 'value': this.metadata[this.secondaryOrder.get(secondaryPosition - 1)].secondaryBoundary, + 'index': secondaryPosition - 1 + } + : { + 'value': Number.MIN_SAFE_INTEGER, + 'index': null + }, + // Secondary axis positive direction + secondaryP: secondaryPosition < this.secondaryOrder.length + ? { + 'value': this.metadata[this.secondaryOrder.get(secondaryPosition)].secondaryBoundary, + 'index': secondaryPosition + } + : { + 'value': Number.MAX_SAFE_INTEGER, + 'index': null + } + }; + } + + selectTo(finalPosition) { + const primaryBoundaryCrossed = (index, extended) => { + if (extended) { + this.secondaryOrder.insert(index); + } else { + this.secondaryOrder.remove(index); + this.selectToggleFunction(this.rectangles[index], false); + } + this.computeBoundaries(finalPosition); + this.selectTo(finalPosition); + }; + + if (finalPosition[0] < this.boundaries.primaryN.value) { + primaryBoundaryCrossed(this.boundaries.primaryN.index, finalPosition[0] < this.initialPosition[0]); + } else if (finalPosition[0] > this.boundaries.primaryP.value) { + primaryBoundaryCrossed(this.boundaries.primaryP.index, this.initialPosition[0] < finalPosition[0]); + } + + + const secondaryBoundaryCrossed = (index, extended) => { + this.selectToggleFunction(this.rectangles[index], extended); + this.computeBoundaries(finalPosition); + this.selectTo(finalPosition); + }; + + if (finalPosition[1] < this.boundaries.secondaryN.value) { + secondaryBoundaryCrossed(this.boundaries.secondaryN.index, finalPosition[1] < this.initialPosition[1]); + } else if (finalPosition[1] > this.boundaries.secondaryP.value) { + secondaryBoundaryCrossed(this.boundaries.secondaryP.index, this.initialPosition[1] < finalPosition[1]); + } + this.finalPosition = finalPosition; + } + +} + +/** + * @typedef {import("./UEBlueprintObject.js").default} UEBlueprintObject + */ class UEBlueprint extends HTMLElement { headerTemplate() { @@ -186,16 +422,13 @@ class UEBlueprint extends HTMLElement {
+
` } - selectorTemplate() { - return `
` - } - static getElement(template) { let div = document.createElement('div'); div.innerHTML = template; @@ -212,22 +445,49 @@ class UEBlueprint extends HTMLElement { constructor() { super(); - /** @type {Set}" */ - this.nodes = new Set(); + /** @type {UEBlueprintObject[]}" */ + this.nodes = new Array(); this.expandGridSize = 400; + /** @type {HTMLElement} */ this.gridElement = null; + /** @type {HTMLElement} */ this.viewportElement = null; + /** @type {HTMLElement} */ this.overlayElement = null; + /** @type {HTMLElement} */ this.selectorElement = null; + /** @type {HTMLElement} */ + this.nodesContainerElement = null; + /** @type {IntersectionObserver} */ this.selectorObserver = null; this.dragObject = null; this.selectObject = null; + /** @type {Array} */ this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0]; + /** @type {Array} */ this.translateValue = /*[this.expandGridSize, this.expandGridSize]*/[0, 0]; + /** @type {number} */ this.zoom = 0; + /** @type {HTMLElement} */ this.headerElement = null; - this.selectFrom = null; - this.selectTo = null; + /** @type {SelectionModel} */ + this.selectionModel = null; + /** @type {(node: UEBlueprintObject) => BoundariesInfo} */ + this.nodeBoundariesSupplier = (node) => { + let rect = node.getBoundingClientRect(); + let gridRect = this.nodesContainerElement.getBoundingClientRect(); + return { + primaryInf: rect.left - gridRect.left, + primarySup: rect.right - gridRect.right, + // Counter intuitive here: the y (secondary axis is positive towards the bottom, therefore upper bound "sup" is bottom) + secondaryInf: rect.top - gridRect.top, + secondarySup: rect.bottom - gridRect.bottom + } + }; + /** @type {(node: UEBlueprintObject, selected: bool) => void}} */ + this.nodeSelectToggleFunction = (node, selected) => { + node.setSelected(selected); + }; } connectedCallback() { @@ -235,16 +495,28 @@ class UEBlueprint extends HTMLElement { this.headerElement = this.constructor.getElement(this.headerTemplate()); this.appendChild(this.headerElement); - this.overlayElement = this.constructor.getElement(this.overlayTemplate()); this.appendChild(this.overlayElement); - this.viewportElement = this.constructor.getElement(this.viewportTemplate()); this.appendChild(this.viewportElement); - this.gridElement = this.viewportElement.querySelector('.ueb-grid'); + this.selectorElement = this.viewportElement.querySelector('.ueb-selector'); + this.nodesContainerElement = this.querySelector('[data-nodes]'); this.insertChildren(); + this.selectorObserver = new IntersectionObserver( + (entries, observer) => { + entries.map(entry => { + /** @type {import("./UEBlueprintObject.js").default;}" */ + let target = entry.target; + target.setSelected(entry.isIntersecting); + }); + }, { + threshold: [0.01], + root: this.selectorElement + }); + this.nodes.forEach(element => this.selectorObserver.observe(element)); + this.dragObject = new UEBlueprintDragScroll(this, { 'clickButton': 2, 'stepSize': 1, @@ -454,35 +726,20 @@ class UEBlueprint extends HTMLElement { * @param {number[]} initialPosition - Selection rectangle initial position (relative to the .ueb-grid element) */ startSelecting(initialPosition) { - if (this.selectorElement) { - this.finishSelecting(); - } initialPosition = this.compensateTranslation(initialPosition); - this.selectorElement = this.constructor.getElement(this.selectorTemplate()); - this.querySelector('[data-nodes]').appendChild(this.selectorElement); + // Set initial position this.selectorElement.style.setProperty('--ueb-select-from-x', initialPosition[0]); this.selectorElement.style.setProperty('--ueb-select-from-y', initialPosition[1]); - this.selectorObserver = new IntersectionObserver( - (entries, observer) => { - entries.map(entry => { - /** @type {import("./UEBlueprintObject.js").default;}" */ - let target = entry.target; - target.setSelected(entry.isIntersecting); - }); - }, { - threshold: [0.01], - root: this.selectorElement - }); - this.nodes.forEach(element => this.selectorObserver.observe(element)); + // Final position coincide with the initial position, at the beginning of selection + this.selectorElement.style.setProperty('--ueb-select-to-x', initialPosition[0]); + this.selectorElement.style.setProperty('--ueb-select-to-y', initialPosition[1]); + this.selectorElement.dataset.selecting = "true"; + this.selectionModel = new SelectionModel(initialPosition, this.nodes, this.nodeBoundariesSupplier, this.nodeSelectToggleFunction); } finishSelecting() { - if (this.selectorElement) { - this.selectorElement.remove(); - this.selectorElement = null; - this.selectorObserver.disconnect(); - this.selectorObserver = null; - } + this.selectorElement.dataset.selecting = "false"; + this.selectionModel = null; } /** @@ -493,13 +750,17 @@ class UEBlueprint extends HTMLElement { finalPosition = this.compensateTranslation(finalPosition); this.selectorElement.style.setProperty('--ueb-select-to-x', finalPosition[0]); this.selectorElement.style.setProperty('--ueb-select-to-y', finalPosition[1]); + this.selectionModel.selectTo(finalPosition); } + /** + * + * @param {...UEBlueprintObject} blueprintNodes + */ addNode(...blueprintNodes) { - [...blueprintNodes].reduce((s, e) => s.add(e), this.nodes); - let nodesDestination = this.querySelector('[data-nodes]'); - if (nodesDestination) { - nodesDestination.append(...blueprintNodes); + [...blueprintNodes].reduce((s, e) => s.push(e), this.nodes); + if (this.nodesContainerElement) { + this.nodesContainerElement.append(...blueprintNodes); } } } @@ -635,6 +896,11 @@ class UEBlueprintObject extends UEBlueprintDraggableObject { setSelected(value = true) { this.selected = value; + if (value) { + this.classList.add('ueb-selected'); + } else { + this.classList.remove('ueb-selected'); + } } }