diff --git a/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.stories.ts b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.stories.ts new file mode 100644 index 0000000000..ef1f26d6b7 --- /dev/null +++ b/lib/core/src/lib/viewer/components/viewer-render/viewer-render.component.stories.ts @@ -0,0 +1,196 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { applicationConfig, Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { provideHttpClient } from '@angular/common/http'; +import { ViewerRenderComponent } from './viewer-render.component'; +import { Track } from '../../models/viewer.model'; +import { provideStoryCore } from '../../../stories/core-story.providers'; + +const SAMPLE_IMAGE_URL = + 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/640px-PNG_transparency_demonstration_1.png'; +const SAMPLE_PDF_URL = 'https://mozilla.github.io/pdf.js/web/compressed.tracemonkey-pldi-09.pdf'; + +const TEXT_BLOB = new Blob(['Hello from ViewerRender stories!\nThis text blob is rendered through the txt viewer renderer.'], { type: 'text/plain' }); + +type ViewerRenderStoryArgs = ViewerRenderComponent & { + sampleTracks?: Track[]; +}; + +const meta: Meta = { + component: ViewerRenderComponent, + title: 'Core/Viewer/Viewer Render', + decorators: [ + moduleMetadata({ + imports: [ViewerRenderComponent] + }), + applicationConfig({ + providers: [provideHttpClient(), ...provideStoryCore()] + }) + ], + parameters: { + docs: { + description: { + component: `Headless rendering surface used by the Viewer to delegate to the appropriate sub-viewer (image, pdf, txt, media, unknown) based on MIME type or file extension.` + } + } + }, + argTypes: { + urlFile: { + control: 'text', + description: 'External URL pointing to the file to render.', + table: { type: { summary: 'string' } } + }, + blobFile: { + control: false, + description: 'In-memory Blob containing the file content.', + table: { type: { summary: 'Blob' } } + }, + mimeType: { + control: 'text', + description: 'MIME type override used when the URL extension cannot be relied on.', + table: { type: { summary: 'string' } } + }, + fileName: { + control: 'text', + description: 'Optional file name; influences extension detection when no MIME type is supplied.', + table: { type: { summary: 'string' } } + }, + allowFullScreen: { + control: 'boolean', + description: 'Toggles fullscreen capability on supported sub-viewers.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + allowThumbnails: { + control: 'boolean', + description: 'Toggles PDF thumbnails support.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + readOnly: { + control: 'boolean', + description: 'Disables editing actions on sub-viewers (e.g. image crop / rotate).', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + allowedEditActions: { + control: 'object', + description: 'Map of editing action toggles forwarded to sub-viewers.', + table: { type: { summary: '{ [key: string]: boolean }' } } + }, + tracks: { + control: 'object', + description: 'Subtitle / caption tracks forwarded to the media-player sub-viewer.', + table: { type: { summary: 'Track[]' } } + }, + nodeId: { + control: 'text', + description: 'Identifier of the node opened in the viewer.', + table: { type: { summary: 'string' } } + }, + customError: { + control: 'text', + description: 'Custom error message displayed when the file format is unsupported.', + table: { type: { summary: 'string' } } + }, + extensionChange: { + action: 'extensionChange', + description: 'Emitted when the detected file extension changes.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + submitFile: { + action: 'submitFile', + description: 'Emitted when an image is saved by the image sub-viewer.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + close: { + action: 'close', + description: 'Emitted when the renderer requests a close (e.g. password dialog dismissed).', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + isSaving: { + action: 'isSaving', + description: 'Emits `true` while an image edit is being saved.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + } + }, + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + allowFullScreen: true, + allowThumbnails: true, + readOnly: true, + allowedEditActions: { rotate: true, crop: true } + }, + render: (args) => ({ + props: args, + template: `
+ +
` + }) +}; + +export default meta; +type Story = StoryObj; + +export const ImageFromUrl: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + mimeType: 'image/png', + fileName: 'sample-image.png' + } +}; + +export const PdfFromUrl: Story = { + args: { + urlFile: SAMPLE_PDF_URL, + mimeType: 'application/pdf', + fileName: 'sample.pdf' + } +}; + +export const TextFromBlob: Story = { + args: { + urlFile: undefined, + blobFile: TEXT_BLOB, + fileName: 'sample.txt', + mimeType: 'text/plain' + } +}; + +export const UnsupportedWithCustomError: Story = { + args: { + urlFile: 'https://example.com/file.bin', + mimeType: 'application/octet-stream', + fileName: 'unsupported.bin', + customError: 'This file type cannot be previewed.' + } +}; diff --git a/lib/core/src/lib/viewer/components/viewer.component.stories.ts b/lib/core/src/lib/viewer/components/viewer.component.stories.ts new file mode 100644 index 0000000000..07b7a9ae69 --- /dev/null +++ b/lib/core/src/lib/viewer/components/viewer.component.stories.ts @@ -0,0 +1,417 @@ +/*! + * @license + * Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { applicationConfig, Meta, StoryObj, moduleMetadata } from '@storybook/angular'; +import { provideHttpClient } from '@angular/common/http'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatMenuModule } from '@angular/material/menu'; +import { ViewerComponent } from './viewer.component'; +import { ViewerToolbarComponent } from './viewer-toolbar.component'; +import { ViewerToolbarActionsComponent } from './viewer-toolbar-actions.component'; +import { ViewerToolbarCustomActionsComponent } from './viewer-toolbar-custom-actions.component'; +import { ViewerSidebarComponent } from './viewer-sidebar.component'; +import { ViewerMoreActionsComponent } from './viewer-more-actions.component'; +import { ViewerOpenWithComponent } from './viewer-open-with.component'; +import { CloseButtonPosition } from '../models/viewer.model'; +import { provideStoryCore } from '../../stories/core-story.providers'; + +const SAMPLE_IMAGE_URL = + 'https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/PNG_transparency_demonstration_1.png/640px-PNG_transparency_demonstration_1.png'; + +type ViewerStoryArgs = ViewerComponent & { + showCustomToolbar?: boolean; + showCustomActions?: boolean; + showOpenWith?: boolean; + showMoreActions?: boolean; + showSidebar?: boolean; +}; + +const meta: Meta = { + component: ViewerComponent, + title: 'Core/Viewer/Viewer', + decorators: [ + moduleMetadata({ + imports: [ + ViewerComponent, + ViewerToolbarComponent, + ViewerToolbarActionsComponent, + ViewerToolbarCustomActionsComponent, + ViewerSidebarComponent, + ViewerMoreActionsComponent, + ViewerOpenWithComponent, + MatButtonModule, + MatIconModule, + MatMenuModule + ] + }), + applicationConfig({ + providers: [provideHttpClient(), ...provideStoryCore()] + }) + ], + parameters: { + docs: { + description: { + component: `Top-level Viewer container that orchestrates the toolbar, sidebar, file rendering and download-prompt dialog flow.` + } + } + }, + argTypes: { + urlFile: { + control: 'text', + description: 'External URL pointing to the file to display.', + table: { type: { summary: 'string' } } + }, + blobFile: { + control: false, + description: 'In-memory Blob containing the file to display.', + table: { type: { summary: 'Blob' } } + }, + showViewer: { + control: 'boolean', + description: 'Toggles visibility of the entire viewer container.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + allowGoBack: { + control: 'boolean', + description: 'Shows the close (back) button on the toolbar.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + allowFullScreen: { + control: 'boolean', + description: 'Toggles the fullscreen action on the toolbar.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + showToolbar: { + control: 'boolean', + description: 'Toggles visibility of the built-in toolbar (ignored when a custom `` is projected).', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + overlayMode: { + control: 'boolean', + description: 'When `true`, renders the viewer as a full-page overlay over the current content.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + allowNavigate: { + control: 'boolean', + description: 'Enables previous/next navigation buttons.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + canNavigateBefore: { + control: 'boolean', + description: 'Toggles the previous (`<`) navigation button. Requires `allowNavigate`.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + canNavigateNext: { + control: 'boolean', + description: 'Toggles the next (`>`) navigation button. Requires `allowNavigate`.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + allowLeftSidebar: { + control: 'boolean', + description: 'Allows showing the left-side info sidebar.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + allowRightSidebar: { + control: 'boolean', + description: 'Allows showing the right-side info sidebar.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + showLeftSidebar: { + control: 'boolean', + description: 'Visibility of the left sidebar. Requires `allowLeftSidebar`.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + showRightSidebar: { + control: 'boolean', + description: 'Visibility of the right sidebar. Requires `allowRightSidebar`.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + readOnly: { + control: 'boolean', + description: 'Disables editing functionality on the underlying renderer.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + allowedEditActions: { + control: 'object', + description: 'Per-action map controlling which editing actions are enabled (e.g. `{ rotate: true, crop: false }`).', + table: { type: { summary: '{ [key: string]: boolean }' } } + }, + tracks: { + control: 'object', + description: 'Media subtitles forwarded to the media player.', + table: { type: { summary: 'Track[]' } } + }, + mimeType: { + control: 'text', + description: 'Override MIME type for the file.', + table: { type: { summary: 'string' } } + }, + fileName: { + control: 'text', + description: 'Override file name displayed in the toolbar.', + table: { type: { summary: 'string' } } + }, + title: { + control: 'text', + description: 'Override content title displayed by the toolbar.', + table: { type: { summary: 'string' } } + }, + nodeId: { + control: 'text', + description: 'Identifier of the node currently opened by the viewer.', + table: { type: { summary: 'string' } } + }, + nodeMimeType: { + control: 'text', + description: 'Original node MIME type, used when the rendition MIME differs.', + table: { type: { summary: 'string' } } + }, + customError: { + control: 'text', + description: 'Custom error message displayed when the format is unsupported.', + table: { type: { summary: 'string' } } + }, + showToolbarDividers: { + control: 'boolean', + description: 'Toggles toolbar divider visibility.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'true' } } + }, + hideInfoButton: { + control: 'boolean', + description: 'Hides the info button on the toolbar.', + table: { type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + closeButtonPosition: { + control: 'inline-radio', + options: [CloseButtonPosition.Left, CloseButtonPosition.Right], + description: 'Position of the close button: `left` or `right`.', + table: { type: { summary: `'left' | 'right'` }, defaultValue: { summary: `'left'` } } + }, + showCustomToolbar: { + control: 'boolean', + description: 'Replaces the built-in toolbar with a custom projected ``.', + table: { category: 'Storybook Helpers' } + }, + showCustomActions: { + control: 'boolean', + description: 'Projects sample buttons inside ``.', + table: { category: 'Storybook Helpers' } + }, + showOpenWith: { + control: 'boolean', + description: 'Projects a sample `` menu in the toolbar.', + table: { category: 'Storybook Helpers' } + }, + showMoreActions: { + control: 'boolean', + description: 'Projects a sample `` overflow menu in the toolbar.', + table: { category: 'Storybook Helpers' } + }, + showSidebar: { + control: 'boolean', + description: 'Projects a sample `` content panel.', + table: { category: 'Storybook Helpers' } + }, + downloadFile: { + action: 'downloadFile', + description: 'Emitted when the user clicks Download in the download prompt dialog.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + navigateBefore: { + action: 'navigateBefore', + description: 'Emitted when the user clicks the previous (`<`) navigation button.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + navigateNext: { + action: 'navigateNext', + description: 'Emitted when the user clicks the next (`>`) navigation button.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + showViewerChange: { + action: 'showViewerChange', + description: 'Emitted when the viewer is closed.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + }, + submitFile: { + action: 'submitFile', + description: 'Emitted when an image edit is submitted by the image viewer.', + table: { type: { summary: 'EventEmitter ' }, category: 'Actions' } + } + }, + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + showViewer: true, + showToolbar: true, + overlayMode: false, + allowGoBack: true, + allowFullScreen: true, + allowNavigate: false, + canNavigateBefore: true, + canNavigateNext: true, + allowLeftSidebar: false, + allowRightSidebar: false, + showLeftSidebar: false, + showRightSidebar: false, + readOnly: true, + allowedEditActions: { rotate: true, crop: true }, + showToolbarDividers: true, + hideInfoButton: false, + closeButtonPosition: CloseButtonPosition.Left, + showCustomToolbar: false, + showCustomActions: false, + showOpenWith: false, + showMoreActions: false, + showSidebar: false + }, + render: (args) => ({ + props: args, + template: `
+ + + + Custom Toolbar · {{ fileName }} + + + + + + + + + + + + + + + + + + + + + + + + + + +

Details

+

Sidebar content goes here.

+
+
+
` + }) +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png' + } +}; + +export const WithSidebar: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + allowRightSidebar: true, + showRightSidebar: true, + showSidebar: true + } +}; + +export const WithCustomToolbarActions: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + showCustomActions: true, + showOpenWith: true, + showMoreActions: true + } +}; + +export const WithCustomToolbar: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + showCustomToolbar: true + } +}; + +export const NavigationEnabled: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + allowNavigate: true, + canNavigateBefore: true, + canNavigateNext: true + } +}; + +export const CloseButtonOnRight: Story = { + args: { + urlFile: SAMPLE_IMAGE_URL, + fileName: 'sample-image.png', + mimeType: 'image/png', + closeButtonPosition: CloseButtonPosition.Right + } +};