From 431bd15ebd59936923ed780cb793a85a295e55f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Molakvo=C3=A6?= <14975046+skjnldsv@users.noreply.github.com> Date: Thu, 1 Oct 2026 17:20:17 +0200 Subject: [PATCH] feat(handlers)!: rename tagname to tagName MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same name as the files sidebar tabs and the other registration APIs. A breaking change to the handler shape, so the shared registry moves to handlers_v2: a copy still on tagname does not share a viewer with this one. Assisted-by: ClaudeCode:claude-opus-5-5 Signed-off-by: John Molakvoæ <14975046+skjnldsv@users.noreply.github.com> --- README.md | 32 +++++----- __tests__/component/Viewer.spec.ts | 14 ++--- __tests__/component/handlerContract.spec.ts | 2 +- __tests__/component/viewerApi.spec.ts | 8 +-- __tests__/encryptedFiles.spec.ts | 4 +- __tests__/factories.ts | 2 +- __tests__/fileActions.spec.ts | 66 ++++++++++----------- __tests__/handlerFaults.spec.ts | 4 +- __tests__/handlerHelper.spec.ts | 4 +- __tests__/models.spec.ts | 2 +- __tests__/registerHandler.spec.ts | 4 +- e2e/copies.spec.ts | 2 +- lib/handlers.ts | 38 ++++++------ lib/models/audios.ts | 8 +-- lib/models/images.ts | 8 +-- lib/models/sheetmusic.ts | 8 +-- lib/models/videos.ts | 8 +-- lib/scope.ts | 2 +- lib/utils/customElements.ts | 10 ++-- lib/views/Viewer.vue | 6 +- 20 files changed, 116 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index c320e94..701582a 100644 --- a/README.md +++ b/README.md @@ -97,16 +97,16 @@ import { registerHandler } from '@nextcloud/viewer' import { defineCustomElement } from 'vue' import NoteView from './views/NoteView.vue' -const tagname = 'myapp-note-view' +const tagName = 'myapp-note-view' -if (!window.customElements.get(tagname)) { - window.customElements.define(tagname, defineCustomElement(NoteView, { shadowRoot: false })) +if (!window.customElements.get(tagName)) { + window.customElements.define(tagName, defineCustomElement(NoteView, { shadowRoot: false })) } registerHandler({ id: 'myapp-notes', displayName: t('myapp', 'Notes'), - tagname, + tagName, enabled: (nodes) => nodes.every((node) => node.mime === 'application/x-myapp-note'), }) ``` @@ -192,7 +192,7 @@ To show your own file type in the viewer, register a handler with the Handlers are rendered as **native custom elements**, not as Vue components passed directly to the viewer. You register a custom element with the browser, then -reference it from your handler by its `tagname`. +reference it from your handler by its `tagName`. #### 1. Create your view component @@ -250,7 +250,7 @@ const src = computed(() => props.file.encodedSource) Turn your component into a custom element with Vue's `defineCustomElement`, define it on `window.customElements`, then register a handler that points at it via -`tagname`: +`tagName`: ```ts import MyIconSvg from '@mdi/svg/svg/file-image.svg?raw' @@ -261,13 +261,13 @@ import MyView from './MyView.vue' // A valid custom element tag name: lowercase, must contain a hyphen, // no consecutive hyphens, no leading/trailing hyphen. -const tagname = 'my-app-viewer' +const tagName = 'my-app-viewer' // Define the custom element. `shadowRoot: false` keeps the element in the // light DOM so Nextcloud's global styles and CSS variables apply. A tag can // only be defined once per page, hence the check. -if (!window.customElements.get(tagname)) { - window.customElements.define(tagname, defineCustomElement(MyView, { shadowRoot: false })) +if (!window.customElements.get(tagName)) { + window.customElements.define(tagName, defineCustomElement(MyView, { shadowRoot: false })) } // Register the handler. @@ -279,7 +279,7 @@ registerHandler({ displayName: t('myapp', 'My viewer'), // The custom element tag name registered above. - tagname, + tagName, // Optional inline SVG icon for the "Open with …" menu entry. iconSvgInline: MyIconSvg, @@ -315,7 +315,7 @@ The full handler shape (see the `IHandler` interface): | --------------- | ------------------------------------- | -------- | ------------------------------------------------------------------ | | `id` | `string` | yes | Unique, non-empty handler identifier | | `displayName` | `string` | yes | Translated name shown in the "Open with …" menu | -| `tagname` | `string` | yes | Registered custom element tag name (must contain a hyphen) | +| `tagName` | `string` | yes | Registered custom element tag name (must contain a hyphen) | | `enabled` | `(nodes: File[]) => boolean` | yes | Whether the handler can open the given files | | `iconSvgInline` | `string` | no | Inline SVG icon for the menu entry | | `group` | `string` | no | Group used to combine handlers when opening a folder | @@ -325,13 +325,13 @@ The full handler shape (see the `IHandler` interface): Gotchas: -- `tagname` must be lowercase, contain a hyphen, and have no leading, trailing +- `tagName` must be lowercase, contain a hyphen, and have no leading, trailing or consecutive hyphens (e.g. `my-app-viewer`). An invalid one throws. - `id` must be unique **across every app on the page**, not just your own: it is not namespaced for you. A collision does not throw: the second registration is silently dropped with a console warning, so pick something specific to your app (`myapp-image`, not `image`). -- Registering the same handler again, with the same `id` and `tagname`, is +- Registering the same handler again, with the same `id` and `tagName`, is quietly ignored. That is what happens when several copies of the package on a page register the defaults, so there is nothing to guard against. - Registering after the viewer has already read the handler list is not an @@ -452,7 +452,7 @@ instead, and the viewer works with `@nextcloud/files` nodes rather than the | `OCA.Viewer.mimetypes.includes(node.mime)` | `canView(node)` | | `OCA.Viewer.mimetypesCompare.includes(node.mime)` | `canView(node)` | | `OCA.Viewer.availableHandlers` | `getHandlers()`, or `canView(node)` to test one file | -| `OCA.Viewer.registerHandler({ component })` | `registerHandler({ tagname })`, see above | +| `OCA.Viewer.registerHandler({ component })` | `registerHandler({ tagName })`, see above | | `canCompare: true` on a handler | nothing, any handler can be compared | | `\OCP\Util::addScript` for the registration | `\OCP\Util::addInitScript` | | A listener for `OCA\Viewer\Event\LoadViewer` | a listener for `BeforeTemplateRenderedEvent`, see the [tutorial](#4-load-it-on-every-page) | @@ -471,7 +471,7 @@ Inside a handler, what used to be read off the global comes in as props: Two changes are worth calling out because they are not a rename: Handlers are custom elements now, not Vue components handed to the viewer. A -handler names a `tagname` you have defined on `window.customElements`, which is +handler names a `tagName` you have defined on `window.customElements`, which is what lets the viewer render a handler written in any framework, or none. The [registration section](#-add-your-own-file-view) walks through it. @@ -514,7 +514,7 @@ The handler registry and the elected viewer live under a window key named for the **handler ABI**, not for this package's version: ```js -window._nc_viewer_scope.handlers_v1 +window._nc_viewer_scope.handlers_v2 ``` Keying it by package version would split the registry on every major release, diff --git a/__tests__/component/Viewer.spec.ts b/__tests__/component/Viewer.spec.ts index 711c058..3b76278 100644 --- a/__tests__/component/Viewer.spec.ts +++ b/__tests__/component/Viewer.spec.ts @@ -19,7 +19,7 @@ import { mountViewer } from './mountViewer.ts' function imageHandler() { return makeHandler({ id: 'image', - tagname: 'oca-viewer-image', + tagName: 'oca-viewer-image', group: 'media', enabled: (nodes) => nodes.every((n) => n.mime?.startsWith('image/')), }) @@ -60,7 +60,7 @@ describe('Viewer.open()', () => { it('filters currentFileList to files of the same handler group', async () => { const pdfHandler = makeHandler({ id: 'pdf', - tagname: 'oca-viewer-pdf', + tagName: 'oca-viewer-pdf', group: 'documents', enabled: (nodes) => nodes.every((n) => n.mime === 'application/pdf'), }) @@ -105,7 +105,7 @@ describe('Viewer.open()', () => { ['dark', false], ['default', false], ] as const)('gives the modal a light backdrop only for a %s themed handler', async (theme, lightBackdrop) => { - const handler = makeHandler({ id: theme, tagname: `oca-viewer-${theme}`, theme, enabled: () => true }) + const handler = makeHandler({ id: theme, tagName: `oca-viewer-${theme}`, theme, enabled: () => true }) const { vm, wrapper, modalProps } = mountViewer([handler]) const f1 = makeFile() await vm.open([f1], f1) @@ -371,7 +371,7 @@ describe('Viewer preload', () => { const preload = vi.fn(async () => {}) const handler = makeHandler({ id: 'image', - tagname: 'oca-viewer-image', + tagName: 'oca-viewer-image', group: 'media', preload, enabled: (nodes) => nodes.every((n) => n.mime?.startsWith('image/')), @@ -396,7 +396,7 @@ describe('Viewer preload', () => { // reason for the file the user clicked to never show up const handler = makeHandler({ id: 'image', - tagname: 'oca-viewer-image', + tagName: 'oca-viewer-image', preload: (() => { throw new Error('boom') }) as never, @@ -645,13 +645,13 @@ describe('opening over a viewer that is still open', () => { function pdfHandler() { return makeHandler({ id: 'pdf', - tagname: 'oca-viewer-pdf', + tagName: 'oca-viewer-pdf', enabled: (nodes) => nodes.every((n) => n.mime === 'application/pdf'), }) } function officeHandler() { - return makeHandler({ id: 'office', tagname: 'oca-viewer-office', enabled: () => false }) + return makeHandler({ id: 'office', tagName: 'oca-viewer-office', enabled: () => false }) } it('still tells the first opener when a handler reopens without options', async () => { diff --git a/__tests__/component/handlerContract.spec.ts b/__tests__/component/handlerContract.spec.ts index 595ddd5..61d013d 100644 --- a/__tests__/component/handlerContract.spec.ts +++ b/__tests__/component/handlerContract.spec.ts @@ -63,7 +63,7 @@ beforeAll(() => { function probeHandler(overrides = {}) { return makeHandler({ id: 'probe', - tagname: 'oca-viewer-probe', + tagName: 'oca-viewer-probe', enabled: () => true, ...overrides, }) diff --git a/__tests__/component/viewerApi.spec.ts b/__tests__/component/viewerApi.spec.ts index 563739a..a85c54c 100644 --- a/__tests__/component/viewerApi.spec.ts +++ b/__tests__/component/viewerApi.spec.ts @@ -34,7 +34,7 @@ const CANNOT_OPEN = 'We were not able to open the file.' function imageHandler(overrides = {}) { return makeHandler({ id: 'image', - tagname: 'oca-viewer-image', + tagName: 'oca-viewer-image', enabled: (nodes) => nodes.every((n) => n.mime?.startsWith('image/')), ...overrides, }) @@ -121,7 +121,7 @@ describe('open() with bad input', () => { describe('open() with a forced handler', () => { // Both take every image, so the first one registered is what // getHandlerForFile answers with, whatever the opener asked for. - const editor = () => imageHandler({ id: 'editor', tagname: 'oca-viewer-editor' }) + const editor = () => imageHandler({ id: 'editor', tagName: 'oca-viewer-editor' }) it('keeps the file list of the handler that was asked for', async () => { const { vm, wrapper, modalProps, renderedTags } = mountViewer([imageHandler(), editor()]) @@ -152,7 +152,7 @@ describe('open() with a forced handler', () => { const images = imageHandler({ group: 'media' }) const videos = makeHandler({ id: 'videos', - tagname: 'oca-viewer-videos', + tagName: 'oca-viewer-videos', group: 'media', enabled: (nodes) => nodes.every((n) => n.mime?.startsWith('video/')), }) @@ -303,7 +303,7 @@ describe('compare() with bad input', () => { }) it('uses the given handler for both sides', async () => { - const other = makeHandler({ id: 'other', tagname: 'oca-viewer-other' }) + const other = makeHandler({ id: 'other', tagName: 'oca-viewer-other' }) const { vm, wrapper, renderedTags } = mountViewer([imageHandler(), other]) await vm.compare(makeFile(), makeFile(), 'other') diff --git a/__tests__/encryptedFiles.spec.ts b/__tests__/encryptedFiles.spec.ts index 4b9c8e9..8a10c24 100644 --- a/__tests__/encryptedFiles.spec.ts +++ b/__tests__/encryptedFiles.spec.ts @@ -18,8 +18,8 @@ describe('an end-to-end encrypted file', () => { const plain = makeFile({ mime: 'text/markdown', attributes: { 'e2ee-is-encrypted': false } }) const unmarked = makeFile({ mime: 'text/markdown' }) - const custom = makeHandler({ id: 'custom', tagname: 'oca-viewer-custom' }) - const dav = makeHandler({ id: 'dav', tagname: 'oca-viewer-dav', supportsEndToEndEncryption: true }) + const custom = makeHandler({ id: 'custom', tagName: 'oca-viewer-custom' }) + const dav = makeHandler({ id: 'dav', tagName: 'oca-viewer-dav', supportsEndToEndEncryption: true }) it('is refused by a handler that has not opted in, without asking it', () => { const enabled = vi.fn(() => true) diff --git a/__tests__/factories.ts b/__tests__/factories.ts index f4067fa..9f991a0 100644 --- a/__tests__/factories.ts +++ b/__tests__/factories.ts @@ -59,7 +59,7 @@ export function makeHandler(overrides: Partial = {}): IHandler { return { id: 'test', displayName: 'Test handler', - tagname: 'oca-viewer-test', + tagName: 'oca-viewer-test', enabled: () => true, ...overrides, } diff --git a/__tests__/fileActions.spec.ts b/__tests__/fileActions.spec.ts index 5840c40..6adaf08 100644 --- a/__tests__/fileActions.spec.ts +++ b/__tests__/fileActions.spec.ts @@ -83,9 +83,9 @@ describe('registerHandler validation', () => { .toThrow('Handler displayName must be a non-empty string') }) - it('throws on empty tagname', () => { - expect(() => registerHandler(makeHandler({ tagname: '' }))) - .toThrow('Handler tagname must be a non-empty string') + it('throws on empty tagName', () => { + expect(() => registerHandler(makeHandler({ tagName: '' }))) + .toThrow('Handler tagName must be a non-empty string') }) it('throws on non-function enabled', () => { @@ -98,29 +98,29 @@ describe('registerHandler validation', () => { .toThrow("Handler theme must be one of 'dark', 'light', 'default' if provided") }) - it('throws on a tagname without a hyphen', () => { - expect(() => registerHandler(makeHandler({ tagname: 'nohyphen' }))) - .toThrow('Handler tagname must contain a hyphen (-)') + it('throws on a tagName without a hyphen', () => { + expect(() => registerHandler(makeHandler({ tagName: 'nohyphen' }))) + .toThrow('Handler tagName must contain a hyphen (-)') }) - it('throws on a tagname starting with an uppercase letter', () => { - expect(() => registerHandler(makeHandler({ tagname: 'Oca-viewer' }))) - .toThrow('Handler tagname must not start with an uppercase letter') + it('throws on a tagName starting with an uppercase letter', () => { + expect(() => registerHandler(makeHandler({ tagName: 'Oca-viewer' }))) + .toThrow('Handler tagName must not start with an uppercase letter') }) - it('throws on a tagname with consecutive hyphens', () => { - expect(() => registerHandler(makeHandler({ tagname: 'oca--viewer' }))) - .toThrow('Handler tagname must not contain consecutive hyphens (--)') + it('throws on a tagName with consecutive hyphens', () => { + expect(() => registerHandler(makeHandler({ tagName: 'oca--viewer' }))) + .toThrow('Handler tagName must not contain consecutive hyphens (--)') }) - it('throws on a tagname starting with a hyphen', () => { - expect(() => registerHandler(makeHandler({ tagname: '-oca-viewer' }))) - .toThrow('Handler tagname must not start or end with a hyphen (-)') + it('throws on a tagName starting with a hyphen', () => { + expect(() => registerHandler(makeHandler({ tagName: '-oca-viewer' }))) + .toThrow('Handler tagName must not start or end with a hyphen (-)') }) - it('throws on a tagname ending with a hyphen', () => { - expect(() => registerHandler(makeHandler({ tagname: 'oca-viewer-' }))) - .toThrow('Handler tagname must not start or end with a hyphen (-)') + it('throws on a tagName ending with a hyphen', () => { + expect(() => registerHandler(makeHandler({ tagName: 'oca-viewer-' }))) + .toThrow('Handler tagName must not start or end with a hyphen (-)') }) }) @@ -130,7 +130,7 @@ describe('registerHandler registry', () => { registerHandler(makeHandler({ id: 'dup' })) // Another handler, not the same one registered twice - registerHandler(makeHandler({ id: 'dup', tagname: 'other-app-dup' })) + registerHandler(makeHandler({ id: 'dup', tagName: 'other-app-dup' })) expect(warn).toHaveBeenCalledTimes(1) expect(warn).toHaveBeenCalledWith(expect.stringContaining('dup')) @@ -140,8 +140,8 @@ describe('registerHandler registry', () => { }) it('registers the shared actions only once across handlers', () => { - registerHandler(makeHandler({ id: 'one', tagname: 'oca-viewer-one' })) - registerHandler(makeHandler({ id: 'two', tagname: 'oca-viewer-two' })) + registerHandler(makeHandler({ id: 'one', tagName: 'oca-viewer-one' })) + registerHandler(makeHandler({ id: 'two', tagName: 'oca-viewer-two' })) expect(registered.filter((a) => a.id === ACTION_VIEWER)).toHaveLength(1) expect(registered.filter((a) => a.id === ACTION_VIEWER_MENU)).toHaveLength(1) @@ -150,7 +150,7 @@ describe('registerHandler registry', () => { describe('action gate', () => { it('with one matching handler: default enabled, "Open with …" not enabled', () => { - registerHandler(makeHandler({ id: 'only', tagname: 'oca-viewer-only', enabled: () => true })) + registerHandler(makeHandler({ id: 'only', tagName: 'oca-viewer-only', enabled: () => true })) const file = makeFile() expect(action(ACTION_VIEWER)!.enabled!(ctx([file]))).toBe(true) @@ -158,8 +158,8 @@ describe('action gate', () => { }) it('with two matching handlers: "Open with …" is enabled', () => { - registerHandler(makeHandler({ id: 'a', tagname: 'oca-viewer-a', enabled: () => true })) - registerHandler(makeHandler({ id: 'b', tagname: 'oca-viewer-b', enabled: () => true })) + registerHandler(makeHandler({ id: 'a', tagName: 'oca-viewer-a', enabled: () => true })) + registerHandler(makeHandler({ id: 'b', tagName: 'oca-viewer-b', enabled: () => true })) const file = makeFile() expect(action(ACTION_VIEWER)!.enabled!(ctx([file]))).toBe(true) @@ -167,8 +167,8 @@ describe('action gate', () => { }) it('is never enabled for folders', () => { - registerHandler(makeHandler({ id: 'a', tagname: 'oca-viewer-a', enabled: () => true })) - registerHandler(makeHandler({ id: 'b', tagname: 'oca-viewer-b', enabled: () => true })) + registerHandler(makeHandler({ id: 'a', tagName: 'oca-viewer-a', enabled: () => true })) + registerHandler(makeHandler({ id: 'b', tagName: 'oca-viewer-b', enabled: () => true })) const folder = makeFolder() expect(action(ACTION_VIEWER)!.enabled!(ctx([folder]))).toBe(false) @@ -176,7 +176,7 @@ describe('action gate', () => { }) it('is not enabled for a file the user cannot read', () => { - registerHandler(makeHandler({ id: 'a', tagname: 'oca-viewer-a', enabled: () => true })) + registerHandler(makeHandler({ id: 'a', tagName: 'oca-viewer-a', enabled: () => true })) const file = makeFile({ permissions: Permission.NONE }) expect(action(ACTION_VIEWER)!.enabled!(ctx([file]))).toBe(false) @@ -184,7 +184,7 @@ describe('action gate', () => { }) it('is enabled for a file that is only readable, as deleted files are', () => { - registerHandler(makeHandler({ id: 'a', tagname: 'oca-viewer-a', enabled: () => true })) + registerHandler(makeHandler({ id: 'a', tagName: 'oca-viewer-a', enabled: () => true })) // The trashbin reports its files as GD: readable and deletable const file = makeFile({ permissions: Permission.READ | Permission.DELETE }) @@ -192,7 +192,7 @@ describe('action gate', () => { }) it('is not enabled when one of several files cannot be read', () => { - registerHandler(makeHandler({ id: 'a', tagname: 'oca-viewer-a', enabled: () => true })) + registerHandler(makeHandler({ id: 'a', tagName: 'oca-viewer-a', enabled: () => true })) const readable = makeFile() const other = makeFile({ permissions: Permission.NONE }) @@ -200,7 +200,7 @@ describe('action gate', () => { }) it('is not enabled when no handler matches the file', () => { - registerHandler(makeHandler({ id: 'none', tagname: 'oca-viewer-none', enabled: () => false })) + registerHandler(makeHandler({ id: 'none', tagName: 'oca-viewer-none', enabled: () => false })) const file = makeFile() expect(action(ACTION_VIEWER)!.enabled!(ctx([file]))).toBe(false) @@ -211,7 +211,7 @@ describe('per-handler child action', () => { it('is enabled only when its own handler matches the file', () => { registerHandler(makeHandler({ id: 'pdf', - tagname: 'oca-viewer-pdf', + tagName: 'oca-viewer-pdf', enabled: (nodes) => nodes.every((n) => n.mime === 'application/pdf'), })) @@ -222,7 +222,7 @@ describe('per-handler child action', () => { }) it('forces its own handler id when opening', async () => { - registerHandler(makeHandler({ id: 'pdf', tagname: 'oca-viewer-pdf', enabled: () => true })) + registerHandler(makeHandler({ id: 'pdf', tagName: 'oca-viewer-pdf', enabled: () => true })) const file = makeFile() await action(`${ACTION_VIEWER_MENU}-pdf`)!.exec(ctx([file])) @@ -232,7 +232,7 @@ describe('per-handler child action', () => { }) it('default action opens without forcing a handler id', async () => { - registerHandler(makeHandler({ id: 'pdf', tagname: 'oca-viewer-pdf', enabled: () => true })) + registerHandler(makeHandler({ id: 'pdf', tagName: 'oca-viewer-pdf', enabled: () => true })) const file = makeFile() await action(ACTION_VIEWER)!.exec(ctx([file])) diff --git a/__tests__/handlerFaults.spec.ts b/__tests__/handlerFaults.spec.ts index 756d87f..0678f11 100644 --- a/__tests__/handlerFaults.spec.ts +++ b/__tests__/handlerFaults.spec.ts @@ -17,12 +17,12 @@ import { makeFile, makeHandler } from './factories.ts' describe('a handler whose enabled() throws', () => { const broken = makeHandler({ id: 'broken', - tagname: 'oca-viewer-broken', + tagName: 'oca-viewer-broken', enabled: () => { throw new Error('boom') }, }) - const fine = makeHandler({ id: 'fine', tagname: 'oca-viewer-fine' }) + const fine = makeHandler({ id: 'fine', tagName: 'oca-viewer-fine' }) const file = makeFile() it('is skipped when looking for the handler of a file', () => { diff --git a/__tests__/handlerHelper.spec.ts b/__tests__/handlerHelper.spec.ts index 32920cb..aa2d24f 100644 --- a/__tests__/handlerHelper.spec.ts +++ b/__tests__/handlerHelper.spec.ts @@ -8,7 +8,7 @@ import { makeFile, makeHandler, registerTestHandlers } from './factories.ts' describe('getHandlerForFile', () => { it('returns the first handler whose enabled matches', () => { - registerTestHandlers(makeHandler({ id: 'image', tagname: 'oca-viewer-image', enabled: (nodes) => nodes.every((n) => n.mime?.startsWith('image/')) })) + registerTestHandlers(makeHandler({ id: 'image', tagName: 'oca-viewer-image', enabled: (nodes) => nodes.every((n) => n.mime?.startsWith('image/')) })) expect(getHandlerForFile(makeFile({ mime: 'image/png' }))?.id).toBe('image') }) @@ -18,7 +18,7 @@ describe('getHandlerForFile', () => { }) it('respects the group filter', () => { - registerTestHandlers(makeHandler({ id: 'video', tagname: 'oca-viewer-video', group: 'media', enabled: () => true })) + registerTestHandlers(makeHandler({ id: 'video', tagName: 'oca-viewer-video', group: 'media', enabled: () => true })) expect(getHandlerForFile(makeFile(), 'media')?.id).toBe('video') expect(getHandlerForFile(makeFile(), 'other')).toBeUndefined() }) diff --git a/__tests__/models.spec.ts b/__tests__/models.spec.ts index 5fcb8ec..1168b4d 100644 --- a/__tests__/models.spec.ts +++ b/__tests__/models.spec.ts @@ -69,7 +69,7 @@ describe('the custom elements', () => { const model = await import(module) as Record Promise> await model[register]!() await expect(model[register]!()).resolves.toBeUndefined() - expect(window.customElements.get(model.tagname as unknown as string)).toBeDefined() + expect(window.customElements.get(model.tagName as unknown as string)).toBeDefined() }) }) diff --git a/__tests__/registerHandler.spec.ts b/__tests__/registerHandler.spec.ts index 0848840..6b0b68a 100644 --- a/__tests__/registerHandler.spec.ts +++ b/__tests__/registerHandler.spec.ts @@ -17,9 +17,9 @@ describe('registering a handler whose id is taken', () => { // Several copies of the package on a page each register the // defaults: that is one handler, not a collision const warn = vi.spyOn(logger, 'warn') - const first = makeHandler({ id: 'repeat', tagname: 'oca-viewer-repeat' }) + const first = makeHandler({ id: 'repeat', tagName: 'oca-viewer-repeat' }) registerHandler(first) - registerHandler(makeHandler({ id: 'repeat', tagname: 'oca-viewer-repeat' })) + registerHandler(makeHandler({ id: 'repeat', tagName: 'oca-viewer-repeat' })) expect(scope.handlers!.get('repeat')).toBe(first) expect(warn).not.toHaveBeenCalled() diff --git a/e2e/copies.spec.ts b/e2e/copies.spec.ts index 399e80d..4017f04 100644 --- a/e2e/copies.spec.ts +++ b/e2e/copies.spec.ts @@ -51,7 +51,7 @@ test.describe('Two copies of the package on one page', () => { await page.goto('/') await page.evaluate((path) => import(/* @vite-ignore */ `/@fs${path}/index.mjs`), secondCopy) - const candidates = await page.evaluate(() => window._nc_viewer_scope!.handlers_v1!.candidates.map((candidate) => candidate.version)) + const candidates = await page.evaluate(() => window._nc_viewer_scope!.handlers_v2!.candidates.map((candidate) => candidate.version)) expect(candidates).toEqual(['0.0.0-playground', version]) // Opened from the playground's copy, shown by the built one diff --git a/lib/handlers.ts b/lib/handlers.ts index a43bd40..f2bed6b 100644 --- a/lib/handlers.ts +++ b/lib/handlers.ts @@ -36,7 +36,7 @@ export interface IHandler { /** * The custom element tag name to use for this handler. */ - tagname: string + tagName: string /** * Identifier to group handlers by. @@ -216,11 +216,11 @@ export function registerHandler(handler: IHandler): void { // Every app bundles its own copy of the package, so the same handler // can be registered more than once: the server's copy and an app's // both register the defaults. The first one stays, as the custom - // element its tagname names is the first copy's too. - if (registered.tagname === handler.tagname) { + // element its tagName names is the first copy's too. + if (registered.tagName === handler.tagName) { logger.debug(`Handler ${handler.id} is already registered, keeping the first registration`) } else { - logger.warn(`Handler with id ${handler.id} is already registered for <${registered.tagname}>, ignoring the one for <${handler.tagname}>.`) + logger.warn(`Handler with id ${handler.id} is already registered for <${registered.tagName}>, ignoring the one for <${handler.tagName}>.`) } return } @@ -287,8 +287,8 @@ function validateHandler(handler: IHandler): void { throw new Error('Handler displayName must be a non-empty string') } - if (typeof handler.tagname !== 'string' || handler.tagname.trim() === '') { - throw new Error('Handler tagname must be a non-empty string') + if (typeof handler.tagName !== 'string' || handler.tagName.trim() === '') { + throw new Error('Handler tagName must be a non-empty string') } if (group && (typeof group !== 'string' || group.trim() === '')) { @@ -307,28 +307,28 @@ function validateHandler(handler: IHandler): void { throw new Error("Handler theme must be one of 'dark', 'light', 'default' if provided") } - validateCustomElementName(handler.tagname) + validateCustomElementName(handler.tagName) } /** * Validate that the given tag name is a valid custom element name. * - * @param tagname - The custom element tag name to validate + * @param tagName - The custom element tag name to validate */ -function validateCustomElementName(tagname: string): void { - if (!tagname.includes('-')) { - throw new Error('Handler tagname must contain a hyphen (-)') +function validateCustomElementName(tagName: string): void { + if (!tagName.includes('-')) { + throw new Error('Handler tagName must contain a hyphen (-)') } - if (/^[A-Z]/.test(tagname)) { - throw new Error('Handler tagname must not start with an uppercase letter') + if (/^[A-Z]/.test(tagName)) { + throw new Error('Handler tagName must not start with an uppercase letter') } - if (/--/.test(tagname)) { - throw new Error('Handler tagname must not contain consecutive hyphens (--)') + if (/--/.test(tagName)) { + throw new Error('Handler tagName must not contain consecutive hyphens (--)') } - if (tagname.startsWith('-') || tagname.endsWith('-')) { - throw new Error('Handler tagname must not start or end with a hyphen (-)') + if (tagName.startsWith('-') || tagName.endsWith('-')) { + throw new Error('Handler tagName must not start or end with a hyphen (-)') } - if (!/^[a-z][a-z0-9-]*$/.test(tagname)) { - throw new Error('Handler tagname must only contain lowercase letters, numbers, and hyphens (-)') + if (!/^[a-z][a-z0-9-]*$/.test(tagName)) { + throw new Error('Handler tagName must only contain lowercase letters, numbers, and hyphens (-)') } } diff --git a/lib/models/audios.ts b/lib/models/audios.ts index a1bf9ea..e8ea265 100644 --- a/lib/models/audios.ts +++ b/lib/models/audios.ts @@ -27,7 +27,7 @@ export const browserSupportedMimes = [ 'audio/x-wav', ] -export const tagname = 'oca-viewer-audio' +export const tagName = 'oca-viewer-audio' /** * Register the audio custom element. @@ -38,7 +38,7 @@ export async function registerAudioCustomElement(): Promise { shadowRoot: false, }) - defineCustomElementOnce(tagname, AudioElement) + defineCustomElementOnce(tagName, AudioElement) } /** @@ -48,7 +48,7 @@ export function registerAudioHandler() { registerHandler({ id: 'audios', displayName: t('Audio player'), - tagname, + tagName, supportsEndToEndEncryption: true, iconSvgInline: AudioOutlineSvg, @@ -70,5 +70,5 @@ export function registerAudioHandler() { }) }, }) - logger.info('Audio handler registered', { tagname }) + logger.info('Audio handler registered', { tagName }) } diff --git a/lib/models/images.ts b/lib/models/images.ts index 5682d0d..343993c 100644 --- a/lib/models/images.ts +++ b/lib/models/images.ts @@ -76,7 +76,7 @@ if (ignoredMimes.length > 0) { logger.warn('Some mimes were ignored because they are not enabled in the server previews config', { ignoredMimes }) } -export const tagname = 'oca-viewer-image' +export const tagName = 'oca-viewer-image' /** * Define the custom element the image handler names. @@ -87,7 +87,7 @@ export async function registerImageCustomElement(): Promise { shadowRoot: false, }) - defineCustomElementOnce(tagname, ImageElement) + defineCustomElementOnce(tagName, ImageElement) } /** @@ -97,7 +97,7 @@ export function registerImageHandler() { registerHandler({ id: 'images', displayName: t('Images'), - tagname, + tagName, supportsEndToEndEncryption: true, canEdit: true, @@ -120,5 +120,5 @@ export function registerImageHandler() { }) }, }) - logger.info('Image handler registered', { tagname, enabledMimes, browserSupportedMimes }) + logger.info('Image handler registered', { tagName, enabledMimes, browserSupportedMimes }) } diff --git a/lib/models/sheetmusic.ts b/lib/models/sheetmusic.ts index 9c89212..a2de70a 100644 --- a/lib/models/sheetmusic.ts +++ b/lib/models/sheetmusic.ts @@ -26,7 +26,7 @@ export const supportedMimes = [ 'application/vnd.recordare.musicxml+xml', ] -export const tagname = 'oca-viewer-sheetmusic' +export const tagName = 'oca-viewer-sheetmusic' /** * Register the sheet music custom element. @@ -37,7 +37,7 @@ export async function registerSheetmusicCustomElement(): Promise { shadowRoot: false, }) - defineCustomElementOnce(tagname, SheetmusicElement) + defineCustomElementOnce(tagName, SheetmusicElement) } /** @@ -47,7 +47,7 @@ export function registerSheetmusicHandler() { registerHandler({ id: 'sheetmusic', displayName: t('Sheet music'), - tagname, + tagName, iconSvgInline: MusicClefTrebleSvg, @@ -59,5 +59,5 @@ export function registerSheetmusicHandler() { return nodes.every((node) => supportedMimes.includes(node.mime)) }, }) - logger.info('Sheet music handler registered', { tagname }) + logger.info('Sheet music handler registered', { tagName }) } diff --git a/lib/models/videos.ts b/lib/models/videos.ts index 7f4cbd2..247bfcd 100644 --- a/lib/models/videos.ts +++ b/lib/models/videos.ts @@ -24,7 +24,7 @@ export const aliasedMimes = { 'video/x-matroska': 'video/webm', } -export const tagname = 'oca-viewer-video' +export const tagName = 'oca-viewer-video' /** * Register the video custom element. @@ -35,7 +35,7 @@ export async function registerVideoCustomElement(): Promise { shadowRoot: false, }) - defineCustomElementOnce(tagname, VideoElement) + defineCustomElementOnce(tagName, VideoElement) } /** @@ -45,7 +45,7 @@ export function registerVideoHandler() { registerHandler({ id: 'videos', displayName: t('Video player'), - tagname, + tagName, supportsEndToEndEncryption: true, iconSvgInline: MovieOutlineSvg, @@ -72,5 +72,5 @@ export function registerVideoHandler() { }) }, }) - logger.info('Video handler registered', { tagname }) + logger.info('Video handler registered', { tagName }) } diff --git a/lib/scope.ts b/lib/scope.ts index 1fb81f2..063ece8 100644 --- a/lib/scope.ts +++ b/lib/scope.ts @@ -18,7 +18,7 @@ import { logger } from './services/logger.ts' * is a single modal on the page, a split registry means handlers that * quietly never open. */ -const ABI = 'handlers_v1' +const ABI = 'handlers_v2' /** A copy of the library offering to be the implementation on this page */ export interface ViewerCandidate { diff --git a/lib/utils/customElements.ts b/lib/utils/customElements.ts index 147f6c9..4a0e779 100644 --- a/lib/utils/customElements.ts +++ b/lib/utils/customElements.ts @@ -12,14 +12,14 @@ import { logger } from '../services/logger.ts' * keeps its own registry, names the same elements. Whichever mounts second * would throw here and never finish mounting. * - * @param tagname - The custom element name to define + * @param tagName - The custom element name to define * @param constructor - The element to define it as */ -export function defineCustomElementOnce(tagname: string, constructor: CustomElementConstructor): void { - if (window.customElements.get(tagname) !== undefined) { - logger.debug(`The custom element ${tagname} is already defined, leaving it alone`, { tagname }) +export function defineCustomElementOnce(tagName: string, constructor: CustomElementConstructor): void { + if (window.customElements.get(tagName) !== undefined) { + logger.debug(`The custom element ${tagName} is already defined, leaving it alone`, { tagName }) return } - window.customElements.define(tagname, constructor) + window.customElements.define(tagName, constructor) } diff --git a/lib/views/Viewer.vue b/lib/views/Viewer.vue index c7cdfa0..233414c 100644 --- a/lib/views/Viewer.vue +++ b/lib/views/Viewer.vue @@ -144,7 +144,7 @@ is patched before it upgrades. -->