mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-13 16:44:49 +08:00
Selection model added
This commit is contained in:
@@ -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
231
js/SelectionModel.js
Normal 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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
342
ueblueprint.js
342
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 {
|
||||
<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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user