@geogirafe/lib-geoportal 1.1.0-dev.2426065451 → 1.1.0-dev.2433737633

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -229,6 +229,7 @@
229
229
  "My Drawing": "Meine Zeichnung",
230
230
  "My selection": "Meine Auswahl",
231
231
  "Name": "Name",
232
+ "news": "Nachrichten",
232
233
  "No": "Nein",
233
234
  "No bookmark": "Kein Lesezeichen",
234
235
  "No data": "Keine Daten",
@@ -383,6 +384,7 @@
383
384
  "Show metadata": "Metadaten anzeigen",
384
385
  "Show or hide measure information": "Massinformationen anzeigen oder ausblenden",
385
386
  "Show or hide shape name": "Formname anzeigen oder ausblenden",
387
+ "show_content": "Inhalt anzeigen",
386
388
  "sitn2001": "LiDAR 2001",
387
389
  "sitn2010": "LiDAR 2010",
388
390
  "sitn2016": "LiDAR 2016",
@@ -230,6 +230,7 @@
230
230
  "My Drawing": "My Drawing",
231
231
  "My selection": "My selection",
232
232
  "Name": "Name",
233
+ "news": "News",
233
234
  "No": "No",
234
235
  "No bookmark": "No bookmark",
235
236
  "No data": "No data",
@@ -384,6 +385,7 @@
384
385
  "Show metadata": "Show metadata",
385
386
  "Show or hide measure information": "Show or hide measure information",
386
387
  "Show or hide shape name": "Show or hide shape name",
388
+ "show_content": "Show content",
387
389
  "sitn2001": "LiDAR 2001",
388
390
  "sitn2010": "LiDAR 2010",
389
391
  "sitn2016": "LiDAR 2016",
@@ -229,6 +229,7 @@
229
229
  "My Drawing": "Mon dessin",
230
230
  "My selection": "Ma sélection",
231
231
  "Name": "Nom",
232
+ "news": "Actualités",
232
233
  "No": "Non",
233
234
  "No bookmark": "Aucun favori",
234
235
  "No data": "Pas de données",
@@ -383,6 +384,7 @@
383
384
  "Show metadata": "Afficher les métadonnées",
384
385
  "Show or hide measure information": "Afficher ou masquer les informations de mesure",
385
386
  "Show or hide shape name": "Afficher ou masquer le nom de la forme",
387
+ "show_content": "Afficher le contenu",
386
388
  "sitn2001": "LiDAR 2001",
387
389
  "sitn2010": "LiDAR 2010",
388
390
  "sitn2016": "LiDAR 2016",
@@ -229,6 +229,7 @@
229
229
  "My Drawing": "Il mio disegno",
230
230
  "My selection": "La mia selezione",
231
231
  "Name": "Nome",
232
+ "news": "Notizie",
232
233
  "No": "No",
233
234
  "No bookmark": "Nessun segnalibro",
234
235
  "No data": "Nessun dato",
@@ -383,6 +384,7 @@
383
384
  "Show metadata": "Mostra metadati",
384
385
  "Show or hide measure information": "Mostra o nascondi le informazioni di misura",
385
386
  "Show or hide shape name": "Mostra o nascondi il nome della forma",
387
+ "show_content": "Visualizzare il contenuto",
386
388
  "sitn2001": "LiDAR 2001",
387
389
  "sitn2010": "LiDAR 2010",
388
390
  "sitn2016": "LiDAR 2016",
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="24" height="24" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="3.131" r="1.411" style="fill:#000;fill-opacity:1;stroke:none;stroke-width:1.63248;stroke-linecap:round;stroke-linejoin:round"/><path d="M4 17.2h16C16 17.2 20 4 12 4S8 17.2 4 17.2" style="fill:none;fill-opacity:1;stroke:#000;stroke-width:2.11754;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"/><path d="M14.566 19.778A2.57 2.57 0 0 1 13.283 22a2.57 2.57 0 0 1-2.566 0 2.57 2.57 0 0 1-1.283-2.222H12Z" style="fill:#000;fill-opacity:1;stroke:none;stroke-width:4.212;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"/><circle cx="16.706" cy="7.332" r="5.168" style="fill:red;fill-opacity:1;stroke:none;stroke-width:33.6314"/></svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="24" height="24" fill="none" viewBox="0 0 24 24"><circle cx="12" cy="3.131" r="1.411" style="fill:#000;fill-opacity:1;stroke:none;stroke-width:1.63248;stroke-linecap:round;stroke-linejoin:round"/><path d="M4 17.2h16C16 17.2 20 4 12 4S8 17.2 4 17.2" style="fill:none;fill-opacity:1;stroke:#000;stroke-width:2.11754;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"/><path d="M14.566 19.778A2.57 2.57 0 0 1 13.283 22a2.57 2.57 0 0 1-2.566 0 2.57 2.57 0 0 1-1.283-2.222H12Z" style="fill:#000;fill-opacity:1;stroke:none;stroke-width:4.212;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"/></svg>
@@ -27,6 +27,8 @@ export { default as MenuMobile } from './menu-mobile/component.js';
27
27
  export { default as MenuButtonComponent } from './menubutton/component.js';
28
28
  export { default as ModalsComponent } from './modals/component.js';
29
29
  export { default as NavigationComponent } from './navigation/component.js';
30
+ export { default as NewsButtonComponent } from './news/news-button/component.js';
31
+ export { default as NewsPanelComponent } from './news/news-panel/component.js';
30
32
  export { default as OfflineComponent } from './offline/component.js';
31
33
  export { default as PrintComponent } from './print/component.js';
32
34
  export { default as PrototypeBannerComponent } from './prototypebanner/component.js';
@@ -27,6 +27,8 @@ export { default as MenuMobile } from './menu-mobile/component.js';
27
27
  export { default as MenuButtonComponent } from './menubutton/component.js';
28
28
  export { default as ModalsComponent } from './modals/component.js';
29
29
  export { default as NavigationComponent } from './navigation/component.js';
30
+ export { default as NewsButtonComponent } from './news/news-button/component.js';
31
+ export { default as NewsPanelComponent } from './news/news-panel/component.js';
30
32
  export { default as OfflineComponent } from './offline/component.js';
31
33
  export { default as PrintComponent } from './print/component.js';
32
34
  export { default as PrototypeBannerComponent } from './prototypebanner/component.js';
@@ -0,0 +1,12 @@
1
+ import GirafeHTMLElement from '../../../base/GirafeHTMLElement.js';
2
+ declare class NewsButtonComponent extends GirafeHTMLElement {
3
+ template: () => import("uhtml").Hole;
4
+ visible: boolean;
5
+ warn: boolean;
6
+ constructor();
7
+ setWarn(): void;
8
+ loadLastViewDate(): void;
9
+ getIcon(): "icons/bell-warn.svg" | "icons/bell.svg";
10
+ connectedCallback(): void;
11
+ }
12
+ export default NewsButtonComponent;
@@ -0,0 +1,47 @@
1
+ import { html as uHtml } from 'uhtml';
2
+ import GirafeHTMLElement from '../../../base/GirafeHTMLElement.js';
3
+ class NewsButtonComponent extends GirafeHTMLElement {
4
+ template = () => {
5
+ return uHtml `<style>
6
+ button img{width:1.5rem}
7
+ </style><style>
8
+ *{font-family:Arial,sans-serif}.hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a,a:visited{color:var(--link-color)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-timing-function:linear;animation-iteration-count:infinite}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}.gg-button,.gg-select,.gg-input,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);box-sizing:border-box;cursor:pointer;border-radius:3px;outline:0;margin:0;padding:0 0 0 .5rem;display:inline-block}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;align-items:center;margin:0;padding:0;display:flex}.gg-button,.gg-select,.gg-input,.gg-label{min-height:calc(var(--app-standard-height)/1.5)}.gg-textarea{max-height:initial;resize:vertical;height:6rem;padding:.5rem;line-height:1.3rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad2);background-color:var(--text-color-grad2);color:var(--bkg-color)}.gg-button:disabled{color:gray;cursor:not-allowed;background-color:#d3d3d3;border:none}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{color:gray;cursor:not-allowed;background-color:#d3d3d3}.gg-button>img{vertical-align:middle}.gg-icon-button{color:var(--text-color);cursor:pointer;background-color:#0000;border:none;flex-direction:column;justify-content:center;align-items:center;padding:0;display:flex}.gg-icon{justify-content:center;align-items:center;display:flex}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{width:calc(var(--app-standard-height) - 1.5rem);margin:0}.gg-big-withtext span{font-variant:small-caps;padding:0 1rem;font-size:.9rem}.gg-medium,.gg-medium-withtext{min-width:calc(var(--app-standard-height)/1.2);min-height:calc(var(--app-standard-height)/1.2);max-height:calc(var(--app-standard-height)/1.2);flex-direction:row}.gg-medium img{width:calc(var(--app-standard-height)/2.4);margin:0}.gg-medium-withtext img{width:calc(var(--app-standard-height)/2.4);margin-left:.5rem}.gg-medium-withtext span{padding:0 1rem 0 .5rem;font-size:.9rem}.gg-small,.gg-small-withtext{min-width:calc(var(--app-standard-height)/2);min-height:calc(var(--app-standard-height)/2);max-height:calc(var(--app-standard-height)/2);flex-direction:row}.gg-small img{width:calc(var(--app-standard-height)/3);margin:0}.gg-small-withtext img{width:calc(var(--app-standard-height)/3);margin-left:.5rem}.gg-small-withtext span{padding:0 .5rem 0 .3rem;font-size:.9rem}.gg-button:hover,.gg-select:hover,.gg-input:hover,.gg-textarea:hover,.gg-icon-button:hover{background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{opacity:1;background-color:#0000}.gg-tabs{cursor:pointer;grid-auto-flow:column;padding-bottom:1rem;font-size:1rem;display:grid}.gg-tab{border:none;border-bottom:var(--app-standard-border);cursor:pointer;color:var(--text-color);background:0 0;padding:.5rem}.gg-tab.active{border-bottom:solid 1px var(--text-color)}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{color:var(--text-color);background-color:#0000;border:none;flex-direction:column;display:flex}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert()}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{height:2rem;margin:.3rem}.girafe-button-large span{height:2rem;margin:.3rem;line-height:2rem}.girafe-button-small{min-width:calc(var(--app-standard-height)/2);height:calc(var(--app-standard-height)/2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{text-align:left;text-overflow:ellipsis;width:100%;overflow:hidden}.girafe-button-tiny{align-items:center;width:1rem;height:1rem;padding:0}.girafe-button-tiny img{overflow:hidden}.girafe-onboarding-theme{background-color:var(--bkg-color)!important;color:var(--text-color)!important}.girafe-onboarding-theme button{background-color:var(--bkg-color)!important;color:var(--text-color)!important;text-shadow:none!important}.girafe-onboarding-theme button.driver-popover-close-btn{z-index:10000}
9
+ </style>
10
+ <button tip="news" class="gg-icon-button gg-big" onclick="document.geogirafe.state.interface.newsPanelVisible=!document.geogirafe.state.interface.newsPanelVisible"><img alt="news-icon" src="${this.getIcon()}"></button>`;
11
+ };
12
+ visible = false;
13
+ warn = false;
14
+ constructor() {
15
+ super('news-button');
16
+ }
17
+ setWarn() {
18
+ this.warn = this.state.news.lastPublishDate > this.state.news.lastViewDate;
19
+ if (this.context.configManager.Config.news?.autoDisplay && this.warn) {
20
+ this.state.interface.newsPanelVisible = true;
21
+ }
22
+ super.render();
23
+ }
24
+ loadLastViewDate() {
25
+ const lastViewDate = this.context.userDataManager.getUserData('lastViewDate');
26
+ if (lastViewDate) {
27
+ this.context.stateManager.state.news.lastViewDate = lastViewDate;
28
+ }
29
+ }
30
+ getIcon() {
31
+ const icon = this.warn ? 'icons/bell-warn.svg' : 'icons/bell.svg';
32
+ return icon;
33
+ }
34
+ connectedCallback() {
35
+ super.connectedCallback();
36
+ if (this.context.configManager.Config.news) {
37
+ this.subscribe('news.lastViewDate', (_oldValue, _newValue) => {
38
+ this.setWarn();
39
+ });
40
+ this.subscribe('news.lastPublishDate', (_oldValue, _newValue) => {
41
+ this.setWarn();
42
+ });
43
+ super.girafeTranslate();
44
+ }
45
+ }
46
+ }
47
+ export default NewsButtonComponent;
@@ -0,0 +1,18 @@
1
+ import type { FeedEntry } from './newsfeedutils.js';
2
+ import GirafeHTMLElement from '../../../base/GirafeHTMLElement.js';
3
+ import IGirafePanel from '../../../tools/state/igirafepanel.js';
4
+ declare class NewsPanelComponent extends GirafeHTMLElement implements IGirafePanel {
5
+ template: () => import("uhtml").Hole;
6
+ isPanelVisible: boolean;
7
+ panelTitle: string;
8
+ panelTogglePath: string;
9
+ entries: FeedEntry[];
10
+ constructor();
11
+ saveLastViewDate(): void;
12
+ loadLastViewDate(): void;
13
+ togglePanel(visible: boolean): void;
14
+ render(): void;
15
+ private initializeNewsState;
16
+ protected connectedCallback(): void;
17
+ }
18
+ export default NewsPanelComponent;
@@ -0,0 +1,66 @@
1
+ import { htmlFor as uHtmlFor } from 'uhtml/keyed';
2
+ import { html as uHtml } from 'uhtml';
3
+ import GirafeHTMLElement from '../../../base/GirafeHTMLElement.js';
4
+ import { loadFeeds } from './newsfeedutils.js';
5
+ class NewsPanelComponent extends GirafeHTMLElement {
6
+ template = () => {
7
+ return uHtml `<style>
8
+ #panel{box-sizing:border-box;flex-direction:column;min-width:20rem;height:100%;min-height:0;display:flex;overflow:auto}#content{background:var(--bkg-color);box-sizing:border-box;width:100%;min-width:0;color:var(--text-color);flex:auto;margin:0;padding:1.5rem}.news-item{box-sizing:border-box;background-color:#c8d1d12b;border:1px solid #d5d5d5;width:100%;min-width:0;margin:16px 0;padding:15px 13px;box-shadow:0 0 5px #0000001f}.news-header{justify-content:space-between;align-items:flex-start;gap:1rem;display:flex}.news-title{flex:auto;min-width:0;font-size:14px}.news-meta{text-align:right;color:#6b7280;white-space:nowrap;flex:none;font-size:.8rem;line-height:1.3}.news-date{font-weight:500}.news-age{opacity:.8}.news-description{margin-top:.75rem}.news-details{width:100%;min-width:0;margin-top:.75rem}.details-content{box-sizing:border-box;contain:inline-size;width:100%;min-width:0;max-width:100%;overflow-x:auto}.news-details-toggle{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:none;padding:6px}.news-details-toggle:hover{background-color:#fdf6c3ba}
9
+ </style><style>
10
+ *{font-family:Arial,sans-serif}.hidden{display:none!important}.gg-rotate90{transform:rotate(90deg)}.gg-rotate180{transform:rotate(180deg)}.gg-rotate270{transform:rotate(270deg)}img{filter:var(--svg-filter)}img.legend-image{filter:var(--svg-map-filter);background:var(--svg-legend-bkg)}div{scrollbar-width:thin}a,a:visited{color:var(--link-color)}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes spin-wait{0%{transform:rotate(0)}7%{transform:rotate(360deg)}to{transform:rotate(360deg)}}.gg-spin{animation-name:spin;animation-duration:2s;animation-timing-function:linear;animation-iteration-count:infinite}.gg-spin-wait{animation-name:spin-wait;animation-duration:10s;animation-timing-function:linear;animation-iteration-count:infinite}::-webkit-scrollbar{width:5px}::-webkit-scrollbar-thumb{background:#999}.gg-button,.gg-select,.gg-input,.gg-textarea{background-color:var(--bkg-color);color:var(--text-color);border:var(--app-standard-border);box-sizing:border-box;cursor:pointer;border-radius:3px;outline:0;margin:0;padding:0 0 0 .5rem;display:inline-block}.gg-label{background-color:var(--bkg-color);color:var(--text-color);border:none;align-items:center;margin:0;padding:0;display:flex}.gg-button,.gg-select,.gg-input,.gg-label{min-height:calc(var(--app-standard-height)/1.5)}.gg-textarea{max-height:initial;resize:vertical;height:6rem;padding:.5rem;line-height:1.3rem}.gg-input{cursor:text}.gg-checkbox{accent-color:var(--text-color);width:1.2rem}.gg-range{accent-color:var(--text-color)}.gg-button{padding:0 .5rem}.gg-button.active{border:solid 1px var(--text-color-grad2);background-color:var(--text-color-grad2);color:var(--bkg-color)}.gg-button:disabled{color:gray;cursor:not-allowed;background-color:#d3d3d3;border:none}.gg-input:disabled,.gg-select:disabled,.gg-textarea:disabled{color:gray;cursor:not-allowed;background-color:#d3d3d3}.gg-button>img{vertical-align:middle}.gg-icon-button{color:var(--text-color);cursor:pointer;background-color:#0000;border:none;flex-direction:column;justify-content:center;align-items:center;padding:0;display:flex}.gg-icon{justify-content:center;align-items:center;display:flex}.gg-big,.gg-big-withtext{min-width:var(--app-standard-height);min-height:var(--app-standard-height);max-height:var(--app-standard-height)}.gg-big img,.gg-big-withtext img{width:calc(var(--app-standard-height) - 1.5rem);margin:0}.gg-big-withtext span{font-variant:small-caps;padding:0 1rem;font-size:.9rem}.gg-medium,.gg-medium-withtext{min-width:calc(var(--app-standard-height)/1.2);min-height:calc(var(--app-standard-height)/1.2);max-height:calc(var(--app-standard-height)/1.2);flex-direction:row}.gg-medium img{width:calc(var(--app-standard-height)/2.4);margin:0}.gg-medium-withtext img{width:calc(var(--app-standard-height)/2.4);margin-left:.5rem}.gg-medium-withtext span{padding:0 1rem 0 .5rem;font-size:.9rem}.gg-small,.gg-small-withtext{min-width:calc(var(--app-standard-height)/2);min-height:calc(var(--app-standard-height)/2);max-height:calc(var(--app-standard-height)/2);flex-direction:row}.gg-small img{width:calc(var(--app-standard-height)/3);margin:0}.gg-small-withtext img{width:calc(var(--app-standard-height)/3);margin-left:.5rem}.gg-small-withtext span{padding:0 .5rem 0 .3rem;font-size:.9rem}.gg-button:hover,.gg-select:hover,.gg-input:hover,.gg-textarea:hover,.gg-icon-button:hover{background-color:var(--bkg-color-grad1)}.gg-opacity{opacity:.5}.gg-opacity:hover{opacity:1;background-color:#0000}.gg-tabs{cursor:pointer;grid-auto-flow:column;padding-bottom:1rem;font-size:1rem;display:grid}.gg-tab{border:none;border-bottom:var(--app-standard-border);cursor:pointer;color:var(--text-color);background:0 0;padding:.5rem}.gg-tab.active{border-bottom:solid 1px var(--text-color)}.girafe-button-big,.girafe-button-large,.girafe-button-small,.girafe-button-tiny{color:var(--text-color);background-color:#0000;border:none;flex-direction:column;display:flex}.girafe-button-big:hover,.girafe-button-large:hover,.girafe-button-small:hover,.girafe-button-tiny:hover{background-color:var(--bkg-color-grad1);cursor:pointer}.girafe-button-big.dark,.girafe-button-large.dark,.girafe-button-small.dark,.girafe-button-tiny.dark{background-color:var(--bkg-color);filter:invert()}.girafe-button-big{width:var(--app-standard-height);height:var(--app-standard-height);align-items:center;padding:1rem}.girafe-button-big img{overflow:hidden}.girafe-button-large{flex-direction:row}.girafe-button-large img{height:2rem;margin:.3rem}.girafe-button-large span{height:2rem;margin:.3rem;line-height:2rem}.girafe-button-small{min-width:calc(var(--app-standard-height)/2);height:calc(var(--app-standard-height)/2);align-items:center;padding:.5rem}.girafe-button-small img{overflow:hidden}.girafe-button-small span{text-align:left;text-overflow:ellipsis;width:100%;overflow:hidden}.girafe-button-tiny{align-items:center;width:1rem;height:1rem;padding:0}.girafe-button-tiny img{overflow:hidden}.girafe-onboarding-theme{background-color:var(--bkg-color)!important;color:var(--text-color)!important}.girafe-onboarding-theme button{background-color:var(--bkg-color)!important;color:var(--text-color)!important;text-shadow:none!important}.girafe-onboarding-theme button.driver-popover-close-btn{z-index:10000}
11
+ </style>
12
+ <div id="panel"><div id="content">${this.entries.map(f => uHtmlFor(f, f.id) `<div class="news-item"><div class="news-header"><div class="news-title"><b>${f.title}</b></div><div class="news-meta"><div class="news-date">${f.date?.toLocaleDateString(undefined, { day: '2-digit', month: 'short', year: 'numeric' }) ?? ''}</div></div></div><div class="news-description">${this.htmlUnsafe(f.description ?? '')}</div>${f.content ? uHtml `<details class="news-details"><summary class="news-details-toggle" i18n="show_content">Show content</summary><div class="details-content">${this.htmlUnsafe(f.content ?? '')}</div></details>` : ''}</div>`)}</div></div>`;
13
+ };
14
+ isPanelVisible = false;
15
+ panelTitle = 'news';
16
+ panelTogglePath = 'interface.newsPanelVisible'; // Subscribes to changes in the state manager's newsPanelVisible property
17
+ entries = [];
18
+ constructor() {
19
+ super('news-panel');
20
+ }
21
+ saveLastViewDate() {
22
+ const currentTimestamp = Date.now();
23
+ this.context.userDataManager.saveUserData('lastViewDate', currentTimestamp);
24
+ this.context.stateManager.state.news.lastViewDate = currentTimestamp;
25
+ }
26
+ loadLastViewDate() {
27
+ const raw = this.context.userDataManager.getUserData('lastViewDate');
28
+ const lastViewDate = Number(raw);
29
+ this.context.stateManager.state.news.lastViewDate = Number.isFinite(lastViewDate) ? lastViewDate : 0;
30
+ }
31
+ togglePanel(visible) {
32
+ this.isPanelVisible = visible;
33
+ if (visible) {
34
+ this.saveLastViewDate();
35
+ }
36
+ this.render();
37
+ }
38
+ render() {
39
+ if (this.isPanelVisible) {
40
+ super.render();
41
+ }
42
+ else {
43
+ this.hide();
44
+ }
45
+ super.girafeTranslate();
46
+ }
47
+ async initializeNewsState() {
48
+ if (!this.context.configManager.Config.news) {
49
+ return;
50
+ }
51
+ this.loadLastViewDate();
52
+ const urls = this.context.configManager.Config.news.urls;
53
+ this.entries = await loadFeeds(urls);
54
+ // Update state manager
55
+ const lastPublishDate = this.entries.length > 0 ? (this.entries[0].date?.getTime() ?? 0) : 0;
56
+ this.context.stateManager.state.news.lastPublishDate = lastPublishDate;
57
+ }
58
+ connectedCallback() {
59
+ super.connectedCallback();
60
+ if (this.context.configManager.Config.news) {
61
+ void this.initializeNewsState();
62
+ this.render();
63
+ }
64
+ }
65
+ }
66
+ export default NewsPanelComponent;
@@ -0,0 +1,10 @@
1
+ export type FeedEntry = {
2
+ id: string;
3
+ title: string;
4
+ date: Date | null;
5
+ description: string;
6
+ content: string;
7
+ link: string;
8
+ author: string;
9
+ };
10
+ export declare function loadFeeds(urls: string[]): Promise<FeedEntry[]>;
@@ -0,0 +1,99 @@
1
+ import DOMPurify from 'dompurify';
2
+ export async function loadFeeds(urls) {
3
+ const entries = [];
4
+ for (const url of urls) {
5
+ const res = await fetch(url);
6
+ const xmlText = await res.text();
7
+ const parser = new DOMParser();
8
+ const xml = parser.parseFromString(xmlText, 'text/xml');
9
+ const feedType = detectFeedType(xml);
10
+ if (feedType === 'rss') {
11
+ entries.push(...parseRssFeed(xml));
12
+ }
13
+ else if (feedType === 'atom') {
14
+ entries.push(...parseAtomFeed(xml));
15
+ }
16
+ else {
17
+ console.warn(`Invalid feed URL or unknown feed format: ${url}`);
18
+ }
19
+ }
20
+ entries.sort((a, b) => (b.date?.getTime() ?? 0) - (a.date?.getTime() ?? 0));
21
+ return entries;
22
+ }
23
+ function detectFeedType(xml) {
24
+ const root = xml.documentElement;
25
+ const tagName = root.tagName.toLowerCase();
26
+ if (tagName.includes('rss')) {
27
+ return 'rss';
28
+ }
29
+ if (tagName.includes('feed')) {
30
+ return 'atom';
31
+ }
32
+ if (tagName.includes('rdf')) {
33
+ return 'rss'; // RSS 1.0
34
+ }
35
+ return 'unknown';
36
+ }
37
+ function parseRssFeed(xml) {
38
+ return Array.from(xml.querySelectorAll('item')).map((entry, index) => {
39
+ const title = entry.querySelector('title')?.textContent?.trim() || '';
40
+ const pubDateText = entry.querySelector('pubDate')?.textContent?.trim() || '';
41
+ const date = pubDateText ? new Date(pubDateText) : null;
42
+ const description = getSanitizedHtml(entry.querySelector('description'));
43
+ const contentNode = entry.getElementsByTagNameNS('http://purl.org/rss/1.0/modules/content/', 'encoded')[0];
44
+ const content = getSanitizedHtml(contentNode);
45
+ const link = entry.querySelector('link')?.textContent?.trim() || '';
46
+ const author = entry.querySelector('author')?.textContent?.trim() ||
47
+ entry.getElementsByTagName('dc:creator')[0]?.textContent?.trim() ||
48
+ '';
49
+ const guid = entry.querySelector('guid')?.textContent?.trim() || '';
50
+ const id = guid || link || `rss-${index}`;
51
+ return {
52
+ id,
53
+ title,
54
+ date,
55
+ description,
56
+ content,
57
+ link,
58
+ author
59
+ };
60
+ });
61
+ }
62
+ function parseAtomFeed(xml) {
63
+ return Array.from(xml.querySelectorAll('entry')).map((entry, index) => {
64
+ const title = entry.querySelector('title')?.textContent?.trim() || '';
65
+ const id = entry.querySelector('id')?.textContent?.trim() || `atom-${index}`;
66
+ const updated = entry.querySelector('updated')?.textContent?.trim() || '';
67
+ const published = entry.querySelector('published')?.textContent?.trim() || '';
68
+ const date = updated || published ? new Date(updated || published) : null;
69
+ const summary = getSanitizedHtml(entry.querySelector('summary'));
70
+ const content = getSanitizedHtml(entry.querySelector('content'));
71
+ const description = summary || content;
72
+ const author = entry.querySelector('author > name')?.textContent?.trim() ||
73
+ entry.querySelector('author')?.textContent?.trim() ||
74
+ '';
75
+ const link = entry.querySelector('link[rel="alternate"]')?.getAttribute('href') ||
76
+ entry.querySelector('link')?.getAttribute('href') ||
77
+ '';
78
+ return {
79
+ id,
80
+ title,
81
+ date,
82
+ description,
83
+ content,
84
+ link,
85
+ author
86
+ };
87
+ });
88
+ }
89
+ function sanitizeHtml(value) {
90
+ return DOMPurify.sanitize(value, {
91
+ USE_PROFILES: { html: true },
92
+ FORBID_TAGS: ['style', 'script', 'iframe', 'object', 'embed'],
93
+ FORBID_ATTR: ['style', 'onerror', 'onload', 'onclick']
94
+ });
95
+ }
96
+ function getSanitizedHtml(node) {
97
+ const raw = node?.textContent?.trim() || '';
98
+ return raw ? sanitizeHtml(raw) : '';
99
+ }
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "GeoGirafe PSC",
6
6
  "url": "https://doc.geomapfish.dev"
7
7
  },
8
- "version": "1.1.0-dev.2426065451",
8
+ "version": "1.1.0-dev.2433737633",
9
9
  "type": "module",
10
10
  "engines": {
11
11
  "node": ">=20.19.0"
@@ -94,6 +94,7 @@
94
94
  </p>
95
95
  </div> -->
96
96
  </girafe-search>
97
+ <girafe-news-button plugin="config:news"></girafe-news-button>
97
98
  <girafe-oauth></girafe-oauth>
98
99
  <girafe-nav-history></girafe-nav-history>
99
100
  <girafe-video-record></girafe-video-record>
@@ -136,6 +137,7 @@
136
137
  <girafe-print slot="main"></girafe-print>
137
138
  <girafe-ext-layer slot="main"></girafe-ext-layer>
138
139
  <girafe-cross-section-settings slot="main"></girafe-cross-section-settings>
140
+ <girafe-news slot="main"></girafe-news>
139
141
  <girafe-edit slot="main"></girafe-edit>
140
142
  <girafe-layout slot="main"></girafe-layout>
141
143
  <girafe-about slot="main"></girafe-about>
@@ -1 +1 @@
1
- {"version":"1.1.0-dev.2426065451", "build":"2426065451", "date":"02/04/2026"}
1
+ {"version":"1.1.0-dev.2433737633", "build":"2433737633", "date":"07/04/2026"}
@@ -21,6 +21,8 @@ import MapComponent from '../../components/map/component.js';
21
21
  import MenuButtonComponent from '../../components/menubutton/component.js';
22
22
  import ModalsComponent from '../../components/modals/component.js';
23
23
  import NavigationComponent from '../../components/navigation/component.js';
24
+ import NewsButtonComponent from '../../components/news/news-button/component.js';
25
+ import NewsPanelComponent from '../../components/news/news-panel/component.js';
24
26
  import OauthComponent from '../../components/auth/component.js';
25
27
  import PrintComponent from '../../components/print/component.js';
26
28
  import PrototypeBannerComponent from '../../components/prototypebanner/component.js';
@@ -110,6 +112,8 @@ export default class GeoGirafeApp {
110
112
  customElements.define('girafe-menu-button', MenuButtonComponent);
111
113
  customElements.define('girafe-modals', ModalsComponent);
112
114
  customElements.define('girafe-nav-history', NavigationComponent);
115
+ customElements.define('girafe-news-button', NewsButtonComponent);
116
+ customElements.define('girafe-news', NewsPanelComponent);
113
117
  customElements.define('girafe-print', PrintComponent);
114
118
  customElements.define('girafe-prototype-banner', PrototypeBannerComponent);
115
119
  customElements.define('girafe-query-builder', QueryBuilderComponent);
@@ -117,6 +117,10 @@ declare class GirafeConfig {
117
117
  lidar?: {
118
118
  url: string;
119
119
  };
120
+ news?: {
121
+ urls: string[];
122
+ autoDisplay: boolean;
123
+ };
120
124
  externalLayers?: {
121
125
  predefinedSources: {
122
126
  label: string;
@@ -257,6 +261,7 @@ declare class GirafeConfig {
257
261
  private initConfigTreeview;
258
262
  private initConfigBasemaps;
259
263
  private initConfigLidar;
264
+ private initConfigNews;
260
265
  private initConfigCsv;
261
266
  private initConfigMetadata;
262
267
  private initConfigInfoWindow;
@@ -15,6 +15,7 @@ class GirafeConfig {
15
15
  map;
16
16
  map3d;
17
17
  lidar;
18
+ news;
18
19
  externalLayers;
19
20
  contextmenu;
20
21
  crs;
@@ -52,6 +53,7 @@ class GirafeConfig {
52
53
  this.drawing = this.initConfigDrawing(config);
53
54
  this.projections = this.initConfigProjections(config);
54
55
  this.map = this.initConfigMap(config);
56
+ this.news = this.initConfigNews(config);
55
57
  this.lidar = this.initConfigLidar(config);
56
58
  this.csv = this.initConfigCsv(config);
57
59
  this.metadata = this.initConfigMetadata(config);
@@ -104,6 +106,14 @@ class GirafeConfig {
104
106
  // We just display a warning in the console
105
107
  console.warn(e);
106
108
  }
109
+ try {
110
+ this.news = this.initConfigNews(config);
111
+ }
112
+ catch (e) {
113
+ // The application can be started even if the news is not correctly configured
114
+ // We just display a warning in the console
115
+ console.warn(e);
116
+ }
107
117
  }
108
118
  initConfigProjections(config) {
109
119
  if (!config.projections) {
@@ -229,6 +239,9 @@ class GirafeConfig {
229
239
  initConfigLidar(config) {
230
240
  return config.lidar;
231
241
  }
242
+ initConfigNews(config) {
243
+ return config.news;
244
+ }
232
245
  initConfigCsv(config) {
233
246
  const defaultConfig = {
234
247
  encoding: 'utf-8',
package/tools/main.d.ts CHANGED
@@ -80,7 +80,7 @@ export type { MapMarker } from './state/mapposition.js';
80
80
  export { default as MapPosition } from './state/mapposition.js';
81
81
  export type { InitialSelectionQuery } from './state/objectselection.js';
82
82
  export { default as ObjectSelection } from './state/objectselection.js';
83
- export type { ThemesConfig, InfoBoxContent, Lidar, InfoWindow, ExtendedState } from './state/state.js';
83
+ export type { ThemesConfig, InfoBoxContent, Lidar, News, InfoWindow, ExtendedState } from './state/state.js';
84
84
  export { default as State } from './state/state.js';
85
85
  export { default as StateToggleManager } from './state/stateToggleManager.js';
86
86
  export type { Callback } from './state/statemanager.js';
@@ -7,6 +7,7 @@ export default class GraphicalInterface {
7
7
  printPanelVisible: boolean;
8
8
  extLayerPanelVisible: boolean;
9
9
  crossSectionPanelVisible: boolean;
10
+ newsPanelVisible: boolean;
10
11
  editPanelVisible: boolean;
11
12
  sharePanelVisible: boolean;
12
13
  selectionComponentVisible: boolean;
@@ -7,6 +7,7 @@ export default class GraphicalInterface {
7
7
  printPanelVisible = false;
8
8
  extLayerPanelVisible = false;
9
9
  crossSectionPanelVisible = false;
10
+ newsPanelVisible = false;
10
11
  editPanelVisible = false;
11
12
  sharePanelVisible = false;
12
13
  selectionComponentVisible = false;
@@ -38,6 +38,11 @@ export type Lidar = {
38
38
  line: OlGeomLineString | null;
39
39
  drawActive: boolean;
40
40
  };
41
+ export type News = {
42
+ urls: string[] | null;
43
+ lastViewDate: number;
44
+ lastPublishDate: number;
45
+ };
41
46
  type Functionalities = {
42
47
  authorized_plugins?: string[];
43
48
  };
@@ -97,6 +102,7 @@ export default class State {
97
102
  userInteractionListeners: GgUserInteractionListener[];
98
103
  language: string | null;
99
104
  lidar: Lidar;
105
+ news: News;
100
106
  loading: boolean;
101
107
  application: {
102
108
  isConfigurationLoaded: boolean;
@@ -50,6 +50,11 @@ export default class State {
50
50
  line: null,
51
51
  drawActive: false
52
52
  };
53
+ news = {
54
+ urls: null,
55
+ lastViewDate: 0,
56
+ lastPublishDate: 0
57
+ };
53
58
  // Is the application currently loading map data?
54
59
  loading = false;
55
60
  // Global variables that represent the state of the application.
@@ -30,7 +30,8 @@ describe('StateToggleManager class', () => {
30
30
  new TestPanel('drawing', 'interface.drawingPanelVisible'),
31
31
  new TestPanel('print', 'interface.printPanelVisible'),
32
32
  new TestPanel('userprefs', 'interface.userPreferencesPanelVisible'),
33
- new TestPanel('share', 'interface.sharePanelVisible')
33
+ new TestPanel('share', 'interface.sharePanelVisible'),
34
+ new TestPanel('news', 'interface.newsPanelVisible')
34
35
  ];
35
36
  beforeEach(() => {
36
37
  stateToggleManager = new StateToggleManager(panels, stateManager);
@@ -43,12 +44,14 @@ describe('StateToggleManager class', () => {
43
44
  expect(state.interface.printPanelVisible).toBeFalsy();
44
45
  expect(state.interface.userPreferencesPanelVisible).toBeFalsy();
45
46
  expect(state.interface.sharePanelVisible).toBeFalsy();
47
+ expect(state.interface.newsPanelVisible).toBeFalsy();
46
48
  state.interface.userPreferencesPanelVisible = true;
47
49
  expect(state.interface.helpVisible).toBeFalsy();
48
50
  expect(state.interface.drawingPanelVisible).toBeFalsy();
49
51
  expect(state.interface.printPanelVisible).toBeFalsy();
50
52
  expect(state.interface.userPreferencesPanelVisible).toBeTruthy();
51
53
  expect(state.interface.sharePanelVisible).toBeFalsy();
54
+ expect(state.interface.newsPanelVisible).toBeFalsy();
52
55
  });
53
56
  it('deactivateAll', () => {
54
57
  const state = stateManager.state;
@@ -58,5 +61,6 @@ describe('StateToggleManager class', () => {
58
61
  expect(state.interface.printPanelVisible).toBeFalsy();
59
62
  expect(state.interface.userPreferencesPanelVisible).toBeFalsy();
60
63
  expect(state.interface.sharePanelVisible).toBeFalsy();
64
+ expect(state.interface.newsPanelVisible).toBeFalsy();
61
65
  });
62
66
  });