src/app/shared/viewboxes/viewboxes.component.ts
OnInit
AfterViewInit
OnDestroy
| encapsulation | ViewEncapsulation.None |
| selector | app-viewboxes |
| styleUrls | ./viewboxes.component.scss |
| templateUrl | ./viewboxes.component.html |
Properties |
|
Inputs |
Outputs |
Accessors |
constructor(ngZone: NgZone, licenceService: LicenceService, sasService: SasService, eventService: EventService, sasStoreService: SasStoreService, loggerService: LoggerService, helperService: HelperService, router: Router, activatedRoute: ActivatedRoute, cdf: ChangeDetectorRef)
|
|||||||||||||||||||||||||||||||||
|
Parameters :
|
| viewboxModal | |
Type : boolean
|
|
| viewboxModalChange | |
Type : EventEmitter<boolean>
|
|
| calculateTableHeight | ||||||||
calculateTableHeight(viewbox: Viewbox)
|
||||||||
|
Calculate available height for Handsontable
Parameters :
Returns :
number
Available height in pixels |
| close | ||||||
close(viewbox: Viewbox)
|
||||||
|
Close Viewbox and remove it's data stored in
Parameters :
Returns :
void
|
| clrModalOpenChange | ||||||
clrModalOpenChange(open: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| collapse | ||||||
collapse(viewbox: Viewbox)
|
||||||
|
Parameters :
Returns :
void
|
| columnsDrop | ||||||
columnsDrop(event: CdkDragDrop
|
||||||
|
Column drop called after column dragged and dropped for Viewbox column to be configurated
Parameters :
Returns :
void
|
| Private createColumnOrder | ||||||
createColumnOrder(viewboxId: number)
|
||||||
|
Creating column order in such format to be encoded in URL
Parameters :
Returns :
number[]
|
| Private createViewboxTableSettings | ||||||
createViewboxTableSettings(viewbox: Viewbox)
|
||||||
|
Create and store Handsontable settings for a specific viewbox
Parameters :
Returns :
void
|
| Private decodeUrlData | ||||||
decodeUrlData(urlData: string)
|
||||||
|
It will decode/parse viewboxes data from URL Url data pattern: {id}-{library}-{table}-{width}-{height}-{x}-{y}-{collapsed}-{minimized}-{filter_pk}-{columns} 1-library-table-100-100-10-10-1-1;library2-table2-100-100-10-10-0-0-3-0123456 ; <- single viewbox separation symbol
Parameters :
Returns :
Viewbox[]
|
| Private encodeUrlData | ||||||
encodeUrlData(viewboxes: Viewbox[])
|
||||||
|
It will encode/inject viewboxes data from URL Url data pattern: {id}-{library}-{table}-{width}-{height}-{x}-{y}-{collapsed}-{minimized}-{filter_pk}-{columns} 1-library-table-100-100-10-10-1-1;library2-table2-100-100-10-10-0-0-3-0123456 ; <- single viewbox separation symbol
Parameters :
Returns :
string
|
| expand | ||||||
expand(viewbox: Viewbox)
|
||||||
|
Parameters :
Returns :
void
|
| focusViewbox | ||||||
focusViewbox(viewbox: Viewbox)
|
||||||
|
When clicked on Viewbox, it will focus it Focused Viewbox is always bring to top
Parameters :
Returns :
void
|
| Private getViewboxHotInstance | ||||||
getViewboxHotInstance(viewboxId?: number)
|
||||||
|
Parameters :
Returns :
Handsontable | undefined
HOT Instance from the given Viewbox |
| getViewboxTableIndex | ||||||
getViewboxTableIndex(viewbox: Viewbox)
|
||||||
|
Used to pair
Parameters :
Returns :
number
|
| getViewboxTableSettings | ||||||
getViewboxTableSettings(viewbox: Viewbox)
|
||||||
|
Get stored Handsontable settings for a specific viewbox
Parameters :
Returns :
Handsontable.GridSettings
|
| libraryOnClick | ||||||
libraryOnClick(data: LibraryClickEmitter)
|
||||||
|
Parameters :
Returns :
void
|
| Async loadData |
loadData(libDataset: string, viewbox: Viewbox)
|
|
Initial data load for particular Viewbox If data has been already found for particular Viewbox and filtering is not active it will return stored data instead of sending new request
Returns :
Promise<void>
Empty Promise - used for awaiting only |
| loadTables |
loadTables(lib: string, library?: any)
|
|
Laods tables to populate
Returns :
void
|
| maxWidthCheker |
maxWidthCheker(width: any, col: any)
|
|
Returns :
any
|
| minimize | ||||||
minimize(viewbox: Viewbox)
|
||||||
|
Parameters :
Returns :
void
|
| minimizeAll |
minimizeAll()
|
|
Returns :
void
|
| ngAfterViewInit |
ngAfterViewInit()
|
|
Returns :
void
|
| ngOnDestroy |
ngOnDestroy()
|
|
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Returns :
void
|
| onColRemove | ||||||
onColRemove(column: string)
|
||||||
|
Viewbox Manager removing the column
Parameters :
Returns :
void
|
| onColsearchChange | ||||||||||||||||
onColsearchChange(inputRef: AutocompleteComponent, value: string, columns: string[])
|
||||||||||||||||
|
Viewbox Manager searching for columns to be added
Parameters :
Returns :
void
|
| openFilter | ||||||
openFilter(viewbox: Viewbox)
|
||||||
|
Opens filter dialog for given Viewbox
Parameters :
Returns :
void
|
| openTableEdit | ||||||
openTableEdit(viewbox: Viewbox)
|
||||||
|
Opens table edit in new tab
Parameters :
Returns :
void
|
| Private parseHandleTranslate | ||||||
parseHandleTranslate(transform: string)
|
||||||
|
Reads the handle's current translate(x, y) transform (empty/unset parses as {x: 0, y: 0}) - used as the baseline a resize drag's pointer delta gets added to, so the handle continues from wherever setHandleTransform() last placed it rather than jumping.
Parameters :
Returns :
{ x: number; y: number }
|
| Private prepareFilterCache |
prepareFilterCache()
|
|
Prepare the init values to
Returns :
void
|
| refreshTableAfterResize | ||||||||
refreshTableAfterResize(viewbox: Viewbox)
|
||||||||
|
Refresh Handsontable instance after resize
Parameters :
Returns :
void
|
| Async reloadData | ||||||||||||
reloadData(libDataset: string, viewbox: Viewbox)
|
||||||||||||
|
Re-loads data particular Viewbox (preserving the filtering PK)
Parameters :
Returns :
unknown
Promise |
| Async reloadTableData | ||||||
reloadTableData(viewbox: Viewbox)
|
||||||
|
Parameters :
Returns :
any
|
| reLoadViewboxtables | ||||||||
reLoadViewboxtables(viewboxes: Viewbox[])
|
||||||||
|
Reloads SAS data for each viewbox that exists
Parameters :
Returns :
void
|
| resetFilter |
resetFilter()
|
|
Resets the data in filter dialog It will also reset the filtering in the given Viewbox
Returns :
void
|
| resetPosSize | ||||||
resetPosSize(viewbox: Viewbox)
|
||||||
|
Resets Viewbox to default position (top right corner)
Parameters :
Returns :
void
|
| resetSelectedViewbox | ||||||
resetSelectedViewbox(viewbox: Viewbox)
|
||||||
|
Re setting the viewbox from parameter to selected viewbox And sets the correct data for that viewbox
Parameters :
Returns :
void
|
| resize | |||||||||
resize(dragHandle: HTMLElement, target: HTMLElement)
|
|||||||||
|
Viewbox resize
Parameters :
Returns :
{ width: number; height: number }
|
| resizeFromLeftEdge | ||||||||||||||||||
resizeFromLeftEdge(pointerClientX: number, target: HTMLElement, startTargetRight: number, startX: number, startWidth: number)
|
||||||||||||||||||
|
Left-edge resize - unlike resizeWidth()/resize(), the ANCHOR here is the target's opposite (right) edge, which must stay visually fixed while the left edge (and so viewbox.x) moves with the pointer. Needs startTargetRight/startX/startWidth captured once at drag start (not read live) since the target's own rect moves as a result of this same calculation - a live read would be self-referential. Takes the raw pointer clientX, not a handle element's rect (unlike every other resize* method here) - the resize handle is a child of target, and target itself moves during this specific drag (viewbox.x feeds CDK's own transform), so getBoundingClientRect() on a child would report a position contaminated by target's own prior movement: each frame's width would include target's last-frame shift on top of the actual pointer delta, compounding into runaway growth and visible jitter. Raw pointer coordinates are always absolute viewport coordinates, immune to any transform applied to elements under them. Reads back the actually-rendered width after writing it, rather than trusting the raw intended value, because CSS min-width can clamp it - if x were derived from the unclamped value, the box would keep drifting left even after its width visibly stopped shrinking.
Parameters :
Returns :
{ width: number; x: number }
|
| resizeHeight | |||||||||
resizeHeight(dragHandle: HTMLElement, target: HTMLElement)
|
|||||||||
|
Bottom-edge-only resize - mirrors resizeWidth() for height. The target's top edge never moves for this handle either.
Parameters :
Returns :
number
|
| resizeWidth | |||||||||
resizeWidth(dragHandle: HTMLElement, target: HTMLElement)
|
|||||||||
|
Right-edge-only resize - same "distance from the target's opposite, fixed edge to the drag handle" math as resize(), but only applies to width. The target's left edge (and so viewbox.x) never moves for this handle, so no position bookkeeping is needed.
Parameters :
Returns :
number
|
| restore | ||||||
restore(viewbox: Viewbox)
|
||||||
|
Parameters :
Returns :
void
|
| restoreAll |
restoreAll()
|
|
Returns :
void
|
| Private scheduleAfterResizeRefresh |
scheduleAfterResizeRefresh()
|
|
Shared debounced tail for every resize path (corner, single-edge, left-edge) - persists the new size/position, notifies listeners the viewport changed shape, and refreshes every viewbox's Handsontable instance to match.
Returns :
void
|
| Async searchTable |
searchTable(inputElement: any, viewbox: Viewbox)
|
|
Returns :
any
|
| selectViewbox | ||||||
selectViewbox(viewbox: Viewbox)
|
||||||
|
Selects Viewbox in the Viewbox Manager for the columns to be configurated
Parameters :
Returns :
void
|
| sendClause |
sendClause()
|
|
Sending filtering request for given Viewbox
Returns :
void
|
| setAllHandleTransform |
setAllHandleTransform()
|
|
Sets every resize handle (corner, corner-left, right/bottom/left edges) to its correct position on every box, keyed off each box's own id suffix so a handle from one box is never matched to another's rect.
Returns :
void
|
| Private setColumnOrder | ||||||
setColumnOrder(viewboxId: number)
|
||||||
|
Parameters :
Returns :
void
|
| setHandleTransform | ||||||||||||
setHandleTransform(dragHandle: HTMLElement, targetRect: ClientRect | DOMRect, position: "x" | "y" | "both" | "left" | "bottomLeft")
|
||||||||||||
|
Sets the 'resize' handle in the correct corner position of the box
Parameters :
Returns :
void
|
| snapToGrid |
snapToGrid()
|
|
Snap to grid calculates the best grid possible for given screen width and height Configurable options are (in PX): gap - gaps between boxes and left/right sides topOffset - clearance on the top bottomOffset - clearance on the bottom
Returns :
void
|
| startResize | |||||||||||||||
startResize(event: PointerEvent, dragHandle: HTMLElement, resizeBox: HTMLElement, viewbox: Viewbox)
|
|||||||||||||||
|
Starts a corner-handle resize drag on plain pointer events rather than CDK's cdkDrag - the handle's transform is also written directly by setHandleTransform() (via resize() below) after every move, so it needs to be the ONLY thing driving that element's transform. A cdkDrag on the handle would track its own transform from cumulative pointer delta, unaware of externally-forced values, letting the handle's visual position and CDK's internal drag state diverge from each other.
Parameters :
Returns :
void
|
| startResizeBottom | |||||||||||||||
startResizeBottom(event: PointerEvent, dragHandle: HTMLElement, target: HTMLElement, viewbox: Viewbox)
|
|||||||||||||||
|
Bottom-edge-only resize drag, affecting only height.
Parameters :
Returns :
void
|
| startResizeBottomLeft | |||||||||||||||
startResizeBottomLeft(event: PointerEvent, dragHandle: HTMLElement, target: HTMLElement, viewbox: Viewbox)
|
|||||||||||||||
|
Bottom-left corner resize drag - combines startResizeLeft's width/x handling with startResizeBottom's height handling.
Parameters :
Returns :
void
|
| startResizeLeft | |||||||||||||||
startResizeLeft(event: PointerEvent, dragHandle: HTMLElement, target: HTMLElement, viewbox: Viewbox)
|
|||||||||||||||
|
Left-edge resize drag - unlike the handles above, this one also moves viewbox.x (the box's own left edge). viewbox.x feeds CDK's [cdkDragFreeDragPosition] input binding, which only takes effect when Angular change detection actually runs - so unlike every other resize handle here (which stay outside the zone for the whole drag), this one re-enters the zone specifically for the x update, or the box's CDK-driven position would silently fall out of sync with its own rendered width. startTargetRight/startX/startWidth are captured once at drag start, not read live - see resizeFromLeftEdge's own doc comment for why.
Parameters :
Returns :
void
|
| startResizeRight | |||||||||||||||
startResizeRight(event: PointerEvent, dragHandle: HTMLElement, target: HTMLElement, viewbox: Viewbox)
|
|||||||||||||||
|
Right-edge-only resize drag - same pointer-event approach as startResize(), affecting only width. resize()'s own debounced refresh is bundled into resize() itself; resizeWidth()/resizeHeight()/ resizeFromLeftEdge() are pure, so every handler below schedules it explicitly.
Parameters :
Returns :
void
|
| stopPropagation | ||||||
stopPropagation(event: any)
|
||||||
|
Parameters :
Returns :
void
|
| Public tableEditExists | ||||||
tableEditExists(viewbox: Viewbox)
|
||||||
|
Parameters :
Returns :
any
|
| Async tableOnClick | ||||||||
tableOnClick(data: TableClickEmitter)
|
||||||||
|
Adding new Viewbox - Fired when table is selected from the
Parameters :
Returns :
any
|
| unsetSelectedViewbox |
unsetSelectedViewbox()
|
|
Returns :
void
|
| Private updateColumnOrderHot | ||||||||||||||||
updateColumnOrderHot(colProp: string, finalIndex: number, viewboxId: number)
|
||||||||||||||||
|
HOT Columns ordering
Parameters :
Returns :
void
|
| Private updateHiddenColumnsHot | |||||||||
updateHiddenColumnsHot(colProps: string[], viewboxId: number)
|
|||||||||
|
Parameters :
Returns :
void
|
| Private updateHotColumns | ||||||||||||
updateHotColumns(hiddenColProps: string[], viewboxId: number)
|
||||||||||||
|
Updates the HOT columns Order and Visibility
Parameters :
Returns :
void
|
| Private viewboxChanged |
viewboxChanged()
|
|
Called after any Viewbox change that needs to be stored to URL It does the data encoding and storing to URL
Returns :
void
|
| viewboxDragEnded | |||||||||
viewboxDragEnded(event: CdkDragEnd, viewbox: Viewbox)
|
|||||||||
|
On drag end Viewbox position is updated As well as the URL
Parameters :
Returns :
void
|
| Public _query |
Type : Subscription | undefined
|
| Private _viewboxModal |
Type : boolean
|
Default value : false
|
| Public clauses |
Type : any
|
| Public defaultConfig |
Type : Viewbox
|
Default value : {
id: -1,
library: '',
table: '',
width: 500,
height: 300,
x: 0,
y: 150,
columns: []
}
|
| dragHandleBottomQuery |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('dragHandleBottom')
|
| dragHandleCornerLeftQuery |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('dragHandleCornerLeft')
|
| dragHandleCornerQuery |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('dragHandleCorner')
|
| dragHandleLeftQuery |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('dragHandleLeft')
|
| dragHandleRightQuery |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('dragHandleRight')
|
| Private endActiveResize |
Type : unknown | undefined
|
| Public filter |
Type : boolean
|
Default value : false
|
| Public filteringViewbox |
Type : Viewbox | undefined
|
| Public filterLibds |
Type : string | undefined
|
| Public filterLoading |
Type : boolean
|
Default value : false
|
| hotTableComponents |
Type : QueryList<HotTableComponent>
|
Decorators :
@ViewChildren(HotTableComponent)
|
| Public hotTableDefault |
Type : ViewboxHotTable
|
Default value : {
data: [],
headerPks: [],
$dataformats: {},
allColHeaders: [],
colHeadersHidden: [],
colHeadersVisible: [],
colHeaders: [],
contextMenu: ['copy_with_column_headers', 'copy_column_headers_only'],
copyPaste: {
copyColumnHeaders: true,
copyColumnHeadersOnly: true
},
columns: [],
cols: [],
height: 200, //WORKAROUND: Changed from '100%' to fixed pixel value because otherwize hot does not load
settings: {},
hiddenColumns: true,
manualColumnMove: false,
afterGetColHeader: undefined,
licenseKey: undefined,
dropdownMenu: undefined
}
|
| Public Infinity |
Type : unknown
|
Default value : Infinity
|
| Public libinfo |
Type : Libinfo[] | null
|
Default value : null
|
| Public libraries |
Type : Array<any>
|
| Public librariesLoading |
Type : boolean
|
Default value : true
|
| Public licenceState |
Type : unknown
|
Default value : this.licenceService.licenceState
|
| Public maxViewboxes |
Type : number
|
Default value : this.licenceState.value.viewbox_limit === Infinity
? 6
: this.licenceState.value.viewbox_limit || 6
|
| Public nullVariables |
Type : boolean
|
Default value : false
|
| resizeBoxQuery |
Type : QueryList<ElementRef>
|
Decorators :
@ViewChildren('resizeBox')
|
| Public sasjsConfig |
Type : SASjsConfig
|
Default value : new SASjsConfig()
|
| Public selectedViewbox |
Type : Viewbox | undefined
|
| Public selectedViewboxTable |
Type : ViewboxTable | undefined
|
| Public tables |
Type : any
|
| Public viewboxes |
Type : Viewbox[]
|
Default value : []
|
| Public viewboxHotSettings |
Type : Map<number | Handsontable.GridSettings>
|
Default value : new Map()
|
| Public viewboxTables |
Type : ViewboxTable[]
|
Default value : []
|
| viewboxModal | ||||||
getviewboxModal()
|
||||||
setviewboxModal(value: boolean)
|
||||||
|
Parameters :
Returns :
void
|
| viewboxLimitReached |
getviewboxLimitReached()
|
import {
CdkDragDrop,
CdkDragEnd,
moveItemInArray,
transferArrayItem
} from '@angular/cdk/drag-drop'
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
Input,
NgZone,
OnDestroy,
OnInit,
Output,
QueryList,
ViewChildren,
ViewEncapsulation
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router'
import { SASjsConfig } from '@sasjs/adapter'
import Handsontable from 'handsontable'
import { HotTableComponent } from '@handsontable/angular-wrapper'
import { cloneDeep } from 'lodash-es'
import { Subscription } from 'rxjs'
import { FilterQuery, FilterGroup } from 'src/app/models/FilterQuery'
import { Libinfo } from 'src/app/models/sas/common/Libinfo'
import { PublicViewtablesServiceResponse } from 'src/app/models/sas/public-viewtables.model'
import { EventService } from 'src/app/services/event.service'
import { HelperService } from 'src/app/services/helper.service'
import { LicenceService } from 'src/app/services/licence.service'
import { LoggerService } from 'src/app/services/logger.service'
import { SasStoreService } from 'src/app/services/sas-store.service'
import { SasService } from 'src/app/services/sas.service'
import { AutocompleteComponent } from 'src/app/shared/autocomplete/autocomplete.component'
import { LibraryClickEmitter } from 'src/app/shared/dc-tree/models/LibraryClickEmitter'
import { TableClickEmitter } from 'src/app/shared/dc-tree/models/TableClickEmitter'
import { mergeColsRules } from 'src/app/shared/dc-validator/utils/mergeColsRules'
import { buildExportMenuItem } from 'src/app/shared/hot-export/hot-export.util'
import { globals, initFilter } from 'src/app/_globals'
import { ViewboxHotTable } from './models/viewbox-hot-table.model'
import { ViewboxTable } from './models/viewbox-table.model'
import { Viewbox } from './models/viewbox.model'
@Component({
selector: 'app-viewboxes',
templateUrl: './viewboxes.component.html',
styleUrls: ['./viewboxes.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class ViewboxesComponent implements OnInit, AfterViewInit, OnDestroy {
@ViewChildren('resizeBox') resizeBoxQuery!: QueryList<ElementRef> //make query list, handle multiple
@ViewChildren('dragHandleCorner')
dragHandleCornerQuery!: QueryList<ElementRef>
@ViewChildren('dragHandleCornerLeft')
dragHandleCornerLeftQuery!: QueryList<ElementRef>
@ViewChildren('dragHandleRight')
dragHandleRightQuery!: QueryList<ElementRef>
@ViewChildren('dragHandleBottom')
dragHandleBottomQuery!: QueryList<ElementRef>
@ViewChildren('dragHandleLeft')
dragHandleLeftQuery!: QueryList<ElementRef>
@ViewChildren(HotTableComponent)
hotTableComponents!: QueryList<HotTableComponent>
private _viewboxModal: boolean = false
get viewboxModal(): boolean {
return this._viewboxModal
}
@Input() set viewboxModal(value: boolean) {
// If feature is disabled, prevent modal show up and show demo notice
if (this.licenceState.value.viewbox === false && !!value) {
this.eventService.showDemoLimitModal('Viewboxes')
this.viewboxModalChange.emit(false)
return
}
this._viewboxModal = value
if (!!value) this.unsetSelectedViewbox()
}
@Output() viewboxModalChange: EventEmitter<boolean> =
new EventEmitter<boolean>()
public libraries!: Array<any>
public tables: any
public libinfo: Libinfo[] | null = null
public librariesLoading: boolean = true
public viewboxes: Viewbox[] = []
public selectedViewbox: Viewbox | undefined
public selectedViewboxTable: ViewboxTable | undefined
public defaultConfig: Viewbox = {
id: -1,
library: '',
table: '',
width: 500,
height: 300,
x: 0,
y: 150,
columns: []
}
public sasjsConfig: SASjsConfig = new SASjsConfig()
public hotTableDefault: ViewboxHotTable = {
data: [],
headerPks: [],
$dataformats: {},
allColHeaders: [],
colHeadersHidden: [],
colHeadersVisible: [],
colHeaders: [],
contextMenu: ['copy_with_column_headers', 'copy_column_headers_only'],
copyPaste: {
copyColumnHeaders: true,
copyColumnHeadersOnly: true
},
columns: [],
cols: [],
height: 200, //WORKAROUND: Changed from '100%' to fixed pixel value because otherwize hot does not load
settings: {},
hiddenColumns: true,
manualColumnMove: false,
afterGetColHeader: undefined,
licenseKey: undefined,
dropdownMenu: undefined
}
public viewboxHotSettings: Map<number, Handsontable.GridSettings> = new Map()
public viewboxTables: ViewboxTable[] = []
public filteringViewbox: Viewbox | undefined
public filter: boolean = false
public filterLoading: boolean = false
public clauses: any
public nullVariables: boolean = false
public filterLibds: string | undefined
public _query: Subscription | undefined
// Set by startResize() while a corner-handle drag is in progress, so
// ngOnDestroy can remove its document-level listeners if the component
// is torn down mid-drag - see startResize's own comment.
private endActiveResize: (() => void) | undefined
public licenceState = this.licenceService.licenceState
public Infinity = Infinity
public maxViewboxes: number =
this.licenceState.value.viewbox_limit === Infinity
? 6
: this.licenceState.value.viewbox_limit || 6
constructor(
private ngZone: NgZone,
private licenceService: LicenceService,
private sasService: SasService,
private eventService: EventService,
private sasStoreService: SasStoreService,
private loggerService: LoggerService,
private helperService: HelperService,
private router: Router,
private activatedRoute: ActivatedRoute,
private cdf: ChangeDetectorRef
) {}
ngOnInit(): void {
// Load libraries
this.sasStoreService
.viewLibs()
.then((res: any) => {
this.libraries = res.saslibs
})
.catch((err: any) => {
this.loggerService.error(err)
})
.finally(() => {
this.librariesLoading = false
})
// Listen for filtering data
this._query = this.sasStoreService.query.subscribe((query: any) => {
this.clauses = query.obj
this.filterLibds = query.libds
})
this.sasjsConfig = this.sasService.getSasjsConfig()
this.licenceService.hot_license_key.subscribe(
(hot_license_key: string | undefined) => {
this.hotTableDefault.licenseKey = hot_license_key
}
)
const viewboxesQueryParam =
this.activatedRoute.snapshot.queryParams.viewboxes
if (viewboxesQueryParam) {
if (this.licenceState.value.viewbox === false) {
setTimeout(() =>
this.eventService.showDemoLimitModal('Linking Viewboxes')
)
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams: {}
})
} else {
this.viewboxes = this.decodeUrlData(viewboxesQueryParam)
setTimeout(() => {
this.setAllHandleTransform()
})
}
}
this.reLoadViewboxtables(this.viewboxes)
}
ngAfterViewInit(): void {
// Set handles for box resize and ensure HOT components are properly initialized
setTimeout(() => {
this.setAllHandleTransform()
// Force refresh of any existing HOT instances after view init
this.viewboxes.forEach((viewbox) => {
if (this.getViewboxTableIndex(viewbox) > -1) {
this.refreshTableAfterResize(viewbox)
}
})
}, 1000)
}
// Maximum number of open viewboxes reached
get viewboxLimitReached(): boolean {
return this.viewboxes.length >= this.maxViewboxes
}
clrModalOpenChange(open: boolean) {
this.viewboxModalChange.emit(open)
}
libraryOnClick(data: LibraryClickEmitter) {
if (!data.tablesLoaded)
this.loadTables(data.library.LIBRARYREF, data.library)
}
/**
* Adding new Viewbox - Fired when table is selected from the `dc-tree`
* @param data Selected table data coming from the `dc-tree`
*/
async tableOnClick(data: TableClickEmitter) {
if (this.viewboxLimitReached) return
const viewbox = {
...this.defaultConfig,
table: data.libTable,
library: data.library.LIBRARYNAME,
loadingData: true,
filter_pk: '0',
id: this.viewboxes.length + 1,
x: window.innerWidth - this.defaultConfig.width,
y: 150
}
this.viewboxes.push(viewbox)
setTimeout(() => {
this.setAllHandleTransform()
})
const libDataset = `${data.library.LIBRARYREF}.${data.libTable}`
await this.loadData(libDataset, viewbox)
viewbox.loadingData = false
this.eventService.dispatchEvent('resize') //Force HOT refresh
this.snapToGrid() //it will call viewboxChanged
}
/**
* Laods tables to populate `dc-tree`
* @param lib
* @param library
*/
loadTables(lib: string, library?: any) {
this.sasStoreService
.viewTables(lib)
.then((res: PublicViewtablesServiceResponse) => {
let tables = res.mptables.map(function (item: any) {
return item.MEMNAME
})
this.libinfo = res.libinfo || []
this.tables = tables
if (library) {
library['tables'] = tables
library['libinfo'] = this.libinfo
library['loadingTables'] = false
if (tables.length > 0) library['expanded'] = true
}
})
.catch((err: any) => {
this.loggerService.error(err)
})
}
/**
* Re-loads data particular Viewbox (preserving the filtering PK)
* @param libDataset library.table
* @param viewbox viewbox to reload data for
* @returns Promise
*/
async reloadData(libDataset: string, viewbox: Viewbox) {
return new Promise((resolve, reject) => {
let viewboxTable = this.viewboxTables.find(
(vbt) => vbt.viewboxId === viewbox.id
)
this.sasStoreService
.viewData(libDataset, parseInt(viewbox.filter_pk || '0'))
.then((res: any) => {
if (viewboxTable) {
viewboxTable.hotTable.data = res.viewdata
// Update settings with new data
this.createViewboxTableSettings(viewbox)
resolve(null)
} else {
resolve(null)
}
})
.catch(() => {
reject()
})
})
}
/**
* Initial data load for particular Viewbox
* If data has been already found for particular Viewbox
* and filtering is not active it will return stored data
* instead of sending new request
*
* @param libDataset library.table
* @param viewbox
* @returns Empty Promise - used for awaiting only
*/
async loadData(libDataset: string, viewbox: Viewbox): Promise<void> {
return new Promise((resolve, reject) => {
let existingViewboxTable: boolean = false
let viewboxTable = this.viewboxTables.find(
(vbt) => vbt.viewboxId === viewbox.id
)
if (viewboxTable && (viewbox.filter_pk === '0' || !viewbox.filter_pk))
resolve()
this.sasStoreService
.viewData(libDataset, parseInt(viewbox.filter_pk || '0'))
.then((res: any) => {
if (!viewboxTable) {
viewboxTable = {
viewboxId: viewbox.id,
viewboxLibDataset: libDataset,
hotTable: cloneDeep(this.hotTableDefault)
}
} else {
existingViewboxTable = true
}
viewboxTable.hotTable.data = res.viewdata
viewboxTable.hotTable.$dataformats = res.$viewdata
viewboxTable.hotTable.cols = res.cols
mergeColsRules(viewboxTable.hotTable.cols, [], res.$viewdata)
let columns: any[] = []
let colArr: string[] = []
for (let key in res.viewdata[0]) {
if (key) {
colArr.push(key)
}
}
for (let index = 0; index < colArr.length; index++) {
columns.push({ data: colArr[index] })
}
viewboxTable.hotTable.headerPks = cloneDeep(
res.sasparams[0].PK_FIELDS.split(' ')
)
viewboxTable.hotTable.allColHeaders = colArr.filter(
(col) => !viewboxTable!.hotTable.headerPks.includes(col)
)
viewboxTable.hotTable.colHeadersHidden = cloneDeep(
viewboxTable.hotTable.allColHeaders
)
viewboxTable.hotTable.colHeadersVisible = colArr.filter((col) =>
viewboxTable!.hotTable.headerPks.includes(col)
)
viewboxTable.hotTable.colHeaders = colArr
viewboxTable.hotTable.columns = columns
if (viewbox.columns && viewbox.columns.length > 0) {
viewboxTable.hotTable.manualColumnMove = viewbox.columns
viewbox.columns?.map((col: number, index: number) => {
const colProp = colArr[col]
const hiddenColIndex =
viewboxTable!.hotTable.colHeadersHidden.indexOf(colProp)
if (hiddenColIndex > -1) {
viewboxTable!.hotTable.colHeadersHidden.splice(
hiddenColIndex,
1
)
viewboxTable!.hotTable.colHeadersVisible[index] = colProp
}
})
} else {
viewboxTable.hotTable.colHeadersVisible.push(
...viewboxTable.hotTable.colHeadersHidden.splice(0, 10)
)
}
viewboxTable.hotTable.colHeadersVisible =
viewboxTable.hotTable.colHeadersVisible.filter((x) => x) //remove empty slots
if (!existingViewboxTable) this.viewboxTables.push(viewboxTable)
viewbox.query = this.helperService.deepClone(res.query)
viewbox.filterText = res.sasparams[0].FILTER_TEXT
// Create settings for this viewbox
this.createViewboxTableSettings(viewbox)
setTimeout(() => {
this.updateHotColumns(
viewboxTable!.hotTable.colHeadersHidden || [],
viewbox.id
)
// HOT Settings are bound in HTML but some settings due to timing issues
// requires to be updated after the HOT is instanced
// Use a longer timeout to ensure the HOT component is fully initialized
setTimeout(() => {
const hotInstance = this.getViewboxHotInstance(viewbox.id)
if (hotInstance) {
hotInstance.updateSettings({
manualColumnMove: viewboxTable!.hotTable.manualColumnMove,
afterGetColHeader: (col: number, th: any) => {
const column = hotInstance?.colToProp(col) as string
// header columns styling - primary keys
const isPKCol =
column &&
viewboxTable!.hotTable.headerPks.indexOf(column) > -1
if (isPKCol) th.classList.add('primaryKeyHeaderStyle')
// Dark mode
th.classList.add(globals.handsontable.darkTableHeaderClass)
}
})
hotInstance.render()
}
if (this.selectedViewbox) {
this.resetSelectedViewbox(viewbox)
}
}, 500)
}, 100)
resolve()
})
.catch((err: any) => {
this.loggerService.error(err)
reject()
})
})
}
/**
* Reloads SAS data for each viewbox that exists
* @param viewboxes viewboxes array
*/
reLoadViewboxtables(viewboxes: Viewbox[]) {
viewboxes.map((viewbox: Viewbox) => {
const libDataset = `${viewbox.library}.${viewbox.table}`
viewbox.loadingData = true
this.loadData(libDataset, viewbox).then(() => {
viewbox.loadingData = false
})
})
}
// HOT cols max width
maxWidthCheker(width: any, col: any) {
if (width > 200) return 200
else return width
}
/**
* Used to pair `Viewbox` with it's data
* Which is stored in different array - ViewboxTables
* @param viewbox
*/
getViewboxTableIndex(viewbox: Viewbox): number {
const index = this.viewboxTables.findIndex(
(x) => x.viewboxId === viewbox.id
)
return index
}
/**
* Create and store Handsontable settings for a specific viewbox
* @param viewbox
*/
private createViewboxTableSettings(viewbox: Viewbox): void {
const viewboxTableIndex = this.getViewboxTableIndex(viewbox)
if (viewboxTableIndex === -1) {
this.viewboxHotSettings.set(viewbox.id, {})
return
}
const viewboxTable = this.viewboxTables[viewboxTableIndex]
const calculatedHeight = this.calculateTableHeight(viewbox)
// HOT v17 settings - data will be loaded manually after initialization
const settings: Handsontable.GridSettings = {
colHeaders: viewboxTable.hotTable.colHeaders,
columns: viewboxTable.hotTable.columns,
height: calculatedHeight,
readOnly: true,
modifyColWidth: this.maxWidthCheker,
copyPaste: viewboxTable.hotTable.copyPaste,
// Built here (not in hotTableDefault) because that template is cloneDeep'd,
// which would strip the export callbacks. Client-side view export.
contextMenu: {
items: {
copy_with_column_headers: {},
copy_column_headers_only: {},
export_file: buildExportMenuItem(() => 'viewbox')
}
},
multiColumnSorting: true,
viewportRowRenderingOffset: 50,
filters: true,
dropdownMenu: viewboxTable.hotTable.dropdownMenu,
stretchH: 'all',
cells: viewboxTable.hotTable.cells,
maxRows: viewboxTable.hotTable.maxRows || Infinity,
manualColumnResize: true,
rowHeaders: true,
licenseKey: viewboxTable.hotTable.licenseKey,
theme: 'ht-theme-classic'
}
// Force a new object reference to trigger change detection
this.viewboxHotSettings.set(viewbox.id, { ...settings })
// Force change detection to ensure the template updates
setTimeout(() => {
this.cdf.detectChanges()
// Try to get the HOT instance and force a render
setTimeout(() => {
const hotInstance = this.getViewboxHotInstance(viewbox.id)
if (hotInstance) {
// Load data manually - this is required for HOT v16 Angular wrapper
hotInstance.loadData(viewboxTable.hotTable.data)
hotInstance.render()
}
}, 500)
})
}
/**
* Get stored Handsontable settings for a specific viewbox
* @param viewbox
*/
getViewboxTableSettings(viewbox: Viewbox): Handsontable.GridSettings {
return this.viewboxHotSettings.get(viewbox.id) || {}
}
/**
* Viewbox resize
* @param dragHandle
* @param target
*/
resize(
dragHandle: HTMLElement,
target: HTMLElement
): { width: number; height: number } {
const dragRect = dragHandle.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const width = dragRect.left - targetRect.left + dragRect.width
const height = dragRect.top - targetRect.top + dragRect.height
target.style.width = width + 'px'
target.style.height = height + 'px'
this.setAllHandleTransform()
this.scheduleAfterResizeRefresh()
return {
width,
height
}
}
/**
* Right-edge-only resize - same "distance from the target's opposite,
* fixed edge to the drag handle" math as resize(), but only applies to
* width. The target's left edge (and so viewbox.x) never moves for this
* handle, so no position bookkeeping is needed.
*/
resizeWidth(dragHandle: HTMLElement, target: HTMLElement): number {
const dragRect = dragHandle.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const width = dragRect.left - targetRect.left + dragRect.width
target.style.width = width + 'px'
return width
}
/**
* Bottom-edge-only resize - mirrors resizeWidth() for height. The
* target's top edge never moves for this handle either.
*/
resizeHeight(dragHandle: HTMLElement, target: HTMLElement): number {
const dragRect = dragHandle.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const height = dragRect.top - targetRect.top + dragRect.height
target.style.height = height + 'px'
return height
}
/**
* Left-edge resize - unlike resizeWidth()/resize(), the ANCHOR here is
* the target's opposite (right) edge, which must stay visually fixed
* while the left edge (and so viewbox.x) moves with the pointer. Needs
* startTargetRight/startX/startWidth captured once at drag start (not
* read live) since the target's own rect moves as a *result* of this
* same calculation - a live read would be self-referential.
*
* Takes the raw pointer clientX, not a handle element's rect (unlike
* every other resize* method here) - the resize handle is a child of
* target, and target itself moves during this specific drag (viewbox.x
* feeds CDK's own transform), so getBoundingClientRect() on a child
* would report a position contaminated by target's own prior movement:
* each frame's width would include target's last-frame shift on top of
* the actual pointer delta, compounding into runaway growth and visible
* jitter. Raw pointer coordinates are always absolute viewport
* coordinates, immune to any transform applied to elements under them.
*
* Reads back the actually-rendered width after writing it, rather than
* trusting the raw intended value, because CSS min-width can clamp it -
* if x were derived from the unclamped value, the box would keep
* drifting left even after its width visibly stopped shrinking.
*/
resizeFromLeftEdge(
pointerClientX: number,
target: HTMLElement,
startTargetRight: number,
startX: number,
startWidth: number
): { width: number; x: number } {
const intendedWidth = startTargetRight - pointerClientX
target.style.width = intendedWidth + 'px'
const actualWidth = target.getBoundingClientRect().width
const x = startX + (startWidth - actualWidth)
return { width: actualWidth, x }
}
/**
* Shared debounced tail for every resize path (corner, single-edge,
* left-edge) - persists the new size/position, notifies listeners the
* viewport changed shape, and refreshes every viewbox's Handsontable
* instance to match.
*/
private scheduleAfterResizeRefresh(): void {
this.helperService.debounceCall(1000, () => {
this.viewboxChanged()
this.eventService.dispatchEvent('resize')
// Refresh all viewbox tables after resize and update their settings
this.viewboxes.forEach((viewbox) => {
// Settings will include updated heights when accessed
this.refreshTableAfterResize(viewbox)
})
})
}
/**
* Reads the handle's current translate(x, y) transform (empty/unset
* parses as {x: 0, y: 0}) - used as the baseline a resize drag's pointer
* delta gets added to, so the handle continues from wherever
* setHandleTransform() last placed it rather than jumping.
*/
private parseHandleTranslate(transform: string): { x: number; y: number } {
const match = transform.match(
/translate\(\s*(-?\d+(?:\.\d+)?)px,\s*(-?\d+(?:\.\d+)?)px\s*\)/
)
if (!match) return { x: 0, y: 0 }
return { x: parseFloat(match[1]), y: parseFloat(match[2]) }
}
/**
* Starts a corner-handle resize drag on plain pointer events rather than
* CDK's cdkDrag - the handle's transform is also written directly by
* setHandleTransform() (via resize() below) after every move, so it needs
* to be the ONLY thing driving that element's transform. A cdkDrag on the
* handle would track its own transform from cumulative pointer delta,
* unaware of externally-forced values, letting the handle's visual
* position and CDK's internal drag state diverge from each other.
*/
startResize(
event: PointerEvent,
dragHandle: HTMLElement,
resizeBox: HTMLElement,
viewbox: Viewbox
) {
event.preventDefault()
event.stopPropagation()
this.ngZone.runOutsideAngular(() => {
const startX = event.clientX
const startY = event.clientY
const startTranslate = this.parseHandleTranslate(
dragHandle.style.transform
)
const onPointerMove = (moveEvent: PointerEvent) => {
const dx = moveEvent.clientX - startX
const dy = moveEvent.clientY - startY
dragHandle.style.transform = `translate(${startTranslate.x + dx}px, ${startTranslate.y + dy}px)`
const newDimnesion = this.resize(dragHandle, resizeBox)
viewbox.width = newDimnesion.width
viewbox.height = newDimnesion.height
}
const onPointerUp = () => {
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
this.endActiveResize = undefined
}
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
// Tracked so ngOnDestroy can remove these document-level listeners
// if the component is torn down mid-drag (e.g. navigating away
// while resizing) - they'd otherwise outlive the elements they
// reference.
this.endActiveResize = onPointerUp
})
}
/**
* Right-edge-only resize drag - same pointer-event approach as
* startResize(), affecting only width. resize()'s own debounced refresh
* is bundled into resize() itself; resizeWidth()/resizeHeight()/
* resizeFromLeftEdge() are pure, so every handler below schedules it
* explicitly.
*/
startResizeRight(
event: PointerEvent,
dragHandle: HTMLElement,
target: HTMLElement,
viewbox: Viewbox
) {
event.preventDefault()
event.stopPropagation()
this.ngZone.runOutsideAngular(() => {
const startX = event.clientX
const startTranslate = this.parseHandleTranslate(
dragHandle.style.transform
)
const onPointerMove = (moveEvent: PointerEvent) => {
const dx = moveEvent.clientX - startX
dragHandle.style.transform = `translate(${startTranslate.x + dx}px, ${startTranslate.y}px)`
viewbox.width = this.resizeWidth(dragHandle, target)
this.setAllHandleTransform()
this.scheduleAfterResizeRefresh()
}
const onPointerUp = () => {
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
this.endActiveResize = undefined
}
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
this.endActiveResize = onPointerUp
})
}
/**
* Bottom-edge-only resize drag, affecting only height.
*/
startResizeBottom(
event: PointerEvent,
dragHandle: HTMLElement,
target: HTMLElement,
viewbox: Viewbox
) {
event.preventDefault()
event.stopPropagation()
this.ngZone.runOutsideAngular(() => {
const startY = event.clientY
const startTranslate = this.parseHandleTranslate(
dragHandle.style.transform
)
const onPointerMove = (moveEvent: PointerEvent) => {
const dy = moveEvent.clientY - startY
dragHandle.style.transform = `translate(${startTranslate.x}px, ${startTranslate.y + dy}px)`
viewbox.height = this.resizeHeight(dragHandle, target)
this.setAllHandleTransform()
this.scheduleAfterResizeRefresh()
}
const onPointerUp = () => {
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
this.endActiveResize = undefined
}
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
this.endActiveResize = onPointerUp
})
}
/**
* Left-edge resize drag - unlike the handles above, this one also moves
* viewbox.x (the box's own left edge). viewbox.x feeds CDK's
* [cdkDragFreeDragPosition] input binding, which only takes effect when
* Angular change detection actually runs - so unlike every other resize
* handle here (which stay outside the zone for the whole drag), this one
* re-enters the zone specifically for the x update, or the box's
* CDK-driven position would silently fall out of sync with its own
* rendered width.
*
* startTargetRight/startX/startWidth are captured once at drag start,
* not read live - see resizeFromLeftEdge's own doc comment for why.
*/
startResizeLeft(
event: PointerEvent,
dragHandle: HTMLElement,
target: HTMLElement,
viewbox: Viewbox
) {
event.preventDefault()
event.stopPropagation()
const startTargetRight = target.getBoundingClientRect().right
const startX = viewbox.x
const startWidth = viewbox.width
this.ngZone.runOutsideAngular(() => {
const startPointerX = event.clientX
const startTranslate = this.parseHandleTranslate(
dragHandle.style.transform
)
const onPointerMove = (moveEvent: PointerEvent) => {
const dx = moveEvent.clientX - startPointerX
dragHandle.style.transform = `translate(${startTranslate.x + dx}px, ${startTranslate.y}px)`
const { width, x } = this.resizeFromLeftEdge(
moveEvent.clientX,
target,
startTargetRight,
startX,
startWidth
)
viewbox.width = width
this.ngZone.run(() => {
viewbox.x = x
})
this.setAllHandleTransform()
this.scheduleAfterResizeRefresh()
}
const onPointerUp = () => {
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
this.endActiveResize = undefined
}
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
this.endActiveResize = onPointerUp
})
}
/**
* Bottom-left corner resize drag - combines startResizeLeft's width/x
* handling with startResizeBottom's height handling.
*/
startResizeBottomLeft(
event: PointerEvent,
dragHandle: HTMLElement,
target: HTMLElement,
viewbox: Viewbox
) {
event.preventDefault()
event.stopPropagation()
const startTargetRight = target.getBoundingClientRect().right
const startX = viewbox.x
const startWidth = viewbox.width
this.ngZone.runOutsideAngular(() => {
const startPointerX = event.clientX
const startPointerY = event.clientY
const startTranslate = this.parseHandleTranslate(
dragHandle.style.transform
)
const onPointerMove = (moveEvent: PointerEvent) => {
const dx = moveEvent.clientX - startPointerX
const dy = moveEvent.clientY - startPointerY
dragHandle.style.transform = `translate(${startTranslate.x + dx}px, ${startTranslate.y + dy}px)`
const { width, x } = this.resizeFromLeftEdge(
moveEvent.clientX,
target,
startTargetRight,
startX,
startWidth
)
const height = this.resizeHeight(dragHandle, target)
viewbox.width = width
viewbox.height = height
this.ngZone.run(() => {
viewbox.x = x
})
this.setAllHandleTransform()
this.scheduleAfterResizeRefresh()
}
const onPointerUp = () => {
document.removeEventListener('pointermove', onPointerMove)
document.removeEventListener('pointerup', onPointerUp)
this.endActiveResize = undefined
}
document.addEventListener('pointermove', onPointerMove)
document.addEventListener('pointerup', onPointerUp)
this.endActiveResize = onPointerUp
})
}
/**
* Sets every resize handle (corner, corner-left, right/bottom/left edges)
* to its correct position on every box, keyed off each box's own id
* suffix so a handle from one box is never matched to another's rect.
*/
setAllHandleTransform() {
const findHandle = (query: QueryList<ElementRef>, handleId: string) =>
query.find((el) => el.nativeElement.id === handleId)?.nativeElement
this.resizeBoxQuery.forEach((resizeBox: ElementRef) => {
const rect = resizeBox.nativeElement.getBoundingClientRect()
const boxId = resizeBox.nativeElement.id
this.setHandleTransform(
findHandle(this.dragHandleCornerQuery, `handle_${boxId}`),
rect,
'both'
)
this.setHandleTransform(
findHandle(
this.dragHandleCornerLeftQuery,
`handle_corner_left_${boxId}`
),
rect,
'bottomLeft'
)
this.setHandleTransform(
findHandle(this.dragHandleRightQuery, `handle_right_${boxId}`),
rect,
'x'
)
this.setHandleTransform(
findHandle(this.dragHandleBottomQuery, `handle_bottom_${boxId}`),
rect,
'y'
)
this.setHandleTransform(
findHandle(this.dragHandleLeftQuery, `handle_left_${boxId}`),
rect,
'left'
)
})
}
/**
* Sets the 'resize' handle in the correct corner position of the box
*/
setHandleTransform(
dragHandle: HTMLElement,
targetRect: ClientRect | DOMRect,
position: 'x' | 'y' | 'both' | 'left' | 'bottomLeft'
) {
const dragRect = dragHandle.getBoundingClientRect()
let translateX = targetRect.width - dragRect.width
let translateY = targetRect.height - dragRect.height
//Fine tune
translateX += 5
translateY += 5
if (position === 'x') {
dragHandle.style.transform = `translate(${translateX}px, 0)`
}
if (position === 'y') {
dragHandle.style.transform = `translate(0, ${translateY}px)`
}
if (position === 'both') {
dragHandle.style.transform = `translate(${translateX}px, ${translateY}px)`
}
// 'left'/'bottomLeft': the handle sits at the box's left edge, which
// never moves relative to the box itself regardless of size (unlike
// the right edge, whose offset from the box's own left edge depends on
// targetRect.width) - a small fixed fine-tune mirroring 'x' mode's own
// +5, on the opposite side.
if (position === 'left') {
dragHandle.style.transform = `translate(-5px, 0)`
}
if (position === 'bottomLeft') {
dragHandle.style.transform = `translate(-5px, ${translateY}px)`
}
}
/**
* When clicked on Viewbox, it will focus it
* Focused Viewbox is always bring to top
* @param viewbox
*/
focusViewbox(viewbox: Viewbox) {
this.viewboxes.map((vbox) => {
vbox.focused = false
})
viewbox.focused = true
}
/**
* On drag end Viewbox position is updated
* As well as the URL
* @param event
* @param viewbox
*/
viewboxDragEnded(event: CdkDragEnd, viewbox: Viewbox) {
let element = event.source.getRootElement()
let boundingClientRect = element.getBoundingClientRect()
viewbox.x = boundingClientRect.left
viewbox.y = boundingClientRect.top
this.viewboxChanged()
}
/**
* Snap to grid calculates the best grid possible
* for given screen width and height
* Configurable options are (in PX):
* gap - gaps between boxes and left/right sides
* topOffset - clearance on the top
* bottomOffset - clearance on the bottom
*/
snapToGrid() {
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
const gap = 5 //px configurable
const topOffset = 250 //px configurable
const bottomOffset = 60 //px configurable
const elementsInTopRow = Math.ceil(this.viewboxes.length / 2)
const elementsInBottomRow = Math.floor(this.viewboxes.length / 2)
const noOfGapsTop = elementsInTopRow + 1
const noOfGapsBottom = elementsInBottomRow + 1
const viewboxWidthTop = (windowWidth - gap * noOfGapsTop) / elementsInTopRow
const viewboxWidthBottom =
(windowWidth - gap * noOfGapsBottom) / elementsInBottomRow
const viewboxHeight = (windowHeight - topOffset - bottomOffset) / 2
let x = 0
let y = topOffset
let height = viewboxHeight
let viewbox_i = 0
let row_i = 0
for (let i = 0; i < this.viewboxes.length; i++) {
let viewbox = this.viewboxes[i]
let topRow = !(i > elementsInTopRow - 1)
const width = topRow ? viewboxWidthTop : viewboxWidthBottom
if (!topRow && row_i === 0) {
viewbox_i = 0
row_i++
x = 0
}
viewbox.x = gap + x + viewbox_i * (width + gap)
viewbox.y = y + row_i * (height + gap)
viewbox.width = width
viewbox.height = height
viewbox_i++
}
this.viewboxChanged()
setTimeout(() => {
this.setAllHandleTransform()
// Refresh all tables after snap to grid
this.viewboxes.forEach((viewbox) => {
// Settings will include correct height when accessed
this.refreshTableAfterResize(viewbox)
})
})
}
minimizeAll() {
this.viewboxes.forEach((viewbox: Viewbox) => {
viewbox.minimized = true
})
this.viewboxChanged()
}
restoreAll() {
this.viewboxes.forEach((viewbox: Viewbox) => {
viewbox.minimized = false
})
this.viewboxChanged()
}
/**
* Resets Viewbox to default position (top right corner)
* @param viewbox
*/
resetPosSize(viewbox: Viewbox) {
viewbox.x = window.innerWidth - this.defaultConfig.width
viewbox.y = this.defaultConfig.y
viewbox.width = this.defaultConfig.width
viewbox.height = this.defaultConfig.height
this.viewboxChanged()
}
minimize(viewbox: Viewbox) {
viewbox.minimized = true
this.viewboxChanged()
}
restore(viewbox: Viewbox) {
viewbox.minimized = false
this.viewboxChanged()
// Refresh table after restoring
setTimeout(() => {
// Settings will include correct height when accessed
this.refreshTableAfterResize(viewbox)
}, 100)
}
collapse(viewbox: Viewbox) {
viewbox.collapsed = true
this.viewboxChanged()
}
expand(viewbox: Viewbox) {
viewbox.collapsed = false
this.viewboxChanged()
// Refresh table after expanding
setTimeout(() => {
// Settings will include correct height when accessed
this.refreshTableAfterResize(viewbox)
}, 100)
}
/**
* Close Viewbox and remove it's data stored in `paired ViewboxTable array`
* @param viewbox
*/
close(viewbox: Viewbox) {
const index = this.viewboxes.findIndex((vb) => vb.id === viewbox.id)
const viewtableIndex = this.viewboxTables.findIndex(
(vbt) => vbt.viewboxId === viewbox.id
)
if (index > -1) this.viewboxes.splice(index, 1)
if (viewtableIndex > -1) this.viewboxTables.splice(viewtableIndex, 1)
// Clean up settings for this viewbox
this.viewboxHotSettings.delete(viewbox.id)
if (this.selectedViewbox?.id === viewbox.id) {
this.unsetSelectedViewbox()
}
globals.viewboxes[viewbox.id] = this.helperService.deepClone(initFilter)
this.viewboxChanged()
}
/**
* Selects Viewbox in the Viewbox Manager for the columns to be configurated
* @param viewbox
*/
selectViewbox(viewbox: Viewbox) {
if (
this.selectedViewboxTable === undefined &&
this.selectedViewbox === undefined
) {
this.resetSelectedViewbox(viewbox)
} else {
if (viewbox.id === this.selectedViewbox?.id) {
this.unsetSelectedViewbox()
} else {
this.resetSelectedViewbox(viewbox)
}
}
}
/**
* Column drop called after column dragged and dropped
* for Viewbox column to be configurated
* @param event
* @returns
*/
columnsDrop(event: CdkDragDrop<string[]>) {
if (!this.selectedViewboxTable?.hotTable.colHeadersHidden) return
if (event.previousContainer === event.container) {
moveItemInArray(
event.container.data,
event.previousIndex,
event.currentIndex
)
} else {
transferArrayItem(
event.previousContainer.data,
event.container.data,
event.previousIndex,
event.currentIndex
)
}
if (
this.selectedViewboxTable.hotTable &&
typeof this.selectedViewboxTable.hotTable.colHeaders === 'object'
) {
const colProp = event.item.data
const finalIndex = event.currentIndex
this.updateColumnOrderHot(
colProp,
finalIndex,
this.selectedViewboxTable.viewboxId
)
}
}
/**
* Viewbox Manager searching for columns to be added
* @param inputRef Input element
* @param value
* @param columns
*/
onColsearchChange(
inputRef: AutocompleteComponent,
value: string,
columns: string[]
) {
const index = columns.indexOf(value)
columns.splice(index, 1)
inputRef.value = ''
if (this.selectedViewboxTable?.hotTable) {
this.selectedViewboxTable.hotTable.colHeadersVisible.push(value)
this.updateHotColumns(
this.selectedViewboxTable?.hotTable.colHeadersHidden,
this.selectedViewboxTable.viewboxId
)
this.updateColumnOrderHot(
value,
this.selectedViewboxTable.hotTable.colHeadersVisible.length - 1,
this.selectedViewboxTable.viewboxId
)
}
}
/**
* Viewbox Manager removing the column
* @param column
*/
onColRemove(column: string) {
if (this.selectedViewboxTable?.hotTable) {
const index =
this.selectedViewboxTable.hotTable.colHeadersVisible.indexOf(column)
this.selectedViewboxTable.hotTable.colHeadersVisible.splice(index, 1)
this.selectedViewboxTable.hotTable.colHeadersHidden.push(column)
this.updateHotColumns(
this.selectedViewboxTable?.hotTable.colHeadersHidden,
this.selectedViewboxTable.viewboxId
)
this.updateColumnOrderHot(
column,
this.selectedViewboxTable.hotTable.colHeadersVisible.length,
this.selectedViewboxTable.viewboxId
)
}
}
/**
* Re setting the viewbox from parameter to selected viewbox
* And sets the correct data for that viewbox
* @param viewbox
*/
resetSelectedViewbox(viewbox: Viewbox) {
this.selectedViewbox = viewbox
this.selectedViewboxTable = this.viewboxTables.find(
(vbt) => vbt.viewboxId === viewbox.id
)
}
unsetSelectedViewbox() {
this.selectedViewbox = undefined
this.selectedViewboxTable = undefined
}
stopPropagation(event: any) {
event.stopPropagation()
}
/**
* Opens filter dialog for given Viewbox
* @param viewbox
*/
openFilter(viewbox: Viewbox) {
this.selectViewbox(viewbox)
const viewboxTable = this.viewboxTables[this.getViewboxTableIndex(viewbox)]
this.filterLibds = `${viewbox.library}.${viewbox.table}`
this.filteringViewbox = viewbox
this.filter = true
this.cdf.detectChanges()
// Table columns when we have them (the normal, cheapest source); when the
// view was aborted by the row limit there is no loaded table to read them
// from, so the store falls back to public/getcols.
const cols =
viewboxTable && viewboxTable.hotTable ? viewboxTable.hotTable.cols : []
this.sasStoreService
.setQueryVariablesFromTable(this.filterLibds, cols)
.catch((err: any) =>
this.eventService.catchResponseError('public/getcols', err)
)
}
/**
* Opens table edit in new tab
* @param viewbox
*/
openTableEdit(viewbox: Viewbox) {
const libDataset = viewbox.library + '.' + viewbox.table
let url = location.href.slice(0, location.href.indexOf('#'))
url = `${url}#/editor/${libDataset}`
window.open(url, '_blank')
}
/**
* Resets the data in filter dialog
* It will also reset the filtering in the given Viewbox
*/
resetFilter() {
if (this.filteringViewbox) {
this.filteringViewbox.filter_pk = '0'
this.reloadTableData(this.filteringViewbox)
this.filter = false
this.viewboxChanged()
globals.viewboxes[this.filteringViewbox.id] =
this.helperService.deepClone(initFilter)
}
}
/**
* Sending filtering request for given Viewbox
*/
sendClause() {
this.filterLoading = true
let nullVariableArr = []
let emptyVariablesArr = []
// to check number of empty clauses
if (typeof this.clauses === 'undefined') {
this.nullVariables = true
this.filterLoading = false
return
} else {
let query = this.clauses.queryObj
for (let index = 0; index < query.length; index++) {
const el = query[index].elements
nullVariableArr = el.filter(function (item: any) {
return item.variable === null
})
if (nullVariableArr.length) {
emptyVariablesArr.push(el)
}
}
}
if (emptyVariablesArr.length) {
this.nullVariables = true
this.filterLoading = false
return
} else {
try {
if (this.clauses !== undefined && this.filterLibds) {
const filterQuery: FilterQuery = {
groupLogic: this.clauses.groupLogic,
filterGroups: []
}
this.clauses.queryObj.forEach((group: any) => {
const filterGroup: FilterGroup = {
filterClauses: []
}
group.elements.forEach((clause: any) => {
filterGroup.filterClauses.push(
this.helperService.deepClone(clause)
)
})
filterGroup.clauseLogic = group.clauseLogic
filterQuery.filterGroups.push(
this.helperService.deepClone(filterGroup)
)
})
const filterQueryClauseTable =
this.sasStoreService.createFilterQueryTable(filterQuery)
this.sasStoreService
.saveQuery(this.filterLibds, filterQueryClauseTable)
.then((res: any) => {
const id = res.result[0].FILTER_RK
const table = res.result[0].FILTER_TABLE
this.filteringViewbox!.filter_pk = id
this.loadData(this.filterLibds!, this.filteringViewbox!).then(
() => {
this.filter = false
this.filterLoading = false
}
)
this.viewboxChanged()
})
.catch((err: any) => {
this.filterLoading = false
})
}
} catch (error: any) {
this.filterLoading = false
}
}
}
async searchTable(inputElement: any, viewbox: Viewbox) {
viewbox.searchLoading = true
let searchValue = inputElement.value
let libDataset = viewbox.library + '.' + viewbox.table
let filter_pk = parseInt(viewbox.filter_pk || '0')
const viewboxTable = this.viewboxTables.find(
(vbt) => vbt.viewboxId === viewbox.id
)
if (!viewboxTable) return
await this.sasStoreService
.viewDataSearch(searchValue, viewbox.searchNumeric, libDataset, filter_pk)
.then((res: any) => {
if (!res.sasparams && !res.viewData) {
viewbox.searchLoading = true
return
}
viewboxTable.hotTable.data = res.viewdata
// Update settings with new data
this.createViewboxTableSettings(viewbox)
})
.catch((err: any) => {
this.loggerService.error(err)
})
viewbox.searchLoading = false
}
async reloadTableData(viewbox: Viewbox) {
const libDataset = `${viewbox.library}.${viewbox.table}`
viewbox.loadingData = true
await this.reloadData(libDataset, viewbox)
viewbox.loadingData = false
this.eventService.dispatchEvent('resize') //Force HOT refresh
}
/**
* Updates the HOT columns Order and Visibility
* @param hiddenColProps Array of indexes of columns to be hidden
* @param viewboxId Viewbox ID for which to apply
*/
private updateHotColumns(hiddenColProps: string[], viewboxId: number) {
this.updateHiddenColumnsHot(hiddenColProps, viewboxId)
this.setColumnOrder(viewboxId)
// Settings will be regenerated when accessed
}
/**
* HOT Columns ordering
* @param colProp Column name
* @param finalIndex Index of where to position it
* @param viewboxId
*/
private updateColumnOrderHot(
colProp: string,
finalIndex: number,
viewboxId: number
) {
const hotInstance = this.getViewboxHotInstance(viewboxId)
if (hotInstance) {
const column = hotInstance.propToCol(colProp) as number
const plugin = hotInstance.getPlugin('manualColumnMove')
plugin.moveColumn(column, finalIndex)
hotInstance.render()
this.setColumnOrder(viewboxId)
}
}
public tableEditExists(viewbox: Viewbox) {
const editTables = globals.editor.libsAndTables
const library = viewbox.library
const table = viewbox.table
// If this line is undefined, that means startupservice failed or similar.
if (!editTables[library]) return false
return editTables[library].includes(table)
}
private setColumnOrder(viewboxId: number) {
const viewbox = this.viewboxes.find((vb) => vb.id === viewboxId)
if (viewbox) {
const columnsOrder = this.createColumnOrder(viewboxId)
viewbox.columns = columnsOrder.length > 0 ? columnsOrder : viewbox.columns
}
this.viewboxChanged()
}
/**
* Creating column order in such format to be encoded in URL
* @param viewboxId
*/
private createColumnOrder(viewboxId: number): number[] {
const hotInstance = this.getViewboxHotInstance(viewboxId)
if (!hotInstance) return []
const hotCols = hotInstance.getColHeader() as string[]
const sasCols = this.selectedViewboxTable?.hotTable.colHeaders as string[]
if (!sasCols) return []
const columnsVisibleLength: number =
this.selectedViewboxTable?.hotTable?.colHeadersVisible.length || 5 //if unexpected happens limit will Be 5 columns
const columnOrder: number[] = []
hotCols.map((hotCol: string, index: number) => {
if (index < columnsVisibleLength) {
const indexofSasCol = sasCols.indexOf(hotCol)
if (indexofSasCol > -1) columnOrder.push(indexofSasCol)
}
})
return columnOrder
}
private updateHiddenColumnsHot(colProps: string[], viewboxId: number) {
const hotInstance = this.getViewboxHotInstance(viewboxId)
if (hotInstance) {
const columns = colProps.map((prop: string) => {
return hotInstance.propToCol(prop) as number
})
hotInstance.updateSettings({
hiddenColumns: {
columns: columns
}
})
hotInstance.render()
}
}
/**
* Calculate available height for Handsontable
* @param viewbox The viewbox to calculate height for
* @returns Available height in pixels
*/
calculateTableHeight(viewbox: Viewbox): number {
// Calculate the exact height of the content div
const dragHandleHeight = 20
const searchFormHeight = 36
const padding = 2
// Return the exact remaining height for the table with minimum height
const calculatedHeight =
viewbox.height - dragHandleHeight - searchFormHeight - padding
return calculatedHeight
}
/**
* Refresh Handsontable instance after resize
* @param viewbox The viewbox to refresh
*/
refreshTableAfterResize(viewbox: Viewbox): void {
const hotInstance = this.getViewboxHotInstance(viewbox.id)
if (hotInstance) {
// Force the table to recalculate its dimensions
setTimeout(() => {
try {
// Update height setting and refresh
hotInstance.updateSettings({
height: this.calculateTableHeight(viewbox)
})
hotInstance.refreshDimensions()
hotInstance.render()
} catch (error) {
// If refresh fails, try again later
setTimeout(() => {
try {
hotInstance.updateSettings({
height: this.calculateTableHeight(viewbox)
})
hotInstance.refreshDimensions()
} catch (e) {
console.warn(
'Failed to refresh HOT dimensions for viewbox',
viewbox.id,
e
)
}
}, 500)
}
}, 100)
}
}
/**
*
* @param viewboxId
* @returns HOT Instance from the given Viewbox
*/
private getViewboxHotInstance(viewboxId?: number): Handsontable | undefined {
if (!viewboxId || !this.hotTableComponents) return
// Find the component corresponding to this viewbox
// Since we only show one table per viewbox and they're rendered in order,
// we can match by the viewbox's position in the array
const viewboxIndex = this.viewboxes.findIndex((vb) => vb.id === viewboxId)
if (viewboxIndex === -1) return
// Get the HOT component at this index
const hotComponents = this.hotTableComponents.toArray()
let hotComponentIndex = 0
// Count how many viewboxes before this one have loaded tables
for (let i = 0; i < viewboxIndex; i++) {
if (this.getViewboxTableIndex(this.viewboxes[i]) > -1) {
hotComponentIndex++
}
}
const hotTableComponent = hotComponents[hotComponentIndex]
return hotTableComponent?.hotInstance || undefined
}
/**
* Called after any Viewbox change that needs to be stored to URL
* It does the data encoding and storing to URL
*/
private viewboxChanged() {
let queryParams: any
const urlData = this.encodeUrlData(this.viewboxes)
if (urlData.length > 0) {
queryParams = {
viewboxes: urlData
}
}
this.router.navigate([], {
relativeTo: this.activatedRoute,
queryParams
})
this.prepareFilterCache()
}
/**
* Prepare the init values to `globals` for the filtering
* that will be used in caching values later
*/
private prepareFilterCache() {
for (let viewbox of this.viewboxes) {
if (!globals.viewboxes[viewbox.id])
globals.viewboxes[viewbox.id] = this.helperService.deepClone(initFilter)
if (viewbox.query && viewbox.query.length > 0) {
const viewboxTable = this.viewboxTables.find(
(vbt) => vbt.viewboxId === viewbox.id
)
const globalsPath = `viewboxes.${viewbox.id}`
globals.viewboxes[viewbox.id].filter.query = viewbox.query
globals.viewboxes[viewbox.id].filter.libds =
viewbox.library + '.' + viewbox.table
this.sasStoreService.initializeGlobalFilterClause(
globalsPath,
viewboxTable?.hotTable.cols
)
}
}
}
/**
* It will encode/inject viewboxes data from URL
* Url data pattern:
*
* {id}-{library}-{table}-{width}-{height}-{x}-{y}-{collapsed}-{minimized}-{filter_pk}-{columns}
* 1-library-table-100-100-10-10-1-1;library2-table2-100-100-10-10-0-0-3-0123456
*
* ; <- single viewbox separation symbol
*/
private encodeUrlData(viewboxes: Viewbox[]): string {
let urlData: string = ''
viewboxes.map((viewbox: Viewbox, index: number) => {
urlData += `${viewbox.id}-${viewbox.library}-${viewbox.table}-${
viewbox.width
}-${viewbox.height}-${viewbox.x}-${viewbox.y}-${
viewbox.collapsed ? 1 : 0
}-${viewbox.minimized ? 1 : 0}-${viewbox.filter_pk || 0}${
viewbox.columns && viewbox.columns.length > 0
? '-' + viewbox.columns?.join(',')
: ''
}`
if (index !== viewboxes.length - 1) urlData += ';'
})
return urlData
}
/**
* It will decode/parse viewboxes data from URL
* Url data pattern:
*
* {id}-{library}-{table}-{width}-{height}-{x}-{y}-{collapsed}-{minimized}-{filter_pk}-{columns}
* 1-library-table-100-100-10-10-1-1;library2-table2-100-100-10-10-0-0-3-0123456
*
* ; <- single viewbox separation symbol
*/
private decodeUrlData(urlData: string): Viewbox[] {
const urlDataMap = {
id: 0,
library: 1,
table: 2,
width: 3,
height: 4,
x: 5,
y: 6,
collapsed: 7,
minimized: 8,
filter_pk: 9,
columns: 10
}
let viewboxes: Viewbox[] = []
const separatedViewboxes = urlData.split(';')
separatedViewboxes.map((viewboxString: string) => {
const viewboxDataArr = viewboxString.split('-')
viewboxes.push({
id: parseInt(viewboxDataArr[urlDataMap.id]),
library: viewboxDataArr[urlDataMap.library],
table: viewboxDataArr[urlDataMap.table],
width: parseInt(viewboxDataArr[urlDataMap.width]),
height: parseInt(viewboxDataArr[urlDataMap.height]),
x: parseInt(viewboxDataArr[urlDataMap.x]),
y: parseInt(viewboxDataArr[urlDataMap.y]),
collapsed: !!parseInt(viewboxDataArr[urlDataMap.collapsed]),
minimized: !!parseInt(viewboxDataArr[urlDataMap.minimized]),
columns:
viewboxDataArr[urlDataMap.columns]
?.split(',')
.map((x) => parseInt(x)) || [],
filter_pk: viewboxDataArr[urlDataMap.filter_pk]
})
})
return viewboxes
}
ngOnDestroy(): void {
this._query?.unsubscribe()
this.endActiveResize?.()
}
}
<clr-modal
appDragNdrop
[clrModalOpen]="viewboxModal"
(clrModalOpenChange)="clrModalOpenChange($event)"
[clrModalClosable]="true"
[clrModalSize]="'lg'"
class="root-modal position-relative"
>
<h3 class="modal-title">
Viewboxes
@if (licenceState.value.viewbox_limit !== Infinity) {
<span class="licence-notice"
>To unlock more then {{ licenceState.value.viewbox_limit }}
{{ licenceState.value.viewbox_limit === 1 ? 'viewbox' : 'viewboxes' }},
contact support@datacontroller.io</span
>
}
</h3>
<div class="modal-body d-flex">
<div class="add-new d-flex clr-flex-column">
<p class="mt-0">Add new</p>
<hr class="w-100" />
@if (librariesLoading) {
<div class="my-10-mx-auto text-center">
<clr-spinner clrMedium></clr-spinner>
</div>
}
@if (viewboxLimitReached) {
<p class="m-0 viewbox-limit-notice">Viewbox open limit reached</p>
}
@if (viewboxModal) {
<dc-tree
[libraryList]="libraries"
(libraryOnClickEmitter)="libraryOnClick($event)"
(tableOnClickEmitter)="tableOnClick($event)"
></dc-tree>
}
</div>
<div class="currently-open">
<div class="d-flex clr-justify-content-between">
<p class="mt-0">Currently open</p>
<div>
<a
(click)="snapToGrid()"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left ml-3"
>
<clr-icon aria-hidden="true" shape="grid-view" size="19"></clr-icon>
<span class="tooltip-content">Snap viewboxes to grid</span>
</a>
<a
(click)="minimizeAll()"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left ml-3"
>
<clr-icon aria-hidden="true" shape="minus" size="19"></clr-icon>
<span class="tooltip-content">Minimize all viewboxes</span>
</a>
<a
(click)="restoreAll()"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left ml-3"
>
<clr-icon
aria-hidden="true"
shape="window-restore"
size="19"
></clr-icon>
<span class="tooltip-content">Restore all viewboxes</span>
</a>
</div>
</div>
<hr />
@if (viewboxes.length < 1) {
<p class="text-center w-100 m-0">
Pick table from the list on the left
</p>
}
@for (viewbox of viewboxes; track viewbox) {
<div
(click)="selectViewbox(viewbox)"
[class.selected]="selectedViewbox?.id === viewbox.id"
class="open-viewbox d-flex clr-justify-content-between clr-align-items-center"
>
<div class="d-flex clr-align-items-center">
#{{ viewbox.id }} {{ viewbox.library }}.{{ viewbox.table }}
@if (viewbox.loadingData) {
<clr-spinner clrSmall class="ml-3"></clr-spinner>
}
</div>
<div>
<a
(click)="resetPosSize(viewbox); stopPropagation($event)"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left"
[class.disabled]="viewbox.x === 0 && viewbox.y === 0"
>
<clr-icon
aria-hidden="true"
shape="cursor-move"
size="13"
></clr-icon>
<span class="tooltip-content">Reset position and size</span>
</a>
@if (!viewbox.minimized) {
<a
(click)="minimize(viewbox); stopPropagation($event)"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left ml-3"
>
<clr-icon aria-hidden="true" shape="minus" size="19"></clr-icon>
<span class="tooltip-content">Minimize viewbox</span>
</a>
}
@if (viewbox.minimized) {
<a
(click)="restore(viewbox); stopPropagation($event)"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left ml-3"
>
<clr-icon
aria-hidden="true"
shape="window-restore"
size="19"
></clr-icon>
<span class="tooltip-content">Restore viewbox</span>
</a>
}
<a
(click)="close(viewbox); stopPropagation($event)"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-md tooltip-bottom-left ml-3"
>
<clr-icon aria-hidden="true" shape="times" size="19"></clr-icon>
<span class="tooltip-content">Close viewbox</span>
</a>
</div>
</div>
}
<div
class="d-flex clr-justify-content-between clr-align-items-center mt-10"
>
<p class="m-0">
Configuration ({{
selectedViewboxTable?.viewboxLibDataset
? '#' +
selectedViewboxTable?.viewboxId +
' ' +
selectedViewboxTable?.viewboxLibDataset
: 'select viewbox from above'
}})
</p>
<!-- <clr-icon shape="times" size="19"></clr-icon> -->
</div>
<hr />
<div
class="configuration-wrapper"
*ngVar="{
colHeadersHidden: $any(
selectedViewboxTable?.hotTable?.colHeadersHidden
),
colHeadersVisible: $any(
selectedViewboxTable?.hotTable?.colHeadersVisible
),
headerPks: $any(selectedViewboxTable?.hotTable?.headerPks)
} as hotColumns"
cdkDropListGroup
>
@if (hotColumns.colHeadersHidden) {
<div class="cols-search">
<!-- <input #columnSearch clrInput (input)="columnSearchChange($event, selectedViewboxTable)" placeholder="Search columns"/> -->
<app-autocomplete
#input
type="text"
autocomplete="off"
[inputId]="'viewbox-column-search'"
autofocus
placeholder="Search columns"
tabindex="0"
[emitOnlySelected]="true"
(valueChange)="
onColsearchChange(input, $event, hotColumns.colHeadersHidden)
"
>
@for (column of hotColumns.colHeadersHidden; track column) {
<option>
{{ column }}
</option>
}
</app-autocomplete>
</div>
}
@if (hotColumns.colHeadersVisible) {
<p class="mt-0">Columns To Display (drag and re-order)</p>
<div
class="cols-list"
cdkDropList
[cdkDropListData]="hotColumns.colHeadersVisible"
(cdkDropListDropped)="columnsDrop($event)"
>
@for (column of hotColumns.colHeadersVisible; track column) {
<div
class="col-box column-{{ column }}"
[class.cursor-default-i]="hotColumns.headerPks.includes(column)"
[class.primaryKeyHeaderStyle]="
hotColumns.headerPks.includes(column)
"
[cdkDragData]="column"
[cdkDragDisabled]="hotColumns.headerPks.includes(column)"
cdkDrag
>
<span>
{{ column }}
{{
selectedViewboxTable?.hotTable?.$dataformats?.vars?.[column]
?.type === 'num'
? '(numeric)'
: ''
}}
@if (hotColumns.headerPks.includes(column)) {
<clr-icon aria-hidden="true" shape="key"></clr-icon>
}
</span>
@if (!hotColumns.headerPks.includes(column)) {
<clr-icon
(click)="onColRemove(column)"
aria-label="Remove column"
shape="trash"
class="cursor-pointer"
></clr-icon>
}
</div>
}
</div>
}
</div>
</div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-sm btn-outline"
(click)="viewboxModal = false"
>
Ok
</button>
</div>
</clr-modal>
<div class="viewboxes-container">
@for (viewbox of viewboxes; track viewbox; let index = $index) {
<div
(mousedown)="focusViewbox(viewbox)"
[class.focused]="viewbox.focused"
[style.display]="viewbox.minimized ? 'none' : ''"
[style.width.px]="viewbox.width"
[style.height.px]="viewbox.height"
[style.pointer-events]="viewbox.collapsed ? 'none' : ''"
[style.box-shadow]="viewbox.collapsed ? 'none' : ''"
class="viewbox"
cdkDragBoundary=".viewboxes-container"
cdkDrag
[cdkDragFreeDragPosition]="{ x: viewbox.x, y: viewbox.y }"
(cdkDragEnded)="viewboxDragEnded($event, viewbox)"
tabindex="0"
[id]="'viewbox_' + viewbox.id"
#resizeBox
>
<div
class="drag-handle d-flex clr-justify-content-between clr-align-items-center"
cdkDragHandle
>
<span class="table-title">
#{{ viewbox.id }} {{ viewbox.library }}.{{ viewbox.table }}
</span>
<div class="actions">
@if (!viewbox.collapsed) {
<clr-icon
(click)="collapse(viewbox)"
aria-label="Collapse"
shape="angle top"
></clr-icon>
}
@if (viewbox.collapsed) {
<clr-icon
(click)="expand(viewbox)"
aria-label="Expand"
shape="angle down"
></clr-icon>
}
<clr-icon
(click)="minimize(viewbox)"
aria-label="Minimize"
shape="minus"
></clr-icon>
<clr-icon
(click)="close(viewbox)"
aria-label="Close"
shape="times"
></clr-icon>
</div>
</div>
<span
#dragHandleCorner
[id]="'handle_viewbox_' + viewbox.id"
class="dragHandle corner"
(pointerdown)="
startResize($event, dragHandleCorner, resizeBox, viewbox)
"
></span>
<span
#dragHandleCornerLeft
[id]="'handle_corner_left_viewbox_' + viewbox.id"
class="dragHandle corner-left"
(pointerdown)="
startResizeBottomLeft(
$event,
dragHandleCornerLeft,
resizeBox,
viewbox
)
"
></span>
<span
#dragHandleRight
[id]="'handle_right_viewbox_' + viewbox.id"
class="dragHandle right"
(pointerdown)="
startResizeRight($event, dragHandleRight, resizeBox, viewbox)
"
></span>
<span
#dragHandleBottom
[id]="'handle_bottom_viewbox_' + viewbox.id"
class="dragHandle bottom"
(pointerdown)="
startResizeBottom($event, dragHandleBottom, resizeBox, viewbox)
"
></span>
<span
#dragHandleLeft
[id]="'handle_left_viewbox_' + viewbox.id"
class="dragHandle left"
(pointerdown)="
startResizeLeft($event, dragHandleLeft, resizeBox, viewbox)
"
></span>
<form
class="d-flex align-items-center clr-justify-content-between clr-flex-wrap table-search-wrapper"
[style.display]="viewbox.collapsed ? 'none' : ''"
clrForm
>
<div class="d-flex align-items-center clr-flex-wrap">
<div class="input-wrapper">
<input
#searchEl
(keyup.enter)="searchTable(searchEl, viewbox)"
clrInput
[type]="viewbox.searchNumeric ? 'number' : 'text'"
placeholder="Search"
name="search-input"
/>
@if (!viewbox.searchLoading) {
<clr-icon
(click)="searchTable(searchEl, viewbox)"
aria-label="Search"
shape="search"
></clr-icon>
}
@if (viewbox.searchLoading) {
<span class="spinner spinner-inline"> Loading... </span>
}
</div>
<clr-checkbox-container>
<clr-checkbox-wrapper>
<input
type="checkbox"
clrCheckbox
[(ngModel)]="viewbox.searchNumeric"
name="numeric_check"
/>
<label>Numeric</label>
</clr-checkbox-wrapper>
</clr-checkbox-container>
</div>
<div class="d-flex clr-align-items-center">
@if (viewbox.loadingData) {
<span class="spinner spinner-sm">Loading...</span>
}
@if (!viewbox.loadingData) {
<clr-icon
(click)="reloadTableData(viewbox); searchEl.value = ''"
aria-label="Reload data"
shape="refresh"
class="click-icon"
></clr-icon>
}
<a
(click)="openFilter(viewbox); searchEl.value = ''"
role="tooltip"
aria-haspopup="true"
class="tooltip tooltip-lg tooltip-bottom-left ml-5"
>
<clr-icon
aria-hidden="true"
shape="filter"
[class.filter-active]="viewbox.filter_pk !== '0'"
class="click-icon"
></clr-icon>
<span class="tooltip-content tooltip-long">{{
viewbox.filterText
}}</span>
</a>
<clr-icon
(click)="openTableEdit(viewbox)"
[class.disabled]="!tableEditExists(viewbox)"
aria-label="Edit table"
shape="pencil"
class="click-icon ml-5"
></clr-icon>
</div>
</form>
<div
class="content"
*ngVar="getViewboxTableIndex(viewbox) as viewboxTableIndex"
[style.display]="viewbox.collapsed ? 'none' : ''"
>
@if (viewbox.loadingData && viewboxTableIndex < 0) {
<div class="hot-spinner my-10-mx-auto text-center">
<clr-spinner clrMedium></clr-spinner>
</div>
}
@if (viewboxTableIndex > -1 && viewboxHotSettings.get(viewbox.id)) {
<hot-table
[settings]="viewboxHotSettings.get(viewbox.id) || {}"
[id]="'hotTable_' + viewbox.id"
></hot-table>
}
</div>
</div>
}
</div>
<clr-modal
[(clrModalOpen)]="filter"
[clrModalSize]="'xl'"
[clrModalClosable]="false"
class="filter-modal"
>
<h3 class="modal-title center text-center color-darker-gray">
Filter for viewbox table:
<span> #{{ filteringViewbox?.id }} {{ filterLibds }} </span>
</h3>
<div class="modal-body">
@if (filter) {
<app-query #queryFilter [viewboxId]="filteringViewbox?.id"></app-query>
}
</div>
<div class="modal-footer">
<button (click)="resetFilter()" type="button" class="btn btn-sm btn-link">
reset filter
</button>
<button
type="button"
class="btn btn-sm btn-outline"
(click)="filter = false"
>
Cancel
</button>
<button
[clrLoading]="filterLoading"
type="button"
class="btn btn-sm btn-success-outline"
(click)="sendClause()"
>
Ok
</button>
</div>
</clr-modal>
./viewboxes.component.scss