diff --git a/packages/solid-crs-components/lib/alerts/popup.component.spec.ts b/packages/solid-crs-components/lib/alerts/popup.component.spec.ts new file mode 100644 index 00000000..20fc35ff --- /dev/null +++ b/packages/solid-crs-components/lib/alerts/popup.component.spec.ts @@ -0,0 +1,86 @@ +import { PopupComponent } from './popup.component'; + +describe('PopupComponent', () => { + + let component: PopupComponent; + + beforeEach(() => { + + component = window.document.createElement('nde-popup') as PopupComponent; + + }); + + afterEach(() => { + + document.getElementsByTagName('html')[0].innerHTML = ''; + + }); + + it('should be correctly instantiated', () => { + + expect(component).toBeTruthy(); + + }); + + it('should add correct class when this.dark is set', async () => { + + window.document.body.appendChild(component); + await component.updateComplete; + + const div = window.document.body.getElementsByTagName('nde-popup')[0].shadowRoot.querySelector('div.overlay'); + expect(div.className).not.toMatch('dark'); + + component.dark = true; + await component.updateComplete; + expect(div.className).toMatch('dark'); + + }); + + it('should hide the component when the background is clicked', async () => { + + window.document.body.appendChild(component); + await component.updateComplete; + component.show(); + expect(component.hidden).toBeFalsy(); + + const div = window.document.body.getElementsByTagName('nde-popup')[0].shadowRoot.querySelector('div.overlay'); + div.click(); + + await component.updateComplete; + expect(component.hidden).toBeTruthy(); + + }); + + it('should not hide the component when the content is clicked', async () => { + + window.document.body.appendChild(component); + await component.updateComplete; + component.show(); + expect(component.hidden).toBeFalsy(); + + const content = window.document.body.getElementsByTagName('nde-popup')[0].shadowRoot.querySelector('slot'); + content.click(); + + await component.updateComplete; + expect(component.hidden).toBeFalsy(); + + }); + + describe('toggle', () => { + + it('should toggle this.hidden', async () => { + + window.document.body.appendChild(component); + await component.updateComplete; + + expect(component.hidden).toEqual(true); + component.toggle(); + expect(component.hidden).toEqual(false); + component.toggle(); + expect(component.hidden).toEqual(true); + + }); + + }); + +}); diff --git a/packages/solid-crs-components/lib/alerts/popup.component.ts b/packages/solid-crs-components/lib/alerts/popup.component.ts new file mode 100644 index 00000000..0ca32187 --- /dev/null +++ b/packages/solid-crs-components/lib/alerts/popup.component.ts @@ -0,0 +1,102 @@ + +import { css, CSSResult, html, property, query, TemplateResult, unsafeCSS } from 'lit-element'; +import { Theme } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; +import { RxLitElement } from 'rx-lit'; + +/** + * A component that displays any content over the whole webpage + * Hide by toggling this.hidden + */ +export class PopupComponent extends RxLitElement { + + /** + * The content element of this component + */ + @query('slot[name="content"]') + content: HTMLSlotElement; + + /** + * Decides whether the component has a dark background + */ + @property({ type: Boolean }) + public dark = false; + + /** + * Hides/shows the component + */ + toggle(): void { + + this.hidden ? this.show() : this.hide(); + + } + + /** + * Shows the component + */ + show(): void { + + this.hidden = false; + + } + + /** + * Hides the component + */ + hide(): void { + + this.hidden = true; + + } + + /** + * Renders the component as HTML. + * + * @returns The rendered HTML of the component. + */ + render(): TemplateResult { + + // initially, always hide the component + this.hide(); + + return html` +
+ +
+ `; + + } + + /** + * The styles associated with the component. + */ + static get styles(): CSSResult[] { + + return [ + unsafeCSS(Theme), + css` + :host { + height: 100%; + width: 100%; + top: 0; + left: 0; + position: fixed; + } + .overlay { + height: 100%; + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + .dark { + background-color: rgba(0, 0, 0, 0.8); + } + `, + ]; + + } + +} + +export default PopupComponent; diff --git a/packages/solid-crs-components/lib/demo.ts b/packages/solid-crs-components/lib/demo.ts index c32068a8..d71d2a9a 100644 --- a/packages/solid-crs-components/lib/demo.ts +++ b/packages/solid-crs-components/lib/demo.ts @@ -16,6 +16,7 @@ import { SidebarItemComponent } from './sidebar/sidebar-item.component'; import { LargeCardComponent } from './collections/large-card.component'; import { DemoLargeCardComponent } from './demo/demo-large-card.component'; import { ProgressBarComponent } from './loading/progress-bar-component'; +import { PopupComponent } from './alerts/popup.component'; /** * Register tags for components. @@ -38,3 +39,4 @@ customElements.define('nde-demo-content-header', DemoContentHeaderComponent); customElements.define('nde-demo-svg', DemoSVGComponent); customElements.define('nde-sidebar-item', SidebarItemComponent); customElements.define('nde-progress-bar', ProgressBarComponent); +customElements.define('nde-popup', PopupComponent); diff --git a/packages/solid-crs-components/lib/index.ts b/packages/solid-crs-components/lib/index.ts index e87c864b..56e64458 100644 --- a/packages/solid-crs-components/lib/index.ts +++ b/packages/solid-crs-components/lib/index.ts @@ -4,6 +4,7 @@ export * from './alerts/alert'; export * from './alerts/alert.component'; export * from './alerts/alert.component'; +export * from './alerts/popup.component'; export * from './collections/card.component'; export * from './collections/large-card.component'; export * from './collections/object-card.component'; diff --git a/packages/solid-crs-components/package.json b/packages/solid-crs-components/package.json index 3cac7f35..ca894033 100644 --- a/packages/solid-crs-components/package.json +++ b/packages/solid-crs-components/package.json @@ -74,10 +74,10 @@ ], "coverageThreshold": { "global": { - "branches": 92.99, - "functions": 98.85, + "branches": 93.17, + "functions": 98.95, "lines": 100, - "statements": 99.66 + "statements": 99.68 } }, "coveragePathIgnorePatterns": [ @@ -97,4 +97,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/solid-crs-components/tests/setup.ts b/packages/solid-crs-components/tests/setup.ts index 31e2a538..a09956f3 100644 --- a/packages/solid-crs-components/tests/setup.ts +++ b/packages/solid-crs-components/tests/setup.ts @@ -11,6 +11,7 @@ import { SidebarComponent } from '../lib/sidebar/sidebar.component'; import { SidebarItemComponent } from '../lib/sidebar/sidebar-item.component'; import { LargeCardComponent } from '../lib/collections/large-card.component'; import { ProgressBarComponent } from '../lib/loading/progress-bar-component'; +import { PopupComponent } from '../lib/alerts/popup.component'; /** * Register tags for components. @@ -28,3 +29,4 @@ customElements.define('nde-collection-card', CollectionCardComponent); customElements.define('nde-card', CardComponent); customElements.define('nde-large-card', LargeCardComponent); customElements.define('nde-progress-bar', ProgressBarComponent); +customElements.define('nde-popup', PopupComponent); diff --git a/packages/solid-crs-presentation/lib/app.machine.ts b/packages/solid-crs-presentation/lib/app.machine.ts index a09b58bc..f8185f60 100644 --- a/packages/solid-crs-presentation/lib/app.machine.ts +++ b/packages/solid-crs-presentation/lib/app.machine.ts @@ -137,7 +137,9 @@ export const appMachine = ( }, [AppEvents.NAVIGATE]: { target: `#${AppRouterStates.NAVIGATING}`, - actions: assign({ path: (context, event) => event.path||window.location.pathname }), + actions: assign({ + path: (context, event) => event.path||window.location.pathname, + }), }, [AppEvents.CLICKED_HOME]: { actions: assign({ selected: (context) => undefined }), diff --git a/packages/solid-crs-presentation/lib/app.ts b/packages/solid-crs-presentation/lib/app.ts index 92fbe6b2..8a462584 100644 --- a/packages/solid-crs-presentation/lib/app.ts +++ b/packages/solid-crs-presentation/lib/app.ts @@ -1,4 +1,4 @@ -import { AlertComponent, CardComponent, CollectionCardComponent, ContentHeaderComponent, FormElementComponent, LargeCardComponent, ObjectCardComponent, SidebarComponent, SidebarItemComponent, SidebarListComponent, SidebarListItemComponent, ProgressBarComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; +import { AlertComponent, CardComponent, CollectionCardComponent, ContentHeaderComponent, FormElementComponent, LargeCardComponent, ObjectCardComponent, SidebarComponent, SidebarItemComponent, SidebarListComponent, SidebarListItemComponent, ProgressBarComponent, PopupComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; import { inspect } from '@xstate/inspect'; import { SearchRootComponent } from './features/search/search-root.component'; import { AppRootComponent } from './app-root.component'; @@ -40,3 +40,4 @@ customElements.define('nde-sidebar-list-item', SidebarListItemComponent); customElements.define('nde-sidebar-list', SidebarListComponent); customElements.define('nde-sidebar-item', SidebarItemComponent); customElements.define('nde-progress-bar', ProgressBarComponent); +customElements.define('nde-popup', PopupComponent); diff --git a/packages/solid-crs-presentation/lib/features/about/about-root.component.spec.ts b/packages/solid-crs-presentation/lib/features/about/about-root.component.spec.ts index 4bd7f75f..52c9410f 100644 --- a/packages/solid-crs-presentation/lib/features/about/about-root.component.spec.ts +++ b/packages/solid-crs-presentation/lib/features/about/about-root.component.spec.ts @@ -1,12 +1,13 @@ -import { Alert } from '@netwerk-digitaal-erfgoed/solid-crs-components'; +import { Alert, LargeCardComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; import { ArgumentError, Collection, CollectionMemoryStore, CollectionObject, CollectionObjectMemoryStore, ConsoleLogger, LoggerLevel, MemoryTranslator } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { interpret, Interpreter } from 'xstate'; import { AppEvents, DismissAlertEvent } from '../../app.events'; import { AppContext, appMachine } from '../../app.machine'; import { SolidMockService } from '../../common/solid/solid-mock.service'; +import { CollectionEvents } from '../collection/collection.events'; import { AboutRootComponent } from './about-root.component'; -describe('SearchRootComponent', () => { +describe('AboutRootComponent', () => { let component: AboutRootComponent; let machine: Interpreter; @@ -51,6 +52,7 @@ describe('SearchRootComponent', () => { component = window.document.createElement('nde-about-root') as AboutRootComponent; component.actor = machine; + component.collections = [ collection1, collection2 ]; component.translator = new MemoryTranslator([], 'nl-NL'); }); @@ -147,4 +149,26 @@ describe('SearchRootComponent', () => { }); + it('should send SelectedCollectionEvent when collection is clicked', async (done) => { + + machine.onEvent((event) => { + + if (event.type === CollectionEvents.SELECTED_COLLECTION) { + + done(); + + } + + }); + + machine.start(); + + window.document.body.appendChild(component); + await component.updateComplete; + + const largeCard = window.document.body.getElementsByTagName('nde-about-root')[0].shadowRoot.querySelector('nde-large-card.collection'); + largeCard.click(); + + }); + }); diff --git a/packages/solid-crs-presentation/lib/features/about/about-root.component.ts b/packages/solid-crs-presentation/lib/features/about/about-root.component.ts index df5af1ed..97c947db 100644 --- a/packages/solid-crs-presentation/lib/features/about/about-root.component.ts +++ b/packages/solid-crs-presentation/lib/features/about/about-root.component.ts @@ -123,13 +123,13 @@ export class AboutRootComponent extends RxLitElement {
+ + ${ alerts }
- - ${ alerts }

${ this.profile?.name }

${ this.profile?.description ? html`

${ this.profile?.description }

` : ''} @@ -182,6 +182,9 @@ export class AboutRootComponent extends RxLitElement { flex-direction: column; height: 100%; } + nde-alert { + margin-bottom: var(--gap-large); + } .content { margin: 0 var(--gap-large); margin-top: 41px; diff --git a/packages/solid-crs-presentation/lib/features/collection/collection-root.component.ts b/packages/solid-crs-presentation/lib/features/collection/collection-root.component.ts index f55d54ad..969c8d81 100644 --- a/packages/solid-crs-presentation/lib/features/collection/collection-root.component.ts +++ b/packages/solid-crs-presentation/lib/features/collection/collection-root.component.ts @@ -184,11 +184,16 @@ export class CollectionRootComponent extends RxLitElement { flex-direction: column; height: 100%; } + nde-alert { + margin-bottom: var(--gap-large); + } .content { margin-top: 1px; padding: var(--gap-large); height: 100%; overflow-y: auto; + display: flex; + flex-direction: column; } nde-progress-bar { position: absolute; diff --git a/packages/solid-crs-presentation/lib/features/object/object-root.component.spec.ts b/packages/solid-crs-presentation/lib/features/object/object-root.component.spec.ts index 60f1f983..b425a10a 100644 --- a/packages/solid-crs-presentation/lib/features/object/object-root.component.spec.ts +++ b/packages/solid-crs-presentation/lib/features/object/object-root.component.spec.ts @@ -1,10 +1,11 @@ -import { Alert } from '@netwerk-digitaal-erfgoed/solid-crs-components'; +import { Alert, PopupComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; import { ArgumentError, CollectionObjectMemoryStore, MemoryTranslator, Collection, CollectionObject, CollectionMemoryStore, ConsoleLogger, LoggerLevel } from '@netwerk-digitaal-erfgoed/solid-crs-core'; import { ObjectImageryComponent } from '@netwerk-digitaal-erfgoed/solid-crs-semcom-components'; import { interpret, Interpreter } from 'xstate'; import { AppEvents, DismissAlertEvent } from '../../app.events'; import { appMachine } from '../../app.machine'; import { SolidMockService } from '../../common/solid/solid-mock.service'; +import { CollectionEvents } from '../collection/collection.events'; import { ObjectRootComponent } from './object-root.component'; import { ObjectContext, objectMachine } from './object.machine'; @@ -30,14 +31,35 @@ describe('ObjectRootComponent', () => { }; const object1: CollectionObject = { + image: 'test-uri', uri: 'object-uri-1', name: 'Object 1', description: 'This is object 1', - image: null, - subject: null, - type: null, updated: '0', collection: 'collection-uri-1', + type: 'http://schema.org/Photograph', + additionalType: [ { 'name':'bidprentjes', 'uri':'https://data.cultureelerfgoed.nl/term/id/cht/1e0adea5-71fa-4197-ad73-90b706d2357c' } ], + identifier: 'SK-A-1115', + maintainer: 'https://data.hetlageland.org/', + creator: [ { 'name':'Jan Willem Pieneman', 'uri':'http://www.wikidata.org/entity/Q512948' } ], + locationCreated: [ { 'name':'Delft', 'uri':'http://www.wikidata.org/entity/Q33432813' } ], + material: [ { 'name':'olieverf', 'uri':'http://vocab.getty.edu/aat/300015050' } ], + dateCreated: '1824-07-24', + subject: [ { 'name':'veldslagen', 'uri':'http://vocab.getty.edu/aat/300185692' } ], + location: [ { 'name':'Waterloo', 'uri':'http://www.wikidata.org/entity/Q31579578' } ], + person: [ { 'name':'Arthur Wellesley of Wellington', 'uri':'http://data.bibliotheken.nl/id/thes/p067531180' } ], + event: [ { 'name':'Slag bij Waterloo', 'uri':'http://www.wikidata.org/entity/Q48314' } ], + organization: [ { 'name':'Slag bij Waterloo', 'uri':'http://www.wikidata.org/entity/Q48314' } ], + height: 52, + width: 82, + depth: 2, + weight: 120, + heightUnit: 'CMT', + widthUnit: 'CMT', + depthUnit: 'CMT', + weightUnit: 'KGM', + mainEntityOfPage: 'http://localhost:3000/hetlageland/heritage-objects/data-1#object-1-digital', + license: 'https://creativecommons.org/publicdomain/zero/1.0/deed.nl', }; beforeEach(() => { @@ -77,8 +99,6 @@ describe('ObjectRootComponent', () => { component.collections = [ collection1, collection2 ]; - component.formCards = []; - }); afterEach(() => { @@ -130,41 +150,6 @@ describe('ObjectRootComponent', () => { }); - it('should select sidebar item when content is scrolled', async () => { - - window.document.body.appendChild(component); - await component.updateComplete; - - const div = document.createElement('nde-object-imagery') as ObjectImageryComponent; - div.id = 'nde.features.object.sidebar.image'; - component.formCards = [ div ]; - component.components = []; - - const content = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('.content') as HTMLElement; - content.dispatchEvent(new CustomEvent('scroll')); - await component.updateComplete; - - expect(component.visibleCard).toBeTruthy(); - - }); - - describe('updateSelected()', () => { - - it('should set this.visibleCard to the currently visible card', async () => { - - const div = document.createElement('nde-object-imagery') as ObjectImageryComponent; - div.id = 'nde.features.object.sidebar.image'; - component.formCards = [ div ]; - - component.updateSelected(); - - expect(component.visibleCard).toBeTruthy(); - expect(component.visibleCard).toEqual(component.formCards[0].id); - - }); - - }); - describe('handleDismiss', () => { const alert: Alert = { message: 'foo', type: 'success' }; @@ -225,208 +210,122 @@ describe('ObjectRootComponent', () => { component.subscribe = jest.fn(); - const div = document.createElement('nde-object-imagery') as ObjectImageryComponent; - div.id = 'nde.features.object.sidebar.image'; - component.formCards = [ div ]; - const map = new Map(); map.set('actor', 'test'); map.set('formActor', 'test'); await component.updated(map); - expect(component.subscribe).toHaveBeenCalledTimes(11); + expect(component.subscribe).toHaveBeenCalledTimes(4); }); - it('should should call registerComponents() when formCards is undefined', async () => { + }); - machine.start(); + it('should show content when object is set', async () => { - window.document.body.appendChild(component); - await component.updateComplete; + window.document.body.appendChild(component); + await component.updateComplete; - component.registerComponents = jest.fn(async() => undefined); + const formContent = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content'); + expect(formContent).toBeTruthy(); - component.formCards = undefined; + }); - component.components = [ - { - description: 'Digita SemCom component voor beeldmateriaal informatie.', - label: 'Erfgoedobject Beeldmateriaal', - uri: 'http://localhost:3004/object-imagery.component.js', - shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], - author: 'https://digita.ai', - tag: 'nde-object-imagery', - version: '0.1.0', - latest: true, - }, - ]; + it('should hide content when object is undefined', async () => { - await component.updateComplete; - await component.updated(undefined); + window.document.body.appendChild(component); - expect(component.registerComponents).toHaveBeenCalled(); + component.object = undefined; + await component.updateComplete; - }); + const formContent = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content'); + expect(formContent).toBeFalsy(); }); - describe('registerComponents()', () => { + it('should call toggleImage and show popup when image is clicked', async () => { - it('should create customElements for this.components', async () => { + window.document.body.appendChild(component); + await component.updateComplete; + component.imagePopup.hidden = false; - window.eval = jest.fn(() => ObjectImageryComponent); - customElements.define = jest.fn(() => ObjectImageryComponent); + const image = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content img:first-of-type'); + image.click(); + expect(component.imagePopup.hidden).toEqual(true); - machine.start(); + }); - window.document.body.appendChild(component); - await component.updateComplete; + it('should call toggleImage and hide popup when cross icon is clicked', async () => { - component.components = [ - { - description: 'Digita SemCom component voor beeldmateriaal informatie.', - label: 'Erfgoedobject Beeldmateriaal', - uri: 'http://localhost:3004/object-imagery.component.js', - shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], - author: 'https://digita.ai', - tag: 'nde-object-imagery', - version: '0.1.0', - latest: true, - }, - { - description: 'Digita SemCom component voor identificatie informatie.', - label: 'Erfgoedobject Identificatie', - uri: 'http://localhost:3004/object-identification.component.js', - shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], - author: 'https://digita.ai', - tag: 'nde-object-identification', - version: '0.1.0', - latest: true, - }, - { - description: 'Digita SemCom component voor vervaardiging informatie.', - label: 'Erfgoedobject Vervaardiging', - uri: 'http://localhost:3004/object-creation.component.js', - shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], - author: 'https://digita.ai', - tag: 'nde-object-creation', - version: '0.1.0', - latest: true, - }, - { - description: 'Digita SemCom component voor voorstellingsinformatie.', - label: 'Erfgoedobject Voorstelling', - uri: 'http://localhost:3004/object-representation.component.js', - shapes: [ 'http://digita.ai/voc/input#input' ], - author: 'https://digita.ai', - tag: 'nde-object-representation', - version: '0.1.0', - latest: true, - }, - { - description: 'Digita SemCom component voor afmeting informatie.', - label: 'Erfgoedobject Afmetingen', - uri: 'http://localhost:3004/object-dimensions.component.js', - shapes: [ 'http://digita.ai/voc/payslip#payslip' ], - author: 'https://digita.ai', - tag: 'nde-object-dimensions', - version: '0.1.0', - latest: true, - }, - ]; - - await component.registerComponents(component.components); + window.document.body.appendChild(component); + await component.updateComplete; + component.imagePopup.hidden = true; - }); + const image = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('#dismiss-popup'); + image.click(); + expect(component.imagePopup.hidden).toEqual(false); }); - it('should show content when formCards is set', async () => { + it('should call toggleInfo and show menu when dots icon is clicked', async () => { window.document.body.appendChild(component); - - const div = document.createElement('nde-object-imagery') as ObjectImageryComponent; - div.id = 'nde.features.object.sidebar.image'; - - component.formCards = [ div ]; await component.updateComplete; + component.infoPopup.hidden = true; - const sidebar = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content-and-sidebar nde-sidebar'); - const formContent = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content-and-sidebar div.content'); - expect(sidebar).toBeTruthy(); - expect(formContent).toBeTruthy(); + const infoIcon = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('nde-content-header div[slot="actions"] div'); + infoIcon.click(); + expect(component.infoPopup.hidden).toEqual(false); }); - it('should hide content when formCards is undefined', async () => { + it('should copy url to clipboard when info menu item is clicked', async () => { - window.document.body.appendChild(component); + navigator.clipboard = { + writeText: jest.fn(async() => undefined), + }; - component.formCards = undefined; - await component.updateComplete; + machine.parent.onEvent((event) => { - const sidebar = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content-and-sidebar nde-sidebar'); - const formContent = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('div.content-and-sidebar div.content'); - expect(sidebar).toBeFalsy(); - expect(formContent).toBeFalsy(); + if (event.type === AppEvents.ADD_ALERT) { - }); + expect(navigator.clipboard.writeText).toHaveBeenCalledTimes(1); - it('should prevent contextmenu events from being propagated on macos chrome', async () => { + } - Object.defineProperty(window, 'navigator', { - value: { userAgent: '... Macintosh ... Chrome/ ...' }, - writable: true, }); - window.eval = jest.fn(() => ObjectImageryComponent); - customElements.define = jest.fn(() => ObjectImageryComponent); - - component.components = [ - { - description: 'Digita SemCom component voor beeldmateriaal informatie.', - label: 'Erfgoedobject Beeldmateriaal', - uri: 'http://localhost:3004/object-imagery.component.js', - shapes: [ 'http://xmlns.com/foaf/0.1/PersonalProfileDocument' ], - author: 'https://digita.ai', - tag: 'nde-object-imagery', - version: '0.1.0', - latest: true, - }, - ]; - window.document.body.appendChild(component); await component.updateComplete; - await component.registerComponents(component.components); - - const event = new MouseEvent('contextmenu'); - event.stopPropagation = jest.fn(); - event.preventDefault = jest.fn(); - component.formCards[0].dispatchEvent(event); + component.infoPopup.hidden = false; - expect(event.stopPropagation).toHaveBeenCalled(); - expect(event.preventDefault).toHaveBeenCalled(); + const copyAnchor = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('nde-content-header div[slot="actions"] div a:last-of-type'); + copyAnchor.click(); }); - describe('appendComponents()', () => { + it('should send SelectedCollectionEvent to parent when collection is clicked', async (done) => { - it('should set formCard attributes', async () => { + machine.parent.onEvent((event) => { - window.document.body.appendChild(component); + if (event.type === CollectionEvents.SELECTED_COLLECTION) { - const imagery = document.createElement('nde-object-imagery') as ObjectImageryComponent; - imagery.id = 'nde.features.object.sidebar.image'; - component.formCards = [ imagery ]; - await component.updateComplete; - component.appendComponents([ imagery ]); + done(); - expect(imagery.object).toBeTruthy(); + } }); + machine.start(); + machine.parent.start(); + + window.document.body.appendChild(component); + await component.updateComplete; + + const collectionAnchor = window.document.body.getElementsByTagName('nde-object-root')[0].shadowRoot.querySelector('#identification-card div a'); + collectionAnchor.click(); + }); }); diff --git a/packages/solid-crs-presentation/lib/features/object/object-root.component.ts b/packages/solid-crs-presentation/lib/features/object/object-root.component.ts index d587e155..f92ea446 100644 --- a/packages/solid-crs-presentation/lib/features/object/object-root.component.ts +++ b/packages/solid-crs-presentation/lib/features/object/object-root.component.ts @@ -1,38 +1,21 @@ import { html, property, PropertyValues, internalProperty, unsafeCSS, css, TemplateResult, CSSResult, query } from 'lit-element'; import { ArgumentError, Collection, CollectionObject, Logger, Translator } from '@netwerk-digitaal-erfgoed/solid-crs-core'; -import { FormEvent, FormActors, FormSubmissionStates, Alert, FormRootStates, FormCleanlinessStates, FormValidationStates, FormUpdatedEvent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; +import { Alert, PopupComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; import { map } from 'rxjs/operators'; import { from } from 'rxjs'; -import { ActorRef, Interpreter, State } from 'xstate'; +import { Interpreter, State } from 'xstate'; import { RxLitElement } from 'rx-lit'; -import { Object as ObjectIcon, Theme } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; -import { ObjectImageryComponent, ObjectCreationComponent, ObjectIdentificationComponent, ObjectRepresentationComponent, ObjectDimensionsComponent } from '@netwerk-digitaal-erfgoed/solid-crs-semcom-components'; +import { Connect, Dots, Identity, Image, Object as ObjectIcon, Download, Theme, Cross } from '@netwerk-digitaal-erfgoed/solid-crs-theme'; import { unsafeSVG } from 'lit-html/directives/unsafe-svg'; -import { ComponentMetadata } from '@digita-ai/semcom-core'; -import { DismissAlertEvent } from '../../app.events'; -import { SemComService } from '../../common/semcom/semcom.service'; +import { AddAlertEvent, DismissAlertEvent } from '../../app.events'; +import { SelectedCollectionEvent } from '../collection/collection.events'; import { ObjectContext } from './object.machine'; -import { ClickedObjectSidebarItem } from './object.events'; /** * The root page of the object feature. */ export class ObjectRootComponent extends RxLitElement { - /** - * The form cards in this component - */ - @internalProperty() - formCards: (ObjectImageryComponent - | ObjectCreationComponent - | ObjectIdentificationComponent - | ObjectRepresentationComponent - | ObjectDimensionsComponent)[]; - /** - * The id of the currently visible form card - */ - @internalProperty() - visibleCard: string; /** * The component's logger. */ @@ -74,65 +57,16 @@ export class ObjectRootComponent extends RxLitElement { */ @property({ type: Object }) object?: CollectionObject; - - /** - * The actor responsible for form validation in this component. - */ - @internalProperty() - formActor: ActorRef; - - /** - * Indicates if the form is being submitted. - */ - @internalProperty() - isSubmitting? = false; - - /** - * Indicates if if the form validation passed. - */ - @internalProperty() - isValid? = false; - - /** - * Indicates if one the form fields has changed. - */ - @internalProperty() - isDirty? = false; - - /** - * Indicates whether the user is editing a field containing a Term. - */ - @internalProperty() - isEditingTermField? = false; - - /** - * The semcom service to use in this component - */ - @internalProperty() - semComService? = new SemComService(); - /** - * The content element to append SemComs to + * The popup component shown when the image preview is clicked */ - @query('.content') - contentElement: HTMLDivElement; - - /** - * The ComponentMetadata of the SemComs - */ - @internalProperty() - components: ComponentMetadata[]; - + @query('nde-popup#image-popup') + imagePopup: PopupComponent; /** - * Hook called on first update after connection to the DOM. + * The popup component shown when the info menu is clicked */ - async firstUpdated(changed: PropertyValues): Promise { - - super.firstUpdated(changed); - - this.subscribe('components', from(this.semComService.queryComponents({ latest: true }))); - - } + @query('nde-popup#info-popup') + infoPopup: PopupComponent; /** * Hook called on at every update after connection to the DOM. @@ -143,20 +77,6 @@ export class ObjectRootComponent extends RxLitElement { if(changed && changed.has('actor') && this.actor){ - this.actor.onEvent(async(event) => { - - if (event instanceof ClickedObjectSidebarItem) { - - this.requestUpdate(); - await this.updateComplete; - - const formCard = Array.from(this.formCards).find((card) => card.id === event.itemId); - formCard?.scrollIntoView({ behavior: 'smooth', block: 'center' }); - - } - - }); - if(this.actor.parent){ this.subscribe('alerts', from(this.actor.parent) @@ -164,16 +84,6 @@ export class ObjectRootComponent extends RxLitElement { } - this.subscribe('formActor', from(this.actor).pipe( - map((state) => { - - this.formCards?.forEach((card) => card.formActor = state.children[FormActors.FORM_MACHINE] as any); - - return state.children[FormActors.FORM_MACHINE]; - - }) - )); - this.subscribe('state', from(this.actor)); this.subscribe('collections', from(this.actor).pipe( @@ -181,58 +91,11 @@ export class ObjectRootComponent extends RxLitElement { )); this.subscribe('object', from(this.actor).pipe( - map((state) => { - - this.formCards?.forEach((card) => card.object = state.context?.object); - - return state.context?.object; - - }) + map((state) => state.context?.object), )); } - if(changed?.has('formActor') && this.formActor){ - - // this validates the form when form machine is started - // needed for validation when coming back from the term machine - // otherwise, form machine state is not_validated and the user can't save - this.formActor.send(new FormUpdatedEvent('name', this.object?.name)); - - this.subscribe('isSubmitting', from(this.formActor).pipe( - map((state) => state.matches(FormSubmissionStates.SUBMITTING)), - )); - - this.subscribe('isValid', from(this.formActor).pipe( - map((state) => !state.matches({ - [FormSubmissionStates.NOT_SUBMITTED]:{ - [FormRootStates.VALIDATION]: FormValidationStates.INVALID, - }, - })), - )); - - this.subscribe('isDirty', from(this.formActor).pipe( - map((state) => state.matches({ - [FormSubmissionStates.NOT_SUBMITTED]:{ - [FormRootStates.CLEANLINESS]: FormCleanlinessStates.DIRTY, - }, - })), - )); - - } - - if (!this.formCards && this.components && this.object && this.formActor && this.translator) { - - await this.registerComponents(this.components); - - } - - if (this.formCards && !this.isEditingTermField && this.components?.length < 1) { - - this.appendComponents(this.formCards); - - } - } /** @@ -256,115 +119,6 @@ export class ObjectRootComponent extends RxLitElement { this.actor.parent.send(new DismissAlertEvent(event.detail)); - } - /** - * Registers and adds all components to DOM - * - * @param components The component metadata to register and add - */ - async registerComponents(components: ComponentMetadata[]): Promise { - - for (const component of components) { - - if (!customElements.get(component.tag)) { - - // eslint-disable-next-line no-eval - const elementComponent = await eval(`import("${component.uri}")`); - - const ctor = customElements.get(component.tag) - || customElements.define(component.tag, elementComponent.default); - - } - - let element; - - if (component.tag.includes('imagery')) { - - element = document.createElement(component.tag) as ObjectImageryComponent; - element.id = 'nde.features.object.sidebar.image'; - - } else if (component.tag.includes('creation')) { - - element = document.createElement(component.tag) as ObjectCreationComponent; - element.id = 'nde.features.object.sidebar.creation'; - - } else if (component.tag.includes('identification')) { - - element = document.createElement(component.tag) as ObjectIdentificationComponent; - element.collections = this.collections; - element.id = 'nde.features.object.sidebar.identification'; - - } else if (component.tag.includes('representation')) { - - element = document.createElement(component.tag) as ObjectRepresentationComponent; - element.id = 'nde.features.object.sidebar.representation'; - - } else if (component.tag.includes('dimensions')) { - - element = document.createElement(component.tag) as ObjectDimensionsComponent; - element.id = 'nde.features.object.sidebar.dimensions'; - - } - - if (window.navigator.userAgent.includes('Macintosh') && window.navigator.userAgent.includes('Chrome/')) { - - element.addEventListener('contextmenu', (event: MouseEvent) => { - - event.stopPropagation(); - event.preventDefault(); - - }); - - } - - this.formCards = this.formCards?.includes(element) - ? this.formCards : [ ...this.formCards ? this.formCards : [], element ]; - - } - - if (this.formCards) { - - this.appendComponents(this.formCards); - - } - - } - - /** - * Appends the formCards to the page content and removes previous children - */ - appendComponents(components: (ObjectImageryComponent - | ObjectCreationComponent - | ObjectIdentificationComponent - | ObjectRepresentationComponent - | ObjectDimensionsComponent)[]): void { - - components?.forEach(async(component) => { - - component.object = this.object; - component.formActor = this.formActor as any; - component.translator = this.translator; - await component?.requestUpdate('object'); - - }); - - this.updateSelected(); - - } - - /** - * Sets this.selected to the currently visible form card's id - */ - updateSelected(): void { - - this.visibleCard = Array.from(this.formCards).find((formCard) => { - - const box = formCard.getBoundingClientRect(); - - return box.top >= -(box.height / (3 + 20)); - - })?.id; - } /** @@ -376,54 +130,201 @@ export class ObjectRootComponent extends RxLitElement { // Create an alert components for each alert. const alerts = this.alerts?.map((alert) => html``); + const collection = this.collections?.find((coll) => coll.uri === this.object?.collection); - const sidebarItems = this.formCards?.map((formCard) => formCard.id); + const toggleImage = () => { this.imagePopup.toggle(); }; - return this.object ? html` + const toggleInfo = () => { this.infoPopup.toggle(); }; - ${ !this.formCards ? html`` : html``} + return this.object ? html`
${ unsafeSVG(ObjectIcon) }
- - - - - - - +
+ ${ this.object.name} +
+
+ ${ this.object.description } +
+
-
- - ${ this.formCards - ? html` - - - ${sidebarItems?.map((item) => html` - -
${this.translator?.translate(item)}
-
- `)} -
-
-
- - ${ this.formCards ? html` -
- - ${ alerts } - - ${ this.formCards } - -
- ` : html`no formcards`} - ` - : html``} -
` - : html``; +
+ + ${ alerts } + +
${ unsafeSVG(Image) }
+
+ ${ this.translator.translate('nde.features.object.card.image.title') } +
+
+ ${ this.translator.translate('nde.features.object.card.image.subtitle') } +
+
+ +
+
${ this.translator.translate('nde.features.object.card.field.license') }
+ +
+
+
${ this.translator.translate('nde.features.object.card.field.download') }
+ +
+ +
+
${ unsafeSVG(Cross) }
+ +
+
+
+
+ + +
${ unsafeSVG(Identity) }
+
+ ${ this.translator.translate('nde.features.object.card.identification.title') } +
+
+ ${ this.translator.translate('nde.features.object.card.identification.subtitle') } +
+
+
+
${ this.translator.translate('nde.features.object.card.field.identifier') }
+
${ this.object.identifier }
+
+
+
${ this.translator.translate('nde.features.object.card.field.type') }
+
${ this.object.type }
+
+
+
${ this.translator.translate('nde.features.object.card.field.additionalType') }
+
${ this.object.additionalType?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.name') }
+
${ this.object.name }
+
+
+
${ this.translator.translate('nde.features.object.card.field.description') }
+
${ this.object.description }
+
+
+
${ this.translator.translate('nde.features.object.card.field.collection') }
+ +
+
+
+ + +
${ unsafeSVG(ObjectIcon) }
+
+ ${ this.translator.translate('nde.features.object.card.creation.title') } +
+
+ ${ this.translator.translate('nde.features.object.card.creation.subtitle') } +
+
+
+
${ this.translator.translate('nde.features.object.card.field.creator') }
+
${ this.object.creator?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.locationCreated') }
+
${ this.object.locationCreated?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.material') }
+
${ this.object.material?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.dateCreated') }
+
${ this.object.dateCreated }
+
+
+
+ + +
${ unsafeSVG(ObjectIcon) }
+
+ ${ this.translator.translate('nde.features.object.card.representation.title') } +
+
+ ${ this.translator.translate('nde.features.object.card.representation.subtitle') } +
+
+
+
${ this.translator.translate('nde.features.object.card.field.subject') }
+
${ this.object.subject?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.location') }
+
${ this.object.location?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.person') }
+
${ this.object.person?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.organization') }
+
${ this.object.organization?.map((term) => html`
${term.name}
`) }
+
+
+
${ this.translator.translate('nde.features.object.card.field.event') }
+
${ this.object.event?.map((term) => html`
${term.name}
`) }
+
+
+
+ + +
${ unsafeSVG(Connect) }
+
+ ${ this.translator.translate('nde.features.object.card.dimensions.title') } +
+
+ ${ this.translator.translate('nde.features.object.card.dimensions.subtitle') } +
+
+
+
${ this.translator.translate('nde.features.object.card.field.height') }
+
${ this.object.height } ${ this.object.heightUnit }
+
+
+
${ this.translator.translate('nde.features.object.card.field.width') }
+
${ this.object.width } ${ this.object.widthUnit }
+
+
+
${ this.translator.translate('nde.features.object.card.field.depth') }
+
${ this.object.depth } ${ this.object.depthUnit }
+
+
+
${ this.translator.translate('nde.features.object.card.field.weight') }
+
${ this.object.weight } ${ this.object.weightUnit }
+
+
+
+
+ ` : html``; } @@ -444,47 +345,100 @@ export class ObjectRootComponent extends RxLitElement { display: flex; flex-direction: column; height: 100%; + width: 100%; } - .content-and-sidebar { - margin-top: 1px; + nde-alert { + margin-bottom: var(--gap-large); + } + #info-popup { + height: auto; + width: auto; + position: absolute; + left: unset; + right: var(--gap-normal); + top: var(--gap-huge); + background-color: var(--colors-background-light); + /* box-shadow: 0 0 5px grey; */ + border: 1px var(--colors-foreground-normal) solid; + } + #info-popup div { display: flex; - flex-direction: row; - height: 1px; - flex: 1 1; + flex-direction: column; + justify-content: center; + } + #info-popup a { + padding: var(--gap-small); + color: var(--colors-primary-normal); + text-decoration: none; + /* text-align: center; */ + } + #info-popup a:hover { + background-color: var(--colors-primary-normal); + color: var(--colors-background-light); } .content { + margin-top: 1px; padding: var(--gap-large); - width: 100%; overflow-y: auto; overflow-x: clip; display: flex; flex-direction: column; - gap: var(--gap-large); } - nde-progress-bar { - position: absolute; + .content div[slot="content"] { + margin: 0 45px; /* gap-large + gap-small */ + } + nde-large-card > div[slot="content"] > img { + height: 200px; width: 100%; - top: 0; - left: 0; + object-fit: cover; + margin-bottom: var(--gap-normal); + cursor: pointer; } - nde-content-header nde-form-element input { - height: var(--gap-normal); - padding: 0; - line-height: var(--gap-normal); + #image-popup div[slot="content"] { + display: flex; + flex-direction: column; + } + #image-popup div[slot="content"] img { + max-width: 100%; + max-height: 95%; + } + #image-popup div[slot="content"] div { + fill: white; + margin-bottom: var(--gap-normal); + min-height: 20px; + min-width: 20px; + align-self: flex-end; + cursor: pointer; } - .name { - font-weight: bold; - font-size: var(--font-size-large); + a { + cursor: pointer; + text-decoration: underline; + color: var(--colors-primary-light); } - .description { - margin-top: var(--gap-tiny); + .object-property { + margin-bottom: var(--gap-small); + width: 100%; + display: flex; + } + .object-property * { + overflow: hidden; + font-size: var(--font-size-small); + line-height: 21px; + } + .object-property > div:first-child { + font-weight: var(--font-weight-bold); + width: 33%; + max-width: 33%; + } + .object-property > div:last-child { + display: inline-flex; + flex-direction: column; } - nde-sidebar-list > slot[name="title"] { - font-weight: bold; + .object-property > div:last-child svg { + fill: var(--colors-primary-light); } - button svg { - max-width: var(--gap-normal); - height: var(--gap-normal); + [hidden] { + display: none; } `, ]; diff --git a/packages/solid-crs-presentation/lib/features/search/search-root.component.ts b/packages/solid-crs-presentation/lib/features/search/search-root.component.ts index d21afb84..f1f65311 100644 --- a/packages/solid-crs-presentation/lib/features/search/search-root.component.ts +++ b/packages/solid-crs-presentation/lib/features/search/search-root.component.ts @@ -205,11 +205,16 @@ export class SearchRootComponent extends RxLitElement { flex-direction: column; height: 100%; } + nde-alert { + margin-bottom: var(--gap-large); + } .content { margin-top: 1px; padding: var(--gap-large); height: 100%; overflow-y: auto; + display: flex; + flex-direction: column; } .content > div:first-child { padding-top: 0; diff --git a/packages/solid-crs-presentation/lib/i8n/nl-NL.json b/packages/solid-crs-presentation/lib/i8n/nl-NL.json index 4961abc9..36bf619e 100644 --- a/packages/solid-crs-presentation/lib/i8n/nl-NL.json +++ b/packages/solid-crs-presentation/lib/i8n/nl-NL.json @@ -194,6 +194,11 @@ "locale": "nl-NL", "value": "Licentie" }, + { + "key": "nde.features.object.card.field.download", + "locale": "nl-NL", + "value": "Download" + }, { "key": "nde.features.object.card.image.field.license.http://rightsstatements.org/vocab/InC/1.0/", "locale": "nl-NL", @@ -419,6 +424,16 @@ "locale": "nl-NL", "value": "Eenheid" }, + { + "key": "nde.features.object.options.view-rdf", + "locale": "nl-NL", + "value": "RDF bekijken" + }, + { + "key": "nde.features.object.options.share", + "locale": "nl-NL", + "value": "URL kopieren" + }, { "key": "nde.features.about.header.title", "locale": "nl-NL", @@ -493,5 +508,10 @@ "key": "nde.common.date.years-ago", "locale": "nl-NL", "value": "jaar geleden" + }, + { + "key": "nde.common.copied-url", + "locale": "nl-NL", + "value": "URL van deze pagina werd gekopieerd" } ] diff --git a/packages/solid-crs-presentation/package.json b/packages/solid-crs-presentation/package.json index fe454bca..30f851c7 100644 --- a/packages/solid-crs-presentation/package.json +++ b/packages/solid-crs-presentation/package.json @@ -85,10 +85,10 @@ ], "coverageThreshold": { "global": { - "statements": 88.52, - "branches": 85.69, - "lines": 89.47, - "functions": 72.59 + "statements": 89.3, + "branches": 86.77, + "lines": 89.96, + "functions": 75.66 } }, "automock": false, @@ -109,4 +109,4 @@ } } } -} +} \ No newline at end of file diff --git a/packages/solid-crs-presentation/tests/setup.ts b/packages/solid-crs-presentation/tests/setup.ts index 8c087dc4..b93b9bcb 100644 --- a/packages/solid-crs-presentation/tests/setup.ts +++ b/packages/solid-crs-presentation/tests/setup.ts @@ -1,4 +1,4 @@ -import { AlertComponent, CardComponent, CollectionCardComponent, ContentHeaderComponent, FormElementComponent, ObjectCardComponent, SidebarComponent, ProgressBarComponent, SidebarItemComponent, SidebarListComponent, SidebarListItemComponent, LargeCardComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; +import { AlertComponent, CardComponent, CollectionCardComponent, ContentHeaderComponent, FormElementComponent, ObjectCardComponent, SidebarComponent, ProgressBarComponent, SidebarItemComponent, SidebarListComponent, SidebarListItemComponent, LargeCardComponent, PopupComponent } from '@netwerk-digitaal-erfgoed/solid-crs-components'; import fetchMock from 'jest-fetch-mock'; import { AppRootComponent } from '../lib/app-root.component'; import { CollectionRootComponent } from '../lib/features/collection/collection-root.component'; @@ -31,3 +31,4 @@ customElements.define('nde-sidebar-list', SidebarListComponent); customElements.define('nde-sidebar-item', SidebarItemComponent); customElements.define('nde-progress-bar', ProgressBarComponent); customElements.define('nde-app-root', AppRootComponent); +customElements.define('nde-popup', PopupComponent); diff --git a/packages/solid-crs-theme/lib/icons/Download.svg b/packages/solid-crs-theme/lib/icons/Download.svg new file mode 100644 index 00000000..f4cfda60 --- /dev/null +++ b/packages/solid-crs-theme/lib/icons/Download.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/solid-crs-theme/lib/index.ts b/packages/solid-crs-theme/lib/index.ts index 9d6447bb..ce76ebdb 100644 --- a/packages/solid-crs-theme/lib/index.ts +++ b/packages/solid-crs-theme/lib/index.ts @@ -25,6 +25,7 @@ export { default as Reset } from './icons/reset.svg?raw'; export { default as CheckboxChecked } from './icons/CheckboxChecked.svg?raw'; export { default as CheckboxUnchecked } from './icons/CheckboxUnchecked.svg?raw'; export { default as Info } from './icons/Info.svg?raw'; +export { default as Download } from './icons/Download.svg?raw'; /** * Export theme