diff --git a/docs/React4xp.md b/docs/React4xp.md index c16b2bea6..7b607d544 100644 --- a/docs/React4xp.md +++ b/docs/React4xp.md @@ -14,4 +14,6 @@ Hydration is needed for react components with client side interaction. You need If we want to make more complex components/containers for react we can create classes that extends the React.Component class. This will open of for the possibility to use [state](https://reactjs.org/docs/state-and-lifecycle.html#adding-local-state-to-a-class) and [lifecycles](https://reactjs.org/docs/react-component.html#the-component-lifecycle). Ref. [Header.jsx](/src/main/resources/react4xp/_entries/Header.jsx) ## Entries +It is important that any file in `react4xp/_entries` is not imported into any other file. An entry is a bridge between XP and React, not a reusable React component. They should only be used through the various render methods in `lib-react4xp` in our controllers. + After you've run `enonic project build` or `enonic project deploy` at least once, you'll have a file in your build folder with a list of all react4xp components available. `/build/resources/main/assets/react4xp/entries.json` diff --git a/src/main/resources/admin/tools/bestbet/bestbet.ts b/src/main/resources/admin/tools/bestbet/bestbet.ts index a2f50647f..1a9315cc5 100644 --- a/src/main/resources/admin/tools/bestbet/bestbet.ts +++ b/src/main/resources/admin/tools/bestbet/bestbet.ts @@ -92,7 +92,7 @@ function renderPart(req: XP.Request): XP.Response { ] const bestBetComponent = r4XpRender( - 'bestbet/Bestbet', + 'Bestbet', { logoUrl: assetUrl({ path: 'SSB_logo_black.svg', diff --git a/src/main/resources/main.es6 b/src/main/resources/main.es6 index f0909bbfa..85781bec5 100644 --- a/src/main/resources/main.es6 +++ b/src/main/resources/main.es6 @@ -95,10 +95,6 @@ try { feature: 'datefns-publication-archive', enabled: false, }, - { - feature: 'csr-on-table-accordion', - enabled: false, - }, ], }, ]) diff --git a/src/main/resources/react4xp/_entries/Accordion.jsx b/src/main/resources/react4xp/_entries/Accordion.jsx index 91e4693d1..84d84a3d3 100644 --- a/src/main/resources/react4xp/_entries/Accordion.jsx +++ b/src/main/resources/react4xp/_entries/Accordion.jsx @@ -1,67 +1,4 @@ import React from 'react' -import { Accordion as AccordionComponent, NestedAccordion } from '@statisticsnorway/ssb-component-library' - -import PropTypes from 'prop-types' - -class Accordion extends React.Component { - renderNestedAccordions(items) { - return items.map((item, i) => ( - - {item.body &&
} - - )) - } - - createMarkup(html) { - return { - __html: html.replace(/ /g, ' '), - } - } - - render() { - const location = window.location - const anchor = location && location.hash !== '' ? location.hash.substr(1) : undefined - - const { accordions } = this.props - - return ( -
-
- {accordions.map((accordion, index) => ( - - - {accordion.body &&
} - {this.renderNestedAccordions(accordion.items)} - - - ))} -
-
- ) - } -} - -Accordion.propTypes = { - accordions: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - open: PropTypes.string.isRequired, - subHeader: PropTypes.string, - body: PropTypes.string, - items: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string, - body: PropTypes.string, - }) - ), - }) - ), -} +import Accordion from '../accordion/Accordion' export default (props) => diff --git a/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx b/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx index 8fdb3fd56..af0964452 100644 --- a/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx +++ b/src/main/resources/react4xp/_entries/AttachmentTablesFigures.jsx @@ -3,7 +3,7 @@ import { Accordion, Button } from '@statisticsnorway/ssb-component-library' import { ChevronDown, ChevronUp } from 'react-feather' import PropTypes from 'prop-types' -import Table from './Table' +import Table from '../table/Table' import { addGtagForEvent } from '/react4xp/ReactGA' function AttachmentTableFigures(props) { diff --git a/src/main/resources/react4xp/_entries/Bestbet.jsx b/src/main/resources/react4xp/_entries/Bestbet.jsx new file mode 100644 index 000000000..98f5185ba --- /dev/null +++ b/src/main/resources/react4xp/_entries/Bestbet.jsx @@ -0,0 +1,4 @@ +import React from 'react' +import Bestbet from '../bestbet/Bestbet' + +export default (props) => diff --git a/src/main/resources/react4xp/_entries/Table.jsx b/src/main/resources/react4xp/_entries/Table.jsx index ca0e5c167..ee111a1a6 100644 --- a/src/main/resources/react4xp/_entries/Table.jsx +++ b/src/main/resources/react4xp/_entries/Table.jsx @@ -1,650 +1,4 @@ -import React, { useState, useRef, useEffect } from 'react' -import PropTypes from 'prop-types' -import { Dropdown, Link } from '@statisticsnorway/ssb-component-library' -import { default as isEmpty } from 'ramda/es/isEmpty' -import NumberFormat from 'react-number-format' -import { Alert, Button } from 'react-bootstrap' -import { ChevronLeft, ChevronRight } from 'react-feather' -import { addGtagForEvent } from '/react4xp/ReactGA' - -function Table(props) { - const [prevClientWidth, setPrevClientWidth] = useState(0) - const [table, setTable] = useState(props.paramShowDraft && props.draftExist ? props.tableDraft : props.table) - const [fetchUnPublished, setFetchUnPublished] = useState(props.paramShowDraft) - - const showPreviewToggle = - props.showPreviewDraft && (!props.pageTypeStatistic || (props.paramShowDraft && props.pageTypeStatistic)) - - const captionRef = useRef(null) - const tableControlsDesktopRef = useRef(null) - const tableControlsMobileRef = useRef(null) - const tableRef = useRef(null) - const tableWrapperRef = useRef(null) - - useEffect(() => { - updateTableControlsDesktop() - - const widthCheckInterval = setInterval(() => { - widthCheck() - }, 250) - window.addEventListener('resize', updateTableControlsDesktop) - return () => { - clearInterval(widthCheckInterval) - window.removeEventListener('resize', updateTableControlsDesktop) - } - }, []) - - function widthCheck() { - if (tableWrapperRef.current.clientWidth !== prevClientWidth) { - setPrevClientWidth(tableWrapperRef.current.clientWidth) - updateTableControlsDesktop() - } - } - - function updateTableControlsDesktop() { - const controls = tableControlsDesktopRef.current - const tableWrapper = tableWrapperRef.current - const left = controls.children.item(0) - const right = controls.children.item(1) - - // hide controlls if there is no scrollbar - if (tableWrapper.scrollWidth > tableWrapper.clientWidth || tableWrapper.clientWidth === 0) { - controls.classList.remove('d-none') - tableControlsMobileRef.current.classList.remove('d-none') - // disable left - if (tableWrapper.scrollLeft <= 0) { - left.classList.add('disabled') - } else { - left.classList.remove('disabled') - } - - // disable right - if (tableWrapper.scrollLeft + tableWrapper.clientWidth >= tableWrapper.scrollWidth) { - right.classList.add('disabled') - } else { - right.classList.remove('disabled') - } - - // move desktop controls to correct pos - const captionHalfHeight = captionRef.current.offsetHeight / 2 - const controlsHalfHeight = left.scrollHeight / 2 - left.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px` - right.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px` - } else { - controls.classList.add('d-none') - tableControlsMobileRef.current.classList.add('d-none') - } - } - - function scrollLeft() { - tableWrapperRef.current.scrollLeft -= 100 - updateTableControlsDesktop() - } - - function scrollRight() { - tableWrapperRef.current.scrollLeft += 100 - updateTableControlsDesktop() - } - - function trimValue(value) { - if (value && typeof value === 'string') { - return value.trim() - } - return value - } - - function formatNumber(value) { - const language = props.table.language - const decimalSeparator = language === 'en' ? '.' : ',' - value = trimValue(value) - if (value) { - if (typeof value === 'number' || (typeof value === 'string' && !isNaN(value))) { - const decimals = value.toString().indexOf('.') > -1 ? value.toString().split('.')[1].length : 0 - return ( - - ) - } - } - return value - } - - function addDownloadTableDropdown(mobile) { - const { downloadTableLabel, downloadTableTitle, downloadTableOptions } = props - - if (downloadTableLabel && downloadTableTitle && downloadTableOptions) { - const downloadTable = (item) => { - if (item.id === 'downloadTableAsCSV') { - { - downloadTableAsCSV() - } - } - - if (item.id === 'downloadTableAsXLSX') { - { - downloadTableAsExcel() - } - } - } - - return ( -
- -
- ) - } - } - - function downloadTableAsCSV() { - if (props.GA_TRACKING_ID) { - addGtagForEvent(props.GA_TRACKING_ID, 'Lastet ned csv tabell', 'Statistikkside tabeller', 'Last ned csv tabell') - } - - if (window && window.downloadTableFile) { - window.downloadTableFile(tableRef.current, { - type: 'csv', - fileName: 'tabell', - csvSeparator: ';', - csvEnclosure: '', - tfootSelector: '', - }) - } - } - - function downloadTableAsExcel() { - if (props.GA_TRACKING_ID) { - addGtagForEvent( - props.GA_TRACKING_ID, - 'Lastet ned excell tabell', - 'Statistikkside tabeller', - 'Last ned excell tabell' - ) - } - - if (window && window.downloadTableFile) { - window.downloadTableFile(tableRef.current, { - type: 'xlsx', - fileName: 'tabell', - numbers: { - html: { - decimalMark: ',', - thousandsSeparator: ' ', - }, - output: { - decimalMark: '.', - thousandsSeparator: '', - }, - }, - }) - } - } - - function createTable() { - const { tableClass } = props.table - - return ( - - {addCaption()} - {table.thead.map((t, index) => { - return ( - - {addThead(index)} - {addTbody(index)} - - ) - })} - {addTFoot()} -
- ) - } - - function addCaption() { - const { caption } = table - if (caption) { - const hasNoteRefs = typeof caption === 'object' - return ( - -
- {hasNoteRefs ? caption.content : caption} - {hasNoteRefs ? addNoteRefs(caption.noterefs) : null} -
- - ) - } - } - - function createScrollControlsMobile() { - return ( -
- -
- ) - } - - function createScrollControlsDesktop() { - return ( -
- scrollLeft()}> - - - scrollRight()}> - - -
- ) - } - - function addThead(index) { - return {createRowsHead(table.thead[index].tr)} - } - - function addTbody(index) { - return {createRowsBody(table.tbody[index].tr)} - } - - function renderCorrectionNotice() { - if (table.tfoot.correctionNotice) { - return ( - - {table.tfoot.correctionNotice} - - ) - } - return null - } - - function addTFoot() { - const { footnotes, correctionNotice } = table.tfoot - - const noteRefs = table.noteRefs - - if ((noteRefs && noteRefs.length > 0) || correctionNotice) { - return ( - - {noteRefs.map((note, index) => { - const current = footnotes && footnotes.find((footnote) => footnote.noteid === note) - if (current) { - return ( - - - {index + 1} - {current.content} - - - ) - } else { - return null - } - })} - {renderCorrectionNotice()} - - ) - } - return null - } - - function createRowsHead(rows) { - if (rows) { - return rows.map((row, i) => { - return {createHeaderCell(row)} - }) - } - } - - function createRowsBody(rows) { - if (rows) { - return rows.map((row, i) => { - return ( - - {createBodyTh(row)} - {createBodyTd(row)} - - ) - }) - } - } - - function createHeaderCell(row) { - return Object.keys(row).map((keyName) => { - const value = row[keyName] - if (keyName === 'th') { - return createHeadTh(value) - } else if (keyName === 'td') { - return createHeadTd(value) - } - }) - } - - function createHeadTh(value) { - return value.map((cellValue, i) => { - if (typeof cellValue === 'object') { - if (Array.isArray(cellValue)) { - // TODO: Because some values is split into array by xmlParser i have to do this, find better fix - return {cellValue.join(' ')} - } else { - return ( - - {trimValue(cellValue.content)} - {addNoteRefs(cellValue.noterefs)} - - ) - } - } else { - return ( - - {trimValue(cellValue)} - - ) - } - }) - } - - function createHeadTd(value) { - return value.map((cellValue, i) => { - if (typeof cellValue === 'object') { - return ( - - {trimValue(cellValue.content)} - {addNoteRefs(cellValue.noterefs)} - - ) - } else { - return {trimValue(cellValue)} - } - }) - } - - function createBodyTh(row) { - return Object.keys(row).map((key) => { - const value = row[key] - if (key === 'th') { - return value.map((cellValue, i) => { - if (typeof cellValue === 'object') { - return ( - - {trimValue(cellValue.content)} - {addNoteRefs(cellValue.noterefs)} - - ) - } else { - return ( - - {trimValue(cellValue)} - - ) - } - }) - } - }) - } - - function createBodyTd(row) { - return Object.keys(row).map((keyName) => { - const value = row[keyName] - if (keyName === 'td') { - return value.map((cellValue, i) => { - if (typeof cellValue === 'object') { - return ( - - {formatNumber(cellValue.content)} - - ) - } else { - return {formatNumber(cellValue)} - } - }) - } - }) - } - - function addNoteRefs(noteRefId) { - if (noteRefId) { - const noteRefs = table.noteRefs - const noteIDs = noteRefId.split(' ') - const notesToReturn = noteRefs.reduce((acc, current, index) => { - // Lag et array av indeksen til alle id-enene i footer - return noteIDs.some((element) => element === current) ? acc.concat(index) : acc - }, []) - - if (notesToReturn) { - return {notesToReturn.map((noteRef) => `${noteRef + 1} `)} - } - } else return '' - } - - function addStandardSymbols() { - const { standardSymbol } = props - - if (standardSymbol && standardSymbol.href && standardSymbol.text) { - return ( - - {standardSymbol.text} - - ) - } - } - - function addPreviewButton() { - if (showPreviewToggle && !props.pageTypeStatistic) { - return ( - - ) - } - return - } - - function toggleDraft() { - setFetchUnPublished(!fetchUnPublished) - setTable(!fetchUnPublished && props.draftExist ? props.tableDraft : props.table) - } - - function addPreviewInfo() { - if (props.showPreviewDraft) { - if (fetchUnPublished && props.draftExist) { - return Tallene i tabellen nedenfor er upublisert - } else if (fetchUnPublished && !props.draftExist) { - return Finnes ikke upubliserte tall for denne tabellen - } - } - return - } - - function renderSources() { - const { sources, sourceLabel, sourceListTables, sourceTableLabel, statBankWebUrl } = props - - if ((sourceListTables && sourceListTables.length > 0) || (sources && sources.length > 0)) { - return ( -
-
- - {sourceLabel} - -
- {sourceListTables.map((tableId, index) => { - return ( -
- - {sourceTableLabel + ' ' + tableId} - -
- ) - })} - {sources.map((source, index) => { - if (source.url && source.urlText) { - return ( -
- - {source.urlText} - -
- ) - } - })} -
- ) - } - return null - } - - const { hiddenTitle } = props - return ( -
- {!isEmpty(table) ? ( - -
- {hiddenTitle} -
-
- {addPreviewButton()} - {addDownloadTableDropdown(false)} - {addPreviewInfo()} - {createScrollControlsDesktop()} - {createScrollControlsMobile()} -
updateTableControlsDesktop()} - ref={tableWrapperRef} - > - {createTable()} -
- {addDownloadTableDropdown(true)} - {addStandardSymbols()} - {renderSources()} -
-
- ) : ( -
-

Ingen tilknyttet Tabell

-
- )} -
- ) -} - -const tableDataShape = PropTypes.shape({ - caption: - PropTypes.string | - PropTypes.shape({ - content: PropTypes.string, - noterefs: PropTypes.string, - }), - tableClass: PropTypes.string, - thead: PropTypes.arrayOf( - PropTypes.shape({ - td: - PropTypes.array | - PropTypes.number | - PropTypes.string | - PropTypes.shape({ - rowspan: PropTypes.number, - colspan: PropTypes.number, - content: PropTypes.string, - class: PropTypes.string, - }), - th: - PropTypes.array | - PropTypes.number | - PropTypes.string | - PropTypes.shape({ - rowspan: PropTypes.number, - colspan: PropTypes.number, - content: PropTypes.string, - class: PropTypes.string, - noterefs: PropTypes.string, - }), - }) - ), - tbody: PropTypes.arrayOf( - PropTypes.shape({ - th: - PropTypes.array | - PropTypes.number | - PropTypes.string | - PropTypes.shape({ - content: PropTypes.string, - class: PropTypes.string, - noterefs: PropTypes.string, - }), - td: - PropTypes.array | - PropTypes.number | - PropTypes.string | - PropTypes.shape({ - content: PropTypes.string, - class: PropTypes.string, - }), - }) - ), - tfoot: PropTypes.shape({ - footnotes: PropTypes.arrayOf( - PropTypes.shape({ - noteid: PropTypes.string, - content: PropTypes.string, - }) - ), - correctionNotice: PropTypes.string, - }), - language: PropTypes.string, - noteRefs: PropTypes.arrayOf(PropTypes.string), -}) - -Table.propTypes = { - downloadTableLabel: PropTypes.string, - downloadTableTitle: PropTypes.object, - downloadTableOptions: PropTypes.arrayOf( - PropTypes.shape({ - title: PropTypes.string, - id: PropTypes.string, - }) - ), - standardSymbol: PropTypes.shape({ - href: PropTypes.string, - text: PropTypes.string, - }), - sourceLabel: PropTypes.string, - sources: PropTypes.arrayOf( - PropTypes.shape({ - urlText: PropTypes.string, - url: PropTypes.string, - }) - ), - iconUrl: PropTypes.string, - table: tableDataShape, - tableDraft: tableDataShape, - showPreviewDraft: PropTypes.bool, - paramShowDraft: PropTypes.bool, - draftExist: PropTypes.bool, - pageTypeStatistic: PropTypes.bool, - sourceListTables: PropTypes.arrayOf(PropTypes.string), - sourceTableLabel: PropTypes.string, - statBankWebUrl: PropTypes.string, - hiddenTitle: PropTypes.string, - GA_TRACKING_ID: PropTypes.string, -} +import React from 'react' +import Table from '../table/Table' export default (props) => diff --git a/src/main/resources/react4xp/_entries/Variables.jsx b/src/main/resources/react4xp/_entries/Variables.jsx new file mode 100644 index 000000000..96907db93 --- /dev/null +++ b/src/main/resources/react4xp/_entries/Variables.jsx @@ -0,0 +1,4 @@ +import React from 'react' +import Variables from '../variables/Variables' + +export default (props) => diff --git a/src/main/resources/react4xp/accordion/Accordion.jsx b/src/main/resources/react4xp/accordion/Accordion.jsx new file mode 100644 index 000000000..63d969c63 --- /dev/null +++ b/src/main/resources/react4xp/accordion/Accordion.jsx @@ -0,0 +1,67 @@ +import React from 'react' +import { Accordion as AccordionComponent, NestedAccordion } from '@statisticsnorway/ssb-component-library' + +import PropTypes from 'prop-types' + +class Accordion extends React.Component { + renderNestedAccordions(items) { + return items.map((item, i) => ( + + {item.body &&
} + + )) + } + + createMarkup(html) { + return { + __html: html.replace(/ /g, ' '), + } + } + + render() { + const location = window.location + const anchor = location && location.hash !== '' ? location.hash.substr(1) : undefined + + const { accordions } = this.props + + return ( +
+
+ {accordions.map((accordion, index) => ( + + + {accordion.body &&
} + {this.renderNestedAccordions(accordion.items)} + + + ))} +
+
+ ) + } +} + +Accordion.propTypes = { + accordions: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string, + open: PropTypes.string.isRequired, + subHeader: PropTypes.string, + body: PropTypes.string, + items: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + body: PropTypes.string, + }) + ), + }) + ), +} + +export default Accordion diff --git a/src/main/resources/react4xp/_entries/bestbet/BestBetForm.jsx b/src/main/resources/react4xp/bestbet/BestBetForm.jsx similarity index 97% rename from src/main/resources/react4xp/_entries/bestbet/BestBetForm.jsx rename to src/main/resources/react4xp/bestbet/BestBetForm.jsx index 25bf2089b..d3abbedf6 100644 --- a/src/main/resources/react4xp/_entries/bestbet/BestBetForm.jsx +++ b/src/main/resources/react4xp/bestbet/BestBetForm.jsx @@ -5,8 +5,8 @@ import { Input, TextArea, Dropdown, Button, Divider, Tabs, RadioGroup } from '@s import axios from 'axios' import AsyncSelect from 'react-select/async' import 'regenerator-runtime' -import { BestBetContext } from '/react4xp/_entries/bestbet/Bestbet' -import { customAsyncSelectStyles } from '/react4xp/_entries/bestbet/customAsyncSelectStyles' +import { BestBetContext } from './Bestbet' +import { customAsyncSelectStyles } from './customAsyncSelectStyles' function BestBetForm(props) { const { formState, dispatch } = useContext(BestBetContext) @@ -242,4 +242,4 @@ BestBetForm.propTypes = { handleTag: PropTypes.func, } -export default (props) => +export default BestBetForm diff --git a/src/main/resources/react4xp/_entries/bestbet/BestBetModal.jsx b/src/main/resources/react4xp/bestbet/BestBetModal.jsx similarity index 91% rename from src/main/resources/react4xp/_entries/bestbet/BestBetModal.jsx rename to src/main/resources/react4xp/bestbet/BestBetModal.jsx index 7afb65ace..1e69c4aea 100644 --- a/src/main/resources/react4xp/_entries/bestbet/BestBetModal.jsx +++ b/src/main/resources/react4xp/bestbet/BestBetModal.jsx @@ -22,4 +22,4 @@ BestBetModal.propTypes = { footer: PropTypes.node, } -export default (props) => +export default BestBetModal diff --git a/src/main/resources/react4xp/_entries/bestbet/Bestbet.jsx b/src/main/resources/react4xp/bestbet/Bestbet.jsx similarity index 98% rename from src/main/resources/react4xp/_entries/bestbet/Bestbet.jsx rename to src/main/resources/react4xp/bestbet/Bestbet.jsx index eee9c8154..6483aa0c1 100644 --- a/src/main/resources/react4xp/_entries/bestbet/Bestbet.jsx +++ b/src/main/resources/react4xp/bestbet/Bestbet.jsx @@ -3,8 +3,8 @@ import PropTypes from 'prop-types' import { Container, Row, Col } from 'react-bootstrap' import { Title, Link, Tag, Button, Divider } from '@statisticsnorway/ssb-component-library' import { XCircle, Edit, Trash, Plus } from 'react-feather' -import BestBetModal from '/react4xp/_entries/bestbet/BestBetModal' -import BestBetForm from '/react4xp/_entries/bestbet/BestBetForm' +import BestBetModal from './BestBetModal' +import BestBetForm from './BestBetForm' import axios from 'axios' export const BestBetContext = createContext() @@ -419,4 +419,4 @@ Bestbet.propTypes = { mainSubjects: PropTypes.array, } -export default (props) => +export default Bestbet diff --git a/src/main/resources/react4xp/_entries/bestbet/customAsyncSelectStyles.ts b/src/main/resources/react4xp/bestbet/customAsyncSelectStyles.ts similarity index 100% rename from src/main/resources/react4xp/_entries/bestbet/customAsyncSelectStyles.ts rename to src/main/resources/react4xp/bestbet/customAsyncSelectStyles.ts diff --git a/src/main/resources/react4xp/table/Table.jsx b/src/main/resources/react4xp/table/Table.jsx new file mode 100644 index 000000000..bd10ddb1c --- /dev/null +++ b/src/main/resources/react4xp/table/Table.jsx @@ -0,0 +1,650 @@ +import React, { useState, useRef, useEffect } from 'react' +import PropTypes from 'prop-types' +import { Dropdown, Link } from '@statisticsnorway/ssb-component-library' +import { default as isEmpty } from 'ramda/es/isEmpty' +import NumberFormat from 'react-number-format' +import { Alert, Button } from 'react-bootstrap' +import { ChevronLeft, ChevronRight } from 'react-feather' +import { addGtagForEvent } from '/react4xp/ReactGA' + +function Table(props) { + const [prevClientWidth, setPrevClientWidth] = useState(0) + const [table, setTable] = useState(props.paramShowDraft && props.draftExist ? props.tableDraft : props.table) + const [fetchUnPublished, setFetchUnPublished] = useState(props.paramShowDraft) + + const showPreviewToggle = + props.showPreviewDraft && (!props.pageTypeStatistic || (props.paramShowDraft && props.pageTypeStatistic)) + + const captionRef = useRef(null) + const tableControlsDesktopRef = useRef(null) + const tableControlsMobileRef = useRef(null) + const tableRef = useRef(null) + const tableWrapperRef = useRef(null) + + useEffect(() => { + updateTableControlsDesktop() + + const widthCheckInterval = setInterval(() => { + widthCheck() + }, 250) + window.addEventListener('resize', updateTableControlsDesktop) + return () => { + clearInterval(widthCheckInterval) + window.removeEventListener('resize', updateTableControlsDesktop) + } + }, []) + + function widthCheck() { + if (tableWrapperRef.current.clientWidth !== prevClientWidth) { + setPrevClientWidth(tableWrapperRef.current.clientWidth) + updateTableControlsDesktop() + } + } + + function updateTableControlsDesktop() { + const controls = tableControlsDesktopRef.current + const tableWrapper = tableWrapperRef.current + const left = controls.children.item(0) + const right = controls.children.item(1) + + // hide controlls if there is no scrollbar + if (tableWrapper.scrollWidth > tableWrapper.clientWidth || tableWrapper.clientWidth === 0) { + controls.classList.remove('d-none') + tableControlsMobileRef.current.classList.remove('d-none') + // disable left + if (tableWrapper.scrollLeft <= 0) { + left.classList.add('disabled') + } else { + left.classList.remove('disabled') + } + + // disable right + if (tableWrapper.scrollLeft + tableWrapper.clientWidth >= tableWrapper.scrollWidth) { + right.classList.add('disabled') + } else { + right.classList.remove('disabled') + } + + // move desktop controls to correct pos + const captionHalfHeight = captionRef.current.offsetHeight / 2 + const controlsHalfHeight = left.scrollHeight / 2 + left.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px` + right.style.marginTop = `${captionHalfHeight - controlsHalfHeight}px` + } else { + controls.classList.add('d-none') + tableControlsMobileRef.current.classList.add('d-none') + } + } + + function scrollLeft() { + tableWrapperRef.current.scrollLeft -= 100 + updateTableControlsDesktop() + } + + function scrollRight() { + tableWrapperRef.current.scrollLeft += 100 + updateTableControlsDesktop() + } + + function trimValue(value) { + if (value && typeof value === 'string') { + return value.trim() + } + return value + } + + function formatNumber(value) { + const language = props.table.language + const decimalSeparator = language === 'en' ? '.' : ',' + value = trimValue(value) + if (value) { + if (typeof value === 'number' || (typeof value === 'string' && !isNaN(value))) { + const decimals = value.toString().indexOf('.') > -1 ? value.toString().split('.')[1].length : 0 + return ( + + ) + } + } + return value + } + + function addDownloadTableDropdown(mobile) { + const { downloadTableLabel, downloadTableTitle, downloadTableOptions } = props + + if (downloadTableLabel && downloadTableTitle && downloadTableOptions) { + const downloadTable = (item) => { + if (item.id === 'downloadTableAsCSV') { + { + downloadTableAsCSV() + } + } + + if (item.id === 'downloadTableAsXLSX') { + { + downloadTableAsExcel() + } + } + } + + return ( +
+ +
+ ) + } + } + + function downloadTableAsCSV() { + if (props.GA_TRACKING_ID) { + addGtagForEvent(props.GA_TRACKING_ID, 'Lastet ned csv tabell', 'Statistikkside tabeller', 'Last ned csv tabell') + } + + if (window && window.downloadTableFile) { + window.downloadTableFile(tableRef.current, { + type: 'csv', + fileName: 'tabell', + csvSeparator: ';', + csvEnclosure: '', + tfootSelector: '', + }) + } + } + + function downloadTableAsExcel() { + if (props.GA_TRACKING_ID) { + addGtagForEvent( + props.GA_TRACKING_ID, + 'Lastet ned excell tabell', + 'Statistikkside tabeller', + 'Last ned excell tabell' + ) + } + + if (window && window.downloadTableFile) { + window.downloadTableFile(tableRef.current, { + type: 'xlsx', + fileName: 'tabell', + numbers: { + html: { + decimalMark: ',', + thousandsSeparator: ' ', + }, + output: { + decimalMark: '.', + thousandsSeparator: '', + }, + }, + }) + } + } + + function createTable() { + const { tableClass } = props.table + + return ( +
+ {addCaption()} + {table.thead.map((t, index) => { + return ( + + {addThead(index)} + {addTbody(index)} + + ) + })} + {addTFoot()} +
+ ) + } + + function addCaption() { + const { caption } = table + if (caption) { + const hasNoteRefs = typeof caption === 'object' + return ( + +
+ {hasNoteRefs ? caption.content : caption} + {hasNoteRefs ? addNoteRefs(caption.noterefs) : null} +
+ + ) + } + } + + function createScrollControlsMobile() { + return ( +
+ +
+ ) + } + + function createScrollControlsDesktop() { + return ( +
+ scrollLeft()}> + + + scrollRight()}> + + +
+ ) + } + + function addThead(index) { + return {createRowsHead(table.thead[index].tr)} + } + + function addTbody(index) { + return {createRowsBody(table.tbody[index].tr)} + } + + function renderCorrectionNotice() { + if (table.tfoot.correctionNotice) { + return ( + + {table.tfoot.correctionNotice} + + ) + } + return null + } + + function addTFoot() { + const { footnotes, correctionNotice } = table.tfoot + + const noteRefs = table.noteRefs + + if ((noteRefs && noteRefs.length > 0) || correctionNotice) { + return ( + + {noteRefs.map((note, index) => { + const current = footnotes && footnotes.find((footnote) => footnote.noteid === note) + if (current) { + return ( + + + {index + 1} + {current.content} + + + ) + } else { + return null + } + })} + {renderCorrectionNotice()} + + ) + } + return null + } + + function createRowsHead(rows) { + if (rows) { + return rows.map((row, i) => { + return {createHeaderCell(row)} + }) + } + } + + function createRowsBody(rows) { + if (rows) { + return rows.map((row, i) => { + return ( + + {createBodyTh(row)} + {createBodyTd(row)} + + ) + }) + } + } + + function createHeaderCell(row) { + return Object.keys(row).map((keyName) => { + const value = row[keyName] + if (keyName === 'th') { + return createHeadTh(value) + } else if (keyName === 'td') { + return createHeadTd(value) + } + }) + } + + function createHeadTh(value) { + return value.map((cellValue, i) => { + if (typeof cellValue === 'object') { + if (Array.isArray(cellValue)) { + // TODO: Because some values is split into array by xmlParser i have to do this, find better fix + return {cellValue.join(' ')} + } else { + return ( + + {trimValue(cellValue.content)} + {addNoteRefs(cellValue.noterefs)} + + ) + } + } else { + return ( + + {trimValue(cellValue)} + + ) + } + }) + } + + function createHeadTd(value) { + return value.map((cellValue, i) => { + if (typeof cellValue === 'object') { + return ( + + {trimValue(cellValue.content)} + {addNoteRefs(cellValue.noterefs)} + + ) + } else { + return {trimValue(cellValue)} + } + }) + } + + function createBodyTh(row) { + return Object.keys(row).map((key) => { + const value = row[key] + if (key === 'th') { + return value.map((cellValue, i) => { + if (typeof cellValue === 'object') { + return ( + + {trimValue(cellValue.content)} + {addNoteRefs(cellValue.noterefs)} + + ) + } else { + return ( + + {trimValue(cellValue)} + + ) + } + }) + } + }) + } + + function createBodyTd(row) { + return Object.keys(row).map((keyName) => { + const value = row[keyName] + if (keyName === 'td') { + return value.map((cellValue, i) => { + if (typeof cellValue === 'object') { + return ( + + {formatNumber(cellValue.content)} + + ) + } else { + return {formatNumber(cellValue)} + } + }) + } + }) + } + + function addNoteRefs(noteRefId) { + if (noteRefId) { + const noteRefs = table.noteRefs + const noteIDs = noteRefId.split(' ') + const notesToReturn = noteRefs.reduce((acc, current, index) => { + // Lag et array av indeksen til alle id-enene i footer + return noteIDs.some((element) => element === current) ? acc.concat(index) : acc + }, []) + + if (notesToReturn) { + return {notesToReturn.map((noteRef) => `${noteRef + 1} `)} + } + } else return '' + } + + function addStandardSymbols() { + const { standardSymbol } = props + + if (standardSymbol && standardSymbol.href && standardSymbol.text) { + return ( + + {standardSymbol.text} + + ) + } + } + + function addPreviewButton() { + if (showPreviewToggle && !props.pageTypeStatistic) { + return ( + + ) + } + return + } + + function toggleDraft() { + setFetchUnPublished(!fetchUnPublished) + setTable(!fetchUnPublished && props.draftExist ? props.tableDraft : props.table) + } + + function addPreviewInfo() { + if (props.showPreviewDraft) { + if (fetchUnPublished && props.draftExist) { + return Tallene i tabellen nedenfor er upublisert + } else if (fetchUnPublished && !props.draftExist) { + return Finnes ikke upubliserte tall for denne tabellen + } + } + return + } + + function renderSources() { + const { sources, sourceLabel, sourceListTables, sourceTableLabel, statBankWebUrl } = props + + if ((sourceListTables && sourceListTables.length > 0) || (sources && sources.length > 0)) { + return ( +
+
+ + {sourceLabel} + +
+ {sourceListTables.map((tableId, index) => { + return ( +
+ + {sourceTableLabel + ' ' + tableId} + +
+ ) + })} + {sources.map((source, index) => { + if (source.url && source.urlText) { + return ( +
+ + {source.urlText} + +
+ ) + } + })} +
+ ) + } + return null + } + + const { hiddenTitle } = props + return ( +
+ {!isEmpty(table) ? ( + +
+ {hiddenTitle} +
+
+ {addPreviewButton()} + {addDownloadTableDropdown(false)} + {addPreviewInfo()} + {createScrollControlsDesktop()} + {createScrollControlsMobile()} +
updateTableControlsDesktop()} + ref={tableWrapperRef} + > + {createTable()} +
+ {addDownloadTableDropdown(true)} + {addStandardSymbols()} + {renderSources()} +
+
+ ) : ( +
+

Ingen tilknyttet Tabell

+
+ )} +
+ ) +} + +const tableDataShape = PropTypes.shape({ + caption: + PropTypes.string | + PropTypes.shape({ + content: PropTypes.string, + noterefs: PropTypes.string, + }), + tableClass: PropTypes.string, + thead: PropTypes.arrayOf( + PropTypes.shape({ + td: + PropTypes.array | + PropTypes.number | + PropTypes.string | + PropTypes.shape({ + rowspan: PropTypes.number, + colspan: PropTypes.number, + content: PropTypes.string, + class: PropTypes.string, + }), + th: + PropTypes.array | + PropTypes.number | + PropTypes.string | + PropTypes.shape({ + rowspan: PropTypes.number, + colspan: PropTypes.number, + content: PropTypes.string, + class: PropTypes.string, + noterefs: PropTypes.string, + }), + }) + ), + tbody: PropTypes.arrayOf( + PropTypes.shape({ + th: + PropTypes.array | + PropTypes.number | + PropTypes.string | + PropTypes.shape({ + content: PropTypes.string, + class: PropTypes.string, + noterefs: PropTypes.string, + }), + td: + PropTypes.array | + PropTypes.number | + PropTypes.string | + PropTypes.shape({ + content: PropTypes.string, + class: PropTypes.string, + }), + }) + ), + tfoot: PropTypes.shape({ + footnotes: PropTypes.arrayOf( + PropTypes.shape({ + noteid: PropTypes.string, + content: PropTypes.string, + }) + ), + correctionNotice: PropTypes.string, + }), + language: PropTypes.string, + noteRefs: PropTypes.arrayOf(PropTypes.string), +}) + +Table.propTypes = { + downloadTableLabel: PropTypes.string, + downloadTableTitle: PropTypes.object, + downloadTableOptions: PropTypes.arrayOf( + PropTypes.shape({ + title: PropTypes.string, + id: PropTypes.string, + }) + ), + standardSymbol: PropTypes.shape({ + href: PropTypes.string, + text: PropTypes.string, + }), + sourceLabel: PropTypes.string, + sources: PropTypes.arrayOf( + PropTypes.shape({ + urlText: PropTypes.string, + url: PropTypes.string, + }) + ), + iconUrl: PropTypes.string, + table: tableDataShape, + tableDraft: tableDataShape, + showPreviewDraft: PropTypes.bool, + paramShowDraft: PropTypes.bool, + draftExist: PropTypes.bool, + pageTypeStatistic: PropTypes.bool, + sourceListTables: PropTypes.arrayOf(PropTypes.string), + sourceTableLabel: PropTypes.string, + statBankWebUrl: PropTypes.string, + hiddenTitle: PropTypes.string, + GA_TRACKING_ID: PropTypes.string, +} + +export default Table diff --git a/src/main/resources/react4xp/_entries/variables/VariableCard.jsx b/src/main/resources/react4xp/variables/VariableCard.jsx similarity index 86% rename from src/main/resources/react4xp/_entries/variables/VariableCard.jsx rename to src/main/resources/react4xp/variables/VariableCard.jsx index e20da9939..f0b7dedcf 100644 --- a/src/main/resources/react4xp/_entries/variables/VariableCard.jsx +++ b/src/main/resources/react4xp/variables/VariableCard.jsx @@ -1,6 +1,6 @@ import React from 'react' import { Card, Text } from '@statisticsnorway/ssb-component-library' -import { variableType } from '/react4xp/_entries/variables/types' +import { variableType } from './types' const VariableCard = ({ variable }) => { const { icon, description, ...rest } = variable diff --git a/src/main/resources/react4xp/_entries/variables/VariableCardsList.jsx b/src/main/resources/react4xp/variables/VariableCardsList.jsx similarity index 78% rename from src/main/resources/react4xp/_entries/variables/VariableCardsList.jsx rename to src/main/resources/react4xp/variables/VariableCardsList.jsx index 988c1ca05..68c0b1857 100644 --- a/src/main/resources/react4xp/_entries/variables/VariableCardsList.jsx +++ b/src/main/resources/react4xp/variables/VariableCardsList.jsx @@ -1,7 +1,7 @@ import React from 'react' import PropTypes from 'prop-types' -import VariableCard from '/react4xp/_entries/variables/VariableCard.jsx' -import { variableType } from '/react4xp/_entries/variables/types' +import VariableCard from './VariableCard.jsx' +import { variableType } from './types' class VariableCardsList extends React.Component { constructor(props) { diff --git a/src/main/resources/react4xp/_entries/variables/Variables.jsx b/src/main/resources/react4xp/variables/Variables.jsx similarity index 80% rename from src/main/resources/react4xp/_entries/variables/Variables.jsx rename to src/main/resources/react4xp/variables/Variables.jsx index 220e40b43..eb6dfcbf4 100644 --- a/src/main/resources/react4xp/_entries/variables/Variables.jsx +++ b/src/main/resources/react4xp/variables/Variables.jsx @@ -1,8 +1,8 @@ import React from 'react' import PropTypes from 'prop-types' import { Text } from '@statisticsnorway/ssb-component-library' -import VariableCardsList from '/react4xp/_entries/variables/VariableCardsList.jsx' -import { variableType } from '/react4xp/_entries/variables/types' +import VariableCardsList from './VariableCardsList.jsx' +import { variableType } from './types' export const DISPLAY_TYPE_CARDS = 'CARDS' export const DISPLAY_TYPE_TABLE = 'TABLE' diff --git a/src/main/resources/react4xp/_entries/variables/Variables.md b/src/main/resources/react4xp/variables/Variables.md similarity index 100% rename from src/main/resources/react4xp/_entries/variables/Variables.md rename to src/main/resources/react4xp/variables/Variables.md diff --git a/src/main/resources/react4xp/_entries/variables/types.ts b/src/main/resources/react4xp/variables/types.ts similarity index 100% rename from src/main/resources/react4xp/_entries/variables/types.ts rename to src/main/resources/react4xp/variables/types.ts diff --git a/src/main/resources/site/parts/accordion/accordion.ts b/src/main/resources/site/parts/accordion/accordion.ts index bbecb0bdf..155b5bd0f 100644 --- a/src/main/resources/site/parts/accordion/accordion.ts +++ b/src/main/resources/site/parts/accordion/accordion.ts @@ -9,7 +9,6 @@ const { } = __non_webpack_require__('/lib/util') const { sanitize } = __non_webpack_require__('/lib/xp/common') const { renderError } = __non_webpack_require__('/lib/ssb/error/error') -const { isEnabled } = __non_webpack_require__('/lib/featureToggle') export function get(req: XP.Request): XP.Response { try { @@ -38,7 +37,6 @@ export function preview(req: XP.Request, accordionIds: Array | string): function renderPart(req: XP.Request, accordionIds: Array) { const accordions: Array = [] - const csrOnTableAccordion: boolean = isEnabled('csr-on-table-accordion', false, 'ssb') accordionIds.map((key) => { const accordion: Content | null = key @@ -93,7 +91,7 @@ function renderPart(req: XP.Request, accordionIds: Array) { accordions, } - return render('Accordion', props, req, { ssr: !csrOnTableAccordion }) + return render('Accordion', props, req) } export interface AccordionData { diff --git a/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx b/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx index 331e69ec6..72aad846d 100644 --- a/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx +++ b/src/main/resources/site/parts/omStatistikken/omStatistikken.jsx @@ -1,6 +1,6 @@ import React from 'react' import PropTypes from 'prop-types' -import Accordion from '/react4xp/_entries/Accordion' +import Accordion from '/react4xp/accordion/Accordion' const OmStatistikken = (props) => { const { ingress, label, accordions } = props diff --git a/src/main/resources/site/parts/table/table.ts b/src/main/resources/site/parts/table/table.ts index 7a92f0f2c..7202ae95b 100644 --- a/src/main/resources/site/parts/table/table.ts +++ b/src/main/resources/site/parts/table/table.ts @@ -23,7 +23,6 @@ const { const { getLanguage, getPhrases } = __non_webpack_require__('/lib/ssb/utils/language') const { DATASET_BRANCH, UNPUBLISHED_DATASET_BRANCH } = __non_webpack_require__('/lib/ssb/repo/dataset') const { hasWritePermissionsAndPreview } = __non_webpack_require__('/lib/ssb/parts/permissions') -const { isEnabled } = __non_webpack_require__('/lib/featureToggle') const view = resolve('./table.html') @@ -140,7 +139,6 @@ function renderPart(req: XP.Request, tableId?: string): XP.Response { if (!page) throw Error('No page found') const phrases: Phrases = getPhrases(page) as Phrases - const csrOnTableAccordion: boolean = isEnabled('csr-on-table-accordion', false, 'ssb') if (!tableId) { if (req.mode === 'edit' && page.type !== `${app.name}:statistics`) { @@ -160,7 +158,6 @@ function renderPart(req: XP.Request, tableId?: string): XP.Response { pageContributions: { bodyEnd: [scriptAsset('js/tableExport.js')], }, - ssr: !csrOnTableAccordion, }) } diff --git a/src/main/resources/site/parts/variables/variables.ts b/src/main/resources/site/parts/variables/variables.ts index 53cde17c2..b3fdd8452 100644 --- a/src/main/resources/site/parts/variables/variables.ts +++ b/src/main/resources/site/parts/variables/variables.ts @@ -47,7 +47,7 @@ function renderVariables(req: XP.Request, variables: Array): XP.Respo }) return render( - 'variables/Variables', + 'Variables', { variables: variables.map(({ title, description, fileHref, fileModifiedDate, href }) => { return {