Selection model added

This commit is contained in:
barsdeveloper
2021-09-29 22:39:41 +02:00
parent ccaad9b677
commit d3ce39ae68
6 changed files with 622 additions and 81 deletions

View File

@@ -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;
}

231
js/SelectionModel.js Normal file
View File

@@ -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
}
}

View File

@@ -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 {
<div class="ueb-grid"
style="--ueb-additional-x:${this.additional[0]}; --ueb-additional-y:${this.additional[1]}; --ueb-translate-x:${this.translateValue[0]}; --ueb-translate-y:${this.translateValue[1]}">
<div class="ueb-grid-content" data-nodes>
<div class="ueb-selector" data-selecting="false"></div>
</div>
</div>
</div>
`
}
selectorTemplate() {
return `<div class="ueb-selector"></div>`
}
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<import("./UEBlueprintObject.js").default>}" */
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<number>} */
this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0]
/** @type {Array<number>} */
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)
}
}
}

View File

@@ -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')
}
}
}

View File

@@ -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)
}
}

View File

@@ -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 {
<div class="ueb-grid"
style="--ueb-additional-x:${this.additional[0]}; --ueb-additional-y:${this.additional[1]}; --ueb-translate-x:${this.translateValue[0]}; --ueb-translate-y:${this.translateValue[1]}">
<div class="ueb-grid-content" data-nodes>
<div class="ueb-selector" data-selecting="false"></div>
</div>
</div>
</div>
`
}
selectorTemplate() {
return `<div class="ueb-selector"></div>`
}
static getElement(template) {
let div = document.createElement('div');
div.innerHTML = template;
@@ -212,22 +445,49 @@ class UEBlueprint extends HTMLElement {
constructor() {
super();
/** @type {Set<import("./UEBlueprintObject.js").default>}" */
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<number>} */
this.additional = /*[2 * this.expandGridSize, 2 * this.expandGridSize]*/[0, 0];
/** @type {Array<number>} */
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');
}
}
}