diff --git a/.gitmodules b/.gitmodules index 9436955b1cedc29130e4c4e33352ea14fe327101..c6af8543b7869b2a26a1772b937ee3aef8cdacbb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -16,3 +16,6 @@ [submodule "vendor/file-upload"] path = vendor/file-upload url = git@gitlab.tugraz.at:VPU/WebComponents/FileUpload.git +[submodule "vendor/app-shell"] + path = vendor/app-shell + url = git@gitlab.tugraz.at:VPU/Apps/AppShell.git diff --git a/package-lock.json b/package-lock.json index 3addabec0d31ebd735150a86e017da1ba26ec284..2e78241b592bbfcb1a3dd5fa9289042c3a9f4ec4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5173,6 +5173,15 @@ "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", "dev": true }, + "vpu-app-shell": { + "version": "file:vendor/app-shell", + "requires": { + "i18next": "^19.1.0", + "lit-element": "^2.2.1", + "lit-html": "^1.1.2", + "universal-router": "^8.3.0" + } + }, "vpu-auth": { "version": "file:vendor/auth", "requires": { diff --git a/package.json b/package.json index 6c4aba564b7738bb919043993f9f626e4d4eb8f9..837d85bfd75bab911d71b34cbf864694da7a954c 100644 --- a/package.json +++ b/package.json @@ -50,13 +50,13 @@ "select2": "^4.0.13", "source-sans-pro": "^2.45.0", "suggestions": "^1.7.0", - "universal-router": "^8.3.0", "vpu-auth": "file:./vendor/auth", "vpu-common": "file:./vendor/common", "vpu-file-upload": "file:./vendor/file-upload", "vpu-language-select": "file:./vendor/language-select", "vpu-notification": "file:./vendor/notification", - "vpu-person-profile": "file:./vendor/person-profile" + "vpu-person-profile": "file:./vendor/person-profile", + "vpu-app-shell": "file:./vendor/app-shell" }, "scripts": { "clean": "rm dist/* -R", diff --git a/src/app/build-info.js b/src/app/build-info.js deleted file mode 100644 index 6be385b6538a539de5ebe711de3c165676bfd4e4..0000000000000000000000000000000000000000 --- a/src/app/build-info.js +++ /dev/null @@ -1,38 +0,0 @@ -import {html, LitElement, css} from 'lit-element'; -import * as commonUtils from 'vpu-common/utils'; -import * as commonStyles from 'vpu-common/styles'; -import buildinfo from 'consts:buildinfo'; - -class VPUBuildInfo extends LitElement { - - constructor() { - super(); - } - - static get styles() { - return css` - ${commonStyles.getThemeCSS()} - ${commonStyles.getGeneralCSS()} - ${commonStyles.getTagCSS()} - - :host { - display: inline-block; - } - `; - } - - render() { - const date = new Date(buildinfo.time); - - return html` - <a href="${buildinfo.url}" style="float: right"> - <div class="tags has-addons" title="Build Time: ${date.toString()}"> - <span class="tag is-light">build</span> - <span class="tag is-dark">${buildinfo.info} (${buildinfo.env})</span> - </div> - </a> - `; - } -} - -commonUtils.defineCustomElement('vpu-build-info', VPUBuildInfo); diff --git a/src/app/index.js b/src/app/index.js deleted file mode 100644 index 5b2f9eee669f90b08c10fa0ef7497321cc5c7641..0000000000000000000000000000000000000000 --- a/src/app/index.js +++ /dev/null @@ -1,715 +0,0 @@ -import {createI18nInstance} from '../i18n.js'; -import {html, css} from 'lit-element'; -import VPULitElement from 'vpu-common/vpu-lit-element' -import 'vpu-language-select'; -import 'vpu-common/vpu-button.js'; -import 'vpu-auth'; -import 'vpu-notification'; -import * as commonUtils from 'vpu-common/utils'; -import * as commonStyles from 'vpu-common/styles'; -import buildinfo from 'consts:buildinfo'; -import {classMap} from 'lit-html/directives/class-map.js'; -// import * as errorreport from 'vpu-common/errorreport'; -import {Router} from './router.js'; -import * as events from 'vpu-common/events.js'; -import './build-info.js'; -import './tugraz-logo.js'; -import {send as notify} from 'vpu-notification'; -import environment from 'consts:environment'; - -// errorreport.init({release: 'vpi-signature-app@' + buildinfo.info}); - - -const i18n = createI18nInstance(); - -/** - * In case the application gets updated future dynamic imports might fail. - * This sends a notification suggesting the user to reload the page. - * - * uage: importNotify(import('<path>')); - * - * @param {Promise} promise - */ -const importNotify = async (promise) => { - try { - return await promise; - } catch (error) { - console.log(error); - notify({ - "body": i18n.t('page-updated-needs-reload'), - "type": "info", - "icon": "warning" - }); - throw error; - } -}; - - -class VPUApp extends VPULitElement { - constructor() { - super(); - this.lang = i18n.language; - this.activeView = ''; - this.entryPointUrl = commonUtils.getAPiUrl(); - this.subtitle = ''; - this.description = ''; - this.routes = []; - this.metadata = {}; - this.topic = {}; - this.basePath = ''; - - this._updateAuth = this._updateAuth.bind(this); - this._loginStatus = 'unknown'; - this._subscriber = new events.EventSubscriber('vpu-auth-update', 'vpu-auth-update-request'); - - this._attrObserver = new MutationObserver(this.onAttributeObserved); - } - - onAttributeObserved(mutationsList, observer) { - for(let mutation of mutationsList) { - if (mutation.type === 'attributes') { - const key = mutation.attributeName; - const value = mutation.target.getAttribute(key); - sessionStorage.setItem('vpu-attr-' + key, value); - } - } - } - - /** - * Fetches the metadata of the components we want to use in the menu, dynamically imports the js modules for them, - * then triggers a rebuilding of the menu and resolves the current route - * - * @param {string} topicURL The topic metadata URL or relative path to load things from - */ - async fetchMetadata(topicURL) { - const metadata = {}; - const routes = []; - - const result = await (await fetch(topicURL, { - headers: {'Content-Type': 'application/json'} - })).json(); - - this.topic = result; - - const fetchOne = async (url) => { - const result = await fetch(url, { - headers: {'Content-Type': 'application/json'} - }); - if (!result.ok) - throw result; - - const jsondata = await result.json(); - if (jsondata["element"] === undefined) - throw new Error("no element defined in metadata"); - - return jsondata; - }; - - let promises = []; - for (const activity of result.activities) { - const actURL = new URL(activity.path, new URL(topicURL, window.location).href).href; - promises.push([activity.visible === undefined || activity.visible, actURL, fetchOne(actURL)]); - } - - for (const [visible, actURL, p] of promises) { - try { - const activity = await p; - activity.visible = visible; - // Resolve module_src relative to the location of the json file - activity.module_src = new URL(activity.module_src, actURL).href; - if (activity.routing_name === 'order-list' && environment === 'production') { - console.warn('NOTE: order-list disabled in production!'); - continue; - } - metadata[activity.routing_name] = activity; - routes.push(activity.routing_name); - } catch (error) { - console.log(error); - } - } - // this also triggers a rebuilding of the menu - this.metadata = metadata; - this.routes = routes; - - // Switch to the first route if none is selected - if (!this.activeView) - this.switchComponent(routes[0]); - else - this.switchComponent(this.activeView); - - } - - initRouter() { - const routes = [ - { - path: '', - action: (context) => { - return { - lang: this.lang, - component: '', - }; - } - }, - { - path: '/:lang', - children: [ - { - path: '', - action: (context, params) => { - return { - lang: params.lang, - component: '', - }; - } - }, - { - name: 'mainRoute', - path: '/:component', - action: (context, params) => { - // remove the additional parameters added by Keycloak - let componentTag = params.component.toLowerCase().replace(/&.+/,""); - return { - lang: params.lang, - component: componentTag, - }; - }, - }, - ], - }, - ]; - - this.router = new Router(routes, { - routeName: 'mainRoute', - getState: () => { - return { - component: this.activeView, - lang: this.lang, - }; - }, - setState: (state) => { - this.updateLangIfChanged(state.lang); - this.switchComponent(state.component); - } - }, { - baseUrl: new URL(this.basePath, window.location).pathname.replace(/\/$/, ''), - }); - - this.router.setStateFromCurrentLocation(); - } - - static get properties() { - return { - lang: { type: String, reflect: true }, - src: { type: String }, - basePath: { type: String, attribute: 'base-path' }, - activeView: { type: String, attribute: false}, - entryPointUrl: { type: String, attribute: 'entry-point-url' }, - metadata: { type: Object, attribute: false }, - topic: { type: Object, attribute: false }, - subtitle: { type: String, attribute: false }, - description: { type: String, attribute: false }, - _loginStatus: { type: Boolean, attribute: false }, - }; - } - - _updateAuth(login) { - if (login.status != this._loginStatus) { - console.log('Login status: ' + login.status); - } - - this._loginStatus = login.status; - - // Clear the session storage when the user logs out - if (this._loginStatus === 'logging-out') { - sessionStorage.clear(); - } - } - - connectedCallback() { - super.connectedCallback(); - - if (this.src) - this.fetchMetadata(this.src); - this.initRouter(); - - // listen to the vpu-auth-profile event to switch to the person profile - window.addEventListener("vpu-auth-profile", () => { - this.switchComponent('person-profile'); - }); - - this._subscriber.subscribe(this._updateAuth); - } - - disconnectedCallback() { - this._subscriber.unsubscribe(this._updateAuth); - super.disconnectedCallback(); - } - - /** - * Switches language if another language is requested - * - * @param {string} lang - */ - updateLangIfChanged(lang) { - if (this.lang !== lang) { - this.lang = lang; - this.router.update(); - - const event = new CustomEvent("vpu-language-changed", { - bubbles: true, - detail: {'lang': lang} - }); - - this.dispatchEvent(event); - } - } - - update(changedProperties) { - changedProperties.forEach((oldValue, propName) => { - if (propName === "lang") { - // For screen readers - document.documentElement.setAttribute("lang", this.lang); - i18n.changeLanguage(this.lang); - } - }); - - super.update(changedProperties); - } - - onMenuItemClick(e) { - e.preventDefault(); - const link = e.composedPath()[0]; - const location = link.getAttribute('href'); - this.router.updateFromPathname(location); - } - - onLanguageChanged(e) { - const newLang = e.detail.lang; - const changed = (this.lang !== newLang); - this.lang = newLang; - if (changed) { - this.router.update(); - this.subtitle = this.activeMetaDataText("short_name"); - this.description = this.activeMetaDataText("description"); - } - } - - switchComponent(componentTag) { - const changed = (componentTag !== this.activeView); - this.activeView = componentTag; - if (changed) - this.router.update(); - const metadata = this.metadata[componentTag]; - - if (metadata === undefined) { - return; - } - - importNotify(import(metadata.module_src)).then(() => { - this.updatePageTitle(); - this.subtitle = this.activeMetaDataText("short_name"); - this.description = this.activeMetaDataText("description"); - }).catch((e) => { - console.error(`Error loading ${ metadata.element }`); - throw e; - }); - } - - metaDataText(routingName, key) { - const metadata = this.metadata[routingName]; - return metadata !== undefined && metadata[key] !== undefined ? metadata[key][this.lang] : ''; - } - - topicMetaDataText(key) { - return (this.topic[key] !== undefined) ? this.topic[key][this.lang] : ''; - } - - activeMetaDataText(key) { - return this.metaDataText(this.activeView, key); - } - - updatePageTitle() { - document.title = `${this.topicMetaDataText('name')} - ${this.activeMetaDataText("short_name")}`; - } - - toggleMenu() { - const menu = this.shadowRoot.querySelector("ul.menu"); - - if (menu === null) { - return; - } - - menu.classList.toggle('hidden'); - - const chevron = this.shadowRoot.querySelector("#menu-chevron-icon"); - if (chevron !== null) { - chevron.name = menu.classList.contains('hidden') ? 'chevron-down' : 'chevron-up'; - } - } - - static get styles() { - // language=css - return css` - ${commonStyles.getThemeCSS()} - ${commonStyles.getGeneralCSS()} - - .hidden {display: none} - - h1.title { - margin-bottom: 0; - font-weight: 300; - } - - #main { - display: grid; - grid-template-columns: minmax(180px, 17%) minmax(0, auto); - grid-template-rows: min-content min-content 1fr min-content; - grid-template-areas: "header header" "headline headline" "sidebar main" "footer footer"; - max-width: 1400px; - margin: auto; - min-height: 100vh; - } - - #main-logo { - padding: 0 50px 0 0; - } - - header { - grid-area: header; - display: grid; - grid-template-columns: 50% 1px auto; - grid-template-rows: 60px 60px; - grid-template-areas: "hd1-left hd1-middle hd1-right" "hd2-left . hd2-right"; - width: 100%; - max-width: 1060px; - margin: 0 auto; - } - - aside { grid-area: sidebar; margin: 30px 15px; } - #headline { grid-area: headline; margin: 30px; text-align: center; } - main { grid-area: main; margin: 30px } - footer { grid-area: footer; margin: 30px; text-align: right; } - - header .hd1-left { - display: flex; - flex-direction: column; - justify-content: center; - grid-area: hd1-left; - text-align: right; - padding-right: 20px; - } - - header .hd1-middle { - grid-area: hd1-middle; - background-color: #000; - background: linear-gradient(180deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 85%, rgba(0,0,0,0) 90%); - } - - header .hd1-right { - grid-area: hd1-right; - display: flex; - flex-direction: column; - justify-content: center; - padding-left: 20px; - } - - header .hd2-left { - grid-area: hd2-left; - display: flex; - flex-direction: column; - white-space: nowrap; - } - - header .hd2-left .header { - margin-left: 50px; - } - - header .hd2-left a:hover { - color: #fff; - background-color: #000; - } - - header .hd2-right { - grid-area: hd2-right; - display: flex; - flex-direction: column; - justify-content: center; - text-align: right; - } - - header a { - color: black; - display: inline; - } - - aside ul.menu, footer ul.menu { - list-style: none; - } - - ul.menu li.close { - display: none; - } - - footer { - display: grid; - grid-gap: 1em; - grid-template-columns: 1fr max-content max-content; - } - - footer a { - border-bottom: 1px solid rgba(0,0,0,0.3); - padding: 0; - } - - footer a:hover { - color: #fff; - background-color: #000; - } - - /* We don't allown inline-svg */ - /* - footer .int-link-external::after { - content: "\\00a0\\00a0\\00a0\\00a0"; - background-image: url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3Ardf%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2F02%2F22-rdf-syntax-ns%23%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%225.6842mm%22%20width%3D%225.6873mm%22%20version%3D%221.1%22%20xmlns%3Acc%3D%22http%3A%2F%2Fcreativecommons.org%2Fns%23%22%20xmlns%3Adc%3D%22http%3A%2F%2Fpurl.org%2Fdc%2Felements%2F1.1%2F%22%20viewBox%3D%220%200%2020.151879%2020.141083%22%3E%3Cg%20transform%3D%22translate(-258.5%20-425.15)%22%3E%3Cpath%20style%3D%22stroke-linejoin%3Around%3Bstroke%3A%23000%3Bstroke-linecap%3Around%3Bstroke-width%3A1.2%3Bfill%3Anone%22%20d%3D%22m266.7%20429.59h-7.5029v15.002h15.002v-7.4634%22%2F%3E%3Cpath%20style%3D%22stroke-linejoin%3Around%3Bstroke%3A%23000%3Bstroke-linecap%3Around%3Bstroke-width%3A1.2%3Bfill%3Anone%22%20d%3D%22m262.94%20440.86%2015.002-15.002%22%2F%3E%3Cpath%20style%3D%22stroke-linejoin%3Around%3Bstroke%3A%23000%3Bstroke-linecap%3Around%3Bstroke-width%3A1.2%3Bfill%3Anone%22%20d%3D%22m270.44%20425.86h7.499v7.499%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E'); - background-size:contain; - background-repeat: no-repeat; - background-position:center center; - margin: 0 0.5% 0 1.5%; - font-size:94%; - } - */ - - .menu a { - padding: 0.3em; - font-weight: 300; - color: #000; - display: block; - } - - .menu a:hover { - color: #E4154B; - } - - .menu a.selected { - color: var(--vpu-light); - background-color: var(--vpu-dark); - } - - aside .subtitle { - display: none; - color: #4a4a4a; - font-size: 1.25rem; - font-weight: 300; - line-height: 1.25; - cursor: pointer; - text-align: center; - } - - ul.menu.hidden { - display: block; - } - - a { transition: background-color 0.15s ease 0s, color 0.15s ease 0s; } - - .description { - text-align: left; - margin-bottom: 1rem; - display: none; - } - - @media (max-width: 680px) { - #main { - grid-template-columns: minmax(0, auto); - grid-template-rows: min-content min-content min-content 1fr min-content; - grid-template-areas: "header" "headline" "sidebar" "main" "footer"; - } - - header { - grid-template-rows: 40px; - grid-template-areas: "hd1-left hd1-middle hd1-right"; - } - - header .hd2-left, header .hd2-right { - display: none; - } - - aside { - margin: 0 15px; - } - - aside h2.subtitle { - display: block; - margin-bottom: 0.5em; - } - - aside h2.subtitle:not(:last-child) { - margin-bottom: 0.5em; - } - - aside .menu { - border: black 1px solid; - } - - .menu li { - padding: 7px; - } - - .menu a { - padding: 8px; - } - - ul.menu li.close { - display: block; - padding: 0 15px 15px 15px; - text-align: right; - cursor: pointer; - } - - ul.menu.hidden { - display: none; - } - } - `; - } - - _createActivityElement(activity) { - // We have to create elements dynamically based on a tag name which isn't possible with lit-html. - // This means we pass the finished element to lit-html and have to handle element caching and - // event binding ourselves. - - if (this._lastElm !== undefined) { - if (this._lastElm.tagName.toLowerCase() == activity.element.toLowerCase()) { - return this._lastElm; - } else { - this._attrObserver.disconnect(); - this._lastElm = undefined; - } - } - - const elm = document.createElement(activity.element); - - for(const key of this.topic.attributes) { - let value = sessionStorage.getItem('vpu-attr-' + key); - if (value !== null) { - elm.setAttribute(key, value); - } - } - - this._attrObserver.observe(elm, {attributes: true, attributeFilter: this.topic.attributes}); - - this._lastElm = elm; - return elm; - } - - _renderActivity() { - const act = this.metadata[this.activeView]; - if (act === undefined) - return html``; - - const elm = this._createActivityElement(act); - elm.setAttribute("entry-point-url", this.entryPointUrl); - elm.setAttribute("lang", this.lang); - return elm; - } - - render() { - const silentCheckSsoUri = commonUtils.getAssetURL('silent-check-sso.html'); - - const getSelectClasses = (name => { - return classMap({selected: this.activeView === name}); - }); - - // We hide the app until we are either fully logged in or logged out - // At the same time when we hide the main app we show the main slot (e.g. a loading spinner) - const appHidden = (this._loginStatus == 'unknown' || this._loginStatus == 'logging-in'); - const mainClassMap = classMap({hidden: appHidden}); - const slotClassMap = classMap({hidden: !appHidden}); - - // XXX: Safari doesn't like CSS being applied to slots or via HTML, - // so we have to remove the slow instead of hiding it - if (!appHidden) { - this.updateComplete.then(() => { - const slot = this.shadowRoot.querySelector("slot"); - if (slot) { - slot.remove(); - } - - const welcomeActivity = this._("vpu-welcome"); - - if (welcomeActivity) { - welcomeActivity.setMetaData(this.metadata, i18n.t('welcome.headline'), i18n.t('welcome.description')); - } - }); - } - - const prodClassMap = classMap({hidden: buildinfo.env === 'production'}); - - this.updatePageTitle(); - - // build the menu - let menuTemplates = []; - for (let routingName of this.routes) { - const data = this.metadata[routingName]; - - if (data['visible']) { - menuTemplates.push(html`<li><a @click="${(e) => this.onMenuItemClick(e)}" href="${this.router.getPathname({component: routingName})}" data-nav class="${getSelectClasses(routingName)}" title="${this.metaDataText(routingName, "description")}">${this.metaDataText(routingName, "short_name")}</a></li>`); - } - } - - return html` - <slot class="${slotClassMap}"></slot> - <div class="${mainClassMap}"> - <div id="main"> - <vpu-notification lang="${this.lang}"></vpu-notification> - <header> - <div class="hd1-left"> - <vpu-language-select @vpu-language-changed=${this.onLanguageChanged.bind(this)}></vpu-language-select> - </div> - <div class="hd1-middle"> - </div> - <div class="hd1-right"> - <vpu-auth lang="${this.lang}" show-profile keycloak-config='{"clientId": "${commonUtils.setting('keyCloakClientId')}", "silentCheckSsoRedirectUri": "${silentCheckSsoUri}"}' load-person try-login></vpu-auth> - </div> - <div class="hd2-left"> - <div class="header"> - <a href="https://www.tugraz.at/" title="TU Graz Home" target="_blank" rel="noopener">TU Graz<br>Graz University of Technology</a> - </div> - </div> - <div class="hd2-right"> - <vpu-tugraz-logo id="main-logo" lang="${this.lang}"></vpu-tugraz-logo> - </div> - </header> - - <div id="headline"> - <h1 class="title">${this.topicMetaDataText('name')}</h1> - </div> - - <aside> - <h2 class="subtitle" @click="${this.toggleMenu}"> - ${this.subtitle} - <vpu-icon name="chevron-down" style="color: red" id="menu-chevron-icon"></vpu-icon> - </h2> - <ul class="menu hidden"> - ${menuTemplates} - <li class="close" @click="${this.toggleMenu}"><vpu-icon name="close" style="color: red"></vpu-icon></li> - </ul> - </aside> - - <main> - <p class="description">${this.description}</p> - ${ this._renderActivity() } - </main> - - <footer> - <div></div> - <a target="_blank" rel="noopener" class="int-link-external" href="https://datenschutz.tugraz.at/erklaerung/">${i18n.t('privacy-policy')}</a> - <vpu-build-info class="${prodClassMap}"></vpu-build-info> - </footer> - </div> - </div> - `; - } -} - -commonUtils.initAssetBaseURL('vpu-app-src'); -commonUtils.defineCustomElement('vpu-app', VPUApp); diff --git a/src/app/router.js b/src/app/router.js deleted file mode 100644 index 2ee466094ed5d6e7531e11ecd9ea7b9c354feb26..0000000000000000000000000000000000000000 --- a/src/app/router.js +++ /dev/null @@ -1,117 +0,0 @@ -import UniversalRouter from 'universal-router'; -import generateUrls from 'universal-router/generateUrls'; - -/** - * A wrapper around UniversalRouter which adds history integration - */ -export class Router { - - /** - * @param {Array} routes The routes passed to UniversalRouter - * @param {object} options Options - * @param {string} options.routeName The main route name - * @param {Function} options.getState Function which should return the current state - * @param {Function} options.setState Function which gets passed the new state based on the route - * @param {object} unioptions options passed to UniversalRouter - */ - constructor(routes, options, unioptions) { - this.getState = options.getState; - this.setState = options.setState; - // XXX: We only have one route atm - // If we need more we need to pass the route name to each function - this.routeName = options.routeName; - - console.assert(this.getState); - console.assert(this.setState); - console.assert(this.routeName); - - // https://github.com/kriasoft/universal-router - this.router = new UniversalRouter(routes, unioptions); - - window.addEventListener('popstate', (event) => { - this.setStateFromCurrentLocation(); - this.dispatchLocationChanged(); - }); - } - - /** - * In case something else has changed the location, update the app state accordingly. - */ - setStateFromCurrentLocation() { - const oldPathName = location.pathname; - this.router.resolve({pathname: oldPathName}).then(page => { - const newPathname = this.getPathname(page); - // In case of a router redirect, set the new location - if (newPathname !== oldPathName) { - const referrerUrl = location.href; - window.history.replaceState({}, '', newPathname); - this.dispatchLocationChanged(referrerUrl); - } - this.setState(page); - }).catch((e) => { - // In case we can't resolve the location, just leave things as is. - // This happens when a user enters a wrong URL or when testing with karma. - }); - } - - /** - * Update the router after some internal state change. - */ - update() { - // Queue updates so we can call this multiple times when changing state - // without it resulting in multiple location changes - setTimeout(() => { - const newPathname = this.getPathname(); - const oldPathname = location.pathname; - if (newPathname === oldPathname) - return; - const referrerUrl = location.href; - window.history.pushState({}, '', newPathname); - this.dispatchLocationChanged(referrerUrl); - }); - } - - /** - * Given a new routing path set the location and the app state. - * - * @param {string} pathname - */ - updateFromPathname(pathname) { - this.router.resolve({pathname: pathname}).then(page => { - if (location.pathname === pathname) - return; - const referrerUrl = location.href; - window.history.pushState({}, '', pathname); - this.setState(page); - this.dispatchLocationChanged(referrerUrl); - }).catch((err) => { - throw new Error(`Route not found: ${pathname}: ${err}`); - }); - } - - /** - * Pass some new router state to get a new router path that can - * be passed to updateFromPathname() later on. If nothing is - * passed the current state is used. - * - * @param {object} [partialState] The optional partial new state - * @returns {string} The new path - */ - getPathname(partialState) { - const currentState = this.getState(); - if (partialState === undefined) - partialState = {}; - let combined = {...currentState, ...partialState}; - return generateUrls(this.router)(this.routeName, combined); - } - - dispatchLocationChanged(referrerUrl = "") { - // fire a locationchanged event - window.dispatchEvent(new CustomEvent('locationchanged', { - detail: { - referrerUrl: referrerUrl, - }, - bubbles: true - })); - } -} diff --git a/src/app/tugraz-logo.js b/src/app/tugraz-logo.js deleted file mode 100644 index 607869f3a839ab179bb4364cad4c954f8a91b36a..0000000000000000000000000000000000000000 --- a/src/app/tugraz-logo.js +++ /dev/null @@ -1,82 +0,0 @@ -import {html, LitElement, css} from 'lit-element'; -import * as commonUtils from 'vpu-common/utils'; -import * as commonStyles from 'vpu-common/styles'; -import {createI18nInstance} from '../i18n.js'; - -const i18n = createI18nInstance(); - -class VPUTUGrazLogo extends LitElement { - - constructor() { - super(); - - this.lang = i18n.language; - } - - static get properties() { - return { - lang: { type: String } - }; - } - - update(changedProperties) { - changedProperties.forEach((oldValue, propName) => { - if (propName === "lang") { - i18n.changeLanguage(this.lang); - } - }); - super.update(changedProperties); - } - - static get styles() { - return css` - ${commonStyles.getThemeCSS()} - ${commonStyles.getGeneralCSS()} - - :host { - display: inline-block; - } - - #claim - { - font-size: 12px; - text-align: right; - padding: 0 17px 0 0; - line-height: 17px; - letter-spacing: 2px; - vertical-align: top; - text-transform: uppercase; - display: inline-block; - white-space: nowrap; - } - - #img { - overflow: visible; - } - - a:hover path, a:focus path { - fill:#000 !important; - transition:none; - } - - * { - transition:fill 0.15s, stroke 0.15s; - } - `; - } - - render() { - return html` - <a href="https://www.tugraz.at" title="TU Graz Home" target="_blank" rel="noopener"> - <div id="claim"> - <div class="int-header-logo-claim-single">${i18n.t('logo.word1')}</div> - <div class="int-header-logo-claim-single">${i18n.t('logo.word2')}</div> - <div class="int-header-logo-claim-single">${i18n.t('logo.word3')}</div> - </div> - <svg id="img" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" height="51.862" width="141.1" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" viewBox="0 0 141.10001 51.862499"><g transform="matrix(1.25 0 0 -1.25 0 51.862)"><g transform="scale(.1)"><path style="fill:#e4154b" d="m0 103.73h207.45v207.46l-207.45 0.01v-207.47z"></path><path style="fill:#e4154b" d="m228.19 103.73h207.46v207.46h-207.46v-207.46z"></path><path style="fill:#e4154b" d="m456.41 103.73h207.44v207.46h-207.44v-207.46z"></path><path style="fill:#e4154b" d="m103.72 0h207.47v207.46h-207.47v-207.46z"></path><path style="fill:#e4154b" d="m352.68 207.46h207.44v207.46h-207.44v-207.46z"></path><path style="fill:#231f20" d="m751.04 277.91h-66.426v33.195h171.19v-33.195h-66.407v-173.73h-38.359v173.73"></path><path style="fill:#231f20" d="m1048.3 180.22c0-12.461-2.25-23.711-6.72-33.75-4.5-10.039-10.61-18.555-18.36-25.567-7.76-7.031-16.9-12.421-27.503-16.21-10.605-3.809-22.109-5.7036-34.551-5.7036-12.422 0-23.945 1.8946-34.551 5.7036-10.605 3.789-19.824 9.179-27.656 16.21-7.851 7.012-13.984 15.528-18.34 25.567-4.394 10.039-6.582 21.289-6.582 33.75v130.89h38.379v-129.59c0-5.039 0.801-10.351 2.442-15.898 1.64-5.547 4.336-10.664 8.125-15.332s8.789-8.516 15.039-11.523c6.211-3.008 13.926-4.512 23.144-4.512 9.199 0 16.914 1.504 23.145 4.512 6.23 3.007 11.25 6.855 15.039 11.523 3.77 4.668 6.48 9.785 8.12 15.332 1.63 5.547 2.45 10.859 2.45 15.898v129.59h38.38v-130.89"></path><path style="fill:#231f20" d="m832.56 75.664c-7.597 3.2812-17.46 4.8632-25.332 4.8632-22.929 0-35.605-14.434-35.605-33.184 0-18.613 12.383-32.637 33.34-32.637 5.351 0 9.59 0.5274 12.969 1.3086v23.867h-20.84v14.414h39.687v-49.297c-10.41-2.6172-21.25-4.707-31.816-4.707-31.797 0-53.906 14.805-53.906 45.742 0 31.348 20.566 48.906 53.906 48.906 11.406 0 20.41-1.4453 28.867-3.8086l-1.27-15.469"></path><path style="fill:#231f20" d="m856.2 69.375h16.758v-15.332h0.293c0.84 6.289 8.574 16.914 19.824 16.914 1.836 0 3.828 0 5.782-0.5273v-17.715c-1.68 0.918-5.059 1.4454-8.457 1.4454-15.333 0-15.333-17.832-15.333-27.52v-24.785h-18.867v67.52"></path><path style="fill:#231f20" d="m913.75 65.84c7.324 3.1446 17.187 5.1172 25.215 5.1172 22.09 0 31.23-8.5351 31.23-28.457v-8.6523c0-6.8165 0.156-11.934 0.293-16.914 0.137-5.1172 0.41-9.8242 0.84-15.078h-16.602c-0.703 3.5352-0.703 8.0078-0.839 10.098h-0.293c-4.36-7.4618-13.81-11.661-22.38-11.661-12.793 0-25.332 7.207-25.332 20.059 0 10.078 5.195 15.976 12.383 19.258 7.187 3.2812 16.464 3.9453 24.355 3.9453h10.41c0 10.879-5.195 14.551-16.328 14.551-8.008 0-16.035-2.8907-22.363-7.3438l-0.586 15.078zm22.11-52.715c5.782 0 10.274 2.3633 13.223 6.0352 3.105 3.8086 3.945 8.6523 3.945 13.906h-8.164c-8.437 0-20.957-1.3086-20.957-11.68 0-5.7617 5.195-8.2617 11.953-8.2617"></path><path style="fill:#231f20" d="m985.69 69.375h57.422v-14.414l-36.04-39.473h37.31v-13.633h-60.235v14.297l36.715 39.59h-35.172v13.633"></path><path style="fill:#e4154b" d="m1059.6 0h69.102v69.121h-69.102v-69.121z"></path></g></g></svg> - </a> - `; - } -} - -commonUtils.defineCustomElement('vpu-tugraz-logo', VPUTUGrazLogo); diff --git a/src/i18n/de/translation.json b/src/i18n/de/translation.json index aa88e156de4e6a34b46c26208307c954025c85bd..df1513fb4999b63c9aff6cfe782206de72431eaf 100644 --- a/src/i18n/de/translation.json +++ b/src/i18n/de/translation.json @@ -1,9 +1,4 @@ { - "logo": { - "word1": "Wissen", - "word2": "Technik", - "word3": "Leidenschaft" - }, "pdf-upload": { "upload-field-label": "PDF Dateien zum Signieren hochladen", "upload-area-text": "Sie können in diesem Bereich PDF Dateien per Drag & Drop oder per Direktauswahl hochladen", @@ -24,7 +19,5 @@ }, "error-summary": "Ein Fehler ist aufgetreten", "error-permission-message": "Sie müssen das Recht auf Amtssignaturen besitzen um diese Funktion nutzen zu können!", - "error-login-message": "Sie müssen eingeloggt sein um diese Funktion nutzen zu können!", - "privacy-policy": "Datenschutz", - "page-updated-needs-reload": "Die Applikation wurde aktualisiert. Bitte laden Sie die Seite neu." + "error-login-message": "Sie müssen eingeloggt sein um diese Funktion nutzen zu können!" } diff --git a/src/i18n/en/translation.json b/src/i18n/en/translation.json index cbced032189a0ae1a7c16f9a33e3d851f147e827..ba9d45350df947103128eb678f8dae89a9896ae3 100644 --- a/src/i18n/en/translation.json +++ b/src/i18n/en/translation.json @@ -1,9 +1,4 @@ { - "logo": { - "word1": "Science", - "word2": "Passion", - "word3": "Technology" - }, "pdf-upload": { "upload-field-label": "Upload PDF files to sign", "upload-area-text": "In this area you can upload PDF files via Drag & Drop or by selecting them directly", @@ -24,7 +19,5 @@ }, "error-summary": "An error occurred", "error-permission-message": "You need have permissions to use the official signature to use this function!", - "error-login-message": "You need to be logged in to use this function!", - "privacy-policy": "Privacy Policy", - "page-updated-needs-reload": "The application has been updated. Please reload the page." + "error-login-message": "You need to be logged in to use this function!" } diff --git a/src/vpu-signature.js b/src/vpu-signature.js index 36c3352e9fe9d7fa40d067da73cdccfa8c2fdbd5..b463f4be36e6455ff979940eb38557b769c8f9c1 100644 --- a/src/vpu-signature.js +++ b/src/vpu-signature.js @@ -1 +1 @@ -import './app'; +import 'vpu-app-shell'; diff --git a/test/unit.js b/test/unit.js index 969d0933029738da5e9186aa962082e0ba8951b7..654fe70cef03c99f6a32b5658edcc2abaedf2a1a 100644 --- a/test/unit.js +++ b/test/unit.js @@ -2,7 +2,6 @@ import {assert} from 'chai'; import '../src/vpu-signature-pdf-upload'; import '../src/vpu-signature.js'; -import {Router} from '../src/app/router.js'; suite('vpu-signature-pdf-upload basics', () => { let node; @@ -40,28 +39,3 @@ suite('vpu-signature-app basics', () => { }); }); -suite('router', () => { - - test('basics', () => { - const routes = [ - { - name: 'foo', - path: '', - action: (context) => { - return {}; - } - }, - ]; - - const router = new Router(routes, { - routeName: 'foo', - getState: () => { return {}; }, - setState: (state) => { }, - }); - - router.setStateFromCurrentLocation(); - router.update(); - router.updateFromPathname("/"); - assert.equal(router.getPathname(), '/'); - }); -}); diff --git a/vendor/app-shell b/vendor/app-shell new file mode 160000 index 0000000000000000000000000000000000000000..0fdb2d6ba4f06ae9bc5bd45915698bd608e06fab --- /dev/null +++ b/vendor/app-shell @@ -0,0 +1 @@ +Subproject commit 0fdb2d6ba4f06ae9bc5bd45915698bd608e06fab