src/app/shared/viewboxes/viewboxes.component.ts

Implements

OnInit AfterViewInit OnDestroy

Metadata

Relationships

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(ngZone: NgZone, licenceService: LicenceService, sasService: SasService, eventService: EventService, sasStoreService: SasStoreService, loggerService: LoggerService, helperService: HelperService, router: Router, activatedRoute: ActivatedRoute, cdf: ChangeDetectorRef)
Parameters :
Name Type Optional
ngZone NgZone No
licenceService LicenceService No
sasService SasService No
eventService EventService No
sasStoreService SasStoreService No
loggerService LoggerService No
helperService HelperService No
router Router No
activatedRoute ActivatedRoute No
cdf ChangeDetectorRef No

Inputs

viewboxModal
Type : boolean

Outputs

viewboxModalChange
Type : EventEmitter<boolean>

Methods

calculateTableHeight
calculateTableHeight(viewbox: Viewbox)

Calculate available height for Handsontable

Parameters :
Name Type Optional Description
viewbox Viewbox No

The viewbox to calculate height for

Returns : number

Available height in pixels

close
close(viewbox: Viewbox)

Close Viewbox and remove it's data stored in paired ViewboxTable array

Parameters :
Name Type Optional
viewbox Viewbox No
Returns : void
clrModalOpenChange
clrModalOpenChange(open: boolean)
Parameters :
Name Type Optional
open boolean No
Returns : void
collapse
collapse(viewbox: Viewbox)
Parameters :
Name Type Optional
viewbox Viewbox No
Returns : void
columnsDrop
columnsDrop(event: CdkDragDrop)

Column drop called after column dragged and dropped for Viewbox column to be configurated

Parameters :
Name Type Optional
event CdkDragDrop<string[]> No
Returns : void
Private createColumnOrder
createColumnOrder(viewboxId: number)

Creating column order in such format to be encoded in URL

Parameters :
Name Type Optional
viewboxId number No
Returns : number[]
Private createViewboxTableSettings
createViewboxTableSettings(viewbox: Viewbox)

Create and store Handsontable settings for a specific viewbox

Parameters :
Name Type Optional
viewbox Viewbox No
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 :
Name Type Optional
urlData string No
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 :
Name Type Optional
viewboxes Viewbox[] No
Returns : string
expand
expand(viewbox: Viewbox)
Parameters :
Name Type Optional
viewbox Viewbox No
Returns : void
focusViewbox
focusViewbox(viewbox: Viewbox)

When clicked on Viewbox, it will focus it Focused Viewbox is always bring to top

Parameters :
Name Type Optional
viewbox Viewbox No
Returns : void
Private getViewboxHotInstance
getViewboxHotInstance(viewboxId?: number)
Parameters :
Name Type Optional
viewboxId number Yes
Returns : Handsontable | undefined

HOT Instance from the given Viewbox

getViewboxTableIndex
getViewboxTableIndex(viewbox: Viewbox)

Used to pair Viewbox with it's data Which is stored in different array - ViewboxTables

Parameters :
Name Type Optional
viewbox Viewbox No
Returns : number
getViewboxTableSettings
getViewboxTableSettings(viewbox: Viewbox)

Get stored Handsontable settings for a specific viewbox

Parameters :
Name Type Optional
viewbox Viewbox No
Returns : Handsontable.GridSettings
libraryOnClick
libraryOnClick(data: LibraryClickEmitter)
Parameters :
Name Type Optional
data LibraryClickEmitter No
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

Parameters :
Name Type Optional Description
libDataset string No

library.table

viewbox Viewbox No
Returns : Promise<void>

Empty Promise - used for awaiting only

loadTables
loadTables(lib: string, library?: any)

Laods tables to populate dc-tree

Parameters :
Name Type Optional
lib string No
library any Yes
Returns : void
maxWidthCheker
maxWidthCheker(width: any, col: any)
Parameters :
Name Type Optional
width any No
col any No
Returns : any
minimize
minimize(viewbox: Viewbox)
Parameters :
Name Type Optional
viewbox Viewbox No
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 :
Name Type Optional
column string No
Returns : void
onColsearchChange
onColsearchChange(inputRef: AutocompleteComponent, value: string, columns: string[])

Viewbox Manager searching for columns to be added

Parameters :
Name Type Optional Description
inputRef AutocompleteComponent No

Input element

value string No
columns string[] No
Returns : void
openFilter
openFilter(viewbox: Viewbox)

Opens filter dialog for given Viewbox

Parameters :
Name Type Optional
viewbox Viewbox No
Returns : void
openTableEdit
openTableEdit(viewbox: Viewbox)

Opens table edit in new tab

Parameters :
Name Type Optional
viewbox Viewbox No
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 :
Name Type Optional
transform string No
Returns : { x: number; y: number }
Private prepareFilterCache
prepareFilterCache()

Prepare the init values to globals for the filtering that will be used in caching values later

Returns : void
refreshTableAfterResize
refreshTableAfterResize(viewbox: Viewbox)

Refresh Handsontable instance after resize

Parameters :
Name Type Optional Description
viewbox Viewbox No

The viewbox to refresh

Returns : void
Async reloadData
reloadData(libDataset: string, viewbox: Viewbox)

Re-loads data particular Viewbox (preserving the filtering PK)

Parameters :
Name Type Optional Description
libDataset string No

library.table

viewbox Viewbox No

viewbox to reload data for

Returns : unknown

Promise

Async reloadTableData
reloadTableData(viewbox: Viewbox)
Parameters :
Name Type Optional
viewbox Viewbox No
Returns : any
reLoadViewboxtables
reLoadViewboxtables(viewboxes: Viewbox[])

Reloads SAS data for each viewbox that exists

Parameters :
Name Type Optional Description
viewboxes Viewbox[] No

viewboxes array

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 :
Name Type Optional
viewbox Viewbox No
Returns : void
resetSelectedViewbox
resetSelectedViewbox(viewbox: Viewbox)

Re setting the viewbox from parameter to selected viewbox And sets the correct data for that viewbox

Parameters :
Name Type Optional
viewbox Viewbox No
Returns : void
resize
resize(dragHandle: HTMLElement, target: HTMLElement)

Viewbox resize

Parameters :
Name Type Optional
dragHandle HTMLElement No
target HTMLElement No
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 :
Name Type Optional
pointerClientX number No
target HTMLElement No
startTargetRight number No
startX number No
startWidth number No
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 :
Name Type Optional
dragHandle HTMLElement No
target HTMLElement No
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 :
Name Type Optional
dragHandle HTMLElement No
target HTMLElement No
Returns : number
restore
restore(viewbox: Viewbox)
Parameters :
Name Type Optional
viewbox Viewbox No
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)
Parameters :
Name Type Optional
inputElement any No
viewbox Viewbox No
Returns : any
selectViewbox
selectViewbox(viewbox: Viewbox)

Selects Viewbox in the Viewbox Manager for the columns to be configurated

Parameters :
Name Type Optional
viewbox Viewbox No
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 :
Name Type Optional
viewboxId number No
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 :
Name Type Optional
dragHandle HTMLElement No
targetRect ClientRect | DOMRect No
position "x" | "y" | "both" | "left" | "bottomLeft" No
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 :
Name Type Optional
event PointerEvent No
dragHandle HTMLElement No
resizeBox HTMLElement No
viewbox Viewbox No
Returns : void
startResizeBottom
startResizeBottom(event: PointerEvent, dragHandle: HTMLElement, target: HTMLElement, viewbox: Viewbox)

Bottom-edge-only resize drag, affecting only height.

Parameters :
Name Type Optional
event PointerEvent No
dragHandle HTMLElement No
target HTMLElement No
viewbox Viewbox No
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 :
Name Type Optional
event PointerEvent No
dragHandle HTMLElement No
target HTMLElement No
viewbox Viewbox No
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 :
Name Type Optional
event PointerEvent No
dragHandle HTMLElement No
target HTMLElement No
viewbox Viewbox No
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 :
Name Type Optional
event PointerEvent No
dragHandle HTMLElement No
target HTMLElement No
viewbox Viewbox No
Returns : void
stopPropagation
stopPropagation(event: any)
Parameters :
Name Type Optional
event any No
Returns : void
Public tableEditExists
tableEditExists(viewbox: Viewbox)
Parameters :
Name Type Optional
viewbox Viewbox No
Returns : any
Async tableOnClick
tableOnClick(data: TableClickEmitter)

Adding new Viewbox - Fired when table is selected from the dc-tree

Parameters :
Name Type Optional Description
data TableClickEmitter No

Selected table data coming from the dc-tree

Returns : any
unsetSelectedViewbox
unsetSelectedViewbox()
Returns : void
Private updateColumnOrderHot
updateColumnOrderHot(colProp: string, finalIndex: number, viewboxId: number)

HOT Columns ordering

Parameters :
Name Type Optional Description
colProp string No

Column name

finalIndex number No

Index of where to position it

viewboxId number No
Returns : void
Private updateHiddenColumnsHot
updateHiddenColumnsHot(colProps: string[], viewboxId: number)
Parameters :
Name Type Optional
colProps string[] No
viewboxId number No
Returns : void
Private updateHotColumns
updateHotColumns(hiddenColProps: string[], viewboxId: number)

Updates the HOT columns Order and Visibility

Parameters :
Name Type Optional Description
hiddenColProps string[] No

Array of indexes of columns to be hidden

viewboxId number No

Viewbox ID for which to apply

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 :
Name Type Optional
event CdkDragEnd No
viewbox Viewbox No
Returns : void

Properties

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 : []

Accessors

viewboxModal
getviewboxModal()
setviewboxModal(value: boolean)
Parameters :
Name Type Optional
value boolean No
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&#64;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

Legend
Html element
Component
Html element with directive

results matching ""

    No results matching ""