diff --git a/__tests__/actions/fileAction.spec.ts b/__tests__/actions/fileAction.spec.ts index 409b6a1f5..39c62f25f 100644 --- a/__tests__/actions/fileAction.spec.ts +++ b/__tests__/actions/fileAction.spec.ts @@ -9,6 +9,7 @@ import type { Folder, Node } from '../../lib/node/index.ts' import { beforeEach, describe, expect, test, vi } from 'vitest' import { DefaultType, getFileActions, registerFileAction } from '../../lib/actions/index.ts' +import { scopedGlobals } from '../../lib/globalScope.ts' import { getRegistry } from '../../lib/registry.ts' import logger from '../../lib/utils/logger.ts' @@ -16,7 +17,7 @@ const folder = {} as Folder const view = {} as View describe('FileActions init', () => { beforeEach(() => { - delete window._nc_fileactions + delete scopedGlobals.fileActions }) test('Getting empty uninitialized FileActions', () => { @@ -39,7 +40,7 @@ describe('FileActions init', () => { registerFileAction(action) - expect(window._nc_fileactions).toHaveLength(1) + expect(scopedGlobals.fileActions).toHaveLength(1) expect(getFileActions()).toHaveLength(1) expect(getFileActions()[0]).toStrictEqual(action) }) diff --git a/__tests__/actions/fileListAction.spec.ts b/__tests__/actions/fileListAction.spec.ts index 0c800a206..e8f5fe92f 100644 --- a/__tests__/actions/fileListAction.spec.ts +++ b/__tests__/actions/fileListAction.spec.ts @@ -9,6 +9,7 @@ import type { Folder } from '../../lib/node/index.ts' import { beforeEach, describe, expect, test, vi } from 'vitest' import { getFileListActions, registerFileListAction } from '../../lib/actions/fileListAction.ts' +import { scopedGlobals } from '../../lib/globalScope.ts' import { getRegistry } from '../../lib/registry.ts' import logger from '../../lib/utils/logger.ts' @@ -28,11 +29,11 @@ function mockAction(id: string): IFileListAction { describe('FileListActions init', () => { beforeEach(() => { - delete window._nc_filelistactions + delete scopedGlobals.fileListActions }) test('Uninitialized file list actions', () => { - expect(window._nc_filelistactions).toBe(undefined) + expect(scopedGlobals.fileListActions).toBe(undefined) const actions = getFileListActions() expect(actions).toHaveLength(0) }) diff --git a/__tests__/dav/davProperties.spec.ts b/__tests__/dav/davProperties.spec.ts index 0297ac16d..606869d77 100644 --- a/__tests__/dav/davProperties.spec.ts +++ b/__tests__/dav/davProperties.spec.ts @@ -15,26 +15,27 @@ import { getRecentSearch, registerDavProperty, } from '../../lib/dav/davProperties.ts' +import { scopedGlobals } from '../../lib/globalScope.ts' import logger from '../../lib/utils/logger.ts' describe('DAV Properties', () => { beforeEach(() => { - delete window._nc_dav_properties - delete window._nc_dav_namespaces + delete scopedGlobals.davNamespaces + delete scopedGlobals.davProperties logger.error = vi.fn() logger.warn = vi.fn() }) test('getDavNameSpaces fall back to defaults', () => { - expect(window._nc_dav_namespaces).toBeUndefined() + expect(scopedGlobals.davNamespaces).toBeUndefined() const namespace = getDavNameSpaces() expect(namespace).toBeTruthy() Object.keys(defaultDavNamespaces).forEach((n) => expect(namespace.includes(n) && namespace.includes(defaultDavNamespaces[n])).toBe(true)) }) test('getDavProperties fall back to defaults', () => { - expect(window._nc_dav_properties).toBeUndefined() + expect(scopedGlobals.davProperties).toBeUndefined() const props = getDavProperties() expect(props).toBeTruthy() defaultDavProperties.forEach((p) => expect(props.includes(p)).toBe(true)) @@ -56,8 +57,8 @@ describe('DAV Properties', () => { }) test('registerDavProperty registers successfully', () => { - expect(window._nc_dav_namespaces).toBeUndefined() - expect(window._nc_dav_properties).toBeUndefined() + expect(scopedGlobals.davNamespaces).toBeUndefined() + expect(scopedGlobals.davProperties).toBeUndefined() expect(registerDavProperty('my:prop', { my: 'https://example.com/ns' })).toBe(true) expect(logger.warn).not.toBeCalled() @@ -67,8 +68,8 @@ describe('DAV Properties', () => { }) test('registerDavProperty fails when registered multiple times', () => { - expect(window._nc_dav_namespaces).toBeUndefined() - expect(window._nc_dav_properties).toBeUndefined() + expect(scopedGlobals.davNamespaces).toBeUndefined() + expect(scopedGlobals.davProperties).toBeUndefined() expect(registerDavProperty('my:prop', { my: 'https://example.com/ns' })).toBe(true) expect(registerDavProperty('my:prop')).toBe(false) @@ -80,8 +81,8 @@ describe('DAV Properties', () => { }) test('registerDavProperty fails with invalid props', () => { - expect(window._nc_dav_namespaces).toBeUndefined() - expect(window._nc_dav_properties).toBeUndefined() + expect(scopedGlobals.davNamespaces).toBeUndefined() + expect(scopedGlobals.davProperties).toBeUndefined() expect(registerDavProperty('my:prop:invalid', { my: 'https://example.com/ns' })).toBe(false) expect(logger.error).toBeCalled() @@ -95,8 +96,8 @@ describe('DAV Properties', () => { }) test('registerDavProperty fails with missing namespace', () => { - expect(window._nc_dav_namespaces).toBeUndefined() - expect(window._nc_dav_properties).toBeUndefined() + expect(scopedGlobals.davNamespaces).toBeUndefined() + expect(scopedGlobals.davProperties).toBeUndefined() expect(registerDavProperty('my:prop', { other: 'https://example.com/ns' })).toBe(false) expect(logger.error).toBeCalled() diff --git a/__tests__/filters/listFilter.spec.ts b/__tests__/filters/listFilter.spec.ts index 199cbc362..88ceac755 100644 --- a/__tests__/filters/listFilter.spec.ts +++ b/__tests__/filters/listFilter.spec.ts @@ -1,4 +1,4 @@ -/** +/* * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -7,6 +7,7 @@ import type { IFileListFilterChip } from '../../lib/filters/index.ts' import { beforeEach, describe, expect, test, vi } from 'vitest' import { FileListFilter, getFileListFilters, registerFileListFilter, unregisterFileListFilter } from '../../lib/filters/index.ts' +import { scopedGlobals } from '../../lib/globalScope.ts' import { getRegistry } from '../../lib/registry.ts' class TestFilter extends FileListFilter { @@ -83,16 +84,16 @@ describe('File list filter class', () => { describe('File list filter functions', () => { beforeEach(() => { - delete window._nc_filelist_filters + delete scopedGlobals.fileListFilters }) test('can register a filter', () => { const filter = new FileListFilter('my:id', 50) registerFileListFilter(filter) - expect(window._nc_filelist_filters).toBeTypeOf('object') - expect(window._nc_filelist_filters!.has(filter.id)).toBe(true) - expect(window._nc_filelist_filters!.get(filter.id)).toBe(filter) + expect(scopedGlobals.fileListFilters).toBeTypeOf('object') + expect(scopedGlobals.fileListFilters!.has(filter.id)).toBe(true) + expect(scopedGlobals.fileListFilters!.get(filter.id)).toBe(filter) }) test('register a filter emits event', () => { @@ -101,7 +102,7 @@ describe('File list filter functions', () => { getRegistry().addEventListener('register:listFilter', spy) - expect(window._nc_filelist_filters).toBe(undefined) + expect(scopedGlobals.fileListFilters).toBe(undefined) registerFileListFilter(filter) expect(spy).toHaveBeenCalled() @@ -121,22 +122,22 @@ describe('File list filter functions', () => { const filter = new FileListFilter('my:id') registerFileListFilter(filter) - expect(window._nc_filelist_filters!.has(filter.id)).toBe(true) + expect(scopedGlobals.fileListFilters!.has(filter.id)).toBe(true) // test unregisterFileListFilter(filter.id) - expect(window._nc_filelist_filters!.has(filter.id)).toBe(false) + expect(scopedGlobals.fileListFilters!.has(filter.id)).toBe(false) }) test('unregister a filter twice does not throw', () => { const filter = new FileListFilter('my:id') registerFileListFilter(filter) - expect(window._nc_filelist_filters!.has(filter.id)).toBe(true) + expect(scopedGlobals.fileListFilters!.has(filter.id)).toBe(true) // test unregisterFileListFilter(filter.id) - expect(window._nc_filelist_filters!.has(filter.id)).toBe(false) + expect(scopedGlobals.fileListFilters!.has(filter.id)).toBe(false) expect(() => unregisterFileListFilter(filter.id)).not.toThrow() }) @@ -150,7 +151,7 @@ describe('File list filter functions', () => { unregisterFileListFilter(filter.id) expect(spy).toHaveBeenCalled() expect(spy.mock.calls[0][0]).toBeInstanceOf(CustomEvent) - expect(spy.mock.calls[0][0].detail).toBe(filter) + expect(spy.mock.calls[0][0].detail).toBe(filter.id) }) test('can get registered filters', () => { diff --git a/__tests__/headers/listHeaders.spec.ts b/__tests__/headers/listHeaders.spec.ts index dac6bc1e3..e72cbebaa 100644 --- a/__tests__/headers/listHeaders.spec.ts +++ b/__tests__/headers/listHeaders.spec.ts @@ -1,4 +1,4 @@ -/** +/* * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ @@ -6,18 +6,19 @@ import type { IFileListHeader, IFolder, IView } from '../../lib/index.ts' import { beforeEach, describe, expect, test, vi } from 'vitest' +import { scopedGlobals } from '../../lib/globalScope.ts' import { getFileListHeaders, registerFileListHeader } from '../../lib/headers/index.ts' import { getRegistry } from '../../lib/registry.ts' import logger from '../../lib/utils/logger.ts' describe('FileListHeader init', () => { beforeEach(() => { - delete window._nc_filelistheader + delete scopedGlobals.fileListHeaders }) test('Getting empty uninitialized FileListHeader', () => { const headers = getFileListHeaders() - expect(window._nc_filelistheader).toBeDefined() + expect(Array.isArray(headers)).toBe(true) expect(headers).toHaveLength(0) }) @@ -29,14 +30,9 @@ describe('FileListHeader init', () => { render: () => {}, updated: () => {}, } - - expect(header.id).toBe('test') - expect(header.order).toBe(1) - expect(header.enabled!({} as IFolder, {} as IView)).toBe(true) - registerFileListHeader(header) - expect(window._nc_filelistheader).toHaveLength(1) + expect(scopedGlobals.fileListHeaders).toHaveLength(1) expect(getFileListHeaders()).toHaveLength(1) expect(getFileListHeaders()[0]).toStrictEqual(header) }) @@ -60,6 +56,25 @@ describe('FileListHeader init', () => { expect(callback.mock.calls[0][0].detail).toBe(header) }) + test('getFileListHeaders() returns array', () => { + expect(getFileListHeaders()).toHaveLength(0) + + const header: IFileListHeader = { + id: 'test', + order: 1, + enabled: () => true, + render: () => {}, + updated: () => {}, + } + + registerFileListHeader(header) + + const headers = getFileListHeaders() + expect(Array.isArray(headers)).toBe(true) + expect(headers).toHaveLength(1) + expect(headers[0]).toStrictEqual(header) + }) + test('Duplicate Header gets rejected', () => { logger.error = vi.fn() const header: IFileListHeader = { diff --git a/__tests__/index.spec.ts b/__tests__/index.spec.ts index 0c04d9ff9..41c4f5b0c 100644 --- a/__tests__/index.spec.ts +++ b/__tests__/index.spec.ts @@ -3,8 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import type { NewMenuEntry } from '../lib/newMenu/NewMenu.ts' - import { describe, expect, test } from 'vitest' import { getFileActions, registerFileAction } from '../lib/actions/fileAction.ts' import { @@ -18,7 +16,6 @@ import { Permission, removeNewFileMenuEntry, } from '../lib/index.ts' -import { NewMenu } from '../lib/newMenu/NewMenu.ts' describe('Exports checks', () => { test('formatFileSize', () => { @@ -76,38 +73,3 @@ describe('Exports checks', () => { expect(typeof Node).toBe('function') }) }) - -describe('NewFileMenu methods', () => { - const entry = { - id: 'empty-file', - displayName: 'Create empty file', - templateName: 'New file.txt', - iconSvgInline: '', - handler: () => {}, - } as NewMenuEntry - - test('Init NewFileMenu', () => { - expect(window._nc_newfilemenu).toBeUndefined() - - const menuEntries = getNewFileMenuEntries() - expect(menuEntries).toHaveLength(0) - - expect(window._nc_newfilemenu).toBeDefined() - expect(window._nc_newfilemenu).toBeInstanceOf(NewMenu) - }) - - test('Use existing initialized NewMenu', () => { - expect(window._nc_newfilemenu).toBeDefined() - expect(window._nc_newfilemenu).toBeInstanceOf(NewMenu) - - addNewFileMenuEntry(entry) - - expect(window._nc_newfilemenu).toBeDefined() - expect(window._nc_newfilemenu).toBeInstanceOf(NewMenu) - - removeNewFileMenuEntry(entry) - - expect(window._nc_newfilemenu).toBeDefined() - expect(window._nc_newfilemenu).toBeInstanceOf(NewMenu) - }) -}) diff --git a/__tests__/navigation.spec.ts b/__tests__/navigation.spec.ts index fe356ccdd..9a7986d64 100644 --- a/__tests__/navigation.spec.ts +++ b/__tests__/navigation.spec.ts @@ -1,9 +1,10 @@ -/** +/* * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ import { describe, expect, it, vi } from 'vitest' +import { scopedGlobals } from '../lib/globalScope.ts' import { View } from '../lib/index.ts' import { getNavigation, Navigation } from '../lib/navigation/navigation.ts' import { mockView } from './fixtures/view.ts' @@ -18,21 +19,15 @@ describe('getNavigation', () => { }) it('stores the navigation globally', () => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - delete window._nc_navigation + delete scopedGlobals.navigation const navigation = getNavigation() expect(navigation).toBeInstanceOf(Navigation) - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - expect(window._nc_navigation).toBeInstanceOf(Navigation) + expect(scopedGlobals.navigation).toBeInstanceOf(Navigation) }) it('reuses an existing navigation', () => { const navigation = new Navigation() - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - window._nc_navigation = navigation + scopedGlobals.navigation = navigation expect(getNavigation()).toBe(navigation) }) }) diff --git a/__tests__/newMenu/newFileMenu.spec.ts b/__tests__/newMenu/newFileMenu.spec.ts index cf6d417f8..c04faf8c6 100644 --- a/__tests__/newMenu/newFileMenu.spec.ts +++ b/__tests__/newMenu/newFileMenu.spec.ts @@ -6,7 +6,8 @@ import type { NewMenuEntry } from '../../lib/newMenu/index.ts' import { afterEach, describe, expect, test, vi } from 'vitest' -import { addNewFileMenuEntry, getNewFileMenu, getNewFileMenuEntries } from '../../lib/newMenu/index.ts' +import { scopedGlobals } from '../../lib/globalScope.ts' +import { addNewFileMenuEntry, getNewFileMenu, getNewFileMenuEntries, removeNewFileMenuEntry } from '../../lib/newMenu/index.ts' import { NewMenu, NewMenuEntryCategory } from '../../lib/newMenu/NewMenu.ts' import { Folder } from '../../lib/node/index.ts' import { Permission } from '../../lib/permissions.ts' @@ -14,18 +15,16 @@ import logger from '../../lib/utils/logger.ts' describe('NewFileMenu init', () => { test('Initializing NewFileMenu', () => { - logger.debug = vi.fn() const newFileMenu = getNewFileMenu() - expect(window._nc_newfilemenu).toBeInstanceOf(NewMenu) - expect(window._nc_newfilemenu).toBe(newFileMenu) - expect(logger.debug).toHaveBeenCalled() + expect(scopedGlobals.newFileMenu).toBeInstanceOf(NewMenu) + expect(scopedGlobals.newFileMenu).toBe(newFileMenu) }) test('Getting existing NewMenu', () => { const newFileMenu = new NewMenu() - Object.assign(window, { _nc_newfilemenu: newFileMenu }) + Object.assign(scopedGlobals, { newFileMenu }) - expect(window._nc_newfilemenu).toBe(newFileMenu) + expect(scopedGlobals.newFileMenu).toBe(newFileMenu) expect(getNewFileMenu()).toBe(newFileMenu) }) }) @@ -399,7 +398,7 @@ describe('NewMenu getEntries filter', () => { describe('NewMenu sort test', () => { afterEach(() => { - delete window._nc_newfilemenu + delete scopedGlobals.newFileMenu }) test('Specified NewMenu order', () => { @@ -490,3 +489,38 @@ describe('NewMenu sort test', () => { expect(entries[3]).toBe(entry2) }) }) + +describe('NewFileMenu methods', () => { + const entry = { + id: 'empty-file', + displayName: 'Create empty file', + templateName: 'New file.txt', + iconSvgInline: '', + handler: () => {}, + } as NewMenuEntry + + test('Init NewFileMenu', () => { + expect(scopedGlobals.newFileMenu).toBeUndefined() + + const menuEntries = getNewFileMenuEntries() + expect(menuEntries).toHaveLength(0) + + expect(scopedGlobals.newFileMenu).toBeDefined() + expect(scopedGlobals.newFileMenu).toBeInstanceOf(NewMenu) + }) + + test('Use existing initialized NewMenu', () => { + expect(scopedGlobals.newFileMenu).toBeDefined() + expect(scopedGlobals.newFileMenu).toBeInstanceOf(NewMenu) + + addNewFileMenuEntry(entry) + + expect(scopedGlobals.newFileMenu).toBeDefined() + expect(scopedGlobals.newFileMenu).toBeInstanceOf(NewMenu) + + removeNewFileMenuEntry(entry) + + expect(scopedGlobals.newFileMenu).toBeDefined() + expect(scopedGlobals.newFileMenu).toBeInstanceOf(NewMenu) + }) +}) diff --git a/__tests__/sidebar/sidebarTab.spec.ts b/__tests__/sidebar/sidebarTab.spec.ts index 4f84de36d..c4efd4ad5 100644 --- a/__tests__/sidebar/sidebarTab.spec.ts +++ b/__tests__/sidebar/sidebarTab.spec.ts @@ -6,6 +6,7 @@ import type { ISidebarTab } from '../../lib/sidebar/SidebarTab.ts' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { scopedGlobals } from '../../lib/globalScope.ts' import { getSidebarTabs, registerSidebarTab } from '../../lib/sidebar/SidebarTab.ts' // missing in JSDom but supported by every browser! @@ -14,16 +15,16 @@ import 'css.escape' describe('Sidebar tabs', () => { beforeEach(() => { vi.restoreAllMocks() - delete window._nc_files_sidebar_tabs + delete scopedGlobals.filesSidebarTabs }) it('can register a tab', () => { const tab = getExampleTab() registerSidebarTab(tab) - expect(window._nc_files_sidebar_tabs).toBeInstanceOf(Map) - expect(window._nc_files_sidebar_tabs!.has(tab.id)).toBe(true) - expect(window._nc_files_sidebar_tabs!.get(tab.id)).toBe(tab) + expect(scopedGlobals.filesSidebarTabs).toBeInstanceOf(Map) + expect(scopedGlobals.filesSidebarTabs!.has(tab.id)).toBe(true) + expect(scopedGlobals.filesSidebarTabs!.get(tab.id)).toBe(tab) }) it('can fetch empty list of sidebar tabs', () => { diff --git a/lib/actions/fileAction.ts b/lib/actions/fileAction.ts index 7771bf80e..8b9753322 100644 --- a/lib/actions/fileAction.ts +++ b/lib/actions/fileAction.ts @@ -5,6 +5,7 @@ import type { ActionContext, ActionContextSingle } from '../types.ts' +import { scopedGlobals } from '../globalScope.ts' import { getRegistry } from '../registry.ts' import logger from '../utils/logger.ts' @@ -128,13 +129,13 @@ export interface IFileAction { export function registerFileAction(action: IFileAction): void { validateAction(action) - window._nc_fileactions ??= [] - if (window._nc_fileactions.find((search) => search.id === action.id)) { + scopedGlobals.fileActions ??= new Map() + if (scopedGlobals.fileActions.has(action.id)) { logger.error(`FileAction ${action.id} already registered`, { action }) return } - window._nc_fileactions.push(action) + scopedGlobals.fileActions.set(action.id, action) getRegistry() .dispatchTypedEvent('register:action', new CustomEvent('register:action', { detail: action })) } @@ -143,7 +144,10 @@ export function registerFileAction(action: IFileAction): void { * Get all registered file actions. */ export function getFileActions(): IFileAction[] { - return window._nc_fileactions ?? [] + if (scopedGlobals.fileActions) { + return [...scopedGlobals.fileActions.values()] + } + return [] } /** diff --git a/lib/actions/fileListAction.ts b/lib/actions/fileListAction.ts index 247cbba65..8cfff0a32 100644 --- a/lib/actions/fileListAction.ts +++ b/lib/actions/fileListAction.ts @@ -5,6 +5,7 @@ import type { ViewActionContext } from '../types.ts' +import { scopedGlobals } from '../globalScope.ts' import { getRegistry } from '../registry.ts' import logger from '../utils/logger.ts' @@ -44,13 +45,13 @@ export interface IFileListAction { export function registerFileListAction(action: IFileListAction) { validateAction(action) - window._nc_filelistactions ??= [] - if (window._nc_filelistactions.find((listAction) => listAction.id === action.id)) { + scopedGlobals.fileListActions ??= new Map() + if (scopedGlobals.fileListActions.has(action.id)) { logger.error(`FileListAction with id "${action.id}" is already registered`, { action }) return } - window._nc_filelistactions.push(action) + scopedGlobals.fileListActions.set(action.id, action) getRegistry() .dispatchTypedEvent('register:listAction', new CustomEvent('register:listAction', { detail: action })) } @@ -59,7 +60,10 @@ export function registerFileListAction(action: IFileListAction) { * Get all currently registered file list actions. */ export function getFileListActions(): IFileListAction[] { - return [...(window._nc_filelistactions ?? [])] + if (scopedGlobals.fileListActions) { + return [...scopedGlobals.fileListActions.values()] + } + return [] } /** diff --git a/lib/dav/davProperties.ts b/lib/dav/davProperties.ts index 3434468da..cda5d34a4 100644 --- a/lib/dav/davProperties.ts +++ b/lib/dav/davProperties.ts @@ -4,6 +4,7 @@ */ import { getCurrentUser } from '@nextcloud/auth' +import { scopedGlobals } from '../globalScope.ts' import logger from '../utils/logger.ts' export type DavProperty = { [key: string]: string } @@ -45,15 +46,13 @@ export const defaultDavNamespaces = { * @param namespace The namespace of the property */ export function registerDavProperty(prop: string, namespace: DavProperty = { nc: 'http://nextcloud.org/ns' }): boolean { - if (typeof window._nc_dav_properties === 'undefined') { - window._nc_dav_properties = [...defaultDavProperties] - window._nc_dav_namespaces = { ...defaultDavNamespaces } - } + scopedGlobals.davNamespaces ??= { ...defaultDavNamespaces } + scopedGlobals.davProperties ??= [...defaultDavProperties] - const namespaces = { ...window._nc_dav_namespaces, ...namespace } + const namespaces = { ...scopedGlobals.davNamespaces, ...namespace } // Check duplicates - if (window._nc_dav_properties.find((search) => search === prop)) { + if (scopedGlobals.davProperties.find((search) => search === prop)) { logger.warn(`${prop} already registered`, { prop }) return false } @@ -69,8 +68,8 @@ export function registerDavProperty(prop: string, namespace: DavProperty = { nc: return false } - window._nc_dav_properties.push(prop) - window._nc_dav_namespaces = namespaces + scopedGlobals.davProperties.push(prop) + scopedGlobals.davNamespaces = namespaces return true } @@ -78,23 +77,17 @@ export function registerDavProperty(prop: string, namespace: DavProperty = { nc: * Get the registered dav properties */ export function getDavProperties(): string { - if (typeof window._nc_dav_properties === 'undefined') { - window._nc_dav_properties = [...defaultDavProperties] - } - - return window._nc_dav_properties.map((prop) => `<${prop} />`).join(' ') + scopedGlobals.davProperties ??= [...defaultDavProperties] + return scopedGlobals.davProperties.map((prop) => `<${prop} />`).join(' ') } /** * Get the registered dav namespaces */ export function getDavNameSpaces(): string { - if (typeof window._nc_dav_namespaces === 'undefined') { - window._nc_dav_namespaces = { ...defaultDavNamespaces } - } - - return Object.keys(window._nc_dav_namespaces) - .map((ns) => `xmlns:${ns}="${window._nc_dav_namespaces?.[ns]}"`) + scopedGlobals.davNamespaces ??= { ...defaultDavNamespaces } + return Object.keys(scopedGlobals.davNamespaces) + .map((ns) => `xmlns:${ns}="${scopedGlobals.davNamespaces?.[ns]}"`) .join(' ') } diff --git a/lib/filters/functions.ts b/lib/filters/functions.ts index b5ee56c78..853907482 100644 --- a/lib/filters/functions.ts +++ b/lib/filters/functions.ts @@ -5,6 +5,7 @@ import type { IFileListFilter } from './listFilters.ts' +import { scopedGlobals } from '../globalScope.ts' import { getRegistry } from '../registry.ts' /** @@ -16,12 +17,12 @@ import { getRegistry } from '../registry.ts' * @param filter The filter to register on the file list */ export function registerFileListFilter(filter: IFileListFilter): void { - window._nc_filelist_filters ??= new Map() - if (window._nc_filelist_filters.has(filter.id)) { + scopedGlobals.fileListFilters ??= new Map() + if (scopedGlobals.fileListFilters.has(filter.id)) { throw new Error(`File list filter "${filter.id}" already registered`) } - window._nc_filelist_filters.set(filter.id, filter) + scopedGlobals.fileListFilters.set(filter.id, filter) getRegistry() .dispatchTypedEvent('register:listFilter', new CustomEvent('register:listFilter', { detail: filter })) } @@ -32,11 +33,10 @@ export function registerFileListFilter(filter: IFileListFilter): void { * @param filterId The unique ID of the filter to remove */ export function unregisterFileListFilter(filterId: string): void { - if (window._nc_filelist_filters && window._nc_filelist_filters.has(filterId)) { - const filter = window._nc_filelist_filters.get(filterId)! - window._nc_filelist_filters.delete(filterId) + if (scopedGlobals.fileListFilters && scopedGlobals.fileListFilters.has(filterId)) { + scopedGlobals.fileListFilters.delete(filterId) getRegistry() - .dispatchTypedEvent('unregister:listFilter', new CustomEvent('unregister:listFilter', { detail: filter })) + .dispatchTypedEvent('unregister:listFilter', new CustomEvent('unregister:listFilter', { detail: filterId })) } } @@ -44,8 +44,8 @@ export function unregisterFileListFilter(filterId: string): void { * Get all registered file list filters */ export function getFileListFilters(): IFileListFilter[] { - if (!window._nc_filelist_filters) { - return [] + if (scopedGlobals.fileListFilters) { + return [...scopedGlobals.fileListFilters.values()] } - return [...window._nc_filelist_filters.values()] + return [] } diff --git a/lib/globalScope.ts b/lib/globalScope.ts new file mode 100644 index 000000000..9e26f7e08 --- /dev/null +++ b/lib/globalScope.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import type { IFileAction, IFileListAction } from './actions/index.ts' +import type { DavProperty } from './dav/index.ts' +import type { + IFileListFilter, + IFileListHeader, + Navigation, + NewMenu, +} from './index.ts' +import type { FilesRegistry } from './registry.ts' +import type { ISidebarTab } from './sidebar/index.ts' +import type { ISidebarAction } from './sidebar/SidebarAction.ts' + +interface InternalGlobalScope { + davNamespaces?: DavProperty + davProperties?: string[] + + newFileMenu?: NewMenu + navigation?: Navigation + registry?: FilesRegistry + + fileActions?: Map + fileListActions?: Map + fileListFilters?: Map + fileListHeaders?: Map + + filesSidebarActions?: Map + filesSidebarTabs?: Map +} + +window._nc_files_scope ??= {} +window._nc_files_scope.v4_0 ??= {} + +/** + * Get the global scope for the files library. + * This is used to store global variables scoped to prevent breaking changes in the future. + * + * @internal + */ +export const scopedGlobals = window._nc_files_scope.v4_0 as InternalGlobalScope diff --git a/lib/headers/listHeaders.ts b/lib/headers/listHeaders.ts index 3d38a5d25..7727ce12a 100644 --- a/lib/headers/listHeaders.ts +++ b/lib/headers/listHeaders.ts @@ -6,6 +6,7 @@ import type { IView } from '../navigation/view.ts' import type { IFolder } from '../node/folder.ts' +import { scopedGlobals } from '../globalScope.ts' import { getRegistry } from '../registry.ts' import logger from '../utils/logger.ts' @@ -30,13 +31,13 @@ export interface IFileListHeader { export function registerFileListHeader(header: IFileListHeader): void { validateHeader(header) - window._nc_filelistheader ??= [] - if (window._nc_filelistheader.find((search) => search.id === header.id)) { + scopedGlobals.fileListHeaders ??= new Map() + if (scopedGlobals.fileListHeaders.has(header.id)) { logger.error(`Header ${header.id} already registered`, { header }) return } - window._nc_filelistheader.push(header) + scopedGlobals.fileListHeaders.set(header.id, header) getRegistry() .dispatchTypedEvent('register:listHeader', new CustomEvent('register:listHeader', { detail: header })) } @@ -45,8 +46,10 @@ export function registerFileListHeader(header: IFileListHeader): void { * Get all currently registered file list headers. */ export function getFileListHeaders(): IFileListHeader[] { - window._nc_filelistheader ??= [] - return [...window._nc_filelistheader] + if (!scopedGlobals.fileListHeaders) { + return [] + } + return [...scopedGlobals.fileListHeaders.values()] } /** diff --git a/lib/navigation/navigation.ts b/lib/navigation/navigation.ts index 9a9c3912b..c6cf05117 100644 --- a/lib/navigation/navigation.ts +++ b/lib/navigation/navigation.ts @@ -6,7 +6,7 @@ import type { IView } from './view.ts' import { TypedEventTarget } from 'typescript-event-target' -import logger from '../utils/logger.ts' +import { scopedGlobals } from '../globalScope.ts' import { validateView } from './view.ts' /** @@ -123,10 +123,6 @@ export class Navigation extends TypedEventTarget<{ updateActive: UpdateActiveVie * Get the current files navigation */ export function getNavigation(): Navigation { - if (typeof window._nc_navigation === 'undefined') { - window._nc_navigation = new Navigation() - logger.debug('Navigation service initialized') - } - - return window._nc_navigation + scopedGlobals.navigation ??= new Navigation() + return scopedGlobals.navigation } diff --git a/lib/newMenu/functions.ts b/lib/newMenu/functions.ts index 2a7077f2e..1ab5a9936 100644 --- a/lib/newMenu/functions.ts +++ b/lib/newMenu/functions.ts @@ -6,18 +6,15 @@ import type { IFolder } from '../node/index.ts' import type { NewMenuEntry } from './NewMenu.ts' -import logger from '../utils/logger.ts' +import { scopedGlobals } from '../globalScope.ts' import { NewMenu } from './NewMenu.ts' /** * Get the NewMenu instance used by the files app. */ export function getNewFileMenu(): NewMenu { - if (typeof window._nc_newfilemenu === 'undefined') { - window._nc_newfilemenu = new NewMenu() - logger.debug('NewFileMenu initialized') - } - return window._nc_newfilemenu + scopedGlobals.newFileMenu ??= new NewMenu() + return scopedGlobals.newFileMenu } /** diff --git a/lib/registry.ts b/lib/registry.ts index deb513020..984aa8afb 100644 --- a/lib/registry.ts +++ b/lib/registry.ts @@ -8,31 +8,59 @@ import type { IFileListFilter } from './filters/index.ts' import type { IFileListHeader } from './headers/index.ts' import { TypedEventTarget } from 'typescript-event-target' +import { scopedGlobals } from './globalScope.ts' interface FilesRegistryEvents { - 'register:action': CustomEvent - 'register:listAction': CustomEvent - 'register:listFilter': CustomEvent - 'unregister:listFilter': CustomEvent - 'register:listHeader': CustomEvent + 'register:action': RegistrationEvent + 'register:listAction': RegistrationEvent + 'register:listFilter': RegistrationEvent + 'register:listHeader': RegistrationEvent + 'unregister:listFilter': UnregisterEvent } -export class FilesRegistryV4 extends TypedEventTarget {} +/** + * Custom event for registry events. + * The detail is the registered item. + */ +class RegistrationEvent extends CustomEvent {} -export type PublicFilesRegistry = Pick +/** + * Custom event for unregistering items from the registry. + * The detail is the id of the unregistered item. + */ +class UnregisterEvent extends RegistrationEvent {} + +/** + * The registry for files app. + * This is used to keep track of registered actions, filters, headers, etc. and to emit events when new items are registered. + * Allowing to keep a reactive state of the registered items in the UI without being coupled to one specific reactivity framework. + * + * This is an internal implementation detail and should not be used directly. + * + * @internal + * @see PublicFilesRegistry - for the public API to listen to registry events. + */ +export class FilesRegistry extends TypedEventTarget {} + +/** + * The registry for files app. + * This is used to keep track of registered actions, filters, headers, etc. and to emit events when new items are registered. + * Allowing to keep a reactive state of the registered items in the UI without being coupled to one specific reactivity framework. + */ +export type PublicFilesRegistry = Pick /** - * Get the global files registry + * Get the global files registry. * * @internal */ export function getRegistry() { - window._nc_files_registry_v4 ??= new FilesRegistryV4() - return window._nc_files_registry_v4 + scopedGlobals.registry ??= new FilesRegistry() + return scopedGlobals.registry } /** - * Get the global files registry + * Get the global files registry. * * This allows to listen for new registrations of actions, filters, headers, etc. * Events are dispatched by the respective registration functions. diff --git a/lib/sidebar/SidebarAction.ts b/lib/sidebar/SidebarAction.ts index 2b29c523f..65fe9860e 100644 --- a/lib/sidebar/SidebarAction.ts +++ b/lib/sidebar/SidebarAction.ts @@ -5,6 +5,7 @@ import type { ISidebarContext } from './SidebarTab.ts' +import { scopedGlobals } from '../globalScope.ts' import logger from '../utils/logger.ts' /** @@ -61,12 +62,12 @@ export interface ISidebarAction { export function registerSidebarAction(action: ISidebarAction): void { validateSidebarAction(action) - window._nc_files_sidebar_actions ??= new Map() - if (window._nc_files_sidebar_actions.has(action.id)) { + scopedGlobals.filesSidebarActions ??= new Map() + if (scopedGlobals.filesSidebarActions.has(action.id)) { logger.warn(`Sidebar action with id "${action.id}" already registered. Skipping.`) return } - window._nc_files_sidebar_actions.set(action.id, action) + scopedGlobals.filesSidebarActions.set(action.id, action) logger.debug(`New sidebar action with id "${action.id}" registered.`) } @@ -74,8 +75,8 @@ export function registerSidebarAction(action: ISidebarAction): void { * Get all currently registered sidebar actions. */ export function getSidebarActions(): ISidebarAction[] { - if (window._nc_files_sidebar_actions) { - return [...window._nc_files_sidebar_actions.values()] + if (scopedGlobals.filesSidebarActions) { + return [...scopedGlobals.filesSidebarActions.values()] } return [] } diff --git a/lib/sidebar/SidebarTab.ts b/lib/sidebar/SidebarTab.ts index d1303b19e..f7f313c7c 100644 --- a/lib/sidebar/SidebarTab.ts +++ b/lib/sidebar/SidebarTab.ts @@ -7,6 +7,7 @@ import type { IView } from '../navigation/view.ts' import type { IFolder, INode } from '../node/index.ts' import isSvg from 'is-svg' +import { scopedGlobals } from '../globalScope.ts' import logger from '../utils/logger.ts' export interface ISidebarContext { @@ -111,12 +112,12 @@ export interface ISidebarTab { export function registerSidebarTab(tab: ISidebarTab): void { validateSidebarTab(tab) - window._nc_files_sidebar_tabs ??= new Map() - if (window._nc_files_sidebar_tabs.has(tab.id)) { + scopedGlobals.filesSidebarTabs ??= new Map() + if (scopedGlobals.filesSidebarTabs.has(tab.id)) { logger.warn(`Sidebar tab with id "${tab.id}" already registered. Skipping.`) return } - window._nc_files_sidebar_tabs.set(tab.id, tab) + scopedGlobals.filesSidebarTabs.set(tab.id, tab) logger.debug(`New sidebar tab with id "${tab.id}" registered.`) } @@ -124,8 +125,8 @@ export function registerSidebarTab(tab: ISidebarTab): void { * Get all currently registered sidebar tabs. */ export function getSidebarTabs(): ISidebarTab[] { - if (window._nc_files_sidebar_tabs) { - return [...window._nc_files_sidebar_tabs.values()] + if (scopedGlobals.filesSidebarTabs) { + return [...scopedGlobals.filesSidebarTabs.values()] } return [] } diff --git a/lib/window.d.ts b/lib/window.d.ts index f953a2892..519145e35 100644 --- a/lib/window.d.ts +++ b/lib/window.d.ts @@ -4,40 +4,16 @@ */ /// -import type { IFileAction, IFileListAction } from './actions/index.ts' -import type { DavProperty } from './dav/index.ts' -import type { - IFileListFilter, - IFileListHeader, - Navigation, - NewMenu, -} from './index.ts' -import type { FilesRegistryV4 } from './registry.ts' -import type { ISidebarTab } from './sidebar/index.ts' -import type { ISidebarAction } from './sidebar/SidebarAction.ts' - -export {} - declare global { interface Window { OC: Nextcloud.v32.OC // eslint-disable-next-line @typescript-eslint/no-explicit-any OCA: any - _nc_dav_namespaces?: DavProperty - _nc_dav_properties?: string[] - _nc_fileactions?: IFileAction[] - _nc_filelistactions?: IFileListAction[] - _nc_filelistheader?: IFileListHeader[] - _nc_newfilemenu?: NewMenu - _nc_navigation?: Navigation - _nc_filelist_filters?: Map - _nc_files_sidebar_actions?: Map - _nc_files_sidebar_tabs?: Map - - _nc_files_registry_v4?: FilesRegistryV4 - + _nc_files_scope?: Record> _oc_config?: { forbidden_filenames_characters: string[] } } } + +export {}