mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
Make scene functions return array instead of mutate array
- Not all functions were changes; so the given argument was a new array to some
This commit is contained in:
parent
299e7e9099
commit
2ef809fe5d
3 changed files with 101 additions and 67 deletions
151
src/index.tsx
151
src/index.tsx
|
@ -53,7 +53,7 @@ import { Panel } from "./components/Panel";
|
||||||
|
|
||||||
import "./styles.scss";
|
import "./styles.scss";
|
||||||
|
|
||||||
const { elements } = createScene();
|
let { elements } = createScene();
|
||||||
const { history } = createHistory();
|
const { history } = createHistory();
|
||||||
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
const DEFAULT_PROJECT_NAME = `excalidraw-${getDateTime()}`;
|
||||||
|
|
||||||
|
@ -106,9 +106,10 @@ export class App extends React.Component<{}, AppState> {
|
||||||
document.addEventListener("keydown", this.onKeyDown, false);
|
document.addEventListener("keydown", this.onKeyDown, false);
|
||||||
window.addEventListener("resize", this.onResize, false);
|
window.addEventListener("resize", this.onResize, false);
|
||||||
|
|
||||||
const savedState = restoreFromLocalStorage(elements);
|
const appState = restoreFromLocalStorage(elements);
|
||||||
if (savedState) {
|
|
||||||
this.setState(savedState);
|
if (appState) {
|
||||||
|
this.setState(appState);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +140,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
if (isInputLike(event.target)) return;
|
if (isInputLike(event.target)) return;
|
||||||
|
|
||||||
if (event.key === KEYS.ESCAPE) {
|
if (event.key === KEYS.ESCAPE) {
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
|
} else if (event.key === KEYS.BACKSPACE || event.key === KEYS.DELETE) {
|
||||||
|
@ -149,13 +150,16 @@ export class App extends React.Component<{}, AppState> {
|
||||||
const step = event.shiftKey
|
const step = event.shiftKey
|
||||||
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
? ELEMENT_SHIFT_TRANSLATE_AMOUNT
|
||||||
: ELEMENT_TRANSLATE_AMOUNT;
|
: ELEMENT_TRANSLATE_AMOUNT;
|
||||||
elements.forEach(element => {
|
elements = elements.map(el => {
|
||||||
if (element.isSelected) {
|
if (el.isSelected) {
|
||||||
|
const element = { ...el };
|
||||||
if (event.key === KEYS.ARROW_LEFT) element.x -= step;
|
if (event.key === KEYS.ARROW_LEFT) element.x -= step;
|
||||||
else if (event.key === KEYS.ARROW_RIGHT) element.x += step;
|
else if (event.key === KEYS.ARROW_RIGHT) element.x += step;
|
||||||
else if (event.key === KEYS.ARROW_UP) element.y -= step;
|
else if (event.key === KEYS.ARROW_UP) element.y -= step;
|
||||||
else if (event.key === KEYS.ARROW_DOWN) element.y += step;
|
else if (event.key === KEYS.ARROW_DOWN) element.y += step;
|
||||||
|
return element;
|
||||||
}
|
}
|
||||||
|
return el;
|
||||||
});
|
});
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
@ -191,9 +195,12 @@ export class App extends React.Component<{}, AppState> {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// Select all: Cmd-A
|
// Select all: Cmd-A
|
||||||
} else if (event[META_KEY] && event.code === "KeyA") {
|
} else if (event[META_KEY] && event.code === "KeyA") {
|
||||||
elements.forEach(element => {
|
let newElements = [...elements];
|
||||||
|
newElements.forEach(element => {
|
||||||
element.isSelected = true;
|
element.isSelected = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements = newElements;
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
|
} else if (shapesShortcutKeys.includes(event.key.toLowerCase())) {
|
||||||
|
@ -219,13 +226,13 @@ export class App extends React.Component<{}, AppState> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private deleteSelectedElements = () => {
|
private deleteSelectedElements = () => {
|
||||||
deleteSelectedElements(elements);
|
elements = deleteSelectedElements(elements);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private clearCanvas = () => {
|
private clearCanvas = () => {
|
||||||
if (window.confirm("This will clear the whole canvas. Are you sure?")) {
|
if (window.confirm("This will clear the whole canvas. Are you sure?")) {
|
||||||
elements.splice(0, elements.length);
|
elements = [];
|
||||||
this.setState({
|
this.setState({
|
||||||
viewBackgroundColor: "#ffffff",
|
viewBackgroundColor: "#ffffff",
|
||||||
scrollX: 0,
|
scrollX: 0,
|
||||||
|
@ -244,36 +251,40 @@ export class App extends React.Component<{}, AppState> {
|
||||||
|
|
||||||
private pasteStyles = () => {
|
private pasteStyles = () => {
|
||||||
const pastedElement = JSON.parse(copiedStyles);
|
const pastedElement = JSON.parse(copiedStyles);
|
||||||
elements.forEach(element => {
|
elements = elements.map(element => {
|
||||||
if (element.isSelected) {
|
if (element.isSelected) {
|
||||||
element.backgroundColor = pastedElement?.backgroundColor;
|
return {
|
||||||
element.strokeWidth = pastedElement?.strokeWidth;
|
...element,
|
||||||
element.strokeColor = pastedElement?.strokeColor;
|
backgroundColor: pastedElement?.backgroundColor,
|
||||||
element.fillStyle = pastedElement?.fillStyle;
|
strokeWidth: pastedElement?.strokeWidth,
|
||||||
element.opacity = pastedElement?.opacity;
|
strokeColor: pastedElement?.strokeColor,
|
||||||
element.roughness = pastedElement?.roughness;
|
fillStyle: pastedElement?.fillStyle,
|
||||||
|
opacity: pastedElement?.opacity,
|
||||||
|
roughness: pastedElement?.roughness
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
return element;
|
||||||
});
|
});
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private moveAllLeft = () => {
|
private moveAllLeft = () => {
|
||||||
moveAllLeft(elements, getSelectedIndices(elements));
|
elements = moveAllLeft([...elements], getSelectedIndices(elements));
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private moveOneLeft = () => {
|
private moveOneLeft = () => {
|
||||||
moveOneLeft(elements, getSelectedIndices(elements));
|
elements = moveOneLeft([...elements], getSelectedIndices(elements));
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private moveAllRight = () => {
|
private moveAllRight = () => {
|
||||||
moveAllRight(elements, getSelectedIndices(elements));
|
elements = moveAllRight([...elements], getSelectedIndices(elements));
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private moveOneRight = () => {
|
private moveOneRight = () => {
|
||||||
moveOneRight(elements, getSelectedIndices(elements));
|
elements = moveOneRight([...elements], getSelectedIndices(elements));
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -283,27 +294,38 @@ export class App extends React.Component<{}, AppState> {
|
||||||
this.setState({ name });
|
this.setState({ name });
|
||||||
}
|
}
|
||||||
|
|
||||||
private changeProperty = (callback: (element: ExcalidrawElement) => void) => {
|
private changeProperty = (
|
||||||
elements.forEach(element => {
|
callback: (element: ExcalidrawElement) => ExcalidrawElement
|
||||||
if (element.isSelected) {
|
) => {
|
||||||
callback(element);
|
elements = elements
|
||||||
}
|
.filter(el => el.isSelected)
|
||||||
});
|
.map(element => {
|
||||||
|
return callback(element);
|
||||||
|
});
|
||||||
|
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
private changeOpacity = (event: React.ChangeEvent<HTMLInputElement>) => {
|
private changeOpacity = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
this.changeProperty(element => (element.opacity = +event.target.value));
|
this.changeProperty(element => ({
|
||||||
|
...element,
|
||||||
|
opacity: +event.target.value
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
private changeStrokeColor = (color: string) => {
|
private changeStrokeColor = (color: string) => {
|
||||||
this.changeProperty(element => (element.strokeColor = color));
|
this.changeProperty(element => ({
|
||||||
|
...element,
|
||||||
|
strokeColor: color
|
||||||
|
}));
|
||||||
this.setState({ currentItemStrokeColor: color });
|
this.setState({ currentItemStrokeColor: color });
|
||||||
};
|
};
|
||||||
|
|
||||||
private changeBackgroundColor = (color: string) => {
|
private changeBackgroundColor = (color: string) => {
|
||||||
this.changeProperty(element => (element.backgroundColor = color));
|
this.changeProperty(element => ({
|
||||||
|
...element,
|
||||||
|
backgroundColor: color
|
||||||
|
}));
|
||||||
this.setState({ currentItemBackgroundColor: color });
|
this.setState({ currentItemBackgroundColor: color });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -336,7 +358,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
"text/plain",
|
"text/plain",
|
||||||
JSON.stringify(elements.filter(element => element.isSelected))
|
JSON.stringify(elements.filter(element => element.isSelected))
|
||||||
);
|
);
|
||||||
deleteSelectedElements(elements);
|
elements = deleteSelectedElements(elements);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}}
|
}}
|
||||||
|
@ -358,7 +380,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
activeTool={this.state.elementType}
|
activeTool={this.state.elementType}
|
||||||
onToolChange={value => {
|
onToolChange={value => {
|
||||||
this.setState({ elementType: value });
|
this.setState({ elementType: value });
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
document.documentElement.style.cursor =
|
document.documentElement.style.cursor =
|
||||||
value === "text" ? "text" : "crosshair";
|
value === "text" ? "text" : "crosshair";
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
|
@ -404,9 +426,10 @@ export class App extends React.Component<{}, AppState> {
|
||||||
element => element.fillStyle
|
element => element.fillStyle
|
||||||
)}
|
)}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
this.changeProperty(element => {
|
this.changeProperty(element => ({
|
||||||
element.fillStyle = value;
|
...element,
|
||||||
});
|
fillStyle: value
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -426,9 +449,10 @@ export class App extends React.Component<{}, AppState> {
|
||||||
element => element.strokeWidth
|
element => element.strokeWidth
|
||||||
)}
|
)}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
this.changeProperty(element => {
|
this.changeProperty(element => ({
|
||||||
element.strokeWidth = value;
|
...element,
|
||||||
});
|
strokeWidth: value
|
||||||
|
}));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -444,9 +468,10 @@ export class App extends React.Component<{}, AppState> {
|
||||||
element => element.roughness
|
element => element.roughness
|
||||||
)}
|
)}
|
||||||
onChange={value =>
|
onChange={value =>
|
||||||
this.changeProperty(element => {
|
this.changeProperty(element => ({
|
||||||
element.roughness = value;
|
...element,
|
||||||
})
|
roughness: value
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
@ -552,7 +577,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!element.isSelected) {
|
if (!element.isSelected) {
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
element.isSelected = true;
|
element.isSelected = true;
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
@ -660,14 +685,15 @@ export class App extends React.Component<{}, AppState> {
|
||||||
} else {
|
} else {
|
||||||
// We unselect every other elements unless shift is pressed
|
// We unselect every other elements unless shift is pressed
|
||||||
if (!e.shiftKey) {
|
if (!e.shiftKey) {
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// No matter what, we select it
|
// No matter what, we select it
|
||||||
hitElement.isSelected = true;
|
hitElement.isSelected = true;
|
||||||
// We duplicate the selected element if alt is pressed on Mouse down
|
// We duplicate the selected element if alt is pressed on Mouse down
|
||||||
if (e.altKey) {
|
if (e.altKey) {
|
||||||
elements.push(
|
elements = [
|
||||||
|
...elements,
|
||||||
...elements.reduce((duplicates, element) => {
|
...elements.reduce((duplicates, element) => {
|
||||||
if (element.isSelected) {
|
if (element.isSelected) {
|
||||||
duplicates.push(duplicateElement(element));
|
duplicates.push(duplicateElement(element));
|
||||||
|
@ -675,11 +701,11 @@ export class App extends React.Component<{}, AppState> {
|
||||||
}
|
}
|
||||||
return duplicates;
|
return duplicates;
|
||||||
}, [] as typeof elements)
|
}, [] as typeof elements)
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// If we don't click on anything, let's remove all the selected elements
|
// If we don't click on anything, let's remove all the selected elements
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
}
|
}
|
||||||
|
|
||||||
isDraggingElements = someElementIsSelected(elements);
|
isDraggingElements = someElementIsSelected(elements);
|
||||||
|
@ -714,8 +740,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
font: this.state.currentItemFont,
|
font: this.state.currentItemFont,
|
||||||
onSubmit: text => {
|
onSubmit: text => {
|
||||||
addTextElement(element, text, this.state.currentItemFont);
|
addTextElement(element, text, this.state.currentItemFont);
|
||||||
elements.push(element);
|
elements = [...elements, { ...element, isSelected: true }];
|
||||||
element.isSelected = true;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
elementType: "selection"
|
elementType: "selection"
|
||||||
|
@ -725,14 +750,14 @@ export class App extends React.Component<{}, AppState> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push(element);
|
|
||||||
if (this.state.elementType === "text") {
|
if (this.state.elementType === "text") {
|
||||||
|
elements = [...elements, { ...element, isSelected: true }];
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
elementType: "selection"
|
elementType: "selection"
|
||||||
});
|
});
|
||||||
element.isSelected = true;
|
|
||||||
} else {
|
} else {
|
||||||
|
elements = [...elements, element];
|
||||||
this.setState({ draggingElement: element });
|
this.setState({ draggingElement: element });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -883,7 +908,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
: height;
|
: height;
|
||||||
|
|
||||||
if (this.state.elementType === "selection") {
|
if (this.state.elementType === "selection") {
|
||||||
setSelection(elements, draggingElement);
|
elements = setSelection([...elements], draggingElement);
|
||||||
}
|
}
|
||||||
// We don't want to save history when moving an element
|
// We don't want to save history when moving an element
|
||||||
history.skipRecording();
|
history.skipRecording();
|
||||||
|
@ -901,7 +926,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
|
|
||||||
// if no element is clicked, clear the selection and redraw
|
// if no element is clicked, clear the selection and redraw
|
||||||
if (draggingElement === null) {
|
if (draggingElement === null) {
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -910,7 +935,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
if (isDraggingElements) {
|
if (isDraggingElements) {
|
||||||
isDraggingElements = false;
|
isDraggingElements = false;
|
||||||
}
|
}
|
||||||
elements.pop();
|
elements = elements.slice(0, -1);
|
||||||
} else {
|
} else {
|
||||||
draggingElement.isSelected = true;
|
draggingElement.isSelected = true;
|
||||||
}
|
}
|
||||||
|
@ -998,8 +1023,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
text,
|
text,
|
||||||
element.font || this.state.currentItemFont
|
element.font || this.state.currentItemFont
|
||||||
);
|
);
|
||||||
elements.push(element);
|
elements = [...elements, { ...element, isSelected: true }];
|
||||||
element.isSelected = true;
|
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
elementType: "selection"
|
elementType: "selection"
|
||||||
|
@ -1031,7 +1055,7 @@ export class App extends React.Component<{}, AppState> {
|
||||||
parsedElements.length > 0 &&
|
parsedElements.length > 0 &&
|
||||||
parsedElements[0].type // need to implement a better check here...
|
parsedElements[0].type // need to implement a better check here...
|
||||||
) {
|
) {
|
||||||
clearSelection(elements);
|
elements = clearSelection([...elements]);
|
||||||
|
|
||||||
if (x == null) x = 10 - this.state.scrollX;
|
if (x == null) x = 10 - this.state.scrollX;
|
||||||
if (y == null) y = 10 - this.state.scrollY;
|
if (y == null) y = 10 - this.state.scrollY;
|
||||||
|
@ -1040,12 +1064,15 @@ export class App extends React.Component<{}, AppState> {
|
||||||
const dx = x - minX;
|
const dx = x - minX;
|
||||||
const dy = y - minY;
|
const dy = y - minY;
|
||||||
|
|
||||||
parsedElements.forEach(parsedElement => {
|
elements = [
|
||||||
const duplicate = duplicateElement(parsedElement);
|
...elements,
|
||||||
duplicate.x += dx;
|
...parsedElements.map(parsedElement => {
|
||||||
duplicate.y += dy;
|
const duplicate = duplicateElement(parsedElement);
|
||||||
elements.push(duplicate);
|
duplicate.x += dx;
|
||||||
});
|
duplicate.y += dy;
|
||||||
|
return duplicate;
|
||||||
|
})
|
||||||
|
];
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -25,20 +25,20 @@ export function setSelection(
|
||||||
selectionX2 >= elementX2 &&
|
selectionX2 >= elementX2 &&
|
||||||
selectionY2 >= elementY2;
|
selectionY2 >= elementY2;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearSelection(elements: ExcalidrawElement[]) {
|
export function clearSelection(elements: ExcalidrawElement[]) {
|
||||||
elements.forEach(element => {
|
elements.forEach(element => {
|
||||||
element.isSelected = false;
|
element.isSelected = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteSelectedElements(elements: ExcalidrawElement[]) {
|
export function deleteSelectedElements(elements: ExcalidrawElement[]) {
|
||||||
for (let i = elements.length - 1; i >= 0; --i) {
|
return elements.filter(el => !el.isSelected);
|
||||||
if (elements[i].isSelected) {
|
|
||||||
elements.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSelectedIndices(elements: ExcalidrawElement[]) {
|
export function getSelectedIndices(elements: ExcalidrawElement[]) {
|
||||||
|
|
|
@ -17,6 +17,8 @@ export function moveOneLeft<T>(elements: T[], indicesToMove: number[]) {
|
||||||
}
|
}
|
||||||
swap(elements, index - 1, index);
|
swap(elements, index - 1, index);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
|
export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
|
||||||
|
@ -35,6 +37,7 @@ export function moveOneRight<T>(elements: T[], indicesToMove: number[]) {
|
||||||
}
|
}
|
||||||
swap(elements, index + 1, index);
|
swap(elements, index + 1, index);
|
||||||
});
|
});
|
||||||
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's go through an example
|
// Let's go through an example
|
||||||
|
@ -112,6 +115,8 @@ export function moveAllLeft<T>(elements: T[], indicesToMove: number[]) {
|
||||||
leftMostElements.forEach((element, i) => {
|
leftMostElements.forEach((element, i) => {
|
||||||
elements[i] = element;
|
elements[i] = element;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Let's go through an example
|
// Let's go through an example
|
||||||
|
@ -190,4 +195,6 @@ export function moveAllRight<T>(elements: T[], indicesToMove: number[]) {
|
||||||
rightMostElements.forEach((element, i) => {
|
rightMostElements.forEach((element, i) => {
|
||||||
elements[elements.length - i - 1] = element;
|
elements[elements.length - i - 1] = element;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue