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');
+ }
}
}