mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2024-11-25 06:56:38 +00:00
Remove old Add/Edit Table Entity code (#755)
This commit is contained in:
parent
5606ef3266
commit
a91ea6c1e4
@ -119,14 +119,10 @@ src/Explorer/Panes/ContextualPaneBase.ts
|
||||
src/Explorer/Panes/DeleteDatabaseConfirmationPane.test.ts
|
||||
src/Explorer/Panes/DeleteDatabaseConfirmationPane.ts
|
||||
src/Explorer/Panes/GraphStylingPane.ts
|
||||
# src/Explorer/Panes/NewVertexPane.ts
|
||||
src/Explorer/Panes/PaneComponents.ts
|
||||
src/Explorer/Panes/RenewAdHocAccessPane.ts
|
||||
src/Explorer/Panes/SetupNotebooksPane.ts
|
||||
src/Explorer/Panes/SwitchDirectoryPane.ts
|
||||
src/Explorer/Panes/Tables/EditTableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/EntityPropertyViewModel.ts
|
||||
src/Explorer/Panes/Tables/TableEntityPane.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyNameValidator.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValidationCommon.ts
|
||||
src/Explorer/Panes/Tables/Validators/EntityPropertyValueValidator.ts
|
||||
|
@ -22,6 +22,4 @@ ko.components.register("throughput-input-autopilot-v3", ThroughputInputComponent
|
||||
ko.components.register("add-database-pane", new PaneComponents.AddDatabasePaneComponent());
|
||||
ko.components.register("add-collection-pane", new PaneComponents.AddCollectionPaneComponent());
|
||||
ko.components.register("graph-styling-pane", new PaneComponents.GraphStylingPaneComponent());
|
||||
ko.components.register("table-add-entity-pane", new PaneComponents.TableAddEntityPaneComponent());
|
||||
ko.components.register("table-edit-entity-pane", new PaneComponents.TableEditEntityPaneComponent());
|
||||
ko.components.register("cassandra-add-collection-pane", new PaneComponents.CassandraAddCollectionPaneComponent());
|
||||
|
@ -2,8 +2,6 @@ import AddCollectionPaneTemplate from "./AddCollectionPane.html";
|
||||
import AddDatabasePaneTemplate from "./AddDatabasePane.html";
|
||||
import CassandraAddCollectionPaneTemplate from "./CassandraAddCollectionPane.html";
|
||||
import GraphStylingPaneTemplate from "./GraphStylingPane.html";
|
||||
import TableAddEntityPaneTemplate from "./Tables/TableAddEntityPane.html";
|
||||
import TableEditEntityPaneTemplate from "./Tables/TableEditEntityPane.html";
|
||||
|
||||
export class PaneComponent {
|
||||
constructor(data: any) {
|
||||
@ -38,23 +36,6 @@ export class GraphStylingPaneComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export class TableAddEntityPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: PaneComponent,
|
||||
template: TableAddEntityPaneTemplate,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class TableEditEntityPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
viewModel: PaneComponent,
|
||||
template: TableEditEntityPaneTemplate,
|
||||
};
|
||||
}
|
||||
}
|
||||
export class CassandraAddCollectionPaneComponent {
|
||||
constructor() {
|
||||
return {
|
||||
|
@ -1,225 +0,0 @@
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import Explorer from "../../Explorer";
|
||||
import * as TableConstants from "../../Tables/Constants";
|
||||
import * as Entities from "../../Tables/Entities";
|
||||
import { CassandraAPIDataClient, CassandraTableKey } from "../../Tables/TableDataClient";
|
||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||
import * as Utilities from "../../Tables/Utilities";
|
||||
import EntityPropertyViewModel from "./EntityPropertyViewModel";
|
||||
import TableEntityPane from "./TableEntityPane";
|
||||
|
||||
export default class EditTableEntityPane extends TableEntityPane {
|
||||
container: Explorer;
|
||||
visible: ko.Observable<boolean>;
|
||||
|
||||
public originEntity: Entities.ITableEntity;
|
||||
public originalNumberOfProperties: number;
|
||||
private originalDocument: any;
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
this.submitButtonText("Update Entity");
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.submitButtonText("Update Row");
|
||||
}
|
||||
this.scrollId = ko.observable<string>("editEntityScroll");
|
||||
}
|
||||
|
||||
public submit() {
|
||||
if (!this.canApply()) {
|
||||
return;
|
||||
}
|
||||
let entity: Entities.ITableEntity = this.updateEntity(this.displayedAttributes());
|
||||
this.container.tableDataClient
|
||||
.updateDocument(this.tableViewModel.queryTablesTab.collection, this.originalDocument, entity)
|
||||
.then((newEntity: Entities.ITableEntity) => {
|
||||
var numberOfProperties = 0;
|
||||
for (var property in newEntity) {
|
||||
if (
|
||||
property !== TableEntityProcessor.keyProperties.attachments &&
|
||||
property !== TableEntityProcessor.keyProperties.etag &&
|
||||
property !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
property !== TableEntityProcessor.keyProperties.self &&
|
||||
(userContext.apiType !== "Cassandra" || property !== TableConstants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
numberOfProperties++;
|
||||
}
|
||||
}
|
||||
|
||||
var propertiesDelta = numberOfProperties - this.originalNumberOfProperties;
|
||||
|
||||
return this.tableViewModel
|
||||
.updateCachedEntity(newEntity)
|
||||
.then(() => {
|
||||
if (!this.tryInsertNewHeaders(this.tableViewModel, newEntity)) {
|
||||
this.tableViewModel.redrawTableThrottled();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
// Selecting updated entity
|
||||
this.tableViewModel.selected.removeAll();
|
||||
this.tableViewModel.selected.push(newEntity);
|
||||
});
|
||||
});
|
||||
this.close();
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.displayedAttributes(this.constructDisplayedAttributes(this.originEntity));
|
||||
if (userContext.apiType === "Tables") {
|
||||
this.originalDocument = TableEntityProcessor.convertEntitiesToDocuments(
|
||||
[<Entities.ITableEntityForTablesAPI>this.originEntity],
|
||||
this.tableViewModel.queryTablesTab.collection
|
||||
)[0]; // TODO change for Cassandra
|
||||
this.originalDocument.id = ko.observable<string>(this.originalDocument.id);
|
||||
} else {
|
||||
this.originalDocument = this.originEntity;
|
||||
}
|
||||
this.updateIsActionEnabled();
|
||||
super.open();
|
||||
}
|
||||
|
||||
private constructDisplayedAttributes(entity: Entities.ITableEntity): EntityPropertyViewModel[] {
|
||||
var displayedAttributes: EntityPropertyViewModel[] = [];
|
||||
const keys = Object.keys(entity);
|
||||
keys &&
|
||||
keys.forEach((key: string) => {
|
||||
if (
|
||||
key !== TableEntityProcessor.keyProperties.attachments &&
|
||||
key !== TableEntityProcessor.keyProperties.etag &&
|
||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
key !== TableEntityProcessor.keyProperties.self &&
|
||||
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
const cassandraKeys = this.tableViewModel.queryTablesTab.collection.cassandraKeys.partitionKeys
|
||||
.concat(this.tableViewModel.queryTablesTab.collection.cassandraKeys.clusteringKeys)
|
||||
.map((key) => key.property);
|
||||
var entityAttribute: Entities.ITableEntityAttribute = entity[key];
|
||||
var entityAttributeType: string = entityAttribute.$;
|
||||
var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType);
|
||||
var removable: boolean = false;
|
||||
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
|
||||
const nonEditableType: boolean =
|
||||
entityAttributeType === TableConstants.CassandraType.Blob ||
|
||||
entityAttributeType === TableConstants.CassandraType.Inet;
|
||||
|
||||
displayedAttributes.push(
|
||||
new EntityPropertyViewModel(
|
||||
this,
|
||||
key,
|
||||
entityAttributeType,
|
||||
displayValue,
|
||||
/* namePlaceholder */ undefined,
|
||||
/* valuePlaceholder */ undefined,
|
||||
false,
|
||||
/* default valid name */ true,
|
||||
/* default valid value */ true,
|
||||
/* isRequired */ false,
|
||||
removable,
|
||||
/*value editable*/ !_.contains<string>(cassandraKeys, key) && !nonEditableType
|
||||
)
|
||||
);
|
||||
} else {
|
||||
var entityAttribute: Entities.ITableEntityAttribute = entity[key];
|
||||
var entityAttributeType: string = entityAttribute.$;
|
||||
var displayValue: any = this.getPropertyDisplayValue(entity, key, entityAttributeType);
|
||||
var editable: boolean = this.isAttributeEditable(key, entityAttributeType);
|
||||
// As per VSO:189935, Binary properties are read-only, we still want to be able to remove them.
|
||||
var removable: boolean = editable || entityAttributeType === TableConstants.TableType.Binary;
|
||||
|
||||
displayedAttributes.push(
|
||||
new EntityPropertyViewModel(
|
||||
this,
|
||||
key,
|
||||
entityAttributeType,
|
||||
displayValue,
|
||||
/* namePlaceholder */ undefined,
|
||||
/* valuePlaceholder */ undefined,
|
||||
editable,
|
||||
/* default valid name */ true,
|
||||
/* default valid value */ true,
|
||||
/* isRequired */ false,
|
||||
removable
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
(<CassandraAPIDataClient>this.container.tableDataClient)
|
||||
.getTableSchema(this.tableViewModel.queryTablesTab.collection)
|
||||
.then((properties: CassandraTableKey[]) => {
|
||||
properties &&
|
||||
properties.forEach((property) => {
|
||||
if (!_.contains(keys, property.property)) {
|
||||
this.insertAttribute(property.property, property.type);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return displayedAttributes;
|
||||
}
|
||||
|
||||
private updateEntity(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity {
|
||||
var updatedEntity: any = {};
|
||||
displayedAttributes &&
|
||||
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
|
||||
if (attribute.name() && (userContext.apiType !== "Cassandra" || attribute.value() !== "")) {
|
||||
var value = attribute.getPropertyValue();
|
||||
var type = attribute.type();
|
||||
if (type === TableConstants.TableType.Int64) {
|
||||
value = Utilities.padLongWithZeros(value);
|
||||
}
|
||||
updatedEntity[attribute.name()] = {
|
||||
_: value,
|
||||
$: type,
|
||||
};
|
||||
}
|
||||
});
|
||||
return updatedEntity;
|
||||
}
|
||||
|
||||
private isAttributeEditable(attributeName: string, entityAttributeType: string) {
|
||||
return !(
|
||||
attributeName === TableConstants.EntityKeyNames.PartitionKey ||
|
||||
attributeName === TableConstants.EntityKeyNames.RowKey ||
|
||||
attributeName === TableConstants.EntityKeyNames.Timestamp ||
|
||||
// As per VSO:189935, Making Binary properties read-only in Edit Entity dialog until we have a full story for it.
|
||||
entityAttributeType === TableConstants.TableType.Binary
|
||||
);
|
||||
}
|
||||
|
||||
private getPropertyDisplayValue(entity: Entities.ITableEntity, name: string, type: string): any {
|
||||
var attribute: Entities.ITableEntityAttribute = entity[name];
|
||||
var displayValue: any = attribute._;
|
||||
var isBinary: boolean = type === TableConstants.TableType.Binary;
|
||||
|
||||
// Showing the value in base64 for binary properties since that is what the Azure Storage Client Library expects.
|
||||
// This means that, even if the Azure Storage API returns a byte[] of binary content, it needs that same array
|
||||
// *base64 - encoded * as the value for the updated property or the whole update operation will fail.
|
||||
if (isBinary && displayValue && $.isArray(displayValue.data)) {
|
||||
var bytes: number[] = displayValue.data;
|
||||
displayValue = this.getBase64DisplayValue(bytes);
|
||||
}
|
||||
|
||||
return displayValue;
|
||||
}
|
||||
|
||||
private getBase64DisplayValue(bytes: number[]): string {
|
||||
var displayValue: string = null;
|
||||
|
||||
try {
|
||||
var chars: string[] = bytes.map((byte: number) => String.fromCharCode(byte));
|
||||
var toEncode: string = chars.join("");
|
||||
displayValue = window.btoa(toEncode);
|
||||
} catch (error) {
|
||||
// Error
|
||||
}
|
||||
|
||||
return displayValue;
|
||||
}
|
||||
}
|
@ -1,164 +0,0 @@
|
||||
import * as ko from "knockout";
|
||||
|
||||
import * as DateTimeUtilities from "../../Tables/QueryBuilder/DateTimeUtilities";
|
||||
import * as EntityPropertyNameValidator from "./Validators/EntityPropertyNameValidator";
|
||||
import EntityPropertyValueValidator from "./Validators/EntityPropertyValueValidator";
|
||||
import * as Constants from "../../Tables/Constants";
|
||||
import * as Utilities from "../../Tables/Utilities";
|
||||
import TableEntityPane from "./TableEntityPane";
|
||||
|
||||
export interface IValidationResult {
|
||||
isInvalid: boolean;
|
||||
help: string;
|
||||
}
|
||||
|
||||
export interface IActionEnabledDialog {
|
||||
updateIsActionEnabled: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* View model for an entity proprety
|
||||
*/
|
||||
export default class EntityPropertyViewModel {
|
||||
/* Constants */
|
||||
public static noTooltip = "";
|
||||
// Maximum number of custom properties, see Azure Service Data Model
|
||||
// At https://msdn.microsoft.com/library/azure/dd179338.aspx
|
||||
public static maximumNumberOfProperties = 252;
|
||||
|
||||
// Labels
|
||||
public closeButtonLabel: string = "Close"; // localize
|
||||
|
||||
/* Observables */
|
||||
public name: ko.Observable<string>;
|
||||
public type: ko.Observable<string>;
|
||||
public value: ko.Observable<any>;
|
||||
public inputType: ko.Computed<string>;
|
||||
|
||||
public nameTooltip: ko.Observable<string>;
|
||||
public isInvalidName: ko.Observable<boolean>;
|
||||
|
||||
public valueTooltip: ko.Observable<string>;
|
||||
public isInvalidValue: ko.Observable<boolean>;
|
||||
|
||||
public namePlaceholder: ko.Observable<string>;
|
||||
public valuePlaceholder: ko.Observable<string>;
|
||||
|
||||
public hasFocus: ko.Observable<boolean>;
|
||||
public valueHasFocus: ko.Observable<boolean>;
|
||||
public isDateType: ko.Computed<boolean>;
|
||||
|
||||
public editable: boolean; // If a property's name or type is editable, these two are always the same regarding editability.
|
||||
public valueEditable: boolean; // If a property's value is editable, could be different from name or type.
|
||||
public removable: boolean; // If a property is removable, usually, PartitionKey, RowKey and TimeStamp (if applicable) are not removable.
|
||||
public isRequired: boolean; // If a property's value is required, used to differentiate the place holder label.
|
||||
public ignoreEmptyValue: boolean;
|
||||
|
||||
/* Members */
|
||||
private tableEntityPane: TableEntityPane;
|
||||
private _validator: EntityPropertyValueValidator;
|
||||
|
||||
constructor(
|
||||
tableEntityPane: TableEntityPane,
|
||||
name: string,
|
||||
type: string,
|
||||
value: any,
|
||||
namePlaceholder: string = "",
|
||||
valuePlaceholder: string = "",
|
||||
editable: boolean = false,
|
||||
defaultValidName: boolean = true,
|
||||
defaultValidValue: boolean = false,
|
||||
isRequired: boolean = false,
|
||||
removable: boolean = editable,
|
||||
valueEditable: boolean = editable,
|
||||
ignoreEmptyValue: boolean = false
|
||||
) {
|
||||
this.name = ko.observable<string>(name);
|
||||
this.type = ko.observable<string>(type);
|
||||
this.isDateType = ko.pureComputed<boolean>(() => this.type() === Constants.TableType.DateTime);
|
||||
if (this.isDateType()) {
|
||||
value = value ? DateTimeUtilities.getLocalDateTime(value) : value;
|
||||
}
|
||||
this.value = ko.observable(value);
|
||||
this.inputType = ko.pureComputed<string>(() => {
|
||||
if (!this.valueHasFocus() && !this.value() && this.isDateType()) {
|
||||
return Constants.InputType.Text;
|
||||
}
|
||||
return Utilities.getInputTypeFromDisplayedName(this.type());
|
||||
});
|
||||
|
||||
this.namePlaceholder = ko.observable<string>(namePlaceholder);
|
||||
this.valuePlaceholder = ko.observable<string>(valuePlaceholder);
|
||||
|
||||
this.editable = editable;
|
||||
this.isRequired = isRequired;
|
||||
this.removable = removable;
|
||||
this.valueEditable = valueEditable;
|
||||
|
||||
this._validator = new EntityPropertyValueValidator(isRequired);
|
||||
|
||||
this.tableEntityPane = tableEntityPane;
|
||||
|
||||
this.nameTooltip = ko.observable<string>(EntityPropertyViewModel.noTooltip);
|
||||
this.isInvalidName = ko.observable<boolean>(!defaultValidName);
|
||||
this.name.subscribe((name: string) => this.validateName(name));
|
||||
if (!defaultValidName) {
|
||||
this.validateName(name);
|
||||
}
|
||||
|
||||
this.valueTooltip = ko.observable<string>(EntityPropertyViewModel.noTooltip);
|
||||
this.isInvalidValue = ko.observable<boolean>(!defaultValidValue);
|
||||
this.value.subscribe((value: string) => this.validateValue(value, this.type()));
|
||||
if (!defaultValidValue) {
|
||||
this.validateValue(value, type);
|
||||
}
|
||||
|
||||
this.type.subscribe((type: string) => this.validateValue(this.value(), type));
|
||||
|
||||
this.hasFocus = ko.observable<boolean>(false);
|
||||
this.valueHasFocus = ko.observable<boolean>(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Javascript value of the entity property based on its EDM type.
|
||||
*/
|
||||
public getPropertyValue(): any {
|
||||
var value: string = this.value();
|
||||
if (this.type() === Constants.TableType.DateTime) {
|
||||
value = DateTimeUtilities.getUTCDateTime(value);
|
||||
}
|
||||
return this._validator.parseValue(value, this.type());
|
||||
}
|
||||
|
||||
private validateName(name: string): void {
|
||||
var result: IValidationResult = this.isInvalidNameInput(name);
|
||||
|
||||
this.isInvalidName(result.isInvalid);
|
||||
this.nameTooltip(result.help);
|
||||
this.namePlaceholder(result.help);
|
||||
this.tableEntityPane.updateIsActionEnabled();
|
||||
}
|
||||
|
||||
private validateValue(value: string, type: string): void {
|
||||
var result: IValidationResult = this.isInvalidValueInput(value, type);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.isInvalidValue(result.isInvalid);
|
||||
this.valueTooltip(result.help);
|
||||
this.valuePlaceholder(result.help);
|
||||
this.tableEntityPane.updateIsActionEnabled();
|
||||
}
|
||||
|
||||
private isInvalidNameInput(name: string): IValidationResult {
|
||||
return EntityPropertyNameValidator.validate(name);
|
||||
}
|
||||
|
||||
private isInvalidValueInput(value: string, type: string): IValidationResult {
|
||||
if (this.ignoreEmptyValue && this.value() === "") {
|
||||
return { isInvalid: false, help: "" };
|
||||
}
|
||||
return this._validator.validate(value, type);
|
||||
}
|
||||
}
|
@ -1,190 +0,0 @@
|
||||
<div data-bind="visible: visible">
|
||||
<div
|
||||
class="contextual-pane-out"
|
||||
data-bind="
|
||||
click: cancel,
|
||||
clickBubble: false"
|
||||
></div>
|
||||
<div class="contextual-pane" style="width: 700px" id="addtableentitypane">
|
||||
<!-- Add table entity form - Start -->
|
||||
<div
|
||||
class="contextual-pane-in"
|
||||
data-bind="
|
||||
visible: !isEditing()"
|
||||
>
|
||||
<form
|
||||
class="paneContentContainer"
|
||||
data-bind="
|
||||
submit: submit"
|
||||
>
|
||||
<!-- Add table entity header - Start -->
|
||||
<div class="firstdivbg headerline">
|
||||
<span role="heading" aria-level="2" data-bind="text: title"></span>
|
||||
<div
|
||||
id="closeAddEntityPane"
|
||||
class="closeImg"
|
||||
role="button"
|
||||
aria-label="Close pane"
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
click: cancel, event: { keydown: onCloseKeyPress }"
|
||||
>
|
||||
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add table entity header - End -->
|
||||
<div class="tableParamContent paneContentContainer">
|
||||
<div class="entity-table">
|
||||
<div class="entity-table-row">
|
||||
<div class="entity-table-cell entity-table-property-header" data-bind="text: attributeNameLabel"></div>
|
||||
<div class="entity-table-cell entity-table-type-header" data-bind="text: dataTypeLabel"></div>
|
||||
<div class="entity-table-cell entity-table-value-header" data-bind="text: attributeValueLabel"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-table-scroll-box" id="addEntityScroll">
|
||||
<div class="entity-table" data-bind="foreach: displayedAttributes">
|
||||
<div class="entity-table-row">
|
||||
<div class="entity-table-cell entity-table-property-column">
|
||||
<input
|
||||
type="text"
|
||||
class="entity-table-field entity-table-property-column"
|
||||
required
|
||||
data-bind="
|
||||
textInput: name,
|
||||
attr: { title: nameTooltip, placeholder: namePlaceholder, 'aria-label': 'property name' },
|
||||
css: { 'invalid-field': isInvalidName },
|
||||
readOnly: !editable,
|
||||
hasFocus: hasFocus"
|
||||
/>
|
||||
</div>
|
||||
<div class="entity-table-cell entity-table-type-column">
|
||||
<select
|
||||
class="entity-table-field"
|
||||
data-bind="
|
||||
options: $parent.edmTypes,
|
||||
optionsAfterRender: $parent.setOptionDisable,
|
||||
value: type,
|
||||
attr: { 'aria-label': 'type' },
|
||||
enable: editable,
|
||||
readOnly: !editable"
|
||||
></select>
|
||||
</div>
|
||||
<!-- ko ifnot: isDateType -->
|
||||
<div class="entity-table-cell entity-table-value-column">
|
||||
<input
|
||||
class="entity-table-field"
|
||||
id="addTableEntityValue"
|
||||
step="1"
|
||||
data-bind="
|
||||
textInput: value,
|
||||
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
|
||||
css: { 'invalid-field': isInvalidValue },
|
||||
readOnly: !valueEditable"
|
||||
/>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: isDateType -->
|
||||
<div class="entity-table-cell entity-table-value-column">
|
||||
<input
|
||||
class="entity-table-field"
|
||||
step="1"
|
||||
data-bind="
|
||||
value: value,
|
||||
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType },
|
||||
css: { 'invalid-field': isInvalidValue },
|
||||
readOnly: !valueEditable,
|
||||
hasFocus: valueHasFocus"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<div class="entity-table-cell entity-table-action-column" data-bind="if: removable || valueEditable">
|
||||
<span
|
||||
class="entity-Edit-Cancel"
|
||||
title="Edit property"
|
||||
role="button"
|
||||
aria-label="Edit property"
|
||||
tabindex="0"
|
||||
data-bind="click: $parent.editAttribute.bind($data, $index()), visible: valueEditable, event: { keydown: $parent.onEditPropertyKeyDown.bind($data, $index()) }"
|
||||
>
|
||||
<img class="entity-Editor-Cancel-Img" src="/Edit_entity.svg" alt="Edit" />
|
||||
</span>
|
||||
<span
|
||||
class="entity-Edit-Cancel"
|
||||
title="Delete property"
|
||||
role="button"
|
||||
aria-label="Delete property"
|
||||
tabindex="0"
|
||||
data-bind="click: $parent.removeAttribute.bind($data, $index()), visible: removable, event: { keydown: $parent.onDeletePropertyKeyDown.bind($data, $index()) }"
|
||||
>
|
||||
<img class="entity-Editor-Cancel-Img" src="/delete.svg" alt="Cancel" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-table addProperty">
|
||||
<div class="entity-table-row">
|
||||
<div class="entity-table-cell">
|
||||
<span
|
||||
class="commandButton"
|
||||
id="addProperty"
|
||||
role="button"
|
||||
aria-label="Add property"
|
||||
tabindex="0"
|
||||
data-bind="visible: canAdd, click: insertAttribute, event: { keydown: onAddPropertyKeyDown }"
|
||||
autofocus
|
||||
>
|
||||
<img class="addPropertyImg" src="/Add-property.svg" alt="Insert attribute" />
|
||||
<span data-bind="text: addButtonLabel"> </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
<input
|
||||
type="submit"
|
||||
class="btncreatecoll1"
|
||||
data-bind="value: submitButtonText, event: { keydown: onSubmitKeyPress }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Add table entity form - End -->
|
||||
<!-- Add table entity editor - Start -->
|
||||
<div id="editor-panel-addEntity" data-bind="visible: isEditing()" style="display: none">
|
||||
<div data-bind="with: editingProperty()">
|
||||
<!-- Add table entity editor header - Start -->
|
||||
<div class="firstdivbg headerline">
|
||||
<span
|
||||
class="backBtn"
|
||||
aria-label="Back"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
click: $parent.finishEditingAttribute, event: { keydown: $parent.onBackButtonKeyDown }"
|
||||
>
|
||||
<img src="/RevertBack.svg" alt="BackIcon" />
|
||||
</span>
|
||||
<span class="edit-value-text" data-bind="text: name"></span>
|
||||
</div>
|
||||
<!-- Add table entity editor header - End -->
|
||||
<div class="seconddivbg paddingspan2">
|
||||
<textarea
|
||||
class="entity-editor-expanded"
|
||||
id="textAreaEditProperty"
|
||||
tabindex="0"
|
||||
rows="21"
|
||||
data-bind="value: value, attr: { 'aria-label': name }"
|
||||
style="width: 95%"
|
||||
autofocus
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add table entity editor - End -->
|
||||
</div>
|
||||
</div>
|
@ -1,187 +0,0 @@
|
||||
<div data-bind="visible: visible">
|
||||
<div
|
||||
class="contextual-pane-out"
|
||||
data-bind="
|
||||
click: cancel,
|
||||
clickBubble: false"
|
||||
></div>
|
||||
<div class="contextual-pane" style="width: 700px" id="edittableentitypane">
|
||||
<!-- Edit table entity form - Start -->
|
||||
<div
|
||||
class="contextual-pane-in"
|
||||
data-bind="
|
||||
visible: !isEditing()"
|
||||
>
|
||||
<form
|
||||
class="paneContentContainer"
|
||||
data-bind="
|
||||
submit: submit"
|
||||
>
|
||||
<!-- Edit table entity header - Start -->
|
||||
<div class="firstdivbg headerline">
|
||||
<span role="heading" aria-level="2" data-bind="text: title"></span>
|
||||
<div
|
||||
class="closeImg"
|
||||
role="button"
|
||||
aria-label="Close pane"
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
click: cancel, event: { keydown: onCloseKeyPress }"
|
||||
>
|
||||
<img src="../../../../images/close-black.svg" title="Close" alt="Close" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit table entity header - End -->
|
||||
<div class="tableParamContent paneContentContainer">
|
||||
<div class="entity-table">
|
||||
<div class="entity-table-row">
|
||||
<div class="entity-table-cell entity-table-property-header" data-bind="text: attributeNameLabel"></div>
|
||||
<div class="entity-table-cell entity-table-type-header" data-bind="text: dataTypeLabel"></div>
|
||||
<div class="entity-table-cell entity-table-value-header" data-bind="text: attributeValueLabel"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-table-scroll-box" id="editEntityScroll">
|
||||
<div class="entity-table" data-bind="foreach: displayedAttributes">
|
||||
<div class="entity-table-row">
|
||||
<div class="entity-table-cell entity-table-property-column">
|
||||
<input
|
||||
type="text"
|
||||
class="entity-table-field entity-table-property-column"
|
||||
required
|
||||
data-bind="
|
||||
textInput: name,
|
||||
attr: { title: nameTooltip, placeholder: namePlaceholder, 'aria-label': 'property name' },
|
||||
css: { 'invalid-field': isInvalidName },
|
||||
readOnly: !editable,
|
||||
hasFocus: hasFocus"
|
||||
/>
|
||||
</div>
|
||||
<div class="entity-table-cell entity-table-type-column">
|
||||
<select
|
||||
class="entity-table-field"
|
||||
data-bind="
|
||||
options: $parent.edmTypes,
|
||||
optionsAfterRender: $parent.setOptionDisable,
|
||||
value: type,
|
||||
attr: { 'aria-label': 'type' },
|
||||
enable: editable,
|
||||
readOnly: !editable"
|
||||
></select>
|
||||
</div>
|
||||
<!-- ko ifnot: isDateType -->
|
||||
<div class="entity-table-cell entity-table-value-column">
|
||||
<input
|
||||
class="entity-table-field"
|
||||
step="1"
|
||||
data-bind="
|
||||
textInput: value,
|
||||
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
|
||||
css: { 'invalid-field': isInvalidValue },
|
||||
readOnly: !valueEditable"
|
||||
/>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<!-- ko if: isDateType -->
|
||||
<div class="entity-table-cell entity-table-value-column">
|
||||
<input
|
||||
class="entity-table-field"
|
||||
step="1"
|
||||
data-bind="
|
||||
value: value,
|
||||
attr: { title: valueTooltip, placeholder: valuePlaceholder, type: inputType, 'aria-label': 'value' },
|
||||
css: { 'invalid-field': isInvalidValue },
|
||||
readOnly: !valueEditable,
|
||||
hasFocus: valueHasFocus"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<!-- /ko -->
|
||||
<div class="entity-table-cell entity-table-action-column" data-bind="if: removable || valueEditable">
|
||||
<span
|
||||
class="entity-Edit-Cancel"
|
||||
role="button"
|
||||
aria-label="Edit property"
|
||||
tabindex="0"
|
||||
data-bind="click: $parent.editAttribute.bind($data, $index()), visible: valueEditable, event: { keydown: $parent.onEditPropertyKeyDown.bind($data, $index()) }"
|
||||
title="Edit property"
|
||||
>
|
||||
<img class="entity-Editor-Cancel-Img" src="/Edit_entity.svg" alt="Edit attribute" />
|
||||
</span>
|
||||
<span
|
||||
class="entity-Edit-Cancel"
|
||||
role="button"
|
||||
aria-label="Delete property"
|
||||
tabindex="0"
|
||||
data-bind="click: $parent.removeAttribute.bind($data, $index()), visible: removable, event: { keydown: $parent.onDeletePropertyKeyDown.bind($data, $index()) }"
|
||||
title="Delete property"
|
||||
>
|
||||
<img class="entity-Editor-Cancel-Img" src="/delete.svg" alt="Remove attribute" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entity-table addProperty">
|
||||
<div class="entity-table-row">
|
||||
<div class="entity-table-cell">
|
||||
<span
|
||||
class="commandButton"
|
||||
role="button"
|
||||
aria-label="Add property"
|
||||
tabindex="0"
|
||||
data-bind="visible: canAdd, click: insertAttribute, event: { keydown: onAddPropertyKeyDown }"
|
||||
autofocus
|
||||
>
|
||||
<img class="addPropertyImg" src="/Add-property.svg" alt="Add attribute" />
|
||||
<span data-bind="text: addButtonLabel"> </span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="paneFooter">
|
||||
<div class="leftpanel-okbut">
|
||||
<input
|
||||
type="submit"
|
||||
value="Update Entity"
|
||||
class="btncreatecoll1"
|
||||
data-bind="value: submitButtonText, event: { keydown: onSubmitKeyPress }"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- Edit table entity form - End -->
|
||||
<!-- Edit table entity editor - Start -->
|
||||
<div id="editor-panel-editEntity" data-bind="visible: isEditing()" style="display: none">
|
||||
<div data-bind="with: editingProperty()">
|
||||
<!-- Edit table entity editor header - Start -->
|
||||
<div class="firstdivbg headerline">
|
||||
<span
|
||||
class="backBtn"
|
||||
aria-label="Back"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
data-bind="
|
||||
click: $parent.finishEditingAttribute, event: { keydown: $parent.onBackButtonKeyDown }"
|
||||
>
|
||||
<img src="/RevertBack.svg" alt="BackIcon" />
|
||||
</span>
|
||||
<span class="edit-value-text" data-bind="text: name"></span>
|
||||
</div>
|
||||
<!-- Edit table entity editor header - End -->
|
||||
<div class="seconddivbg paddingspan2">
|
||||
<textarea
|
||||
class="entity-editor-expanded"
|
||||
id="editor-area"
|
||||
tabindex="0"
|
||||
rows="21"
|
||||
data-bind="value: value, attr: { 'aria-label': name }"
|
||||
autofocus
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Edit table entity editor - End -->
|
||||
</div>
|
||||
</div>
|
@ -1,279 +0,0 @@
|
||||
import * as ko from "knockout";
|
||||
import _ from "underscore";
|
||||
import { KeyCodes } from "../../../Common/Constants";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { userContext } from "../../../UserContext";
|
||||
import * as TableConstants from "../../Tables/Constants";
|
||||
import * as DataTableUtilities from "../../Tables/DataTable/DataTableUtilities";
|
||||
import TableEntityListViewModel from "../../Tables/DataTable/TableEntityListViewModel";
|
||||
import * as Entities from "../../Tables/Entities";
|
||||
import * as TableEntityProcessor from "../../Tables/TableEntityProcessor";
|
||||
import * as Utilities from "../../Tables/Utilities";
|
||||
import { ContextualPaneBase } from "../ContextualPaneBase";
|
||||
import EntityPropertyViewModel from "./EntityPropertyViewModel";
|
||||
|
||||
// Class with variables and functions that are common to both adding and editing entities
|
||||
export default abstract class TableEntityPane extends ContextualPaneBase {
|
||||
protected static requiredFieldsForTablesAPI: string[] = [
|
||||
TableConstants.EntityKeyNames.PartitionKey,
|
||||
TableConstants.EntityKeyNames.RowKey,
|
||||
];
|
||||
|
||||
/* Labels */
|
||||
public attributeNameLabel = "Property Name"; // localize
|
||||
public dataTypeLabel = "Type"; // localize
|
||||
public attributeValueLabel = "Value"; // localize
|
||||
|
||||
/* Controls */
|
||||
public removeButtonLabel = "Remove"; // localize
|
||||
public editButtonLabel = "Edit"; // localize
|
||||
public addButtonLabel = "Add Property"; // localize
|
||||
|
||||
public edmTypes: ko.ObservableArray<string> = ko.observableArray([
|
||||
TableConstants.TableType.String,
|
||||
TableConstants.TableType.Boolean,
|
||||
TableConstants.TableType.Binary,
|
||||
TableConstants.TableType.DateTime,
|
||||
TableConstants.TableType.Double,
|
||||
TableConstants.TableType.Guid,
|
||||
TableConstants.TableType.Int32,
|
||||
TableConstants.TableType.Int64,
|
||||
]);
|
||||
|
||||
public canAdd: ko.Computed<boolean>;
|
||||
public canApply: ko.Observable<boolean>;
|
||||
public displayedAttributes = ko.observableArray<EntityPropertyViewModel>();
|
||||
public editingProperty = ko.observable<EntityPropertyViewModel>();
|
||||
public isEditing = ko.observable<boolean>(false);
|
||||
public submitButtonText = ko.observable<string>();
|
||||
|
||||
public tableViewModel: TableEntityListViewModel;
|
||||
|
||||
protected scrollId: ko.Observable<string>;
|
||||
|
||||
constructor(options: ViewModels.PaneOptions) {
|
||||
super(options);
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
this.edmTypes([
|
||||
TableConstants.CassandraType.Text,
|
||||
TableConstants.CassandraType.Ascii,
|
||||
TableConstants.CassandraType.Bigint,
|
||||
TableConstants.CassandraType.Blob,
|
||||
TableConstants.CassandraType.Boolean,
|
||||
TableConstants.CassandraType.Decimal,
|
||||
TableConstants.CassandraType.Double,
|
||||
TableConstants.CassandraType.Float,
|
||||
TableConstants.CassandraType.Int,
|
||||
TableConstants.CassandraType.Uuid,
|
||||
TableConstants.CassandraType.Varchar,
|
||||
TableConstants.CassandraType.Varint,
|
||||
TableConstants.CassandraType.Inet,
|
||||
TableConstants.CassandraType.Smallint,
|
||||
TableConstants.CassandraType.Tinyint,
|
||||
]);
|
||||
}
|
||||
|
||||
this.canAdd = ko.computed<boolean>(() => {
|
||||
// Cassandra can't add since the schema can't be changed once created
|
||||
if (userContext.apiType === "Cassandra") {
|
||||
return false;
|
||||
}
|
||||
// Adding '2' to the maximum to take into account PartitionKey and RowKey
|
||||
return this.displayedAttributes().length < EntityPropertyViewModel.maximumNumberOfProperties + 2;
|
||||
});
|
||||
this.canApply = ko.observable<boolean>(true);
|
||||
this.editingProperty(this.displayedAttributes()[0]);
|
||||
}
|
||||
|
||||
public removeAttribute = (index: number, data: any): void => {
|
||||
this.displayedAttributes.splice(index, 1);
|
||||
this.updateIsActionEnabled();
|
||||
document.getElementById("addProperty").focus();
|
||||
};
|
||||
|
||||
public editAttribute = (index: number, data: EntityPropertyViewModel): void => {
|
||||
this.editingProperty(data);
|
||||
this.isEditing(true);
|
||||
document.getElementById("textAreaEditProperty").focus();
|
||||
};
|
||||
|
||||
public finishEditingAttribute = (): void => {
|
||||
this.isEditing(false);
|
||||
this.editingProperty(null);
|
||||
};
|
||||
|
||||
public onKeyUp = (data: any, event: KeyboardEvent): boolean => {
|
||||
var handled: boolean = Utilities.onEsc(event, ($sourceElement: JQuery) => {
|
||||
this.finishEditingAttribute();
|
||||
});
|
||||
|
||||
return !handled;
|
||||
};
|
||||
|
||||
public onAddPropertyKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.insertAttribute();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public onEditPropertyKeyDown = (
|
||||
index: number,
|
||||
data: EntityPropertyViewModel,
|
||||
event: KeyboardEvent,
|
||||
source: any
|
||||
): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.editAttribute(index, data);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public onDeletePropertyKeyDown = (
|
||||
index: number,
|
||||
data: EntityPropertyViewModel,
|
||||
event: KeyboardEvent,
|
||||
source: any
|
||||
): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.removeAttribute(index, data);
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public onBackButtonKeyDown = (source: any, event: KeyboardEvent): boolean => {
|
||||
if (event.keyCode === KeyCodes.Enter || event.keyCode === KeyCodes.Space) {
|
||||
this.finishEditingAttribute();
|
||||
event.stopPropagation();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
public insertAttribute = (name?: string, type?: string): void => {
|
||||
let entityProperty: EntityPropertyViewModel;
|
||||
if (!!name && !!type && userContext.apiType === "Cassandra") {
|
||||
// TODO figure out validation story for blob and Inet so we can allow adding/editing them
|
||||
const nonEditableType: boolean =
|
||||
type === TableConstants.CassandraType.Blob || type === TableConstants.CassandraType.Inet;
|
||||
entityProperty = new EntityPropertyViewModel(
|
||||
this,
|
||||
name,
|
||||
type,
|
||||
"", // default to empty string
|
||||
/* namePlaceholder */ undefined,
|
||||
/* valuePlaceholder */ undefined,
|
||||
/* editable */ false,
|
||||
/* default valid name */ false,
|
||||
/* default valid value */ true,
|
||||
/* isRequired */ false,
|
||||
/* removable */ false,
|
||||
/*value editable*/ !nonEditableType
|
||||
);
|
||||
} else {
|
||||
entityProperty = new EntityPropertyViewModel(
|
||||
this,
|
||||
"",
|
||||
this.edmTypes()[0], // default to the first Edm type: 'string'
|
||||
"", // default to empty string
|
||||
/* namePlaceholder */ undefined,
|
||||
/* valuePlaceholder */ undefined,
|
||||
/* editable */ true,
|
||||
/* default valid name */ false,
|
||||
/* default valid value */ true
|
||||
);
|
||||
}
|
||||
|
||||
this.displayedAttributes.push(entityProperty);
|
||||
this.updateIsActionEnabled();
|
||||
this.scrollToBottom();
|
||||
|
||||
entityProperty.hasFocus(true);
|
||||
};
|
||||
|
||||
public updateIsActionEnabled(needRequiredFields: boolean = true): void {
|
||||
var properties: EntityPropertyViewModel[] = this.displayedAttributes() || [];
|
||||
var disable: boolean = _.some(properties, (property: EntityPropertyViewModel) => {
|
||||
return property.isInvalidName() || property.isInvalidValue();
|
||||
});
|
||||
|
||||
this.canApply(!disable);
|
||||
}
|
||||
|
||||
protected entityFromAttributes(displayedAttributes: EntityPropertyViewModel[]): Entities.ITableEntity {
|
||||
var entity: any = {};
|
||||
|
||||
displayedAttributes &&
|
||||
displayedAttributes.forEach((attribute: EntityPropertyViewModel) => {
|
||||
if (attribute.name() && (attribute.value() !== "" || attribute.isRequired)) {
|
||||
var value = attribute.getPropertyValue();
|
||||
var type = attribute.type();
|
||||
if (type === TableConstants.TableType.Int64) {
|
||||
value = Utilities.padLongWithZeros(value);
|
||||
}
|
||||
entity[attribute.name()] = {
|
||||
_: value,
|
||||
$: type,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
// Removing Binary from Add Entity dialog until we have a full story for it.
|
||||
protected setOptionDisable(option: Node, value: string): void {
|
||||
ko.applyBindingsToNode(option, { disable: value === TableConstants.TableType.Binary }, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the updated entity to see if there are any new attributes that old headers don't have.
|
||||
* In this case, add these attributes names as new headers.
|
||||
* Remarks: adding new headers will automatically trigger table redraw.
|
||||
*/
|
||||
protected tryInsertNewHeaders(viewModel: TableEntityListViewModel, newEntity: Entities.ITableEntity): boolean {
|
||||
var newHeaders: string[] = [];
|
||||
const keys = Object.keys(newEntity);
|
||||
keys &&
|
||||
keys.forEach((key: string) => {
|
||||
if (
|
||||
!_.contains(viewModel.headers, key) &&
|
||||
key !== TableEntityProcessor.keyProperties.attachments &&
|
||||
key !== TableEntityProcessor.keyProperties.etag &&
|
||||
key !== TableEntityProcessor.keyProperties.resourceId &&
|
||||
key !== TableEntityProcessor.keyProperties.self &&
|
||||
(userContext.apiType !== "Cassandra" || key !== TableConstants.EntityKeyNames.RowKey)
|
||||
) {
|
||||
newHeaders.push(key);
|
||||
}
|
||||
});
|
||||
|
||||
var newHeadersInserted: boolean = false;
|
||||
if (newHeaders.length) {
|
||||
if (!DataTableUtilities.checkForDefaultHeader(viewModel.headers)) {
|
||||
newHeaders = viewModel.headers.concat(newHeaders);
|
||||
}
|
||||
viewModel.updateHeaders(newHeaders, /* notifyColumnChanges */ true, /* enablePrompt */ false);
|
||||
newHeadersInserted = true;
|
||||
}
|
||||
return newHeadersInserted;
|
||||
}
|
||||
|
||||
protected scrollToBottom(): void {
|
||||
var scrollBox = document.getElementById(this.scrollId());
|
||||
var isScrolledToBottom = scrollBox.scrollHeight - scrollBox.clientHeight <= scrollBox.scrollHeight + 1;
|
||||
if (isScrolledToBottom) {
|
||||
scrollBox.scrollTop = scrollBox.scrollHeight - scrollBox.clientHeight;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user