Compare commits
9 Commits
imagery-ag
...
vista/limi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c756adad6f | ||
|
|
f3d593bc1e | ||
|
|
b637307de6 | ||
|
|
b6e0208e71 | ||
|
|
631876cab3 | ||
|
|
a192d46c2b | ||
|
|
6923f17645 | ||
|
|
87a45de05b | ||
|
|
ab76451360 |
@@ -27,7 +27,7 @@ define([
|
||||
) {
|
||||
function ImageryPlugin() {
|
||||
|
||||
var IMAGE_SAMPLES = [
|
||||
const IMAGE_SAMPLES = [
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18731.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18732.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18733.jpg",
|
||||
@@ -47,13 +47,14 @@ define([
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18747.jpg",
|
||||
"https://www.hq.nasa.gov/alsj/a16/AS16-117-18748.jpg"
|
||||
];
|
||||
const IMAGE_DELAY = 20000;
|
||||
|
||||
function pointForTimestamp(timestamp, name) {
|
||||
return {
|
||||
name: name,
|
||||
utc: Math.floor(timestamp / 5000) * 5000,
|
||||
local: Math.floor(timestamp / 5000) * 5000,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / 5000) % IMAGE_SAMPLES.length]
|
||||
utc: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
local: Math.floor(timestamp / IMAGE_DELAY) * IMAGE_DELAY,
|
||||
url: IMAGE_SAMPLES[Math.floor(timestamp / IMAGE_DELAY) % IMAGE_SAMPLES.length]
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,7 +65,7 @@ define([
|
||||
subscribe: function (domainObject, callback) {
|
||||
var interval = setInterval(function () {
|
||||
callback(pointForTimestamp(Date.now(), domainObject.name));
|
||||
}, 5000);
|
||||
}, IMAGE_DELAY);
|
||||
|
||||
return function () {
|
||||
clearInterval(interval);
|
||||
@@ -81,9 +82,9 @@ define([
|
||||
var start = options.start;
|
||||
var end = Math.min(options.end, Date.now());
|
||||
var data = [];
|
||||
while (start <= end && data.length < 5000) {
|
||||
while (start <= end && data.length < IMAGE_DELAY) {
|
||||
data.push(pointForTimestamp(start, domainObject.name));
|
||||
start += 5000;
|
||||
start += IMAGE_DELAY;
|
||||
}
|
||||
|
||||
return Promise.resolve(data);
|
||||
|
||||
@@ -153,10 +153,11 @@ define(["objectUtils"],
|
||||
return this.$q.when(true);
|
||||
}
|
||||
|
||||
return this.persistenceService.readObject(
|
||||
this.getSpace(),
|
||||
this.getKey()
|
||||
).then(updateModel);
|
||||
return this.openmct.objects.get(domainObject.getId()).then((newStyleObject) => {
|
||||
let oldStyleObject = this.openmct.legacyObject(newStyleObject);
|
||||
|
||||
return updateModel(oldStyleObject.getModel());
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,7 @@ define(
|
||||
key = "persistence key",
|
||||
id = "object identifier",
|
||||
model,
|
||||
refreshModel,
|
||||
SPACE = "some space",
|
||||
persistence,
|
||||
mockOpenMCT,
|
||||
@@ -60,6 +61,7 @@ define(
|
||||
someKey: "some value",
|
||||
name: "domain object"
|
||||
};
|
||||
refreshModel = {someOtherKey: "some other value"};
|
||||
|
||||
mockPersistenceService = jasmine.createSpyObj(
|
||||
"persistenceService",
|
||||
@@ -110,8 +112,16 @@ define(
|
||||
}
|
||||
});
|
||||
|
||||
mockOpenMCT = {};
|
||||
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save']);
|
||||
mockOpenMCT = {
|
||||
legacyObject: function (object) {
|
||||
return {
|
||||
getModel: function () {
|
||||
return object;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
mockOpenMCT.objects = jasmine.createSpyObj('Object API', ['save', 'get']);
|
||||
|
||||
mockIdentifierService.parse.and.returnValue(mockIdentifier);
|
||||
mockIdentifier.getSpace.and.returnValue(SPACE);
|
||||
@@ -131,6 +141,7 @@ define(
|
||||
describe("successful persistence", function () {
|
||||
beforeEach(function () {
|
||||
mockOpenMCT.objects.save.and.returnValue(Promise.resolve(true));
|
||||
mockOpenMCT.objects.get.and.returnValue(Promise.resolve(refreshModel));
|
||||
});
|
||||
it("creates unpersisted objects with the persistence service", function () {
|
||||
// Verify precondition; no call made during constructor
|
||||
@@ -146,11 +157,10 @@ define(
|
||||
});
|
||||
|
||||
it("refreshes the domain object model from persistence", function () {
|
||||
var refreshModel = {someOtherKey: "some other value"};
|
||||
model.persisted = 1;
|
||||
mockPersistenceService.readObject.and.returnValue(asPromise(refreshModel));
|
||||
persistence.refresh();
|
||||
expect(model).toEqual(refreshModel);
|
||||
persistence.refresh().then(() => {
|
||||
expect(model).toEqual(refreshModel);
|
||||
});
|
||||
});
|
||||
|
||||
it("does not trigger error notification on successful"
|
||||
|
||||
@@ -128,7 +128,14 @@ define([
|
||||
};
|
||||
|
||||
ObjectServiceProvider.prototype.get = function (key) {
|
||||
const keyString = utils.makeKeyString(key);
|
||||
let keyString = utils.makeKeyString(key);
|
||||
const space = this.getSpace(keyString);
|
||||
|
||||
let identifier = utils.parseKeyString(keyString);
|
||||
// We assign to the space for legacy persistence providers since they register themselves using a defaultSpace.
|
||||
// This is the way to make everyone happy.
|
||||
identifier.namespace = space;
|
||||
keyString = utils.makeKeyString(identifier);
|
||||
|
||||
return this.objectService.getObjects([keyString])
|
||||
.then(function (results) {
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
* at runtime from the About dialog for additional information.
|
||||
*****************************************************************************/
|
||||
|
||||
import utils from 'objectUtils';
|
||||
|
||||
export default class LegacyPersistenceAdapter {
|
||||
constructor(openmct) {
|
||||
this.openmct = openmct;
|
||||
@@ -33,8 +35,31 @@ export default class LegacyPersistenceAdapter {
|
||||
return Promise.resolve(Object.keys(this.openmct.objects.providers));
|
||||
}
|
||||
|
||||
updateObject(legacyDomainObject) {
|
||||
return this.openmct.objects.save(legacyDomainObject.useCapability('adapter'));
|
||||
createObject(space, key, legacyDomainObject) {
|
||||
let object = utils.toNewFormat(legacyDomainObject, {
|
||||
namespace: space,
|
||||
key: key
|
||||
});
|
||||
|
||||
return this.openmct.objects.save(object);
|
||||
}
|
||||
|
||||
deleteObject(space, key) {
|
||||
const identifier = {
|
||||
namespace: space,
|
||||
key: key
|
||||
};
|
||||
|
||||
return this.openmct.objects.delete(identifier);
|
||||
}
|
||||
|
||||
updateObject(space, key, legacyDomainObject) {
|
||||
let object = utils.toNewFormat(legacyDomainObject, {
|
||||
namespace: space,
|
||||
key: key
|
||||
});
|
||||
|
||||
return this.openmct.objects.save(object);
|
||||
}
|
||||
|
||||
readObject(space, key) {
|
||||
|
||||
@@ -47,6 +47,7 @@ define([
|
||||
this.providers = {};
|
||||
this.rootRegistry = new RootRegistry();
|
||||
this.rootProvider = new RootObjectProvider.default(this.rootRegistry);
|
||||
this.cache = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,6 +155,11 @@ define([
|
||||
* has been saved, or be rejected if it cannot be saved
|
||||
*/
|
||||
ObjectAPI.prototype.get = function (identifier) {
|
||||
let keystring = this.makeKeyString(identifier);
|
||||
if (this.cache[keystring] !== undefined) {
|
||||
return this.cache[keystring];
|
||||
}
|
||||
|
||||
identifier = utils.parseKeyString(identifier);
|
||||
const provider = this.getProvider(identifier);
|
||||
|
||||
@@ -165,7 +171,15 @@ define([
|
||||
throw new Error('Provider does not support get!');
|
||||
}
|
||||
|
||||
return provider.get(identifier);
|
||||
let objectPromise = provider.get(identifier);
|
||||
|
||||
this.cache[keystring] = objectPromise;
|
||||
|
||||
return objectPromise.then(result => {
|
||||
delete this.cache[keystring];
|
||||
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
ObjectAPI.prototype.delete = function () {
|
||||
|
||||
@@ -59,4 +59,25 @@ describe("The Object API", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("The get function", () => {
|
||||
describe("when a provider is available", () => {
|
||||
let mockProvider;
|
||||
beforeEach(() => {
|
||||
mockProvider = jasmine.createSpyObj("mock provider", [
|
||||
"get"
|
||||
]);
|
||||
mockProvider.get.and.returnValue(Promise.resolve(mockDomainObject));
|
||||
objectAPI.addProvider(TEST_NAMESPACE, mockProvider);
|
||||
});
|
||||
|
||||
it("Caches multiple requests for the same object", () => {
|
||||
expect(mockProvider.get.calls.count()).toBe(0);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
objectAPI.get(mockDomainObject.identifier);
|
||||
expect(mockProvider.get.calls.count()).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<template>
|
||||
<div class="c-imagery">
|
||||
<div
|
||||
tabindex="0"
|
||||
class="c-imagery"
|
||||
@keyup="arrowUpHandler"
|
||||
@keydown="arrowDownHandler"
|
||||
@mouseover="focusElement"
|
||||
>
|
||||
<div class="c-imagery__main-image-wrapper has-local-controls">
|
||||
<div class="h-local-controls h-local-controls--overlay-content c-local-controls--show-on-hover l-flex-row c-imagery__lc">
|
||||
<span class="holder flex-elem grows c-imagery__lc__sliders">
|
||||
@@ -22,98 +28,188 @@
|
||||
></a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="main-image s-image-main c-imagery__main-image has-local-controls js-imageryView-image"
|
||||
:class="{'paused unnsynced': paused(),'stale':false }"
|
||||
:style="{'background-image': getImageUrl() ? `url(${getImageUrl()})` : 'none',
|
||||
<div class="main-image s-image-main c-imagery__main-image has-local-controls"
|
||||
:class="{'paused unnsynced': isPaused,'stale':false }"
|
||||
:style="{'background-image': imageUrl ? `url(${imageUrl})` : 'none',
|
||||
'filter': `brightness(${filters.brightness}%) contrast(${filters.contrast}%)`}"
|
||||
:data-openmct-image-timestamp="getTime()"
|
||||
:data-openmct-object-keystring="keystring"
|
||||
:data-openmct-image-timestamp="time"
|
||||
:data-openmct-object-keystring="keyString"
|
||||
>
|
||||
<div class="c-local-controls c-local-controls--show-on-hover c-imagery__prev-next-buttons">
|
||||
<button class="c-nav c-nav--prev"
|
||||
title="Previous image"
|
||||
:disabled="isPrevDisabled()"
|
||||
:disabled="isPrevDisabled"
|
||||
@click="prevImage()"
|
||||
></button>
|
||||
<button class="c-nav c-nav--next"
|
||||
title="Next image"
|
||||
:disabled="isNextDisabled()"
|
||||
:disabled="isNextDisabled"
|
||||
@click="nextImage()"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="c-imagery__control-bar">
|
||||
<div class="c-imagery__timestamp">{{ getTime() }}</div>
|
||||
<div class="c-imagery__time">
|
||||
<div class="c-imagery__timestamp">{{ time }}</div>
|
||||
<div
|
||||
v-if="canTrackDuration"
|
||||
:class="{'c-imagery--new': isImageNew && !refreshCSS}"
|
||||
class="c-imagery__age icon-timer"
|
||||
>{{ formattedDuration }}</div>
|
||||
</div>
|
||||
<div class="h-local-controls flex-elem">
|
||||
<button
|
||||
class="c-button icon-pause pause-play"
|
||||
:class="{'is-paused': paused()}"
|
||||
@click="paused(!paused(), true)"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@click="paused(!isPaused, 'button')"
|
||||
></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="thumbsWrapper"
|
||||
class="c-imagery__thumbs-wrapper"
|
||||
:class="{'is-paused': paused()}"
|
||||
:class="{'is-paused': isPaused}"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
<div v-for="(imageData, index) in imageHistory"
|
||||
:key="index"
|
||||
<div v-for="(datum, index) in imageHistory"
|
||||
:key="datum.url"
|
||||
class="c-imagery__thumb c-thumb"
|
||||
:class="{selected: imageData.selected}"
|
||||
@click="setSelectedImage(imageData)"
|
||||
:class="{ selected: focusedImageIndex === index && isPaused }"
|
||||
@click="setFocusedImage(index, thumbnailClick)"
|
||||
>
|
||||
<img class="c-thumb__image"
|
||||
:src="getImageUrl(imageData)"
|
||||
:src="formatImageUrl(datum)"
|
||||
>
|
||||
<div class="c-thumb__timestamp">{{ getTime(imageData) }}</div>
|
||||
<div class="c-thumb__timestamp">{{ formatTime(datum) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
|
||||
const DEFAULT_DURATION_FORMATTER = 'duration';
|
||||
const REFRESH_CSS_MS = 500;
|
||||
const DURATION_TRACK_MS = 1000;
|
||||
const ARROW_DOWN_DELAY_CHECK_MS = 400;
|
||||
const ARROW_SCROLL_RATE_MS = 100;
|
||||
const THUMBNAIL_CLICKED = true;
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
const FIVE_MINUTES = 5 * ONE_MINUTE;
|
||||
const ONE_HOUR = ONE_MINUTE * 60;
|
||||
const EIGHT_HOURS = 8 * ONE_HOUR;
|
||||
const TWENTYFOUR_HOURS = EIGHT_HOURS * 3;
|
||||
|
||||
const ARROW_RIGHT = 39;
|
||||
const ARROW_LEFT = 37;
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
data() {
|
||||
let timeSystem = this.openmct.time.timeSystem();
|
||||
|
||||
return {
|
||||
autoScroll: true,
|
||||
durationFormatter: undefined,
|
||||
filters: {
|
||||
brightness: 100,
|
||||
contrast: 100
|
||||
},
|
||||
image: {
|
||||
selected: ''
|
||||
},
|
||||
imageFormat: '',
|
||||
imageHistory: [],
|
||||
imageUrl: '',
|
||||
thumbnailClick: THUMBNAIL_CLICKED,
|
||||
isPaused: false,
|
||||
metadata: {},
|
||||
requestCount: 0,
|
||||
timeFormat: '',
|
||||
keystring: ''
|
||||
timeSystem: timeSystem,
|
||||
timeFormatter: undefined,
|
||||
refreshCSS: false,
|
||||
keyString: undefined,
|
||||
focusedImageIndex: undefined,
|
||||
numericDuration: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
bounds() {
|
||||
return this.openmct.time.bounds();
|
||||
time() {
|
||||
return this.formatTime(this.focusedImage);
|
||||
},
|
||||
imageUrl() {
|
||||
return this.formatImageUrl(this.focusedImage);
|
||||
},
|
||||
isImageNew() {
|
||||
let cutoff = FIVE_MINUTES;
|
||||
let age = this.numericDuration;
|
||||
|
||||
return age < cutoff && !this.refreshCSS;
|
||||
},
|
||||
canTrackDuration() {
|
||||
return this.openmct.time.clock() && this.timeSystem.isUTCBased;
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
if (this.focusedImageIndex === -1 || this.focusedImageIndex === this.imageHistory.length - 1) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
isPrevDisabled() {
|
||||
let disabled = false;
|
||||
|
||||
if (this.focusedImageIndex === 0 || this.imageHistory.length < 2) {
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
focusedImage() {
|
||||
return this.imageHistory[this.focusedImageIndex];
|
||||
},
|
||||
parsedSelectedTime() {
|
||||
return this.parseTime(this.focusedImage);
|
||||
},
|
||||
formattedDuration() {
|
||||
let result = 'N/A';
|
||||
let negativeAge = -1;
|
||||
|
||||
if (this.numericDuration > TWENTYFOUR_HOURS) {
|
||||
negativeAge *= (this.numericDuration / TWENTYFOUR_HOURS);
|
||||
result = moment.duration(negativeAge, 'days').humanize(true);
|
||||
} else if (this.numericDuration > EIGHT_HOURS) {
|
||||
negativeAge *= (this.numericDuration / ONE_HOUR);
|
||||
result = moment.duration(negativeAge, 'hours').humanize(true);
|
||||
} else if (this.durationFormatter) {
|
||||
result = this.durationFormatter.format(this.numericDuration);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
focusedImageIndex() {
|
||||
this.trackDuration();
|
||||
this.resetAgeCSS();
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// set
|
||||
this.keystring = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.imageFormat = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
// initialize
|
||||
this.timeKey = this.openmct.time.timeSystem().key;
|
||||
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
|
||||
// listen
|
||||
this.openmct.time.on('bounds', this.boundsChange);
|
||||
this.openmct.time.on('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.on('clock', this.clockChange);
|
||||
|
||||
// set
|
||||
this.keyString = this.openmct.objects.makeKeyString(this.domainObject.identifier);
|
||||
this.metadata = this.openmct.telemetry.getMetadata(this.domainObject);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.imageFormatter = this.openmct.telemetry.getValueFormatter(this.metadata.valuesForHints(['image'])[0]);
|
||||
|
||||
// initialize
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
|
||||
// kickoff
|
||||
this.subscribe();
|
||||
this.requestHistory();
|
||||
@@ -127,41 +223,55 @@ export default {
|
||||
delete this.unsubscribe;
|
||||
}
|
||||
|
||||
this.stopDurationTracking();
|
||||
this.openmct.time.off('bounds', this.boundsChange);
|
||||
this.openmct.time.off('timeSystem', this.timeSystemChange);
|
||||
this.openmct.time.off('clock', this.clockChange);
|
||||
},
|
||||
methods: {
|
||||
focusElement() {
|
||||
this.$el.focus();
|
||||
},
|
||||
datumIsNotValid(datum) {
|
||||
if (this.imageHistory.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const datumTime = this.timeFormat.format(datum);
|
||||
const datumURL = this.imageFormat.format(datum);
|
||||
const lastHistoryTime = this.timeFormat.format(this.imageHistory.slice(-1)[0]);
|
||||
const lastHistoryURL = this.imageFormat.format(this.imageHistory.slice(-1)[0]);
|
||||
const datumURL = this.formatImageUrl(datum);
|
||||
const lastHistoryURL = this.formatImageUrl(this.imageHistory.slice(-1)[0]);
|
||||
|
||||
// datum is not valid if it matches the last datum in history,
|
||||
// or it is before the last datum in the history
|
||||
const datumTimeCheck = this.timeFormat.parse(datum);
|
||||
const historyTimeCheck = this.timeFormat.parse(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTime === lastHistoryTime) && (datumURL === lastHistoryURL);
|
||||
const datumTimeCheck = this.parseTime(datum);
|
||||
const historyTimeCheck = this.parseTime(this.imageHistory.slice(-1)[0]);
|
||||
const matchesLast = (datumTimeCheck === historyTimeCheck) && (datumURL === lastHistoryURL);
|
||||
const isStale = datumTimeCheck < historyTimeCheck;
|
||||
|
||||
return matchesLast || isStale;
|
||||
},
|
||||
getImageUrl(datum) {
|
||||
return datum
|
||||
? this.imageFormat.format(datum)
|
||||
: this.imageUrl;
|
||||
formatImageUrl(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.imageFormatter.format(datum);
|
||||
},
|
||||
getTime(datum) {
|
||||
let dateTimeStr = datum
|
||||
? this.timeFormat.format(datum)
|
||||
: this.time;
|
||||
formatTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
let dateTimeStr = this.timeFormatter.format(datum);
|
||||
|
||||
// Replace ISO "T" with a space to allow wrapping
|
||||
return dateTimeStr ? dateTimeStr.replace("T", " ") : "";
|
||||
return dateTimeStr.replace("T", " ");
|
||||
},
|
||||
parseTime(datum) {
|
||||
if (!datum) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.timeFormatter.parse(datum);
|
||||
},
|
||||
handleScroll() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
@@ -174,26 +284,35 @@ export default {
|
||||
|| (scrollHeight - scrollTop) > 2 * clientHeight;
|
||||
this.autoScroll = !disableScroll;
|
||||
},
|
||||
paused(state, button = false) {
|
||||
if (arguments.length > 0 && state !== this.isPaused) {
|
||||
this.unselectAllImages();
|
||||
this.isPaused = state;
|
||||
if (state === true && button) {
|
||||
// If we are pausing, select the latest image in imageHistory
|
||||
this.setSelectedImage(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
paused(state, type) {
|
||||
|
||||
if (this.nextDatum) {
|
||||
this.updateValues(this.nextDatum);
|
||||
delete this.nextDatum;
|
||||
} else {
|
||||
this.updateValues(this.imageHistory[this.imageHistory.length - 1]);
|
||||
}
|
||||
this.isPaused = state;
|
||||
|
||||
this.autoScroll = true;
|
||||
if (type === 'button') {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
|
||||
return this.isPaused;
|
||||
if (this.nextImageIndex) {
|
||||
this.setFocusedImage(this.nextImageIndex);
|
||||
delete this.nextImageIndex;
|
||||
}
|
||||
|
||||
this.autoScroll = true;
|
||||
},
|
||||
scrollToFocused() {
|
||||
const thumbsWrapper = this.$refs.thumbsWrapper;
|
||||
if (!thumbsWrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
let domThumb = thumbsWrapper.children[this.focusedImageIndex];
|
||||
|
||||
if (domThumb) {
|
||||
domThumb.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center'
|
||||
});
|
||||
}
|
||||
},
|
||||
scrollToRight() {
|
||||
if (this.isPaused || !this.$refs.thumbsWrapper || !this.autoScroll) {
|
||||
@@ -207,22 +326,17 @@ export default {
|
||||
|
||||
setTimeout(() => this.$refs.thumbsWrapper.scrollLeft = scrollWidth, 0);
|
||||
},
|
||||
setSelectedImage(image) {
|
||||
// If we are paused and the current image IS selected, unpause
|
||||
// Otherwise, set current image and pause
|
||||
if (!image) {
|
||||
setFocusedImage(index, thumbnailClick = false) {
|
||||
if (this.isPaused && !thumbnailClick) {
|
||||
this.nextImageIndex = index;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPaused && image.selected) {
|
||||
this.paused(false);
|
||||
this.unselectAllImages();
|
||||
} else {
|
||||
this.imageUrl = this.getImageUrl(image);
|
||||
this.time = this.getTime(image);
|
||||
this.focusedImageIndex = index;
|
||||
|
||||
if (thumbnailClick && !this.isPaused) {
|
||||
this.paused(true);
|
||||
this.unselectAllImages();
|
||||
image.selected = true;
|
||||
}
|
||||
},
|
||||
boundsChange(bounds, isTick) {
|
||||
@@ -230,98 +344,158 @@ export default {
|
||||
this.requestHistory();
|
||||
}
|
||||
},
|
||||
requestHistory() {
|
||||
const requestId = ++this.requestCount;
|
||||
async requestHistory() {
|
||||
let bounds = this.openmct.time.bounds();
|
||||
this.requestCount++;
|
||||
const requestId = this.requestCount;
|
||||
this.imageHistory = [];
|
||||
this.openmct.telemetry
|
||||
.request(this.domainObject, this.bounds)
|
||||
.then((values = []) => {
|
||||
if (this.requestCount === requestId) {
|
||||
// add each image to the history
|
||||
// update values for the very last image (set current image time and url)
|
||||
values.forEach((datum, index) => this.updateHistory(datum, index === values.length - 1));
|
||||
}
|
||||
let data = await this.openmct.telemetry
|
||||
.request(this.domainObject, bounds) || [];
|
||||
|
||||
if (this.requestCount === requestId) {
|
||||
data.forEach((datum, index) => {
|
||||
this.updateHistory(datum, index === data.length - 1);
|
||||
});
|
||||
}
|
||||
},
|
||||
timeSystemChange(system) {
|
||||
// reset timesystem dependent variables
|
||||
this.timeKey = system.key;
|
||||
this.timeFormat = this.openmct.telemetry.getValueFormatter(this.metadata.value(this.timeKey));
|
||||
this.timeSystem = this.openmct.time.timeSystem();
|
||||
this.timeKey = this.timeSystem.key;
|
||||
this.timeFormatter = this.getFormatter(this.timeKey);
|
||||
this.durationFormatter = this.getFormatter(this.timeSystem.durationFormat || DEFAULT_DURATION_FORMATTER);
|
||||
this.trackDuration();
|
||||
},
|
||||
clockChange(clock) {
|
||||
this.trackDuration();
|
||||
},
|
||||
subscribe() {
|
||||
this.unsubscribe = this.openmct.telemetry
|
||||
.subscribe(this.domainObject, (datum) => {
|
||||
let parsedTimestamp = this.timeFormat.parse(datum);
|
||||
let parsedTimestamp = this.parseTime(datum);
|
||||
let bounds = this.openmct.time.bounds();
|
||||
|
||||
if (parsedTimestamp >= this.bounds.start && parsedTimestamp <= this.bounds.end) {
|
||||
if (parsedTimestamp >= bounds.start && parsedTimestamp <= bounds.end) {
|
||||
this.updateHistory(datum);
|
||||
}
|
||||
});
|
||||
},
|
||||
unselectAllImages() {
|
||||
this.imageHistory.forEach(image => image.selected = false);
|
||||
},
|
||||
updateHistory(datum, updateValues = true) {
|
||||
updateHistory(datum, setFocused = true) {
|
||||
if (this.datumIsNotValid(datum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.imageHistory.push(datum);
|
||||
|
||||
if (updateValues) {
|
||||
this.updateValues(datum);
|
||||
if (setFocused) {
|
||||
this.setFocusedImage(this.imageHistory.length - 1);
|
||||
}
|
||||
},
|
||||
updateValues(datum) {
|
||||
if (this.isPaused) {
|
||||
this.nextDatum = datum;
|
||||
getFormatter(key) {
|
||||
let metadataValue = this.metadata.value(key) || { format: key };
|
||||
let valueFormatter = this.openmct.telemetry.getValueFormatter(metadataValue);
|
||||
|
||||
return valueFormatter;
|
||||
},
|
||||
trackDuration() {
|
||||
if (this.canTrackDuration) {
|
||||
this.stopDurationTracking();
|
||||
this.updateDuration();
|
||||
this.durationTracker = window.setInterval(
|
||||
this.updateDuration, DURATION_TRACK_MS
|
||||
);
|
||||
} else {
|
||||
this.stopDurationTracking();
|
||||
}
|
||||
},
|
||||
stopDurationTracking() {
|
||||
window.clearInterval(this.durationTracker);
|
||||
},
|
||||
updateDuration() {
|
||||
let currentTime = this.openmct.time.clock().currentValue();
|
||||
this.numericDuration = currentTime - this.parsedSelectedTime;
|
||||
},
|
||||
resetAgeCSS() {
|
||||
this.refreshCSS = true;
|
||||
// unable to make this work with nextTick
|
||||
setTimeout(() => {
|
||||
this.refreshCSS = false;
|
||||
}, REFRESH_CSS_MS);
|
||||
},
|
||||
nextImage() {
|
||||
if (this.isNextDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.time = this.timeFormat.format(datum);
|
||||
this.imageUrl = this.imageFormat.format(datum);
|
||||
},
|
||||
selectedImageIndex() {
|
||||
return this.imageHistory.findIndex(image => image.selected);
|
||||
},
|
||||
setSelectedByIndex(index) {
|
||||
this.setSelectedImage(this.imageHistory[index]);
|
||||
},
|
||||
nextImage() {
|
||||
let index = this.selectedImageIndex();
|
||||
this.setSelectedByIndex(++index);
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
this.setFocusedImage(++index, THUMBNAIL_CLICKED);
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.paused(false);
|
||||
}
|
||||
},
|
||||
prevImage() {
|
||||
let index = this.selectedImageIndex();
|
||||
if (index === -1) {
|
||||
this.setSelectedByIndex(this.imageHistory.length - 2);
|
||||
if (this.isPrevDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = this.focusedImageIndex;
|
||||
|
||||
if (index === this.imageHistory.length - 1) {
|
||||
this.setFocusedImage(this.imageHistory.length - 2, THUMBNAIL_CLICKED);
|
||||
} else {
|
||||
this.setSelectedByIndex(--index);
|
||||
this.setFocusedImage(--index, THUMBNAIL_CLICKED);
|
||||
}
|
||||
},
|
||||
isNextDisabled() {
|
||||
let disabled = false;
|
||||
let index = this.selectedImageIndex();
|
||||
arrowDownHandler(event) {
|
||||
let key = event.keyCode;
|
||||
|
||||
if (index === -1 || index === this.imageHistory.length - 1) {
|
||||
disabled = true;
|
||||
if (this.isLeftOrRightArrowKey(key)) {
|
||||
this.arrowDown = true;
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
this.arrowDownDelayTimeout = window.setTimeout(() => {
|
||||
this.arrowKeyScroll(this.directionByKey(key));
|
||||
}, ARROW_DOWN_DELAY_CHECK_MS);
|
||||
}
|
||||
|
||||
return disabled;
|
||||
},
|
||||
isPrevDisabled() {
|
||||
let disabled = false;
|
||||
let index = this.selectedImageIndex();
|
||||
arrowUpHandler(event) {
|
||||
let key = event.keyCode;
|
||||
|
||||
if (index === 0 || this.imageHistory.length < 2) {
|
||||
disabled = true;
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
|
||||
if (this.isLeftOrRightArrowKey(key)) {
|
||||
this.arrowDown = false;
|
||||
let direction = this.directionByKey(key);
|
||||
this[direction + 'Image']();
|
||||
}
|
||||
},
|
||||
arrowKeyScroll(direction) {
|
||||
if (this.arrowDown) {
|
||||
this.arrowKeyScrolling = true;
|
||||
this[direction + 'Image']();
|
||||
setTimeout(() => {
|
||||
this.arrowKeyScroll(direction);
|
||||
}, ARROW_SCROLL_RATE_MS);
|
||||
} else {
|
||||
window.clearTimeout(this.arrowDownDelayTimeout);
|
||||
this.arrowKeyScrolling = false;
|
||||
this.scrollToFocused();
|
||||
}
|
||||
},
|
||||
directionByKey(keyCode) {
|
||||
let direction;
|
||||
|
||||
if (keyCode === ARROW_LEFT) {
|
||||
direction = 'prev';
|
||||
}
|
||||
|
||||
return disabled;
|
||||
if (keyCode === ARROW_RIGHT) {
|
||||
direction = 'next';
|
||||
}
|
||||
|
||||
return direction;
|
||||
},
|
||||
isLeftOrRightArrowKey(keyCode) {
|
||||
return [ARROW_RIGHT, ARROW_LEFT].includes(keyCode);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-top: $interiorMargin;
|
||||
}
|
||||
@@ -25,14 +29,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__control-bar {
|
||||
padding: 5px 0 0 0;
|
||||
&__control-bar,
|
||||
&__time {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: baseline;
|
||||
|
||||
> * + * {
|
||||
margin-left: $interiorMarginSm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&__control-bar {
|
||||
margin-top: 2px;
|
||||
padding: $interiorMarginSm 0;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
&__time {
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__timestamp,
|
||||
&__age {
|
||||
@include ellipsize();
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
|
||||
&__timestamp {
|
||||
flex: 1 1 auto;
|
||||
flex-shrink: 10;
|
||||
}
|
||||
|
||||
&__age {
|
||||
border-radius: $controlCr;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: baseline;
|
||||
padding: 1px $interiorMarginSm;
|
||||
|
||||
&:before {
|
||||
opacity: 0.5;
|
||||
margin-right: $interiorMarginSm;
|
||||
}
|
||||
}
|
||||
|
||||
&--new {
|
||||
// New imagery
|
||||
$bgColor: $colorOk;
|
||||
background: rgba($bgColor, 0.5);
|
||||
@include flash($animName: flashImageAge, $dur: 250ms, $valStart: rgba($colorOk, 0.7), $valEnd: rgba($colorOk, 0));
|
||||
}
|
||||
|
||||
&__thumbs-wrapper {
|
||||
@@ -151,6 +198,8 @@
|
||||
/*************************************** BUTTONS */
|
||||
.c-button.pause-play {
|
||||
// Pause icon set by default in markup
|
||||
justify-self: end;
|
||||
|
||||
&.is-paused {
|
||||
background: $colorPausedBg !important;
|
||||
color: $colorPausedFg;
|
||||
|
||||
@@ -111,11 +111,9 @@ import Search from '@/ui/components/search.vue';
|
||||
import SearchResults from './SearchResults.vue';
|
||||
import Sidebar from './Sidebar.vue';
|
||||
import { clearDefaultNotebook, getDefaultNotebook, setDefaultNotebook, setDefaultNotebookSection, setDefaultNotebookPage } from '../utils/notebook-storage';
|
||||
import { addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import { DEFAULT_CLASS, addNotebookEntry, createNewEmbed, getNotebookEntries } from '../utils/notebook-entries';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
const DEFAULT_CLASS = 'is-notebook-default';
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject', 'snapshotContainer'],
|
||||
components: {
|
||||
@@ -197,15 +195,6 @@ export default {
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
addDefaultClass() {
|
||||
const classList = this.internalDomainObject.classList || [];
|
||||
if (classList.includes(DEFAULT_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.push(DEFAULT_CLASS);
|
||||
this.mutateObject('classList', classList);
|
||||
},
|
||||
changeSelectedSection({ sectionId, pageId }) {
|
||||
const sections = this.sections.map(s => {
|
||||
s.isSelected = false;
|
||||
@@ -442,11 +431,18 @@ export default {
|
||||
},
|
||||
async updateDefaultNotebook(notebookStorage) {
|
||||
const defaultNotebookObject = await this.getDefaultNotebookObject();
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
this.addDefaultClass();
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
if (defaultNotebookObject.identifier.key !== notebookStorage.notebookMeta.identifier.key) {
|
||||
this.removeDefaultClass(defaultNotebookObject);
|
||||
setDefaultNotebook(this.openmct, notebookStorage);
|
||||
}
|
||||
|
||||
if (this.defaultSectionId.length === 0 || this.defaultSectionId !== notebookStorage.section.id) {
|
||||
this.defaultSectionId = notebookStorage.section.id;
|
||||
}
|
||||
|
||||
if (this.defaultPageId.length === 0 || this.defaultPageId !== notebookStorage.page.id) {
|
||||
this.defaultPageId = notebookStorage.page.id;
|
||||
}
|
||||
},
|
||||
updateDefaultNotebookPage(pages, id) {
|
||||
if (!id) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import objectLink from '../../../ui/mixins/object-link';
|
||||
|
||||
export const DEFAULT_CLASS = 'is-notebook-default';
|
||||
const TIME_BOUNDS = {
|
||||
START_BOUND: 'tc.startBound',
|
||||
END_BOUND: 'tc.endBound',
|
||||
@@ -128,6 +129,7 @@ export function addNotebookEntry(openmct, domainObject, notebookStorage, embed =
|
||||
embeds
|
||||
});
|
||||
|
||||
addDefaultClass(domainObject);
|
||||
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
|
||||
|
||||
return id;
|
||||
@@ -193,5 +195,15 @@ export function deleteNotebookEntries(openmct, domainObject, selectedSection, se
|
||||
}
|
||||
|
||||
delete entries[selectedSection.id][selectedPage.id];
|
||||
|
||||
openmct.objects.mutate(domainObject, 'configuration.entries', entries);
|
||||
}
|
||||
|
||||
function addDefaultClass(domainObject) {
|
||||
const classList = domainObject.classList || [];
|
||||
if (classList.includes(DEFAULT_CLASS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
classList.push(DEFAULT_CLASS);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ export function setDefaultNotebookSection(section) {
|
||||
|
||||
notebookStorage.section = section;
|
||||
saveDefaultNotebook(notebookStorage);
|
||||
|
||||
}
|
||||
|
||||
export function setDefaultNotebookPage(page) {
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
|
||||
import CouchObjectProvider from './CouchObjectProvider';
|
||||
const NAMESPACE = '';
|
||||
const PERSISTENCE_SPACE = '';
|
||||
|
||||
export default function CouchPlugin(url) {
|
||||
return function install(openmct) {
|
||||
openmct.objects.addProvider(NAMESPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
|
||||
openmct.objects.addProvider(PERSISTENCE_SPACE, new CouchObjectProvider(openmct, url, NAMESPACE));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -50,6 +50,18 @@
|
||||
}
|
||||
|
||||
/************************** EFFECTS */
|
||||
@mixin flash($animName: flash, $dur: 500ms, $dir: alternate, $iter: 20, $prop: background, $valStart: rgba($colorOk, 1), $valEnd: rgba($colorOk, 0)) {
|
||||
@keyframes #{$animName} {
|
||||
0% { #{$prop}: $valStart; }
|
||||
100% { #{$prop}: $valEnd; }
|
||||
}
|
||||
animation-name: $animName;
|
||||
animation-duration: $dur;
|
||||
animation-direction: $dir;
|
||||
animation-iteration-count: $iter;
|
||||
animation-timing-function: ease-out;
|
||||
}
|
||||
|
||||
@mixin mixedBg() {
|
||||
$c1: nth($mixedSettingBg, 1);
|
||||
$c2: nth($mixedSettingBg, 2);
|
||||
|
||||
@@ -26,10 +26,8 @@
|
||||
overflow: hidden;
|
||||
transition: all;
|
||||
|
||||
.scrollable-children {
|
||||
.c-tree__item-h {
|
||||
width: 100%;
|
||||
}
|
||||
&__scrollable-children {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
&__item--empty {
|
||||
@@ -57,7 +55,11 @@
|
||||
|
||||
li {
|
||||
position: relative;
|
||||
&[class*="__item-h"] { display: block; }
|
||||
&[class*="__item-h"] {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
+ li {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
class="c-tree-and-search__tree c-tree"
|
||||
>
|
||||
<!-- ancestors -->
|
||||
<div v-if="!activeSearch">
|
||||
<li v-if="!activeSearch">
|
||||
<tree-item
|
||||
v-for="(ancestor, index) in ancestors"
|
||||
:key="ancestor.id"
|
||||
@@ -43,20 +43,20 @@
|
||||
:show-up="index < ancestors.length - 1"
|
||||
:show-down="false"
|
||||
:left-offset="index * 10 + 'px'"
|
||||
:emit-height="getChildHeight"
|
||||
@emittedHeight="setChildHeight"
|
||||
:should-emit-height="shouldEmitHeight"
|
||||
@emittedHeight="setItemHeight"
|
||||
@resetTree="handleReset"
|
||||
/>
|
||||
<!-- loading -->
|
||||
<li
|
||||
v-if="isLoading"
|
||||
<div
|
||||
v-if="isLoading || !itemHeightCalculated"
|
||||
:style="indicatorLeftOffset"
|
||||
class="c-tree__item c-tree-and-search__loading loading"
|
||||
>
|
||||
<span class="c-tree__item__label">Loading...</span>
|
||||
</li>
|
||||
</div>
|
||||
<!-- end loading -->
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<!-- currently viewed children -->
|
||||
<transition
|
||||
@@ -64,17 +64,17 @@
|
||||
appear
|
||||
>
|
||||
<li
|
||||
v-if="!isLoading && !searchLoading"
|
||||
v-if="!isLoading && !searchLoading && itemHeightCalculated"
|
||||
:style="childrenListStyles()"
|
||||
:class="childrenSlideClass"
|
||||
>
|
||||
<ul
|
||||
ref="scrollable"
|
||||
class="scrollable-children"
|
||||
class="c-tree__scrollable-children"
|
||||
:style="scrollableStyles()"
|
||||
@scroll="scrollItems"
|
||||
>
|
||||
<div :style="{ height: childrenHeight + 'px'}">
|
||||
<div :style="{ height: childrenHeight + 'px' }">
|
||||
<tree-item
|
||||
v-for="(treeItem, index) in visibleItems"
|
||||
:key="treeItem.id"
|
||||
@@ -83,7 +83,7 @@
|
||||
:item-offset="itemOffset"
|
||||
:item-index="index"
|
||||
:item-height="itemHeight"
|
||||
:virtual-scroll="!noScroll"
|
||||
:virtual-scroll="true"
|
||||
:show-down="activeSearch ? false : true"
|
||||
@expanded="handleExpanded"
|
||||
/>
|
||||
@@ -143,20 +143,18 @@ export default {
|
||||
ancestors: [],
|
||||
childrenSlideClass: 'down',
|
||||
availableContainerHeight: 0,
|
||||
noScroll: true,
|
||||
updatingView: false,
|
||||
itemHeightCalculated: false,
|
||||
itemHeight: 28,
|
||||
itemOffset: 0,
|
||||
childrenHeight: 0,
|
||||
scrollable: undefined,
|
||||
pageThreshold: 50,
|
||||
activeSearch: false,
|
||||
getChildHeight: false,
|
||||
settingChildrenHeight: false,
|
||||
shouldEmitHeight: false,
|
||||
isMobile: isMobile.mobileName,
|
||||
multipleRootChildren: false,
|
||||
noVisibleItems: false,
|
||||
observedAncestors: {}
|
||||
observedAncestors: {},
|
||||
mainTreeTopMargin: undefined
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -189,6 +187,21 @@ export default {
|
||||
return {
|
||||
paddingLeft: offset + 'px'
|
||||
};
|
||||
},
|
||||
ancestorsHeight() {
|
||||
if (this.activeSearch) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.itemHeight * this.ancestors.length;
|
||||
},
|
||||
pageThreshold() {
|
||||
return Math.ceil(this.availableContainerHeight / this.itemHeight) + ITEM_BUFFER;
|
||||
},
|
||||
childrenHeight() {
|
||||
let childrenCount = this.focusedItems.length || 1;
|
||||
|
||||
return (this.itemHeight * childrenCount) - this.mainTreeTopMargin; // 5px margin
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -199,7 +212,7 @@ export default {
|
||||
let jumpAndScroll = currentLocationPath
|
||||
&& hasParent
|
||||
&& !this.currentPathIsActivePath();
|
||||
let justScroll = this.currentPathIsActivePath() && !this.noScroll;
|
||||
let justScroll = this.currentPathIsActivePath();
|
||||
|
||||
if (this.searchValue) {
|
||||
this.searchValue = '';
|
||||
@@ -233,20 +246,27 @@ export default {
|
||||
this.searchDeactivated();
|
||||
}
|
||||
},
|
||||
searchResultItems() {
|
||||
this.setContainerHeight();
|
||||
},
|
||||
allTreeItems() {
|
||||
// catches an edge case race condition and when new items are added (ex. folder)
|
||||
// catches an edge case when new items are added (ex. folder)
|
||||
if (!this.isLoading) {
|
||||
this.setContainerHeight();
|
||||
}
|
||||
},
|
||||
ancestors() {
|
||||
this.observeAncestors();
|
||||
},
|
||||
availableContainerHeight() {
|
||||
this.updateVisibleItems();
|
||||
},
|
||||
focusedItems() {
|
||||
this.updateVisibleItems();
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
|
||||
// only reliable way to get final tree top margin
|
||||
this.readyStateCheck();
|
||||
|
||||
this.backwardsCompatibilityCheck();
|
||||
|
||||
let savedPath = this.getSavedNavigatedPath();
|
||||
@@ -257,13 +277,20 @@ export default {
|
||||
|
||||
if (root.identifier !== undefined) {
|
||||
let rootNode = this.buildTreeItem(root);
|
||||
let rootCompositionCollection = this.openmct.composition.get(root);
|
||||
let rootComposition = await rootCompositionCollection.load();
|
||||
|
||||
// if more than one root item, set multipleRootChildren to true and add root to ancestors
|
||||
if (root.composition && root.composition.length > 1) {
|
||||
if (rootComposition && rootComposition.length > 1) {
|
||||
this.ancestors.push(rootNode);
|
||||
if (!this.itemHeightCalculated) {
|
||||
await this.calculateItemHeight();
|
||||
}
|
||||
|
||||
this.multipleRootChildren = true;
|
||||
} else if (!savedPath && root.composition[0] !== undefined) {
|
||||
} else if (!savedPath && rootComposition[0] !== undefined) {
|
||||
// needed if saved path is not set, need to set it to the only root child
|
||||
savedPath = root.composition[0];
|
||||
savedPath = rootComposition[0].identifier;
|
||||
}
|
||||
|
||||
if (savedPath) {
|
||||
@@ -282,13 +309,32 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.getSearchResults = _.debounce(this.getSearchResults, 400);
|
||||
this.setContainerHeight = _.debounce(this.setContainerHeight, 200);
|
||||
},
|
||||
updated() {
|
||||
this.$nextTick(() => {
|
||||
this.setContainerHeight();
|
||||
});
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.handleWindowResize);
|
||||
this.stopObservingAncestors();
|
||||
document.removeEventListener('readystatechange', this.setTreeTopMargin);
|
||||
},
|
||||
methods: {
|
||||
updatevisibleItems() {
|
||||
readyStateCheck() {
|
||||
if (document.readyState !== 'complete') {
|
||||
document.addEventListener('readystatechange', this.setTreeTopMargin);
|
||||
} else {
|
||||
this.setTreeTopMargin();
|
||||
}
|
||||
},
|
||||
setTreeTopMargin() {
|
||||
if (document.readyState === 'complete') {
|
||||
this.mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
|
||||
}
|
||||
},
|
||||
updateVisibleItems() {
|
||||
if (this.updatingView) {
|
||||
return;
|
||||
}
|
||||
@@ -326,105 +372,54 @@ export default {
|
||||
this.updatingView = false;
|
||||
});
|
||||
},
|
||||
async setContainerHeight() {
|
||||
await this.$nextTick();
|
||||
setContainerHeight() {
|
||||
let mainTree = this.$refs.mainTree;
|
||||
let mainTreeHeight = mainTree && mainTree.clientHeight ? mainTree.clientHeight : 0;
|
||||
|
||||
if (mainTreeHeight !== 0) {
|
||||
this.calculateChildHeight(() => {
|
||||
let ancestorsHeight = this.calculateAncestorHeight();
|
||||
let allChildrenHeight = this.calculateChildrenHeight();
|
||||
|
||||
if (this.activeSearch) {
|
||||
ancestorsHeight = 0;
|
||||
}
|
||||
|
||||
this.availableContainerHeight = mainTreeHeight - ancestorsHeight;
|
||||
|
||||
if (allChildrenHeight > this.availableContainerHeight) {
|
||||
this.setPageThreshold();
|
||||
this.noScroll = false;
|
||||
} else {
|
||||
this.noScroll = true;
|
||||
}
|
||||
|
||||
this.updatevisibleItems();
|
||||
});
|
||||
this.availableContainerHeight = mainTreeHeight - this.ancestorsHeight;
|
||||
} else {
|
||||
window.setTimeout(this.setContainerHeight, RECHECK_DELAY);
|
||||
}
|
||||
},
|
||||
calculateFirstVisibleItem() {
|
||||
if (!this.$refs.scrollable) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scrollTop = this.$refs.scrollable.scrollTop;
|
||||
|
||||
return Math.floor(scrollTop / this.itemHeight);
|
||||
},
|
||||
calculateLastVisibleItem() {
|
||||
if (!this.$refs.scrollable) {
|
||||
return;
|
||||
}
|
||||
|
||||
let scrollBottom = this.$refs.scrollable.scrollTop + this.$refs.scrollable.offsetHeight;
|
||||
|
||||
return Math.ceil(scrollBottom / this.itemHeight);
|
||||
},
|
||||
calculateChildrenHeight() {
|
||||
let mainTreeTopMargin = this.getElementStyleValue(this.$refs.mainTree, 'marginTop');
|
||||
let childrenCount = this.focusedItems.length;
|
||||
calculateItemHeight() {
|
||||
this.shouldEmitHeight = true;
|
||||
|
||||
return (this.itemHeight * childrenCount) - mainTreeTopMargin; // 5px margin
|
||||
return new Promise((resolve, reject) => {
|
||||
this.itemHeightResolve = resolve;
|
||||
});
|
||||
},
|
||||
setChildrenHeight() {
|
||||
this.childrenHeight = this.calculateChildrenHeight();
|
||||
},
|
||||
calculateAncestorHeight() {
|
||||
let ancestorCount = this.ancestors.length;
|
||||
async setItemHeight(height) {
|
||||
|
||||
return this.itemHeight * ancestorCount;
|
||||
},
|
||||
calculateChildHeight(callback) {
|
||||
if (callback) {
|
||||
this.afterChildHeight = callback;
|
||||
}
|
||||
|
||||
if (!this.activeSearch) {
|
||||
this.getChildHeight = true;
|
||||
} else if (this.afterChildHeight) {
|
||||
// keep the height from before
|
||||
this.afterChildHeight();
|
||||
delete this.afterChildHeight;
|
||||
}
|
||||
},
|
||||
async setChildHeight(item) {
|
||||
if (!this.getChildHeight || this.settingChildrenHeight) {
|
||||
if (this.itemHeightCalculated) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.settingChildrenHeight = true;
|
||||
if (this.isMobile) {
|
||||
item = item.children[0];
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
let topMargin = this.getElementStyleValue(item, 'marginTop');
|
||||
let bottomMargin = this.getElementStyleValue(item, 'marginBottom');
|
||||
let totalVerticalMargin = topMargin + bottomMargin;
|
||||
|
||||
this.itemHeight = item.clientHeight + totalVerticalMargin;
|
||||
this.setChildrenHeight();
|
||||
if (this.afterChildHeight) {
|
||||
this.afterChildHeight();
|
||||
delete this.afterChildHeight;
|
||||
}
|
||||
this.itemHeight = height;
|
||||
this.itemHeightCalculated = true;
|
||||
this.shouldEmitHeight = false;
|
||||
|
||||
this.getChildHeight = false;
|
||||
this.settingChildrenHeight = false;
|
||||
},
|
||||
setPageThreshold() {
|
||||
let threshold = Math.ceil(this.availableContainerHeight / this.itemHeight) + ITEM_BUFFER;
|
||||
// all items haven't loaded yet (nextTick not working for this)
|
||||
if (threshold === ITEM_BUFFER) {
|
||||
window.setTimeout(this.setPageThreshold, RECHECK_DELAY);
|
||||
} else {
|
||||
this.pageThreshold = threshold;
|
||||
}
|
||||
this.itemHeightResolve();
|
||||
},
|
||||
handleWindowResize() {
|
||||
if (!windowResizing) {
|
||||
@@ -544,6 +539,9 @@ export default {
|
||||
let currentNode = await this.openmct.objects.get(nodes[i]);
|
||||
let newParent = this.buildTreeItem(currentNode);
|
||||
this.ancestors.push(newParent);
|
||||
if (!this.itemHeightCalculated) {
|
||||
await this.calculateItemHeight();
|
||||
}
|
||||
|
||||
if (i === nodes.length - 1) {
|
||||
this.jumpPath = '';
|
||||
@@ -570,14 +568,8 @@ export default {
|
||||
let scrollTopAmount = indexOfScroll * this.itemHeight;
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
this.$refs.scrollable.scrollTop = scrollTopAmount;
|
||||
// race condition check
|
||||
if (scrollTopAmount > 0 && this.$refs.scrollable.scrollTop === 0) {
|
||||
window.setTimeout(this.autoScroll, RECHECK_DELAY);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.scrollTo = undefined;
|
||||
} else {
|
||||
window.setTimeout(this.autoScroll, RECHECK_DELAY);
|
||||
@@ -635,7 +627,6 @@ export default {
|
||||
this.activeSearch = false;
|
||||
await this.$nextTick();
|
||||
this.$refs.scrollable.scrollTop = 0;
|
||||
this.setContainerHeight();
|
||||
},
|
||||
handleReset(node) {
|
||||
this.childrenSlideClass = 'up';
|
||||
@@ -688,17 +679,16 @@ export default {
|
||||
},
|
||||
scrollItems(event) {
|
||||
if (!windowResizing) {
|
||||
this.updatevisibleItems();
|
||||
this.updateVisibleItems();
|
||||
}
|
||||
},
|
||||
childrenListStyles() {
|
||||
return { position: 'relative' };
|
||||
},
|
||||
scrollableStyles() {
|
||||
return {
|
||||
height: this.availableContainerHeight + 'px',
|
||||
overflow: this.noScroll ? 'hidden' : 'scroll'
|
||||
};
|
||||
let height = this.availableContainerHeight + 'px';
|
||||
|
||||
return { height };
|
||||
},
|
||||
getElementStyleValue(el, style) {
|
||||
let styleString = window.getComputedStyle(el)[style];
|
||||
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
emitHeight: {
|
||||
shouldEmitHeight: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
@@ -116,16 +116,14 @@ export default {
|
||||
watch: {
|
||||
expanded() {
|
||||
this.$emit('expanded', this.domainObject);
|
||||
},
|
||||
emitHeight() {
|
||||
this.$nextTick(() => {
|
||||
this.$emit('emittedHeight', this.$refs.me);
|
||||
});
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
let objectComposition = this.openmct.composition.get(this.node.object);
|
||||
|
||||
// only reliable way to get final item height
|
||||
this.readyStateCheck();
|
||||
|
||||
this.domainObject = this.node.object;
|
||||
let removeListener = this.openmct.objects.observe(this.domainObject, '*', (newObject) => {
|
||||
this.domainObject = newObject;
|
||||
@@ -137,14 +135,24 @@ export default {
|
||||
}
|
||||
|
||||
this.openmct.router.on('change:path', this.highlightIfNavigated);
|
||||
if (this.emitHeight) {
|
||||
this.$emit('emittedHeight', this.$refs.me);
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.openmct.router.off('change:path', this.highlightIfNavigated);
|
||||
document.removeEventListener('readystatechange', this.emitHeight);
|
||||
},
|
||||
methods: {
|
||||
readyStateCheck() {
|
||||
if (document.readyState !== 'complete') {
|
||||
document.addEventListener('readystatechange', this.emitHeight);
|
||||
} else {
|
||||
this.emitHeight();
|
||||
}
|
||||
},
|
||||
emitHeight() {
|
||||
if (this.shouldEmitHeight && document.readyState === 'complete') {
|
||||
this.$emit('emittedHeight', this.$el.offsetHeight);
|
||||
}
|
||||
},
|
||||
buildPathString(parentPath) {
|
||||
return [parentPath, this.openmct.objects.makeKeyString(this.node.object.identifier)].join('/');
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user