mirror of
https://github.com/JulianGro/overte.git
synced 2025-08-13 01:37:42 +02:00
Merge pull request #13977 from dback2/newEntityListView
New optimized entity list view
This commit is contained in:
commit
9a614d7523
3 changed files with 656 additions and 250 deletions
|
@ -15,6 +15,7 @@
|
||||||
<script type="text/javascript" src="js/jquery-2.1.4.min.js"></script>
|
<script type="text/javascript" src="js/jquery-2.1.4.min.js"></script>
|
||||||
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
<script type="text/javascript" src="js/eventBridgeLoader.js"></script>
|
||||||
<script type="text/javascript" src="js/spinButtons.js"></script>
|
<script type="text/javascript" src="js/spinButtons.js"></script>
|
||||||
|
<script type="text/javascript" src="js/listView.js"></script>
|
||||||
<script type="text/javascript" src="js/entityList.js"></script>
|
<script type="text/javascript" src="js/entityList.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body onload='loaded();'>
|
<body onload='loaded();'>
|
||||||
|
|
|
@ -15,9 +15,31 @@ const VISIBLE_GLYPH = "";
|
||||||
const TRANSPARENCY_GLYPH = "";
|
const TRANSPARENCY_GLYPH = "";
|
||||||
const BAKED_GLYPH = ""
|
const BAKED_GLYPH = ""
|
||||||
const SCRIPT_GLYPH = "k";
|
const SCRIPT_GLYPH = "k";
|
||||||
|
const BYTES_PER_MEGABYTE = 1024 * 1024;
|
||||||
|
const IMAGE_MODEL_NAME = 'default-image-model.fbx';
|
||||||
|
const COLLAPSE_EXTRA_INFO = "E";
|
||||||
|
const EXPAND_EXTRA_INFO = "D";
|
||||||
|
const FILTER_IN_VIEW_ATTRIBUTE = "pressed";
|
||||||
|
const WINDOW_NONVARIABLE_HEIGHT = 207;
|
||||||
|
const NUM_COLUMNS = 12;
|
||||||
|
const EMPTY_ENTITY_ID = "0";
|
||||||
const DELETE = 46; // Key code for the delete key.
|
const DELETE = 46; // Key code for the delete key.
|
||||||
const KEY_P = 80; // Key code for letter p used for Parenting hotkey.
|
const KEY_P = 80; // Key code for letter p used for Parenting hotkey.
|
||||||
const MAX_ITEMS = Number.MAX_VALUE; // Used to set the max length of the list of discovered entities.
|
|
||||||
|
const COLUMN_INDEX = {
|
||||||
|
TYPE: 0,
|
||||||
|
NAME: 1,
|
||||||
|
URL: 2,
|
||||||
|
LOCKED: 3,
|
||||||
|
VISIBLE: 4,
|
||||||
|
VERTICLES_COUNT: 5,
|
||||||
|
TEXTURES_COUNT: 6,
|
||||||
|
TEXTURES_SIZE: 7,
|
||||||
|
HAS_TRANSPARENT: 8,
|
||||||
|
IS_BAKED: 9,
|
||||||
|
DRAW_CALLS: 10,
|
||||||
|
HAS_SCRIPT: 11
|
||||||
|
};
|
||||||
|
|
||||||
const COMPARE_ASCENDING = function(a, b) {
|
const COMPARE_ASCENDING = function(a, b) {
|
||||||
let va = a[currentSortColumn];
|
let va = a[currentSortColumn];
|
||||||
|
@ -37,17 +59,21 @@ const COMPARE_DESCENDING = function(a, b) {
|
||||||
return COMPARE_ASCENDING(b, a);
|
return COMPARE_ASCENDING(b, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// List of all entities
|
// List of all entities
|
||||||
let entities = []
|
var entities = []
|
||||||
// List of all entities, indexed by Entity ID
|
// List of all entities, indexed by Entity ID
|
||||||
var entitiesByID = {};
|
var entitiesByID = {};
|
||||||
// The filtered and sorted list of entities
|
// The filtered and sorted list of entities passed to ListView
|
||||||
var visibleEntities = [];
|
var visibleEntities = [];
|
||||||
|
// List of all entities that are currently selected
|
||||||
var selectedEntities = [];
|
var selectedEntities = [];
|
||||||
|
|
||||||
|
var entityList = null; // The ListView
|
||||||
|
|
||||||
var currentSortColumn = 'type';
|
var currentSortColumn = 'type';
|
||||||
var currentSortOrder = ASCENDING_SORT;
|
var currentSortOrder = ASCENDING_SORT;
|
||||||
|
var isFilterInView = false;
|
||||||
|
var showExtraInfo = false;
|
||||||
|
|
||||||
const ENABLE_PROFILING = false;
|
const ENABLE_PROFILING = false;
|
||||||
var profileIndent = '';
|
var profileIndent = '';
|
||||||
|
@ -56,19 +82,25 @@ const PROFILE_NOOP = function(_name, fn, args) {
|
||||||
} ;
|
} ;
|
||||||
const PROFILE = !ENABLE_PROFILING ? PROFILE_NOOP : function(name, fn, args) {
|
const PROFILE = !ENABLE_PROFILING ? PROFILE_NOOP : function(name, fn, args) {
|
||||||
console.log("PROFILE-Web " + profileIndent + "(" + name + ") Begin");
|
console.log("PROFILE-Web " + profileIndent + "(" + name + ") Begin");
|
||||||
var previousIndent = profileIndent;
|
let previousIndent = profileIndent;
|
||||||
profileIndent += ' ';
|
profileIndent += ' ';
|
||||||
var before = Date.now();
|
let before = Date.now();
|
||||||
fn.apply(this, args);
|
fn.apply(this, args);
|
||||||
var delta = Date.now() - before;
|
let delta = Date.now() - before;
|
||||||
profileIndent = previousIndent;
|
profileIndent = previousIndent;
|
||||||
console.log("PROFILE-Web " + profileIndent + "(" + name + ") End " + delta + "ms");
|
console.log("PROFILE-Web " + profileIndent + "(" + name + ") End " + delta + "ms");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
debugPrint = function (message) {
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
function loaded() {
|
function loaded() {
|
||||||
openEventBridge(function() {
|
openEventBridge(function() {
|
||||||
elEntityTable = document.getElementById("entity-table");
|
elEntityTable = document.getElementById("entity-table");
|
||||||
elEntityTableBody = document.getElementById("entity-table-body");
|
elEntityTableBody = document.getElementById("entity-table-body");
|
||||||
|
elEntityTableScroll = document.getElementById("entity-table-scroll");
|
||||||
|
elEntityTableHeaderRow = document.querySelectorAll("#entity-table thead th");
|
||||||
elRefresh = document.getElementById("refresh");
|
elRefresh = document.getElementById("refresh");
|
||||||
elToggleLocked = document.getElementById("locked");
|
elToggleLocked = document.getElementById("locked");
|
||||||
elToggleVisible = document.getElementById("visible");
|
elToggleVisible = document.getElementById("visible");
|
||||||
|
@ -78,14 +110,12 @@ function loaded() {
|
||||||
elRadius = document.getElementById("radius");
|
elRadius = document.getElementById("radius");
|
||||||
elExport = document.getElementById("export");
|
elExport = document.getElementById("export");
|
||||||
elPal = document.getElementById("pal");
|
elPal = document.getElementById("pal");
|
||||||
elEntityTable = document.getElementById("entity-table");
|
|
||||||
elInfoToggle = document.getElementById("info-toggle");
|
elInfoToggle = document.getElementById("info-toggle");
|
||||||
elInfoToggleGlyph = elInfoToggle.firstChild;
|
elInfoToggleGlyph = elInfoToggle.firstChild;
|
||||||
elFooter = document.getElementById("footer-text");
|
elFooter = document.getElementById("footer-text");
|
||||||
elNoEntitiesMessage = document.getElementById("no-entities");
|
elNoEntitiesMessage = document.getElementById("no-entities");
|
||||||
elNoEntitiesInView = document.getElementById("no-entities-in-view");
|
elNoEntitiesInView = document.getElementById("no-entities-in-view");
|
||||||
elNoEntitiesRadius = document.getElementById("no-entities-radius");
|
elNoEntitiesRadius = document.getElementById("no-entities-radius");
|
||||||
elEntityTableScroll = document.getElementById("entity-table-scroll");
|
|
||||||
|
|
||||||
document.getElementById("entity-name").onclick = function() {
|
document.getElementById("entity-name").onclick = function() {
|
||||||
setSortColumn('name');
|
setSortColumn('name');
|
||||||
|
@ -123,14 +153,46 @@ function loaded() {
|
||||||
document.getElementById("entity-hasScript").onclick = function () {
|
document.getElementById("entity-hasScript").onclick = function () {
|
||||||
setSortColumn('hasScript');
|
setSortColumn('hasScript');
|
||||||
};
|
};
|
||||||
|
elRefresh.onclick = function() {
|
||||||
|
refreshEntities();
|
||||||
|
}
|
||||||
|
elToggleLocked.onclick = function() {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' }));
|
||||||
|
}
|
||||||
|
elToggleVisible.onclick = function() {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' }));
|
||||||
|
}
|
||||||
|
elExport.onclick = function() {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'export'}));
|
||||||
|
}
|
||||||
|
elPal.onclick = function() {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' }));
|
||||||
|
}
|
||||||
|
elDelete.onclick = function() {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
|
||||||
|
}
|
||||||
|
elFilter.onkeyup = refreshEntityList;
|
||||||
|
elFilter.onpaste = refreshEntityList;
|
||||||
|
elFilter.onchange = onFilterChange;
|
||||||
|
elFilter.onblur = refreshFooter;
|
||||||
|
elInView.onclick = toggleFilterInView;
|
||||||
|
elRadius.onchange = onRadiusChange;
|
||||||
|
elInfoToggle.onclick = toggleInfo;
|
||||||
|
|
||||||
|
elNoEntitiesInView.style.display = "none";
|
||||||
|
|
||||||
|
entityList = new ListView(elEntityTableBody, elEntityTableScroll, elEntityTableHeaderRow,
|
||||||
|
createRow, updateRow, clearRow, WINDOW_NONVARIABLE_HEIGHT);
|
||||||
|
|
||||||
function onRowClicked(clickEvent) {
|
function onRowClicked(clickEvent) {
|
||||||
let entityID = this.dataset.entityID;
|
let entityID = this.dataset.entityID;
|
||||||
let selection = [entityID];
|
let selection = [entityID];
|
||||||
|
|
||||||
if (clickEvent.ctrlKey) {
|
if (clickEvent.ctrlKey) {
|
||||||
let selectedIndex = selectedEntities.indexOf(entityID);
|
let selectedIndex = selectedEntities.indexOf(entityID);
|
||||||
if (selectedIndex >= 0) {
|
if (selectedIndex >= 0) {
|
||||||
selection = selectedEntities;
|
selection = [];
|
||||||
|
selection = selection.concat(selectedEntities);
|
||||||
selection.splice(selectedIndex, 1)
|
selection.splice(selectedIndex, 1)
|
||||||
} else {
|
} else {
|
||||||
selection = selection.concat(selectedEntities);
|
selection = selection.concat(selectedEntities);
|
||||||
|
@ -145,35 +207,26 @@ function loaded() {
|
||||||
} else if (previousItemFound === -1 && selectedEntities[0] === entity.id) {
|
} else if (previousItemFound === -1 && selectedEntities[0] === entity.id) {
|
||||||
previousItemFound = i;
|
previousItemFound = i;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
if (previousItemFound !== -1 && clickedItemFound !== -1) {
|
if (previousItemFound !== -1 && clickedItemFound !== -1) {
|
||||||
let betweenItems = [];
|
selection = [];
|
||||||
let toItem = Math.max(previousItemFound, clickedItemFound);
|
let toItem = Math.max(previousItemFound, clickedItemFound);
|
||||||
// skip first and last item in this loop, we add them to selection after the loop
|
for (let i = Math.min(previousItemFound, clickedItemFound); i <= toItem; i++) {
|
||||||
for (let i = (Math.min(previousItemFound, clickedItemFound) + 1); i < toItem; i++) {
|
selection.push(visibleEntities[i].id);
|
||||||
visibleEntities[i].el.className = 'selected';
|
|
||||||
betweenItems.push(visibleEntities[i].id);
|
|
||||||
}
|
}
|
||||||
if (previousItemFound > clickedItemFound) {
|
if (previousItemFound > clickedItemFound) {
|
||||||
// always make sure that we add the items in the right order
|
// always make sure that we add the items in the right order
|
||||||
betweenItems.reverse();
|
selection.reverse();
|
||||||
}
|
}
|
||||||
selection = selection.concat(betweenItems, selectedEntities);
|
}
|
||||||
|
} else if (!clickEvent.ctrlKey && !clickEvent.shiftKey && selectedEntities.length === 1) {
|
||||||
|
// if reselecting the same entity then deselect it
|
||||||
|
if (selectedEntities[0] === entityID) {
|
||||||
|
selection = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedEntities.forEach(function(entityID) {
|
updateSelectedEntities(selection);
|
||||||
if (selection.indexOf(entityID) === -1) {
|
|
||||||
let entity = entitiesByID[entityID];
|
|
||||||
if (entity !== undefined) {
|
|
||||||
entity.el.className = '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
selectedEntities = selection;
|
|
||||||
|
|
||||||
this.className = 'selected';
|
|
||||||
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({
|
EventBridge.emitWebEvent(JSON.stringify({
|
||||||
type: "selectionUpdate",
|
type: "selectionUpdate",
|
||||||
|
@ -192,8 +245,6 @@ function loaded() {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
const BYTES_PER_MEGABYTE = 1024 * 1024;
|
|
||||||
|
|
||||||
function decimalMegabytes(number) {
|
function decimalMegabytes(number) {
|
||||||
return number ? (number / BYTES_PER_MEGABYTE).toFixed(1) : "";
|
return number ? (number / BYTES_PER_MEGABYTE).toFixed(1) : "";
|
||||||
}
|
}
|
||||||
|
@ -207,13 +258,10 @@ function loaded() {
|
||||||
return urlParts[urlParts.length - 1];
|
return urlParts[urlParts.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the entity list with the new set of data sent from edit.js
|
function updateEntityData(entityData) {
|
||||||
function updateEntityList(entityData) {
|
entities = [];
|
||||||
const IMAGE_MODEL_NAME = 'default-image-model.fbx';
|
|
||||||
|
|
||||||
entities = []
|
|
||||||
entitiesByID = {};
|
entitiesByID = {};
|
||||||
visibleEntities = [];
|
visibleEntities.length = 0; // maintains itemData reference in ListView
|
||||||
|
|
||||||
PROFILE("map-data", function() {
|
PROFILE("map-data", function() {
|
||||||
entityData.forEach(function(entity) {
|
entityData.forEach(function(entity) {
|
||||||
|
@ -231,13 +279,15 @@ function loaded() {
|
||||||
fullUrl: entity.url,
|
fullUrl: entity.url,
|
||||||
locked: entity.locked,
|
locked: entity.locked,
|
||||||
visible: entity.visible,
|
visible: entity.visible,
|
||||||
verticesCount: entity.verticesCount,
|
verticesCount: displayIfNonZero(entity.verticesCount),
|
||||||
texturesCount: entity.texturesCount,
|
texturesCount: displayIfNonZero(entity.texturesCount),
|
||||||
texturesSize: entity.texturesSize,
|
texturesSize: decimalMegabytes(entity.texturesSize),
|
||||||
hasTransparent: entity.hasTransparent,
|
hasTransparent: entity.hasTransparent,
|
||||||
isBaked: entity.isBaked,
|
isBaked: entity.isBaked,
|
||||||
drawCalls: entity.drawCalls,
|
drawCalls: displayIfNonZero(entity.drawCalls),
|
||||||
hasScript: entity.hasScript,
|
hasScript: entity.hasScript,
|
||||||
|
elRow: null, // if this entity has a visible row element assigned to it
|
||||||
|
selected: false // if this entity is selected for edit regardless of having a visible row
|
||||||
}
|
}
|
||||||
|
|
||||||
entities.push(entityData);
|
entities.push(entityData);
|
||||||
|
@ -245,44 +295,7 @@ function loaded() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
PROFILE("create-rows", function() {
|
|
||||||
entities.forEach(function(entity) {
|
|
||||||
let row = document.createElement('tr');
|
|
||||||
row.dataset.entityID = entity.id;
|
|
||||||
row.attributes.title = entity.fullUrl;
|
|
||||||
function addColumn(cls, text) {
|
|
||||||
let col = document.createElement('td');
|
|
||||||
col.className = cls;
|
|
||||||
col.innerText = text;
|
|
||||||
row.append(col);
|
|
||||||
}
|
|
||||||
function addColumnHTML(cls, text) {
|
|
||||||
let col = document.createElement('td');
|
|
||||||
col.className = cls;
|
|
||||||
col.innerHTML = text;
|
|
||||||
row.append(col);
|
|
||||||
}
|
|
||||||
addColumn('type', entity.type);
|
|
||||||
addColumn('name', entity.name);
|
|
||||||
addColumn('url', entity.url);
|
|
||||||
addColumnHTML('locked glyph', entity.locked ? LOCKED_GLYPH : null);
|
|
||||||
addColumnHTML('visible glyph', entity.visible ? VISIBLE_GLYPH : null);
|
|
||||||
addColumn('verticesCount', displayIfNonZero(entity.verticesCount));
|
|
||||||
addColumn('texturesCount', displayIfNonZero(entity.texturesCount));
|
|
||||||
addColumn('texturesSize', decimalMegabytes(entity.texturesSize));
|
|
||||||
addColumnHTML('hasTransparent glyph', entity.hasTransparent ? TRANSPARENCY_GLYPH : null);
|
|
||||||
addColumnHTML('isBaked glyph', entity.isBaked ? BAKED_GLYPH : null);
|
|
||||||
addColumn('drawCalls', displayIfNonZero(entity.drawCalls));
|
|
||||||
addColumn('hasScript glyph', entity.hasScript ? SCRIPT_GLYPH : null);
|
|
||||||
row.addEventListener('click', onRowClicked);
|
|
||||||
row.addEventListener('dblclick', onRowDoubleClicked);
|
|
||||||
|
|
||||||
entity.el = row;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshEntityList();
|
refreshEntityList();
|
||||||
updateSelectedEntities(selectedEntities);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshEntityList() {
|
function refreshEntityList() {
|
||||||
|
@ -307,37 +320,88 @@ function loaded() {
|
||||||
});
|
});
|
||||||
|
|
||||||
PROFILE("update-dom", function() {
|
PROFILE("update-dom", function() {
|
||||||
elEntityTableBody.innerHTML = '';
|
entityList.itemData = visibleEntities;
|
||||||
for (let i = 0, len = visibleEntities.length; i < len; ++i) {
|
entityList.refresh();
|
||||||
elEntityTableBody.append(visibleEntities[i].el);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
refreshFooter();
|
||||||
|
refreshNoEntitiesMessage();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeEntities(deletedIDs) {
|
function removeEntities(deletedIDs) {
|
||||||
// Loop from the back so we can pop items off while iterating
|
// Loop from the back so we can pop items off while iterating
|
||||||
|
|
||||||
|
// delete any entities matching deletedIDs list from entities and entitiesByID lists
|
||||||
|
// if the entity had an associated row element then ensure row is unselected and clear it's entity
|
||||||
for (let j = entities.length - 1; j >= 0; --j) {
|
for (let j = entities.length - 1; j >= 0; --j) {
|
||||||
let id = entities[j];
|
let id = entities[j].id;
|
||||||
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
|
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
|
||||||
if (id === deletedIDs[i]) {
|
if (id === deletedIDs[i]) {
|
||||||
|
let elRow = entities[j].elRow;
|
||||||
|
if (elRow) {
|
||||||
|
elRow.className = '';
|
||||||
|
elRow.dataset.entityID = EMPTY_ENTITY_ID;
|
||||||
|
}
|
||||||
entities.splice(j, 1);
|
entities.splice(j, 1);
|
||||||
entitiesByID[id].el.remove();
|
|
||||||
delete entitiesByID[id];
|
delete entitiesByID[id];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
refreshEntities();
|
|
||||||
|
// delete any entities matching deletedIDs list from selectedEntities list
|
||||||
|
for (let j = selectedEntities.length - 1; j >= 0; --j) {
|
||||||
|
let id = selectedEntities[j].id;
|
||||||
|
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
|
||||||
|
if (id === deletedIDs[i]) {
|
||||||
|
selectedEntities.splice(j, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete any entities matching deletedIDs list from visibleEntities list
|
||||||
|
// if this was a row that was above our current row offset (a hidden top row in the top buffer),
|
||||||
|
// then decrease row offset accordingly
|
||||||
|
let firstVisibleRow = entityList.getFirstVisibleRowIndex();
|
||||||
|
for (let j = visibleEntities.length - 1; j >= 0; --j) {
|
||||||
|
let id = visibleEntities[j].id;
|
||||||
|
for (let i = 0, length = deletedIDs.length; i < length; ++i) {
|
||||||
|
if (id === deletedIDs[i]) {
|
||||||
|
if (j < firstVisibleRow && entityList.rowOffset > 0) {
|
||||||
|
entityList.rowOffset--;
|
||||||
|
}
|
||||||
|
visibleEntities.splice(j, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityList.refresh();
|
||||||
|
|
||||||
|
refreshFooter();
|
||||||
|
refreshNoEntitiesMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearEntities() {
|
function clearEntities() {
|
||||||
entities = []
|
// clear the associated entity ID from all visible row elements
|
||||||
|
let firstVisibleRow = entityList.getFirstVisibleRowIndex();
|
||||||
|
let lastVisibleRow = entityList.getLastVisibleRowIndex();
|
||||||
|
for (let i = firstVisibleRow; i <= lastVisibleRow && i < visibleEntities.length; i++) {
|
||||||
|
let entity = visibleEntities[i];
|
||||||
|
entity.elRow.dataset.entityID = EMPTY_ENTITY_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
entities = [];
|
||||||
entitiesByID = {};
|
entitiesByID = {};
|
||||||
visibleEntities = [];
|
visibleEntities.length = 0; // maintains itemData reference in ListView
|
||||||
elEntityTableBody.innerHTML = '';
|
|
||||||
|
entityList.resetToTop();
|
||||||
|
entityList.clear();
|
||||||
|
|
||||||
refreshFooter();
|
refreshFooter();
|
||||||
|
refreshNoEntitiesMessage();
|
||||||
}
|
}
|
||||||
|
|
||||||
var elSortOrder = {
|
var elSortOrder = {
|
||||||
|
@ -363,14 +427,15 @@ function loaded() {
|
||||||
currentSortColumn = column;
|
currentSortColumn = column;
|
||||||
currentSortOrder = ASCENDING_SORT;
|
currentSortOrder = ASCENDING_SORT;
|
||||||
}
|
}
|
||||||
elSortOrder[column].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING;
|
refreshSortOrder();
|
||||||
refreshEntityList();
|
refreshEntityList();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSortColumn('type');
|
function refreshSortOrder() {
|
||||||
|
elSortOrder[currentSortColumn].innerHTML = currentSortOrder === ASCENDING_SORT ? ASCENDING_STRING : DESCENDING_STRING;
|
||||||
|
}
|
||||||
|
|
||||||
function refreshEntities() {
|
function refreshEntities() {
|
||||||
clearEntities();
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' }));
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'refresh' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,75 +451,141 @@ function loaded() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function refreshNoEntitiesMessage() {
|
||||||
|
if (visibleEntities.length > 0) {
|
||||||
|
elNoEntitiesMessage.style.display = "none";
|
||||||
|
} else {
|
||||||
|
elNoEntitiesMessage.style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateSelectedEntities(selectedIDs) {
|
function updateSelectedEntities(selectedIDs) {
|
||||||
let notFound = false;
|
let notFound = false;
|
||||||
|
|
||||||
|
// reset all currently selected entities and their rows first
|
||||||
selectedEntities.forEach(function(id) {
|
selectedEntities.forEach(function(id) {
|
||||||
let entity = entitiesByID[id];
|
let entity = entitiesByID[id];
|
||||||
if (entity !== undefined) {
|
if (entity !== undefined) {
|
||||||
entity.el.className = '';
|
entity.selected = false;
|
||||||
|
if (entity.elRow) {
|
||||||
|
entity.elRow.className = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// then reset selected entities list with newly selected entities and set them selected
|
||||||
selectedEntities = [];
|
selectedEntities = [];
|
||||||
for (let i = 0; i < selectedIDs.length; i++) {
|
selectedIDs.forEach(function(id) {
|
||||||
let id = selectedIDs[i];
|
|
||||||
selectedEntities.push(id);
|
selectedEntities.push(id);
|
||||||
let entity = entitiesByID[id];
|
let entity = entitiesByID[id];
|
||||||
if (entity !== undefined) {
|
if (entity !== undefined) {
|
||||||
entity.el.className = 'selected';
|
entity.selected = true;
|
||||||
|
if (entity.elRow) {
|
||||||
|
entity.elRow.className = 'selected';
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
notFound = true;
|
notFound = true;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
refreshFooter();
|
refreshFooter();
|
||||||
|
|
||||||
return notFound;
|
return notFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
elRefresh.onclick = function() {
|
function isGlyphColumn(columnIndex) {
|
||||||
refreshEntities();
|
return columnIndex === COLUMN_INDEX.LOCKED || columnIndex === COLUMN_INDEX.VISIBLE ||
|
||||||
}
|
columnIndex === COLUMN_INDEX.HAS_TRANSPARENT || columnIndex === COLUMN_INDEX.IS_BAKED ||
|
||||||
elToggleLocked.onclick = function () {
|
columnIndex === COLUMN_INDEX.HAS_SCRIPT;
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleLocked' }));
|
|
||||||
}
|
|
||||||
elToggleVisible.onclick = function () {
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'toggleVisible' }));
|
|
||||||
}
|
|
||||||
elExport.onclick = function() {
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'export'}));
|
|
||||||
}
|
|
||||||
elPal.onclick = function () {
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'pal' }));
|
|
||||||
}
|
|
||||||
elDelete.onclick = function() {
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("keydown", function (keyDownEvent) {
|
function createRow() {
|
||||||
if (keyDownEvent.target.nodeName === "INPUT") {
|
let row = document.createElement("tr");
|
||||||
return;
|
for (let i = 0; i < NUM_COLUMNS; i++) {
|
||||||
|
let column = document.createElement("td");
|
||||||
|
if (isGlyphColumn(i)) {
|
||||||
|
column.className = 'glyph';
|
||||||
}
|
}
|
||||||
var keyCode = keyDownEvent.keyCode;
|
row.appendChild(column);
|
||||||
if (keyCode === DELETE) {
|
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
|
|
||||||
refreshEntities();
|
|
||||||
}
|
}
|
||||||
if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) {
|
row.onclick = onRowClicked;
|
||||||
if (keyDownEvent.shiftKey) {
|
row.ondblclick = onRowDoubleClicked;
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' }));
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRow(elRow, itemData) {
|
||||||
|
// update all column texts and glyphs to this entity's data
|
||||||
|
let typeCell = elRow.childNodes[COLUMN_INDEX.TYPE];
|
||||||
|
typeCell.innerText = itemData.type;
|
||||||
|
let nameCell = elRow.childNodes[COLUMN_INDEX.NAME];
|
||||||
|
nameCell.innerText = itemData.name;
|
||||||
|
let urlCell = elRow.childNodes[COLUMN_INDEX.URL];
|
||||||
|
urlCell.innerText = itemData.url;
|
||||||
|
let lockedCell = elRow.childNodes[COLUMN_INDEX.LOCKED];
|
||||||
|
lockedCell.innerHTML = itemData.locked ? LOCKED_GLYPH : null;
|
||||||
|
let visibleCell = elRow.childNodes[COLUMN_INDEX.VISIBLE];
|
||||||
|
visibleCell.innerHTML = itemData.visible ? VISIBLE_GLYPH : null;
|
||||||
|
let verticesCountCell = elRow.childNodes[COLUMN_INDEX.VERTICLES_COUNT];
|
||||||
|
verticesCountCell.innerText = itemData.verticesCount;
|
||||||
|
let texturesCountCell = elRow.childNodes[COLUMN_INDEX.TEXTURES_COUNT];
|
||||||
|
texturesCountCell.innerText = itemData.texturesCount;
|
||||||
|
let texturesSizeCell = elRow.childNodes[COLUMN_INDEX.TEXTURES_SIZE];
|
||||||
|
texturesSizeCell.innerText = itemData.texturesSize;
|
||||||
|
let hasTransparentCell = elRow.childNodes[COLUMN_INDEX.HAS_TRANSPARENT];
|
||||||
|
hasTransparentCell.innerHTML = itemData.hasTransparent ? TRANSPARENCY_GLYPH : null;
|
||||||
|
let isBakedCell = elRow.childNodes[COLUMN_INDEX.IS_BAKED];
|
||||||
|
isBakedCell.innerHTML = itemData.isBaked ? BAKED_GLYPH : null;
|
||||||
|
let drawCallsCell = elRow.childNodes[COLUMN_INDEX.DRAW_CALLS];
|
||||||
|
drawCallsCell.innerText = itemData.drawCalls;
|
||||||
|
let hasScriptCell = elRow.childNodes[COLUMN_INDEX.HAS_SCRIPT];
|
||||||
|
hasScriptCell.innerHTML = itemData.hasScript ? SCRIPT_GLYPH : null;
|
||||||
|
|
||||||
|
// if this entity was previously selected flag it's row as selected
|
||||||
|
if (itemData.selected) {
|
||||||
|
elRow.className = 'selected';
|
||||||
} else {
|
} else {
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' }));
|
elRow.className = '';
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
|
|
||||||
var isFilterInView = false;
|
// if this row previously had an associated entity ID that wasn't the new entity ID then clear
|
||||||
var FILTER_IN_VIEW_ATTRIBUTE = "pressed";
|
// the ID from the row and the row element from the previous entity's data, then set the new
|
||||||
elNoEntitiesInView.style.display = "none";
|
// entity ID to the row and the row element to the new entity's data
|
||||||
elInView.onclick = function () {
|
let prevEntityID = elRow.dataset.entityID;
|
||||||
|
let newEntityID = itemData.id;
|
||||||
|
let validPrevItemID = prevEntityID !== undefined && prevEntityID !== EMPTY_ENTITY_ID;
|
||||||
|
if (validPrevItemID && prevEntityID !== newEntityID && entitiesByID[prevEntityID].elRow === elRow) {
|
||||||
|
elRow.dataset.entityID = EMPTY_ENTITY_ID;
|
||||||
|
entitiesByID[prevEntityID].elRow = null;
|
||||||
|
}
|
||||||
|
if (!validPrevItemID || prevEntityID !== newEntityID) {
|
||||||
|
elRow.dataset.entityID = newEntityID;
|
||||||
|
entitiesByID[newEntityID].elRow = elRow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRow(elRow) {
|
||||||
|
// reset all texts and glyphs for each of the row's column
|
||||||
|
for (let i = 0; i < NUM_COLUMNS; i++) {
|
||||||
|
let cell = elRow.childNodes[i];
|
||||||
|
if (isGlyphColumn(i)) {
|
||||||
|
cell.innerHTML = "";
|
||||||
|
} else {
|
||||||
|
cell.innerText = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the row from any associated entity
|
||||||
|
let entityID = elRow.dataset.entityID;
|
||||||
|
if (entityID && entitiesByID[entityID]) {
|
||||||
|
entitiesByID[entityID].elRow = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset the row to hidden and clear the entity from the row
|
||||||
|
elRow.className = '';
|
||||||
|
elRow.dataset.entityID = EMPTY_ENTITY_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleFilterInView() {
|
||||||
isFilterInView = !isFilterInView;
|
isFilterInView = !isFilterInView;
|
||||||
if (isFilterInView) {
|
if (isFilterInView) {
|
||||||
elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE);
|
elInView.setAttribute(FILTER_IN_VIEW_ATTRIBUTE, FILTER_IN_VIEW_ATTRIBUTE);
|
||||||
|
@ -467,99 +598,19 @@ function loaded() {
|
||||||
refreshEntities();
|
refreshEntities();
|
||||||
}
|
}
|
||||||
|
|
||||||
elRadius.onchange = function () {
|
function onFilterChange() {
|
||||||
|
refreshEntityList();
|
||||||
|
entityList.resize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRadiusChange() {
|
||||||
elRadius.value = Math.max(elRadius.value, 0);
|
elRadius.value = Math.max(elRadius.value, 0);
|
||||||
|
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
|
||||||
|
elNoEntitiesMessage.style.display = "none";
|
||||||
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value }));
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'radius', radius: elRadius.value }));
|
||||||
refreshEntities();
|
refreshEntities();
|
||||||
elNoEntitiesRadius.firstChild.nodeValue = elRadius.value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (window.EventBridge !== undefined) {
|
|
||||||
EventBridge.scriptEventReceived.connect(function(data) {
|
|
||||||
data = JSON.parse(data);
|
|
||||||
|
|
||||||
if (data.type === "clearEntityList") {
|
|
||||||
clearEntities();
|
|
||||||
} else if (data.type == "selectionUpdate") {
|
|
||||||
var notFound = updateSelectedEntities(data.selectedIDs);
|
|
||||||
if (notFound) {
|
|
||||||
refreshEntities();
|
|
||||||
}
|
|
||||||
} else if (data.type === "update" && data.selectedIDs !== undefined) {
|
|
||||||
PROFILE("update", function() {
|
|
||||||
var newEntities = data.entities;
|
|
||||||
if (newEntities && newEntities.length === 0) {
|
|
||||||
elNoEntitiesMessage.style.display = "block";
|
|
||||||
elFooter.firstChild.nodeValue = "0 entities found";
|
|
||||||
} else if (newEntities) {
|
|
||||||
elNoEntitiesMessage.style.display = "none";
|
|
||||||
updateEntityList(newEntities);
|
|
||||||
updateSelectedEntities(data.selectedIDs);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) {
|
|
||||||
removeEntities(data.deletedIDs);
|
|
||||||
updateSelectedEntities(data.selectedIDs);
|
|
||||||
} else if (data.type === "deleted" && data.ids) {
|
|
||||||
removeEntities(data.ids);
|
|
||||||
refreshFooter();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setTimeout(refreshEntities, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize() {
|
|
||||||
// Take up available window space
|
|
||||||
elEntityTableScroll.style.height = window.innerHeight - 207;
|
|
||||||
|
|
||||||
var SCROLLABAR_WIDTH = 21;
|
|
||||||
var tds = document.querySelectorAll("#entity-table-body tr:first-child td");
|
|
||||||
var ths = document.querySelectorAll("#entity-table thead th");
|
|
||||||
if (tds.length >= ths.length) {
|
|
||||||
// Update the widths of the header cells to match the body
|
|
||||||
for (var i = 0; i < ths.length; i++) {
|
|
||||||
ths[i].width = tds[i].offsetWidth;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reasonable widths if nothing is displayed
|
|
||||||
var tableWidth = document.getElementById("entity-table").offsetWidth - SCROLLABAR_WIDTH;
|
|
||||||
if (showExtraInfo) {
|
|
||||||
ths[0].width = 0.10 * tableWidth;
|
|
||||||
ths[1].width = 0.20 * tableWidth;
|
|
||||||
ths[2].width = 0.20 * tableWidth;
|
|
||||||
ths[3].width = 0.04 * tableWidth;
|
|
||||||
ths[4].width = 0.04 * tableWidth;
|
|
||||||
ths[5].width = 0.08 * tableWidth;
|
|
||||||
ths[6].width = 0.08 * tableWidth;
|
|
||||||
ths[7].width = 0.10 * tableWidth;
|
|
||||||
ths[8].width = 0.04 * tableWidth;
|
|
||||||
ths[9].width = 0.08 * tableWidth;
|
|
||||||
ths[10].width = 0.04 * tableWidth + SCROLLABAR_WIDTH;
|
|
||||||
} else {
|
|
||||||
ths[0].width = 0.16 * tableWidth;
|
|
||||||
ths[1].width = 0.34 * tableWidth;
|
|
||||||
ths[2].width = 0.34 * tableWidth;
|
|
||||||
ths[3].width = 0.08 * tableWidth;
|
|
||||||
ths[4].width = 0.08 * tableWidth;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.onresize = resize;
|
|
||||||
|
|
||||||
elFilter.onkeyup = refreshEntityList;
|
|
||||||
elFilter.onpaste = refreshEntityList;
|
|
||||||
elFilter.onchange = function() {
|
|
||||||
refreshEntityList();
|
|
||||||
resize();
|
|
||||||
};
|
|
||||||
elFilter.onblur = refreshFooter;
|
|
||||||
|
|
||||||
|
|
||||||
var showExtraInfo = false;
|
|
||||||
var COLLAPSE_EXTRA_INFO = "E";
|
|
||||||
var EXPAND_EXTRA_INFO = "D";
|
|
||||||
|
|
||||||
function toggleInfo(event) {
|
function toggleInfo(event) {
|
||||||
showExtraInfo = !showExtraInfo;
|
showExtraInfo = !showExtraInfo;
|
||||||
if (showExtraInfo) {
|
if (showExtraInfo) {
|
||||||
|
@ -569,13 +620,60 @@ function loaded() {
|
||||||
elEntityTable.className = "";
|
elEntityTable.className = "";
|
||||||
elInfoToggleGlyph.innerHTML = EXPAND_EXTRA_INFO;
|
elInfoToggleGlyph.innerHTML = EXPAND_EXTRA_INFO;
|
||||||
}
|
}
|
||||||
resize();
|
entityList.resize();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}
|
}
|
||||||
elInfoToggle.addEventListener("click", toggleInfo, true);
|
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (keyDownEvent) {
|
||||||
|
if (keyDownEvent.target.nodeName === "INPUT") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let keyCode = keyDownEvent.keyCode;
|
||||||
|
if (keyCode === DELETE) {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'delete' }));
|
||||||
|
}
|
||||||
|
if (keyDownEvent.keyCode === KEY_P && keyDownEvent.ctrlKey) {
|
||||||
|
if (keyDownEvent.shiftKey) {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'unparent' }));
|
||||||
|
} else {
|
||||||
|
EventBridge.emitWebEvent(JSON.stringify({ type: 'parent' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, false);
|
||||||
|
|
||||||
resize();
|
if (window.EventBridge !== undefined) {
|
||||||
|
EventBridge.scriptEventReceived.connect(function(data) {
|
||||||
|
data = JSON.parse(data);
|
||||||
|
if (data.type === "clearEntityList") {
|
||||||
|
clearEntities();
|
||||||
|
} else if (data.type == "selectionUpdate") {
|
||||||
|
let notFound = updateSelectedEntities(data.selectedIDs);
|
||||||
|
if (notFound) {
|
||||||
|
refreshEntities();
|
||||||
|
}
|
||||||
|
} else if (data.type === "update" && data.selectedIDs !== undefined) {
|
||||||
|
PROFILE("update", function() {
|
||||||
|
let newEntities = data.entities;
|
||||||
|
if (newEntities) {
|
||||||
|
if (newEntities.length === 0) {
|
||||||
|
clearEntities();
|
||||||
|
} else {
|
||||||
|
updateEntityData(newEntities);
|
||||||
|
updateSelectedEntities(data.selectedIDs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (data.type === "removeEntities" && data.deletedIDs !== undefined && data.selectedIDs !== undefined) {
|
||||||
|
removeEntities(data.deletedIDs);
|
||||||
|
updateSelectedEntities(data.selectedIDs);
|
||||||
|
} else if (data.type === "deleted" && data.ids) {
|
||||||
|
removeEntities(data.ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshSortOrder();
|
||||||
|
refreshEntities();
|
||||||
});
|
});
|
||||||
|
|
||||||
augmentSpinButtons();
|
augmentSpinButtons();
|
||||||
|
|
307
scripts/system/html/js/listView.js
Normal file
307
scripts/system/html/js/listView.js
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
// listView.js
|
||||||
|
//
|
||||||
|
// Created by David Back on 27 Aug 2018
|
||||||
|
// Copyright 2018 High Fidelity, Inc.
|
||||||
|
//
|
||||||
|
// Distributed under the Apache License, Version 2.0.
|
||||||
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
||||||
|
|
||||||
|
const SCROLL_ROWS = 2; // number of rows used as scrolling buffer, each time we pass this number of rows we scroll
|
||||||
|
const FIRST_ROW_INDEX = 3; // the first elRow element's index in the child nodes of the table body
|
||||||
|
|
||||||
|
debugPrint = function (message) {
|
||||||
|
console.log(message);
|
||||||
|
};
|
||||||
|
|
||||||
|
function ListView(elTableBody, elTableScroll, elTableHeaderRow, createRowFunction,
|
||||||
|
updateRowFunction, clearRowFunction, WINDOW_NONVARIABLE_HEIGHT) {
|
||||||
|
this.elTableBody = elTableBody;
|
||||||
|
this.elTableScroll = elTableScroll;
|
||||||
|
this.elTableHeaderRow = elTableHeaderRow;
|
||||||
|
|
||||||
|
this.elTopBuffer = null;
|
||||||
|
this.elBottomBuffer = null;
|
||||||
|
|
||||||
|
this.createRowFunction = createRowFunction;
|
||||||
|
this.updateRowFunction = updateRowFunction;
|
||||||
|
this.clearRowFunction = clearRowFunction;
|
||||||
|
|
||||||
|
// the list of row elements created in the table up to max viewable height plus SCROLL_ROWS rows for scrolling buffer
|
||||||
|
this.elRows = [];
|
||||||
|
// the list of all row item data to show in the scrolling table, passed to updateRowFunction to set to each row
|
||||||
|
this.itemData = [];
|
||||||
|
// the current index within the itemData list that is set to the top most elRow element
|
||||||
|
this.rowOffset = 0;
|
||||||
|
// height of the elRow elements
|
||||||
|
this.rowHeight = 0;
|
||||||
|
// the previous elTableScroll.scrollTop value when the elRows were last shifted for scrolling
|
||||||
|
this.lastRowShiftScrollTop = 0;
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
};
|
||||||
|
|
||||||
|
ListView.prototype = {
|
||||||
|
getNumRows: function() {
|
||||||
|
return this.elRows.length;
|
||||||
|
},
|
||||||
|
|
||||||
|
getScrollHeight: function() {
|
||||||
|
return this.rowHeight * SCROLL_ROWS;
|
||||||
|
},
|
||||||
|
|
||||||
|
getFirstVisibleRowIndex: function() {
|
||||||
|
return this.rowOffset;
|
||||||
|
},
|
||||||
|
|
||||||
|
getLastVisibleRowIndex: function() {
|
||||||
|
return this.getFirstVisibleRowIndex() + entityList.getNumRows() - 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetToTop: function() {
|
||||||
|
this.rowOffset = 0;
|
||||||
|
this.lastRowShiftScrollTop = 0;
|
||||||
|
this.refreshBuffers();
|
||||||
|
this.elTableScroll.scrollTop = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
clear: function() {
|
||||||
|
for (let i = 0; i < this.getNumRows(); i++) {
|
||||||
|
let elRow = this.elTableBody.childNodes[i + FIRST_ROW_INDEX];
|
||||||
|
this.clearRowFunction(elRow);
|
||||||
|
elRow.style.display = "none"; // hide cleared rows
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onScroll: function() {
|
||||||
|
var that = this.listView;
|
||||||
|
that.scroll();
|
||||||
|
},
|
||||||
|
|
||||||
|
scroll: function() {
|
||||||
|
let scrollTop = this.elTableScroll.scrollTop;
|
||||||
|
let scrollHeight = this.getScrollHeight();
|
||||||
|
let nextRowChangeScrollTop = this.lastRowShiftScrollTop + scrollHeight;
|
||||||
|
let totalItems = this.itemData.length;
|
||||||
|
let numRows = this.getNumRows();
|
||||||
|
|
||||||
|
// if the top of the scroll area has past the amount of scroll row space since the last point of scrolling and there
|
||||||
|
// are still more rows to scroll to then trigger a scroll down by the min of the scroll row space or number of
|
||||||
|
// remaining rows below
|
||||||
|
// if the top of the scroll area has gone back above the last point of scrolling then trigger a scroll up by min of
|
||||||
|
// the scroll row space or number of rows above
|
||||||
|
if (scrollTop >= nextRowChangeScrollTop && numRows + this.rowOffset < totalItems) {
|
||||||
|
let numScrolls = Math.ceil((scrollTop - nextRowChangeScrollTop) / scrollHeight);
|
||||||
|
let numScrollRows = numScrolls * SCROLL_ROWS;
|
||||||
|
if (numScrollRows + this.rowOffset + numRows > totalItems) {
|
||||||
|
numScrollRows = totalItems - this.rowOffset - numRows;
|
||||||
|
}
|
||||||
|
this.scrollRows(numScrollRows);
|
||||||
|
} else if (scrollTop < this.lastRowShiftScrollTop) {
|
||||||
|
let numScrolls = Math.ceil((this.lastRowShiftScrollTop - scrollTop) / scrollHeight);
|
||||||
|
let numScrollRows = numScrolls * SCROLL_ROWS;
|
||||||
|
if (this.rowOffset - numScrollRows < 0) {
|
||||||
|
numScrollRows = this.rowOffset;
|
||||||
|
}
|
||||||
|
this.scrollRows(-numScrollRows);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollRows: function(numScrollRows) {
|
||||||
|
let numScrollRowsAbsolute = Math.abs(numScrollRows);
|
||||||
|
if (numScrollRowsAbsolute === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scrollDown = numScrollRows > 0;
|
||||||
|
|
||||||
|
let prevTopHeight = parseInt(this.elTopBuffer.getAttribute("height"));
|
||||||
|
let prevBottomHeight = parseInt(this.elBottomBuffer.getAttribute("height"));
|
||||||
|
|
||||||
|
// if the number of rows to scroll at once is greater than the total visible number of row elements,
|
||||||
|
// then just advance the rowOffset accordingly and allow the refresh below to update all rows
|
||||||
|
if (numScrollRowsAbsolute > this.getNumRows()) {
|
||||||
|
this.rowOffset += numScrollRows;
|
||||||
|
} else {
|
||||||
|
// for each row to scroll down, move the top row element to the bottom of the
|
||||||
|
// table before the bottom buffer and reset it's row data to the new item
|
||||||
|
// for each row to scroll up, move the bottom row element to the top of
|
||||||
|
// the table before the top row and reset it's row data to the new item
|
||||||
|
for (let i = 0; i < numScrollRowsAbsolute; i++) {
|
||||||
|
let topRow = this.elTableBody.childNodes[FIRST_ROW_INDEX];
|
||||||
|
let rowToMove = scrollDown ? topRow : this.elTableBody.childNodes[FIRST_ROW_INDEX + this.getNumRows() - 1];
|
||||||
|
let rowIndex = scrollDown ? this.getNumRows() + this.rowOffset : this.rowOffset - 1;
|
||||||
|
let moveRowBefore = scrollDown ? this.elBottomBuffer : topRow;
|
||||||
|
this.elTableBody.removeChild(rowToMove);
|
||||||
|
this.elTableBody.insertBefore(rowToMove, moveRowBefore);
|
||||||
|
this.updateRowFunction(rowToMove, this.itemData[rowIndex]);
|
||||||
|
this.rowOffset += scrollDown ? 1 : -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add/remove the row space that was scrolled away to the top buffer height and last scroll point
|
||||||
|
// add/remove the row space that was scrolled away to the bottom buffer height
|
||||||
|
let scrolledSpace = this.rowHeight * numScrollRows;
|
||||||
|
let newTopHeight = prevTopHeight + scrolledSpace;
|
||||||
|
let newBottomHeight = prevBottomHeight - scrolledSpace;
|
||||||
|
this.elTopBuffer.setAttribute("height", newTopHeight);
|
||||||
|
this.elBottomBuffer.setAttribute("height", newBottomHeight);
|
||||||
|
this.lastRowShiftScrollTop += scrolledSpace;
|
||||||
|
|
||||||
|
// if scrolling more than the total number of visible rows at once then refresh all row data
|
||||||
|
if (numScrollRowsAbsolute > this.getNumRows()) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function() {
|
||||||
|
// block refreshing before rows are initialized
|
||||||
|
let numRows = this.getNumRows();
|
||||||
|
if (numRows === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevScrollTop = this.elTableScroll.scrollTop;
|
||||||
|
|
||||||
|
// start with all row data cleared and initially set to invisible
|
||||||
|
this.clear();
|
||||||
|
|
||||||
|
// if we are at the bottom of the list adjust row offset to make sure all rows stay in view
|
||||||
|
this.refreshRowOffset();
|
||||||
|
|
||||||
|
// update all row data and set rows visible until max visible items reached
|
||||||
|
for (let i = 0; i < numRows; i++) {
|
||||||
|
let rowIndex = i + this.rowOffset;
|
||||||
|
if (rowIndex >= this.itemData.length) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let rowElementIndex = i + FIRST_ROW_INDEX;
|
||||||
|
let elRow = this.elTableBody.childNodes[rowElementIndex];
|
||||||
|
let itemData = this.itemData[rowIndex];
|
||||||
|
this.updateRowFunction(elRow, itemData);
|
||||||
|
elRow.style.display = ""; // make sure the row is visible
|
||||||
|
}
|
||||||
|
|
||||||
|
// update the top and bottom buffer heights to adjust for above changes
|
||||||
|
this.refreshBuffers();
|
||||||
|
|
||||||
|
// adjust the last row shift scroll point based on how much the current scroll point changed
|
||||||
|
let scrollTopDifference = this.elTableScroll.scrollTop - prevScrollTop;
|
||||||
|
if (scrollTopDifference !== 0) {
|
||||||
|
this.lastRowShiftScrollTop += scrollTopDifference;
|
||||||
|
if (this.lastRowShiftScrollTop < 0) {
|
||||||
|
this.lastRowShiftScrollTop = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshBuffers: function() {
|
||||||
|
// top buffer height is the number of hidden rows above the top row
|
||||||
|
let topHiddenRows = this.rowOffset;
|
||||||
|
let topBufferHeight = this.rowHeight * topHiddenRows;
|
||||||
|
this.elTopBuffer.setAttribute("height", topBufferHeight);
|
||||||
|
|
||||||
|
// bottom buffer height is the number of hidden rows below the bottom row (last scroll buffer row)
|
||||||
|
let bottomHiddenRows = this.itemData.length - this.getNumRows() - this.rowOffset;
|
||||||
|
let bottomBufferHeight = this.rowHeight * bottomHiddenRows;
|
||||||
|
if (bottomHiddenRows < 0) {
|
||||||
|
bottomBufferHeight = 0;
|
||||||
|
}
|
||||||
|
this.elBottomBuffer.setAttribute("height", bottomBufferHeight);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshRowOffset: function() {
|
||||||
|
// make sure the row offset isn't causing visible rows to pass the end of the item list and is clamped to 0
|
||||||
|
var numRows = this.getNumRows();
|
||||||
|
if (this.rowOffset + numRows > this.itemData.length) {
|
||||||
|
this.rowOffset = this.itemData.length - numRows;
|
||||||
|
}
|
||||||
|
if (this.rowOffset < 0) {
|
||||||
|
this.rowOffset = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onResize: function() {
|
||||||
|
var that = this.listView;
|
||||||
|
that.resize();
|
||||||
|
},
|
||||||
|
|
||||||
|
resize: function() {
|
||||||
|
if (!this.elTableBody || !this.elTableScroll) {
|
||||||
|
debugPrint("ListView.resize - no valid table body or table scroll element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevScrollTop = this.elTableScroll.scrollTop;
|
||||||
|
|
||||||
|
// take up available window space
|
||||||
|
this.elTableScroll.style.height = window.innerHeight - WINDOW_NONVARIABLE_HEIGHT;
|
||||||
|
let viewableHeight = parseInt(this.elTableScroll.style.height) + 1;
|
||||||
|
|
||||||
|
// remove all existing row elements and clear row list
|
||||||
|
for (let i = 0; i < this.getNumRows(); i++) {
|
||||||
|
let elRow = this.elRows[i];
|
||||||
|
this.elTableBody.removeChild(elRow);
|
||||||
|
}
|
||||||
|
this.elRows = [];
|
||||||
|
|
||||||
|
// create new row elements inserted between the top and bottom buffers up until the max viewable scroll area
|
||||||
|
let usedHeight = 0;
|
||||||
|
while (usedHeight < viewableHeight) {
|
||||||
|
let newRow = this.createRowFunction();
|
||||||
|
this.elTableBody.insertBefore(newRow, this.elBottomBuffer);
|
||||||
|
this.rowHeight = newRow.offsetHeight;
|
||||||
|
usedHeight += this.rowHeight;
|
||||||
|
this.elRows.push(newRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add SCROLL_ROWS extras rows for scrolling buffer purposes
|
||||||
|
for (let i = 0; i < SCROLL_ROWS; i++) {
|
||||||
|
let scrollRow = this.createRowFunction();
|
||||||
|
this.elTableBody.insertBefore(scrollRow, this.elBottomBuffer);
|
||||||
|
this.elRows.push(scrollRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
let ths = this.elTableHeaderRow;
|
||||||
|
let tds = this.getNumRows() > 0 ? this.elRows[0].childNodes : [];
|
||||||
|
if (!ths) {
|
||||||
|
debugPrint("ListView.resize - no valid table header row");
|
||||||
|
} else if (tds.length !== ths.length) {
|
||||||
|
debugPrint("ListView.resize - td list size " + tds.length + " does not match th list size " + ths.length);
|
||||||
|
}
|
||||||
|
// update the widths of the header cells to match the body cells (using first body row)
|
||||||
|
for (let i = 0; i < ths.length; i++) {
|
||||||
|
ths[i].width = tds[i].offsetWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore the scroll point to the same scroll point from before above changes
|
||||||
|
this.elTableScroll.scrollTop = prevScrollTop;
|
||||||
|
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function() {
|
||||||
|
if (!this.elTableBody || !this.elTableScroll) {
|
||||||
|
debugPrint("ListView.initialize - no valid table body or table scroll element");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete initial blank row
|
||||||
|
this.elTableBody.deleteRow(0);
|
||||||
|
|
||||||
|
this.elTopBuffer = document.createElement("tr");
|
||||||
|
this.elTableBody.appendChild(this.elTopBuffer);
|
||||||
|
this.elTopBuffer.setAttribute("height", 0);
|
||||||
|
|
||||||
|
this.elBottomBuffer = document.createElement("tr");
|
||||||
|
this.elTableBody.appendChild(this.elBottomBuffer);
|
||||||
|
this.elBottomBuffer.setAttribute("height", 0);
|
||||||
|
|
||||||
|
this.elTableScroll.listView = this;
|
||||||
|
this.elTableScroll.onscroll = this.onScroll;
|
||||||
|
window.listView = this;
|
||||||
|
window.onresize = this.onResize;
|
||||||
|
|
||||||
|
// initialize all row elements
|
||||||
|
this.resize();
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in a new issue