Compare commits
23 Commits
vista-r4.9
...
vista-r4.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f70ab7dd3 | ||
|
|
7f73e1e765 | ||
|
|
ea7cf080c3 | ||
|
|
9b0762f201 | ||
|
|
31aa291672 | ||
|
|
65e8d8b23c | ||
|
|
fdb35094f4 | ||
|
|
3b99a12d7f | ||
|
|
25ee2d8099 | ||
|
|
c14cc25977 | ||
|
|
50997270e9 | ||
|
|
67e3094c6c | ||
|
|
526e31d10c | ||
|
|
3b316ed491 | ||
|
|
282ead581a | ||
|
|
c4f18a4797 | ||
|
|
c273e83093 | ||
|
|
e4b9242864 | ||
|
|
ceddadcac6 | ||
|
|
6e2437b09e | ||
|
|
5a44931537 | ||
|
|
f165d9c064 | ||
|
|
613973d936 |
@@ -1,5 +1,6 @@
|
||||
import AutoCompleteField from './components/controls/AutoCompleteField.vue';
|
||||
import ClockDisplayFormatField from './components/controls/ClockDisplayFormatField.vue';
|
||||
import CheckBoxField from './components/controls/CheckBoxField.vue';
|
||||
import Datetime from './components/controls/Datetime.vue';
|
||||
import FileInput from './components/controls/FileInput.vue';
|
||||
import Locator from './components/controls/Locator.vue';
|
||||
@@ -12,6 +13,7 @@ import Vue from 'vue';
|
||||
|
||||
export const DEFAULT_CONTROLS_MAP = {
|
||||
'autocomplete': AutoCompleteField,
|
||||
'checkbox': CheckBoxField,
|
||||
'composite': ClockDisplayFormatField,
|
||||
'datetime': Datetime,
|
||||
'file-input': FileInput,
|
||||
|
||||
@@ -172,7 +172,9 @@ export default class FormsAPI {
|
||||
|
||||
function onFormSave(save) {
|
||||
return () => {
|
||||
overlay.dismiss();
|
||||
if (overlay) {
|
||||
overlay.dismiss();
|
||||
}
|
||||
|
||||
if (save) {
|
||||
save(changes);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@@ -24,49 +24,60 @@
|
||||
<div class="c-form">
|
||||
<div class="c-overlay__top-bar c-form__top-bar">
|
||||
<div class="c-overlay__dialog-title">{{ model.title }}</div>
|
||||
<div class="c-overlay__dialog-hint hint">All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||
<div
|
||||
v-if="hasRequiredFields"
|
||||
class="c-overlay__dialog-hint hint"
|
||||
>All fields marked <span class="req icon-asterisk"></span> are required.</div>
|
||||
</div>
|
||||
<form name="mctForm"
|
||||
class="c-form__contents"
|
||||
autocomplete="off"
|
||||
@submit.prevent
|
||||
<form
|
||||
name="mctForm"
|
||||
class="c-form__contents"
|
||||
autocomplete="off"
|
||||
@submit.prevent
|
||||
>
|
||||
<div v-for="section in formSections"
|
||||
:key="section.id"
|
||||
class="c-form__section"
|
||||
:class="section.cssClass"
|
||||
<div
|
||||
v-for="section in formSections"
|
||||
:key="section.id"
|
||||
class="c-form__section"
|
||||
:class="section.cssClass"
|
||||
>
|
||||
<h2 v-if="section.name"
|
||||
<h2
|
||||
v-if="section.name"
|
||||
class="c-form__section-header"
|
||||
>
|
||||
{{ section.name }}
|
||||
</h2>
|
||||
<div v-for="(row, index) in section.rows"
|
||||
:key="row.id"
|
||||
class="u-contents"
|
||||
<div
|
||||
v-for="(row, index) in section.rows"
|
||||
:key="row.id"
|
||||
class="u-contents"
|
||||
>
|
||||
<FormRow :css-class="section.cssClass"
|
||||
:first="index < 1"
|
||||
:row="row"
|
||||
@onChange="onChange"
|
||||
<FormRow
|
||||
:css-class="section.cssClass"
|
||||
:first="index < 1"
|
||||
:row="row"
|
||||
@onChange="onChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mct-form__controls c-overlay__button-bar c-form__bottom-bar">
|
||||
<button tabindex="0"
|
||||
:disabled="isInvalid"
|
||||
class="c-button c-button--major"
|
||||
@click="onSave"
|
||||
<button
|
||||
tabindex="0"
|
||||
:disabled="isInvalid"
|
||||
class="c-button c-button--major"
|
||||
@click="onSave"
|
||||
>
|
||||
OK
|
||||
{{ submitLabel }}
|
||||
</button>
|
||||
<button tabindex="0"
|
||||
class="c-button"
|
||||
@click="onDismiss"
|
||||
<button
|
||||
v-if="!hideCancel"
|
||||
tabindex="0"
|
||||
class="c-button"
|
||||
@click="onDismiss"
|
||||
>
|
||||
Cancel
|
||||
{{ cancelLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,11 +111,42 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
hasRequiredFields() {
|
||||
return this.model.sections.some(section =>
|
||||
section.rows.some(row => row.required));
|
||||
},
|
||||
isInvalid() {
|
||||
return Object.entries(this.invalidProperties)
|
||||
.some(([key, value]) => {
|
||||
return value;
|
||||
});
|
||||
},
|
||||
submitLabel() {
|
||||
if (
|
||||
this.model.buttons
|
||||
&& this.model.buttons.submit
|
||||
&& this.model.buttons.submit.label
|
||||
) {
|
||||
return this.model.buttons.submit.label;
|
||||
}
|
||||
|
||||
return 'OK';
|
||||
},
|
||||
cancelLabel() {
|
||||
if (
|
||||
this.model.buttons
|
||||
&& this.model.buttons.cancel
|
||||
&& this.model.buttons.cancel.label
|
||||
) {
|
||||
return this.model.buttons.submit.label;
|
||||
}
|
||||
|
||||
return 'Cancel';
|
||||
},
|
||||
hideCancel() {
|
||||
return this.model.buttons
|
||||
&& this.model.buttons.cancel
|
||||
&& this.model.buttons.cancel.hide === true;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -75,10 +75,12 @@ export default {
|
||||
rowClass() {
|
||||
let cssClass = this.cssClass;
|
||||
|
||||
if (this.row.required) {
|
||||
cssClass = `${cssClass} req`;
|
||||
if (!this.row.required) {
|
||||
return;
|
||||
}
|
||||
|
||||
cssClass = `${cssClass} req`;
|
||||
|
||||
if (this.visited && this.valid !== undefined) {
|
||||
if (this.valid === true) {
|
||||
cssClass = `${cssClass} valid`;
|
||||
|
||||
62
src/api/forms/components/controls/CheckBoxField.vue
Normal file
62
src/api/forms/components/controls/CheckBoxField.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
* Open MCT is licensed under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* http://www.apache.org/licenses/LICENSE-2.0.
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
* Open MCT includes source code licensed under additional open source
|
||||
* licenses. See the Open Source Licenses file (LICENSES.md) included with
|
||||
* this source code distribution or the Licensing information page available
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
<template>
|
||||
<span class="form-control shell">
|
||||
<span
|
||||
class="field control"
|
||||
:class="model.cssClass"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="isChecked"
|
||||
@input="toggleCheckBox"
|
||||
>
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
model: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isChecked: this.model.value
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
toggleCheckBox() {
|
||||
this.isChecked = !this.isChecked;
|
||||
const data = {
|
||||
model: this.model,
|
||||
value: this.isChecked
|
||||
};
|
||||
this.$emit('onChange', data);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
/*****************************************************************************
|
||||
* Open MCT, Copyright (c) 2014-2021, United States Government
|
||||
* Open MCT, Copyright (c) 2014-2022, United States Government
|
||||
* as represented by the Administrator of the National Aeronautics and Space
|
||||
* Administration. All rights reserved.
|
||||
*
|
||||
@@ -36,13 +36,14 @@ class InMemorySearchProvider {
|
||||
*/
|
||||
this.MAX_CONCURRENT_REQUESTS = 100;
|
||||
/**
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
* If max results is not specified in query, use this as default.
|
||||
*/
|
||||
this.DEFAULT_MAX_RESULTS = 100;
|
||||
|
||||
this.openmct = openmct;
|
||||
|
||||
this.indexedIds = {};
|
||||
this.indexedCompositions = {};
|
||||
this.idsToIndex = [];
|
||||
this.pendingIndex = {};
|
||||
this.pendingRequests = 0;
|
||||
@@ -58,7 +59,6 @@ class InMemorySearchProvider {
|
||||
this.onWorkerMessageError = this.onWorkerMessageError.bind(this);
|
||||
this.onerror = this.onWorkerError.bind(this);
|
||||
this.startIndexing = this.startIndexing.bind(this);
|
||||
this.onMutationOfIndexedObject = this.onMutationOfIndexedObject.bind(this);
|
||||
|
||||
this.openmct.on('start', this.startIndexing);
|
||||
this.openmct.on('destroy', () => {
|
||||
@@ -68,6 +68,9 @@ class InMemorySearchProvider {
|
||||
this.worker.port.onmessageerror = null;
|
||||
this.worker.port.close();
|
||||
}
|
||||
|
||||
this.destroyObservers(this.indexedIds);
|
||||
this.destroyObservers(this.indexedCompositions);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,7 +140,7 @@ class InMemorySearchProvider {
|
||||
};
|
||||
modelResults.hits = await Promise.all(event.data.results.map(async (hit) => {
|
||||
const identifier = this.openmct.objects.parseKeyString(hit.keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(identifier);
|
||||
|
||||
return domainObject;
|
||||
}));
|
||||
@@ -213,29 +216,52 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
onMutationOfIndexedObject(domainObject) {
|
||||
onNameMutation(domainObject, name) {
|
||||
const provider = this;
|
||||
provider.index(domainObject.identifier, domainObject);
|
||||
|
||||
domainObject.name = name;
|
||||
provider.index(domainObject);
|
||||
}
|
||||
|
||||
onCompositionMutation(domainObject, composition) {
|
||||
const provider = this;
|
||||
const indexedComposition = domainObject.composition;
|
||||
const identifiersToIndex = composition
|
||||
.filter(identifier => !indexedComposition
|
||||
.some(indexedIdentifier => this.openmct.objects
|
||||
.areIdsEqual([identifier, indexedIdentifier])));
|
||||
|
||||
identifiersToIndex.forEach(identifier => {
|
||||
this.openmct.objects.get(identifier).then(objectToIndex => provider.index(objectToIndex));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Pass an id and model to the worker to be indexed. If the model has
|
||||
* composition, schedule those ids for later indexing.
|
||||
* Pass a domainObject to the worker to be indexed.
|
||||
* If the object has composition, schedule those ids for later indexing.
|
||||
* Watch for object changes and re-index object and children if so
|
||||
*
|
||||
* @private
|
||||
* @param id a model id
|
||||
* @param model a model
|
||||
* @param domainObject a domainObject
|
||||
*/
|
||||
async index(id, domainObject) {
|
||||
async index(domainObject) {
|
||||
const provider = this;
|
||||
const keyString = this.openmct.objects.makeKeyString(id);
|
||||
const keyString = this.openmct.objects.makeKeyString(domainObject.identifier);
|
||||
|
||||
if (!this.indexedIds[keyString]) {
|
||||
this.openmct.objects.observe(domainObject, `*`, this.onMutationOfIndexedObject);
|
||||
this.indexedIds[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'name',
|
||||
this.onNameMutation.bind(this, domainObject)
|
||||
);
|
||||
this.indexedCompositions[keyString] = this.openmct.objects.observe(
|
||||
domainObject,
|
||||
'composition',
|
||||
this.onCompositionMutation.bind(this, domainObject)
|
||||
);
|
||||
}
|
||||
|
||||
this.indexedIds[keyString] = true;
|
||||
|
||||
if ((id.key !== 'ROOT')) {
|
||||
if ((keyString !== 'ROOT')) {
|
||||
if (this.worker) {
|
||||
this.worker.port.postMessage({
|
||||
request: 'index',
|
||||
@@ -247,15 +273,12 @@ class InMemorySearchProvider {
|
||||
}
|
||||
}
|
||||
|
||||
const composition = this.openmct.composition.registry.find(foundComposition => {
|
||||
return foundComposition.appliesTo(domainObject);
|
||||
});
|
||||
const composition = this.openmct.composition.get(domainObject);
|
||||
|
||||
if (composition) {
|
||||
const childIdentifiers = await composition.load(domainObject);
|
||||
childIdentifiers.forEach(function (childIdentifier) {
|
||||
provider.scheduleForIndexing(childIdentifier);
|
||||
});
|
||||
if (composition !== undefined) {
|
||||
const children = await composition.load();
|
||||
|
||||
children.forEach(child => provider.scheduleForIndexing(child.identifier));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,12 +294,12 @@ class InMemorySearchProvider {
|
||||
const provider = this;
|
||||
|
||||
this.pendingRequests += 1;
|
||||
const identifier = await this.openmct.objects.parseKeyString(keyString);
|
||||
const domainObject = await this.openmct.objects.get(identifier.key);
|
||||
const domainObject = await this.openmct.objects.get(keyString);
|
||||
delete provider.pendingIndex[keyString];
|
||||
|
||||
try {
|
||||
if (domainObject) {
|
||||
await provider.index(identifier, domainObject);
|
||||
await provider.index(domainObject);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to index domain object ' + keyString, error);
|
||||
@@ -305,9 +328,9 @@ class InMemorySearchProvider {
|
||||
}
|
||||
|
||||
/**
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
* A local version of the same SharedWorker function
|
||||
* if we don't have SharedWorkers available (e.g., iOS)
|
||||
*/
|
||||
localIndexItem(keyString, model) {
|
||||
this.localIndexedItems[keyString] = {
|
||||
type: model.type,
|
||||
@@ -347,6 +370,16 @@ class InMemorySearchProvider {
|
||||
};
|
||||
this.onWorkerMessage(eventToReturn);
|
||||
}
|
||||
|
||||
destroyObservers(observers) {
|
||||
Object.entries(observers).forEach(([keyString, unobserve]) => {
|
||||
if (typeof unobserve === 'function') {
|
||||
unobserve();
|
||||
}
|
||||
|
||||
delete observers[keyString];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default InMemorySearchProvider;
|
||||
|
||||
@@ -33,8 +33,10 @@
|
||||
|
||||
port.onmessage = function (event) {
|
||||
if (event.data.request === 'index') {
|
||||
console.log('onmessage index: ', event.data);
|
||||
indexItem(event.data.keyString, event.data.model);
|
||||
} else if (event.data.request === 'search') {
|
||||
console.log('onmessage search: ', event.data);
|
||||
port.postMessage(search(event.data));
|
||||
}
|
||||
};
|
||||
@@ -77,6 +79,8 @@
|
||||
queryId: data.queryId
|
||||
};
|
||||
|
||||
console.log('indexed on search: ', indexedItems);
|
||||
|
||||
results = Object.values(indexedItems).filter((indexedItem) => {
|
||||
return indexedItem.name.toLowerCase().includes(input);
|
||||
});
|
||||
|
||||
@@ -43,9 +43,6 @@ function ObjectAPI(typeRegistry, openmct) {
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.inMemorySearchProvider = new InMemorySearchProvider(openmct);
|
||||
this.injectIdentifierService = function () {
|
||||
this.identifierService = this.openmct.$injector.get("identifierService");
|
||||
};
|
||||
|
||||
this.rootProvider = new RootObjectProvider(this.rootRegistry);
|
||||
this.cache = {};
|
||||
@@ -66,32 +63,17 @@ ObjectAPI.prototype.supersecretSetFallbackProvider = function (p) {
|
||||
this.fallbackProvider = p;
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getIdentifierService = function () {
|
||||
// Lazily acquire identifier service
|
||||
if (!this.identifierService) {
|
||||
this.injectIdentifierService();
|
||||
}
|
||||
|
||||
return this.identifierService;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieve the provider for a given identifier.
|
||||
* @private
|
||||
*/
|
||||
ObjectAPI.prototype.getProvider = function (identifier) {
|
||||
const keyString = utils.makeKeyString(identifier);
|
||||
const identifierService = this.openmct.$injector.get("identifierService");
|
||||
const namespace = identifierService.parse(keyString).getSpace();
|
||||
|
||||
if (identifier.key === 'ROOT') {
|
||||
return this.rootProvider;
|
||||
}
|
||||
|
||||
return this.providers[namespace] || this.fallbackProvider;
|
||||
return this.providers[identifier.namespace] || this.fallbackProvider;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,6 +55,11 @@ define([
|
||||
*/
|
||||
function parseKeyString(keyString) {
|
||||
if (isIdentifier(keyString)) {
|
||||
// hack to workaround a bug mashing keyString into identifier.key
|
||||
if (!keyString.namespace && keyString.key.includes(':')) {
|
||||
return parseKeyString(keyString.key);
|
||||
}
|
||||
|
||||
return keyString;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import EventEmitter from 'EventEmitter';
|
||||
|
||||
const ERRORS = {
|
||||
TIMESYSTEM_KEY: 'All telemetry metadata must have a telemetry value with a key that matches the key of the active time system.',
|
||||
TIMESYSTEM_KEY_NOTIFICATION: 'Telemetry metadata does not match the active time system.',
|
||||
LOADED: 'Telemetry Collection has already been loaded.'
|
||||
};
|
||||
|
||||
@@ -266,6 +267,10 @@ export class TelemetryCollection extends EventEmitter {
|
||||
this.lastBounds = bounds;
|
||||
|
||||
if (isTick) {
|
||||
if (this.timeKey === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// need to check futureBuffer and need to check
|
||||
// if anything has fallen out of bounds
|
||||
let startIndex = 0;
|
||||
@@ -305,7 +310,6 @@ export class TelemetryCollection extends EventEmitter {
|
||||
if (added.length > 0) {
|
||||
this.emit('add', added);
|
||||
}
|
||||
|
||||
} else {
|
||||
// user bounds change, reset
|
||||
this._reset();
|
||||
@@ -325,12 +329,14 @@ export class TelemetryCollection extends EventEmitter {
|
||||
let domains = this.metadata.valuesForHints(['domain']);
|
||||
let domain = domains.find((d) => d.key === timeSystem.key);
|
||||
|
||||
if (domain === undefined) {
|
||||
this._error(ERRORS.TIMESYSTEM_KEY);
|
||||
if (domain !== undefined) {
|
||||
// timeKey is used to create a dummy datum used for sorting
|
||||
this.timeKey = domain.source;
|
||||
} else {
|
||||
this._warn(ERRORS.TIMESYSTEM_KEY);
|
||||
this.openmct.notifications.alert(ERRORS.TIMESYSTEM_KEY_NOTIFICATION)
|
||||
}
|
||||
|
||||
// timeKey is used to create a dummy datum used for sorting
|
||||
this.timeKey = domain.source; // this defaults to key if no source is set
|
||||
|
||||
let metadataValue = this.metadata.value(timeSystem.key) || { format: timeSystem.key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
@@ -400,4 +406,8 @@ export class TelemetryCollection extends EventEmitter {
|
||||
_error(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
_warn(message) {
|
||||
console.warn(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,10 +66,11 @@ export default class CreateAction extends PropertiesAction {
|
||||
});
|
||||
|
||||
const parentDomainObject = parentDomainObjectPath[0];
|
||||
const namespace = parentDomainObject.identifier.namespace || parentDomainObject.key || '';
|
||||
|
||||
this.domainObject.modified = Date.now();
|
||||
this.domainObject.location = this.openmct.objects.makeKeyString(parentDomainObject.identifier);
|
||||
this.domainObject.identifier.namespace = parentDomainObject.identifier.namespace;
|
||||
this.domainObject.identifier.namespace = namespace;
|
||||
|
||||
// Show saving progress dialog
|
||||
let dialog = this.openmct.overlays.progressDialog({
|
||||
@@ -107,7 +108,7 @@ export default class CreateAction extends PropertiesAction {
|
||||
}
|
||||
|
||||
const url = '#/browse/' + objectPath
|
||||
.map(object => object && this.openmct.objects.makeKeyString(object.identifier.key))
|
||||
.map(object => object && this.openmct.objects.makeKeyString(object.identifier))
|
||||
.reverse()
|
||||
.join('/');
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@ export default {
|
||||
// Have to throw away the old canvas elements and replace with new
|
||||
// canvas elements in order to get new drawing contexts.
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = this.TEMPLATE;
|
||||
div.innerHTML = this.canvasTemplate + this.canvasTemplate;
|
||||
const mainCanvas = div.querySelectorAll("canvas")[1];
|
||||
const overlayCanvas = div.querySelectorAll("canvas")[0];
|
||||
this.canvas.parentNode.replaceChild(mainCanvas, this.canvas);
|
||||
|
||||
Reference in New Issue
Block a user