diff --git a/src/components/FileAttachmentPanel/FileAttachmentPanel.stories.js b/src/components/FileAttachmentPanel/FileAttachmentPanel.stories.js
new file mode 100644
index 0000000000..ef08c4250a
--- /dev/null
+++ b/src/components/FileAttachmentPanel/FileAttachmentPanel.stories.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import FileAttachmentPanelComponent from './FileAttachmentPanel';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import Panel from 'components/Panel';
+
+export default {
+ title: 'ModularComponents/FileAttachmentPanel',
+ component: FileAttachmentPanelComponent
+};
+
+const initialState = {
+ viewer: {
+ openElements: {
+ panel: true,
+ },
+ disabledElements: {},
+ customElementOverrides: {},
+ tab: {},
+ panelWidths: { panel: 300 },
+ modularHeaders: {},
+ }
+};
+
+export function FileAttachmentPanelLeftEmpty() {
+ return (
+
initialState })}>
+
+
+
+
+ );
+}
+
+export function FileAttachmentPanelRightEmpty() {
+ return (
+
initialState })}>
+
+
+
+
+ );
+}
+
+const filesMock = {
+ embeddedFiles: [
+ { filename: '1.png' },
+ { filename: '15pages.pdf' },
+ ],
+ fileAttachmentAnnotations: [],
+};
+filesMock.fileAttachmentAnnotations[1] = [{ PageNumber: 1, filename: '2.png' }];
+filesMock.fileAttachmentAnnotations[5] = [{ PageNumber: 5, filename: 'signature.png' }];
+filesMock.fileAttachmentAnnotations[8] = [{ PageNumber: 8, filename: 'q.jpeg' }];
+
+export function FileAttachmentPanelLeftWithFiles() {
+ return (
+
initialState })}>
+
+
+
+
+ );
+}
+
+export function FileAttachmentPanelRightWithFiles() {
+ return (
+
initialState })}>
+
+
+
+
+ );
+}
diff --git a/src/components/FilePicker/FilePicker.js b/src/components/FilePicker/FilePicker.js
new file mode 100644
index 0000000000..1dad49a155
--- /dev/null
+++ b/src/components/FilePicker/FilePicker.js
@@ -0,0 +1,125 @@
+import React, { useState, useRef } from 'react';
+import { useTranslation } from 'react-i18next';
+import Icon from 'components/Icon';
+import classNames from 'classnames';
+import { isMobile } from 'helpers/device';
+
+import './FilePicker.scss';
+
+const FilePicker = ({
+ onChange = () => { },
+ onDrop = () => { },
+ shouldShowIcon = false,
+ acceptFormats,
+ allowMultiple = false,
+ errorMessage = ''
+}) => {
+ const [t] = useTranslation();
+ const [isDragging, setIsDragging] = useState(false);
+ const fileInputRef = useRef(null);
+
+ const onClick = () => {
+ fileInputRef?.current?.click();
+ };
+
+ const onKeyDown = (event) => {
+ if (event.key === 'Enter') {
+ onClick();
+ }
+ };
+
+ const handleChange = (e) => {
+ const files = e.target.files;
+ files.length > 0 && onChange(Array.from(files));
+ };
+
+ const handleDragEnter = (e) => {
+ e.preventDefault();
+ setIsDragging(true);
+ };
+
+ const handleDragOver = (e) => {
+ e.preventDefault();
+ };
+
+ const handleDragLeave = (e) => {
+ e.preventDefault();
+
+ if (!e.target.parentNode.contains(e.relatedTarget)) {
+ setIsDragging(false);
+ }
+ };
+
+ const handleDragExit = (e) => {
+ e.preventDefault();
+ setIsDragging(false);
+ };
+
+ const handleFileDrop = async (e) => {
+ e.preventDefault();
+ setIsDragging(false);
+ const { files } = e.dataTransfer;
+ files.length > 0 && onDrop(Array.from(files));
+ };
+
+ const renderPrompt = () => {
+ if (isMobile()) {
+ return (
+
+ {t('filePicker.selectFile')}
+
+ );
+ }
+ return (
+ <>
+
+ {t('filePicker.dragAndDrop')}
+
+
+ {t('filePicker.or')}
+
+ >
+ );
+ };
+
+ return (
+
+
+
+ {shouldShowIcon &&
}
+ {renderPrompt()}
+
{t('action.browse')}
+ {
+ handleChange(event);
+ event.target.value = null;
+ }}
+ />
+
+
+ {errorMessage && (
+
{errorMessage}
+ )}
+
+
+ );
+};
+
+export default FilePicker;
diff --git a/src/components/FilePicker/FilePicker.scss b/src/components/FilePicker/FilePicker.scss
new file mode 100644
index 0000000000..c0bb05c3c7
--- /dev/null
+++ b/src/components/FilePicker/FilePicker.scss
@@ -0,0 +1,74 @@
+@import '../../constants/styles';
+@import '../../constants/modal';
+
+.file-picker-component {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ border-radius: 4px;
+
+ .file-picker-container {
+ position: relative;
+ border: 1px dashed var(--modal-stroke-and-border);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+
+ &.dragging {
+ background: var(--file-picker-drop-background);
+ border: 1px dashed var(--file-picker-drop-border);
+ }
+
+ .file-picker-body {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ position: absolute;
+
+ .label-separator {
+ margin: 10px;
+ }
+
+ color: var(--faded-text);
+
+ .modal-btn-file {
+ border-radius: 4px;
+ border: 1px solid var(--primary-button);
+ color: var(--primary-button);
+ padding-top: 2px;
+ padding-right: 20px;
+ padding-bottom: 4px;
+ padding-left: 20px;
+ cursor: pointer;
+ }
+
+ .Icon {
+ width: fit-content;
+ height: fit-content;
+ margin-bottom: 15px;
+
+ svg {
+ height: 45px;
+ }
+ }
+
+ .file-picker-separator {
+ margin: 10px;
+ }
+ }
+
+ .file-picker-error {
+ position: absolute;
+ color: red;
+ bottom: 0px;
+ right: 0px;
+ margin: 0px 5px 5px 0px;
+ }
+ }
+}
diff --git a/src/components/FilePicker/index.js b/src/components/FilePicker/index.js
new file mode 100644
index 0000000000..e6a188ce94
--- /dev/null
+++ b/src/components/FilePicker/index.js
@@ -0,0 +1,3 @@
+import FilePicker from './FilePicker';
+
+export default FilePicker;
diff --git a/src/components/FilterAnnotModal/FilterAnnotModal.spec.js b/src/components/FilterAnnotModal/FilterAnnotModal.spec.js
new file mode 100644
index 0000000000..718fde7f65
--- /dev/null
+++ b/src/components/FilterAnnotModal/FilterAnnotModal.spec.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { UserPanel, ColorPanel, TypePanel, DocumentFilterActive } from './FilterAnnotModal.stories';
+
+const noop = () => { };
+
+jest.mock('core', () => ({
+ getDisplayAuthor: () => 'Test Author',
+ addEventListener: noop,
+ removeEventListener: noop,
+ getAnnotationsList: () => [],
+ getDocumentViewers: () => [{
+ getAnnotationManager: () => ({
+ getAnnotationsList: () => []
+ })
+ }],
+}));
+
+const UserPanelFilterAnnotModalStory = withI18n(UserPanel);
+const ColorPanelFilterAnnotModalStory = withI18n(ColorPanel);
+const TypePanelFilterAnnotModalStory = withI18n(TypePanel);
+const DocumentFilterActiveStory = withI18n(DocumentFilterActive);
+
+describe('FilterAnnotModal', () => {
+ it('Stories should not throw any errors', () => {
+ expect(() => {
+ render(
);
+ render(
);
+ render(
);
+ render(
);
+ }).not.toThrow();
+ });
+});
\ No newline at end of file
diff --git a/src/components/FontSizeDropdown/FontSizeDropdown.scss b/src/components/FontSizeDropdown/FontSizeDropdown.scss
index 96c8159abe..e42ad93c0c 100644
--- a/src/components/FontSizeDropdown/FontSizeDropdown.scss
+++ b/src/components/FontSizeDropdown/FontSizeDropdown.scss
@@ -21,11 +21,15 @@
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-
&.error {
border-color: var(--error-border-color);
}
}
+
+ .disabledText {
+ color: var(--disabled-text);
+ }
+
// To remove arrows from input
/* Chrome, Safari, Edge, Opera */
input::-webkit-outer-spin-button,
diff --git a/src/components/FontSizeDropdown/pdfEditHelper.js b/src/components/FontSizeDropdown/pdfEditHelper.js
new file mode 100644
index 0000000000..585a982193
--- /dev/null
+++ b/src/components/FontSizeDropdown/pdfEditHelper.js
@@ -0,0 +1,111 @@
+const SELECTION_BACKGROUND = '#50A5F5FE';
+let range;
+let docViewer;
+let inputElement;
+
+/**
+ * @ignore
+ * Helper function to keep the highlight of the selected text in the text edit box before the elemnt focus is changed.
+ */
+export function keepTextEditSelectionOnInputFocus(core) {
+ inputElement = document.activeElement;
+ docViewer = core.getDocumentViewer();
+ // When the input is still in focus but we changed page, we need to un-focus the input.
+ docViewer.addEventListener('pageNumberUpdated', handlePageChange, { once: true });
+ // When we click anywhere other than the input field itself, it should unfocus.
+ document.addEventListener('mousedown', handleClick);
+
+ const currentRange = window.getSelection().getRangeAt(0);
+ const isFocusOutsideTextBox = currentRange.startContainer.nodeName === 'DIV';
+ // Component re-renders when we focus into the input field because it is a dropdown/input combo.
+ // In that case the focus is already shifted out from text edit box and this function is executed again
+ // due to re-render. The selection will no longer include the text nodes we initially selected
+ // in the text edit box.
+ if (isFocusOutsideTextBox) {
+ return;
+ }
+
+ // When we have nothing selected, simply return
+ const isEmptySelection = currentRange.startContainer === currentRange.endContainer && currentRange.startOffset === currentRange.endOffset;
+ if (isEmptySelection) {
+ return;
+ }
+
+ // When the color / font style of the selected text was changed at least once before changing
+ // the font size with input field, the selection we get from `getSelection` would have been modified
+ // by worker API. In this case, we need to reinitialize the range so that the range object returns to its initial state.
+ const isRangeModifiedByWorkerAPI = currentRange.startContainer.nodeName === 'SPAN';
+ if (isRangeModifiedByWorkerAPI) {
+ range = reinitializeRange(currentRange);
+ } else {
+ range = currentRange;
+ }
+
+ toggleSelectionHighlight(range.startContainer, range.endContainer);
+}
+
+/**
+ * @ignore
+ * Helper function to restore the text edit box selection
+ */
+export function restoreSelection() {
+ if (!range) {
+ return;
+ }
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+ range = null;
+ docViewer.removeEventListener('pageNumberUpdated', handlePageChange);
+ document.removeEventListener('mousedown', handleClick);
+}
+
+/**
+ * @ignore
+ * Toggles the background color of the selected elements recursively.
+ * @param {Text} startTextNode
+ * @param {Text} endTextNode
+ */
+function toggleSelectionHighlight(startTextNode, endTextNode) {
+ const startElement = startTextNode.parentElement;
+ const selectionEnd = !startElement.nextElementSibling && startElement.parentElement.nextElementSibling === endTextNode;
+ if (selectionEnd) {
+ return;
+ }
+ startElement.style.background = SELECTION_BACKGROUND;
+ const highlighNextCharacterSameLine = startElement.nextElementSibling?.tagName === 'SPAN' && startTextNode !== endTextNode;
+ const highlighNextCharacterNextLine = startElement.nextElementSibling === null && startElement.parentElement.nextElementSibling;
+ if (highlighNextCharacterSameLine) {
+ // When the next character in selection is in the same line of current character, we simply pass that in
+ toggleSelectionHighlight(startElement.nextElementSibling.firstChild, endTextNode);
+ } else if (highlighNextCharacterNextLine) {
+ // when the next character in selection is in the next line, we need to go into the next line and continue our recursion
+ toggleSelectionHighlight(startElement.parentElement.nextElementSibling.firstElementChild.firstChild, endTextNode);
+ }
+}
+
+function reinitializeRange(workerAPIEditedRange) {
+ const selection = window.getSelection();
+ selection.removeAllRanges();
+ const startNode = workerAPIEditedRange.startContainer.firstChild;
+ const endNode = workerAPIEditedRange.endContainer.previousSibling.firstChild;
+ workerAPIEditedRange.setStart(startNode, 0);
+ workerAPIEditedRange.setEnd(endNode, 1);
+ return workerAPIEditedRange;
+}
+
+function handlePageChange() {
+ if (document.activeElement?.tagName === 'INPUT') {
+ document.activeElement.blur();
+ }
+ docViewer.removeEventListener('pageNumberUpdated', handlePageChange);
+ document.removeEventListener('mousedown', handleClick);
+}
+
+function handleClick(e) {
+ const isClickingFontSizeInput = e.target === inputElement;
+ if (!isClickingFontSizeInput) {
+ document.activeElement.blur();
+ document.removeEventListener('mousedown', handleClick);
+ }
+}
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldEditPopupIndicator/FormFieldEditPopupIndicator.js b/src/components/FormFieldEditPopup/FormFieldEditPopupIndicator/FormFieldEditPopupIndicator.js
new file mode 100644
index 0000000000..453917ec9c
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditPopupIndicator/FormFieldEditPopupIndicator.js
@@ -0,0 +1,37 @@
+import React from 'react';
+import { Choice, Input } from '@pdftron/webviewer-react-toolkit';
+import { useTranslation } from 'react-i18next';
+
+
+const FormFieldEditPopupIndicator = ({ indicator, indicatorPlaceholder }) => {
+ const { t } = useTranslation();
+ const onIndicatorChange = (showIndicator) => {
+ if (indicator.value.length < 1 && showIndicator) {
+ indicator.onChange(indicatorPlaceholder);
+ }
+ indicator.toggleIndicator(showIndicator);
+ };
+
+ return (
+
+
{t('formField.formFieldPopup.fieldIndicator')}
+
onIndicatorChange(event.target.checked)}
+ label={t(indicator.label)}
+ />
+
+ indicator.onChange(event.target.value)}
+ value={indicator.value}
+ fillWidth="false"
+ placeholder={indicatorPlaceholder}
+ disabled={!indicator.isChecked}
+ />
+
+
+ );
+};
+
+export default FormFieldEditPopupIndicator;
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldEditPopupIndicator/index.js b/src/components/FormFieldEditPopup/FormFieldEditPopupIndicator/index.js
new file mode 100644
index 0000000000..da4b611b59
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditPopupIndicator/index.js
@@ -0,0 +1,3 @@
+import FormFieldEditPopupIndicator from './FormFieldEditPopupIndicator';
+
+export default FormFieldEditPopupIndicator;
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/FormFieldEditSignaturePopup.stories.js b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/FormFieldEditSignaturePopup.stories.js
new file mode 100644
index 0000000000..b09af8cdab
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/FormFieldEditSignaturePopup.stories.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import FormFieldEditSignaturePopup from './FormFieldEditSignaturePopup';
+import { configureStore } from '@reduxjs/toolkit';
+
+
+import { Provider } from 'react-redux';
+
+
+export default {
+ title: 'Components/FormFieldEditPopup',
+ component: FormFieldEditSignaturePopup,
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ }
+};
+
+const store = configureStore({ reducer: () => initialState });
+
+const annotation = {
+ Width: 100,
+ Height: 100,
+};
+
+export function SignatureFieldPopup() {
+ const signatureFields = [
+ {
+ label: 'formField.formFieldPopup.fieldName',
+ onChange: () => { },
+ value: 'SignatureField1',
+ required: true,
+ type: 'text',
+ },
+ ];
+
+ const signatureFlags = [
+ {
+ label: 'formField.formFieldPopup.readOnly',
+ onChange: () => { },
+ isChecked: true,
+ },
+ {
+ label: 'formField.formFieldPopup.required',
+ onChange: () => { },
+ isChecked: true,
+ }
+ ];
+
+ const indicator = {
+ label: 'formField.formFieldPopup.documentFieldIndicator',
+ toggleIndicator: () => { },
+ isChecked: true,
+ onChange: () => { },
+ value: 'This is an indicator'
+ };
+
+ const props = {
+ fields: signatureFields,
+ flags: signatureFlags,
+ annotation,
+ isValid: true,
+ getSignatureOptionHandler: () => 'initialsSignature',
+ indicator,
+ };
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/SignatureOptionsDropdown.js b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/SignatureOptionsDropdown.js
new file mode 100644
index 0000000000..dd643496b8
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/SignatureOptionsDropdown.js
@@ -0,0 +1,82 @@
+import React, { useState } from 'react';
+import Select from 'react-select';
+import { useTranslation } from 'react-i18next';
+import DataElementWrapper from 'components/DataElementWrapper';
+import SignatureModes from 'constants/signatureModes';
+import ReactSelectCustomArrowIndicator from 'components/ReactSelectCustomArrowIndicator';
+import ReactSelectWebComponentProvider from 'src/components/ReactSelectWebComponentProvider';
+
+import './SignatureOptionsDropdown.scss';
+
+const getStyles = () => ({
+ control: (provided) => ({
+ ...provided,
+ minHeight: '28px',
+ backgroundColor: 'var(--component-background)',
+ borderColor: 'hsl(0, 0%, 80%)',
+ boxShadow: null,
+ '&:hover': null,
+ }),
+ valueContainer: (provided) => ({
+ ...provided,
+ padding: '2px',
+ }),
+ singleValue: (provided) => ({
+ ...provided,
+ color: 'var(--text-color)',
+ }),
+ menu: (provided) => ({
+ ...provided,
+ backgroundColor: 'var(--component-background)',
+ }),
+ option: (provided) => ({
+ ...provided,
+ backgroundColor: 'var(--component-background)',
+ color: 'var(--text-color)',
+ '&:hover': {
+ backgroundColor: 'var(--popup-button-hover)',
+ }
+ }),
+ indicatorsContainer: (provided) => ({
+ ...provided,
+ paddingRight: '6px',
+ height: '26px',
+ }),
+});
+
+const SignatureOptionsDropdown = (props) => {
+ const { onChangeHandler, initialOption } = props;
+ const { t } = useTranslation();
+ const styles = getStyles();
+ const signatureOptions = [
+ { value: SignatureModes.FULL_SIGNATURE, label: t('formField.types.signature') },
+ { value: SignatureModes.INITIALS, label: t('option.type.initials') },
+ ];
+
+ const init = signatureOptions.find((option) => option.value === initialOption);
+ const [value, setValue] = useState(init);
+
+ const onChange = (option) => {
+ setValue(option);
+ onChangeHandler(option);
+ };
+
+ return (
+
+
+
+
+
+
+ );
+};
+
+export default SignatureOptionsDropdown;
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/SignatureOptionsDropdown.scss b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/SignatureOptionsDropdown.scss
new file mode 100644
index 0000000000..07d5aab5ef
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/SignatureOptionsDropdown.scss
@@ -0,0 +1,17 @@
+.signature-options-container {
+ padding: 5px 0px 5px 0px;
+ display: grid;
+ grid-template-columns: 0.5fr 1fr;
+ grid-template-areas: "label dropdown";
+ align-items: center;
+
+ label {
+ grid-area: label;
+ }
+}
+
+.arrow {
+ width: 12px;
+ height: 16px;
+ margin-top: 2px;
+}
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/index.js b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/index.js
new file mode 100644
index 0000000000..6d7db923f7
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/SignatureOptionsDropdown/index.js
@@ -0,0 +1,3 @@
+import SignatureOptionsDropdown from './SignatureOptionsDropdown';
+
+export default SignatureOptionsDropdown;
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/index.js b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/index.js
new file mode 100644
index 0000000000..7ef2728376
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldEditSignaturePopup/index.js
@@ -0,0 +1,3 @@
+import FormFieldEditSignaturePopup from './FormFieldEditSignaturePopup';
+
+export default FormFieldEditSignaturePopup;
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldPopupDimensionsInput/FormFieldPopupDimensionsInput.js b/src/components/FormFieldEditPopup/FormFieldPopupDimensionsInput/FormFieldPopupDimensionsInput.js
new file mode 100644
index 0000000000..a98434ef19
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldPopupDimensionsInput/FormFieldPopupDimensionsInput.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+const FormFieldPopupDimensionsInput = ({ width, height, onWidthChange, onHeightChange }) => {
+ const { t } = useTranslation();
+
+ return (
+
+
{t('formField.formFieldPopup.size')}:
+
+ onWidthChange(e.target.value)}
+ /> {t('formField.formFieldPopup.width')}
+
+
+ onHeightChange(e.target.value)}
+ /> {t('formField.formFieldPopup.height')}
+
+
+ );
+};
+
+export default FormFieldPopupDimensionsInput;
\ No newline at end of file
diff --git a/src/components/FormFieldEditPopup/FormFieldPopupDimensionsInput/index.js b/src/components/FormFieldEditPopup/FormFieldPopupDimensionsInput/index.js
new file mode 100644
index 0000000000..928474c06d
--- /dev/null
+++ b/src/components/FormFieldEditPopup/FormFieldPopupDimensionsInput/index.js
@@ -0,0 +1,3 @@
+import FormFieldPopupDimensionsInput from './FormFieldPopupDimensionsInput';
+
+export default FormFieldPopupDimensionsInput;
\ No newline at end of file
diff --git a/src/components/FormFieldIndicator/FormFieldIndicator.scss b/src/components/FormFieldIndicator/FormFieldIndicator.scss
new file mode 100644
index 0000000000..64ef8fad85
--- /dev/null
+++ b/src/components/FormFieldIndicator/FormFieldIndicator.scss
@@ -0,0 +1,60 @@
+@import '../../constants/modal';
+@import '../../constants/popup';
+
+@media print {
+ #form-field-indicator-wrapper {
+ opacity: 0;
+ }
+}
+
+#form-field-indicator-wrapper {
+ position: relative;
+ z-index: min($modal-z-index, $popup-z-index) - 10;
+}
+
+.formFieldIndicator {
+ background-color: var(--color-blue-gray-5);
+ color: var(--color-gray-1);
+ width: 98px;
+ min-height: 32px;
+ font-size: 13px;
+ position: fixed;
+ padding: 4px 0px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-family: 'Lato', sans-serif;
+
+ .formFieldIndicator-text {
+ margin: 0;
+ position: absolute;
+ padding: 0 8px;
+ }
+}
+
+.formFieldIndicator::before {
+ content: '';
+ display: block;
+ position: absolute;
+ right: -20px;
+ bottom: 0;
+ width: 0;
+ height: 0;
+ border-left: 20px solid var(--color-blue-gray-5);
+ border-top: 20px solid transparent;
+ border-bottom: 20px solid transparent;
+}
+
+.formFieldIndicator.rightSidePage::before {
+ content: '';
+ display: block;
+ position: absolute;
+ left: -20px;
+ bottom: 0;
+ width: 0;
+ height: 0;
+ border-right: 20px solid var(--color-blue-gray-5);
+ border-left: 0;
+ border-top: 20px solid transparent;
+ border-bottom: 20px solid transparent;
+}
\ No newline at end of file
diff --git a/src/components/FormFieldIndicator/FormFieldIndicatorContainer.js b/src/components/FormFieldIndicator/FormFieldIndicatorContainer.js
new file mode 100644
index 0000000000..4aa3d6634e
--- /dev/null
+++ b/src/components/FormFieldIndicator/FormFieldIndicatorContainer.js
@@ -0,0 +1,113 @@
+import React, { useEffect, useState } from 'react';
+import { useSelector } from 'react-redux';
+import debounce from 'lodash/debounce';
+import useOnFormFieldsChanged from '../../hooks/useOnFormFieldsChanged';
+import core from 'core';
+import selectors from 'selectors';
+import FormFieldIndicator from './FormFieldIndicator';
+import './FormFieldIndicator.scss';
+import DataElements from 'src/constants/dataElement';
+import getRootNode from 'helpers/getRootNode';
+import { createPortal } from 'react-dom';
+
+const FormFieldIndicatorContainer = () => {
+ const [
+ isOpen,
+ isDisabled,
+ documentContainerWidth,
+ documentContainerHeight,
+ leftPanelWidth,
+ notePanelWidth,
+ ] = useSelector((state) => [
+ selectors.isElementOpen(state, DataElements['FORM_FIELD_INDICATOR_CONTAINER']),
+ selectors.isElementDisabled(state, DataElements['FORM_FIELD_INDICATOR_CONTAINER']),
+ selectors.getDocumentContainerWidth(state),
+ selectors.getDocumentContainerHeight(state),
+ selectors.getLeftPanelWidth(state),
+ selectors.getDocumentContentContainerWidthStyle(state),
+ selectors.getNotesPanelWidth(state),
+ ]);
+ const formFieldAnnotationsList = useOnFormFieldsChanged();
+ const [indicators, setIndicators] = useState([]);
+
+ const getIndicators = () => {
+ if (!core.getDocument()) {
+ return [];
+ }
+ return formFieldAnnotationsList
+ .filter((fieldAnnotation) => {
+ return fieldAnnotation.getCustomData('trn-form-field-show-indicator') === 'true';
+ }).map((fieldAnnotation) => {
+ return createFormFieldIndicator(fieldAnnotation);
+ });
+ };
+
+ const resetIndicators = () => {
+ setIndicators([]);
+ };
+
+ useEffect(() => {
+ core.addEventListener('documentUnloaded', resetIndicators);
+ return () => {
+ core.removeEventListener('documentUnloaded', resetIndicators);
+ };
+ }, []);
+
+
+ useEffect(() => {
+ setIndicators(getIndicators());
+
+ const onScroll = debounce(() => {
+ if (isOpen && !isDisabled) {
+ setIndicators(getIndicators());
+ }
+ }, 0);
+
+ const scrollViewElement = core.getScrollViewElement();
+
+ scrollViewElement?.addEventListener('scroll', onScroll);
+ return () => {
+ scrollViewElement?.removeEventListener('scroll', onScroll);
+ };
+ }, [
+ formFieldAnnotationsList,
+ isOpen,
+ isDisabled,
+ documentContainerWidth,
+ documentContainerHeight,
+ leftPanelWidth,
+ notePanelWidth,
+ ]);
+
+ const createFormFieldIndicator = (annotation) => {
+ const { scrollLeft, scrollTop } = core.getScrollViewElement();
+ const payload = {
+ displayMode: core.getDocumentViewer().getDisplayModeManager().getDisplayMode(),
+ viewerBoundingRect: core.getViewerElement().getBoundingClientRect(),
+ appBoundingRect: getRootNode().getElementById('app').getBoundingClientRect(),
+ scrollLeft: scrollLeft,
+ scrollTop: scrollTop,
+ };
+ return (
);
+ };
+
+ if (isOpen && !isDisabled) {
+ return (<>
+ {
+ createPortal(
, (window.isApryseWebViewerWebComponent)
+ ? getRootNode().getElementById('app') : document.body)
+ }
+ >);
+ }
+
+ return null;
+};
+
+export default FormFieldIndicatorContainer;
diff --git a/src/components/FormFieldIndicator/index.js b/src/components/FormFieldIndicator/index.js
new file mode 100644
index 0000000000..33db0c2d60
--- /dev/null
+++ b/src/components/FormFieldIndicator/index.js
@@ -0,0 +1,3 @@
+import FormFieldIndicator from './FormFieldIndicatorContainer';
+
+export default FormFieldIndicator;
diff --git a/src/components/InlineCommentingPopup/InlineCommentingPopup.scss b/src/components/InlineCommentingPopup/InlineCommentingPopup.scss
new file mode 100644
index 0000000000..cdfb46b88f
--- /dev/null
+++ b/src/components/InlineCommentingPopup/InlineCommentingPopup.scss
@@ -0,0 +1,171 @@
+@import '../../constants/styles.scss';
+@import '../../constants/popup';
+
+.InlineCommentingPopup {
+ @extend %popup;
+ border-radius: 4px;
+ box-shadow: 0 0 3px 0 var(--document-box-shadow);
+ background: var(--component-background);
+ align-items: flex-start;
+
+ @include mobile {
+ position: fixed;
+ left: 0;
+ bottom: 0;
+ z-index: 100;
+ flex-direction: column;
+ justify-content: flex-end;
+ width: 100%;
+ background: var(--modal-negative-space);
+ }
+
+ @include tablet-and-desktop {
+ overflow: auto;
+ max-height: calc(100% - 100px);
+ }
+
+ .inline-comment-container {
+ display: flex;
+ flex-direction: column;
+
+ @include mobile {
+ flex-basis: auto;
+ width: 100%;
+ max-height: 40%;
+ background: var(--component-background);
+ box-shadow: 0 0 3px 0 var(--document-box-shadow);
+ border-radius: 4px 4px 0px 0px;
+ }
+
+ @include tablet-and-desktop {
+ max-width: 260px;
+ }
+
+ &.expanded {
+ @include mobile {
+ flex-grow: 1;
+ max-height: 90%;
+ }
+ }
+ }
+
+ .Note {
+ border-radius: 0;
+ background: none;
+ margin: 0;
+ cursor: default;
+
+ @include mobile {
+ flex-grow: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: auto;
+ box-shadow: 0 0 3px 0 var(--document-box-shadow);
+ }
+
+ @include tablet-and-desktop {
+ box-shadow: none;
+ }
+
+ &>div:not(:nth-last-child(2)) {
+ @include mobile {
+ flex-grow: 0;
+ }
+ }
+
+ &>div:nth-last-child(2) {
+ @include mobile {
+ flex-grow: 1;
+ }
+ }
+
+ &>.NoteContent:only-child {
+ @include mobile {
+ flex-grow: 1;
+ }
+
+ .edit-content {
+ @include mobile {
+ flex-grow: 0;
+ }
+ }
+ }
+ }
+
+ .NoteHeader {
+ @include mobile {
+ flex-grow: 0;
+ }
+ }
+
+ .NoteContent .edit-content {
+ margin-top: 16px;
+ }
+
+ .note-popup-options:not(.options-reply) {
+ top: 33px;
+ }
+
+ .quill,
+ .ql-container,
+ .ql-editor {
+ @include mobile {
+ font-size: 16px;
+ }
+ }
+
+ .inline-comment-header {
+ flex-grow: 0;
+ flex-shrink: 0;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .inline-comment-header-title {
+ flex-grow: 1;
+ font-size: 16px;
+ }
+
+ .Button {
+
+ &.add-attachment,
+ &.reply-button {
+ margin: 0;
+
+ .Icon {
+ width: 22px;
+ height: 22px;
+ }
+ }
+
+ &.add-attachment {
+ width: 24px;
+ height: 24px;
+
+ @include mobile {
+ width: 24px;
+ height: 24px;
+ }
+ }
+
+ &.reply-button {
+ width: 28px;
+ height: 28px;
+
+ @include mobile {
+ width: 28px;
+ height: 28px;
+ }
+ }
+ }
+}
+
+// fix for storybook
+.sb-show-main {
+ .InlineCommentingPopup {
+ .quill.comment-textarea {
+ padding: 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/InlineCommentingPopup/InlineCommentingPopup.spec.js b/src/components/InlineCommentingPopup/InlineCommentingPopup.spec.js
new file mode 100644
index 0000000000..d02323dba1
--- /dev/null
+++ b/src/components/InlineCommentingPopup/InlineCommentingPopup.spec.js
@@ -0,0 +1,44 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import * as reactRedux from 'react-redux';
+import { Basic, Mobile, initialState } from './InlineCommentingPopup.stories';
+
+const TestInlineCommentPopup = withProviders(Basic);
+const TestInlineCommentPopupMobile = withProviders(Mobile);
+
+jest.mock('core', () => ({
+ getGroupAnnotations: () => [],
+ getDisplayAuthor: () => '',
+ canModify: () => true,
+ canModifyContents: () => true,
+ addEventListener: () => { },
+ removeEventListener: () => { },
+}));
+
+describe('InlineCommentPopup Component', () => {
+ beforeEach(() => {
+ const useDispatchMock = jest.spyOn(reactRedux, 'useDispatch');
+ useDispatchMock.mockImplementation(() => { });
+
+ const useSelectorMock = jest.spyOn(reactRedux, 'useSelector');
+ useSelectorMock.mockImplementation((selector) => selector(initialState));
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ });
+
+ it('Should not throw any errors when rendering storybook component', () => {
+ expect(() => {
+ render(
);
+ }).not.toThrow();
+ });
+
+ it('Should show header for mobile correctly', () => {
+ const { container } = render(
+
+ );
+ const mobileHeader = container.querySelector('.inline-comment-header');
+ expect(mobileHeader).not.toBeNull();
+ });
+});
\ No newline at end of file
diff --git a/src/components/InlineCommentingPopup/InlineCommentingPopup.stories.js b/src/components/InlineCommentingPopup/InlineCommentingPopup.stories.js
new file mode 100644
index 0000000000..a8d6ed08dd
--- /dev/null
+++ b/src/components/InlineCommentingPopup/InlineCommentingPopup.stories.js
@@ -0,0 +1,83 @@
+import React from 'react';
+import InlineCommentingPopup from './InlineCommentingPopup';
+import { configureStore } from '@reduxjs/toolkit';
+import { Provider } from 'react-redux';
+
+const noop = () => { };
+
+export default {
+ title: 'Components/InlineCommentPopup',
+ component: InlineCommentingPopup,
+ includeStories: ['Basic', 'Mobile'],
+};
+
+export const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ openElements: { inlineCommentPopup: true },
+ customPanels: [],
+ unreadAnnotationIdSet: new Set(),
+ colorMap: [{ colorMapKey: () => '#F1A099' }],
+ },
+};
+
+export const context = {
+ searchInput: '',
+ resize: noop,
+ isSelected: true,
+ setCurAnnotId: noop,
+ onTopNoteContentClicked: noop,
+ pendingEditTextMap: {},
+ pendingReplyMap: {},
+ pendingAttachmentMap: {}
+};
+
+
+const mockAnnotation = {
+ Author: 'Mikel Landa',
+ isFormFieldPlaceholder: () => false,
+ getReplies: () => [],
+ getStatus: () => '',
+ isReply: () => false,
+ getAssociatedNumber: () => 1,
+ getContents: noop,
+ getCustomData: () => '',
+ getAttachments: noop,
+ getRichTextStyle: noop,
+};
+
+export const basicProps = {
+ isOpen: true,
+ isNotesPanelOpen: false,
+ commentingAnnotation: mockAnnotation,
+ position: { top: 0, left: 0 },
+ contextValue: context,
+};
+
+export const Basic = () => {
+ return (
+
initialState })}>
+
+
+ );
+};
+
+export const mobileProps = {
+ ...basicProps,
+ isMobile: true,
+};
+
+export const Mobile = () => {
+ return (
+
initialState })}>
+
+
+ );
+};
+
+Mobile.parameters = {
+ viewport: {
+ defaultViewport: 'mobile1',
+ },
+};
\ No newline at end of file
diff --git a/src/components/InlineCommentingPopup/index.js b/src/components/InlineCommentingPopup/index.js
new file mode 100644
index 0000000000..6af61d6503
--- /dev/null
+++ b/src/components/InlineCommentingPopup/index.js
@@ -0,0 +1,3 @@
+import InlineCommentingPopupContainer from './InlineCommentingPopupContainer';
+
+export default InlineCommentingPopupContainer;
\ No newline at end of file
diff --git a/src/components/InsertPageModal/InsertBlankPagePanel/InsertBlankPagePanel.scss b/src/components/InsertPageModal/InsertBlankPagePanel/InsertBlankPagePanel.scss
new file mode 100644
index 0000000000..ebf9c666ea
--- /dev/null
+++ b/src/components/InsertPageModal/InsertBlankPagePanel/InsertBlankPagePanel.scss
@@ -0,0 +1,202 @@
+@import '../../../constants/styles';
+
+.insert-blank-page-panel {
+ width: 100%;
+
+ .dimension-input-container {
+ min-width: 100%;
+ margin: 0;
+ height: 32px;
+ }
+
+ .subheader {
+ font-size: 13px;
+ font-weight: 700;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ }
+
+ .panel-container {
+
+ .section {
+ display: flex;
+ padding-top: 8px;
+ padding-bottom: 8px;
+ gap: 16px;
+
+ .input-container {
+ display: flex;
+ flex-direction: column;
+
+ p {
+ margin: 0;
+ padding-bottom: 8px;
+ font-size: 13px;
+ }
+
+ .page-number-input {
+ width: 100%;
+ height: 32px;
+ margin: 0;
+ }
+
+ .customSelector {
+ margin-left: 0;
+ height: 28px;
+
+ .customSelector__selectedItem {
+ width: 100%;
+ border-radius: 4px;
+ }
+
+ .Icon {
+ width: 13px;
+ height: 13px;
+ }
+
+ ul {
+ width: 100%;
+
+ @include mobile {
+ top: auto;
+ bottom: calc(100% + 4px);
+ }
+ }
+
+ & li:first-child {
+ color: var(--faded-text);
+ font-size: 13px;
+
+ @include mobile {
+ display: none;
+ }
+ }
+
+ li .optionSelected {
+ color: var(--text-color);
+ background: var(--popup-button-active);
+ }
+ }
+
+ select {
+ height: 28px;
+ width: 100%;
+ }
+
+ .Dropdown {
+ height: 32px;
+ min-width: 150px;
+ width: 100% !important;
+
+ .arrow {
+ flex: 0 1 auto;
+ }
+
+ .picked-option .picked-option-text {
+ width: 150px;
+ text-align: left;
+ }
+ }
+
+ .Dropdown__items {
+ top: -52px;
+ z-index: 80;
+ width: 100%;
+ }
+
+ .input-sub-text {
+ margin-top: 8px;
+ padding-bottom: 0;
+ color: var(--faded-text);
+ }
+
+ .page-number-error {
+ margin-top: 8px;
+ font-size: 13px;
+ color: var(--color-message-error);
+ }
+ }
+
+ @include mobile {
+ .ui__choice__label, input, button {
+ font-size: 13px;
+ }
+ }
+ }
+
+ .section > * {
+ flex: 1;
+ }
+ }
+}
+.incrementNumberInput {
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ display: flex;
+ height: 32px;
+
+ input[type=number]::-webkit-inner-spin-button,
+ input[type=number]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+ }
+
+ input[type=number] {
+ -moz-appearance:textfield;
+ }
+
+ .ui__input {
+ border: 0;
+ height: 100%;
+
+ .ui__input__input {
+ width: 100%;
+ height: 100%;
+ padding: 6px;
+ line-height: normal;
+
+ }
+ }
+
+ .ui__input--message-default.ui__input--focused {
+ outline: none;
+ box-shadow: none;
+ }
+
+ .increment-buttons {
+ @include mobile {
+ display: none;
+ }
+
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ justify-content: center;
+ padding: 2px;
+
+ .increment-arrow-button {
+ border: 0;
+ border-radius: 2px;
+ height: 10px;
+ width: 20px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ line-height: 10px;
+ padding: 0;
+
+ .Icon {
+ height: 10px;
+ width: 10px;
+ }
+
+ &:active {
+ box-shadow: 0 0 1px 0 var(--document-box-shadow);
+ }
+ }
+ }
+
+ &:focus-within {
+ border: 1px solid var(--focus-border);
+ }
+}
diff --git a/src/components/InsertPageModal/InsertBlankPagePanel/index.js b/src/components/InsertPageModal/InsertBlankPagePanel/index.js
new file mode 100644
index 0000000000..35b87e586d
--- /dev/null
+++ b/src/components/InsertPageModal/InsertBlankPagePanel/index.js
@@ -0,0 +1,3 @@
+import InsertBlankPagePanel from './InsertBlankPagePanel';
+
+export default InsertBlankPagePanel;
\ No newline at end of file
diff --git a/src/components/InsertPageModal/InsertUploadedPagePanel/InsertUploadedPagePanel.scss b/src/components/InsertPageModal/InsertUploadedPagePanel/InsertUploadedPagePanel.scss
index 8ba7533d5f..8691b1083e 100644
--- a/src/components/InsertPageModal/InsertUploadedPagePanel/InsertUploadedPagePanel.scss
+++ b/src/components/InsertPageModal/InsertUploadedPagePanel/InsertUploadedPagePanel.scss
@@ -17,7 +17,7 @@
width: 100%;
font-size: 16px;
line-height: 24px;
- color: var(--gray-9);
+ color: var(--gray-8);
font-weight: 700;
box-shadow: inset 0px -1px 0px var(--divider);
padding: 20px 16px 20px 16px;
diff --git a/src/components/LazyLoadWrapper/index.js b/src/components/LazyLoadWrapper/index.js
new file mode 100644
index 0000000000..c0f8af0a5e
--- /dev/null
+++ b/src/components/LazyLoadWrapper/index.js
@@ -0,0 +1,5 @@
+import LazyLoadWrapper from './LazyLoadWrapper';
+
+export { default as LazyLoadComponents } from './LazyLoadComponents';
+
+export default LazyLoadWrapper;
diff --git a/src/components/LeftPanelOverlay/ThumbnailMoreOptionsPopupSmall.js b/src/components/LeftPanelOverlay/ThumbnailMoreOptionsPopupSmall.js
new file mode 100644
index 0000000000..5fe573e7f5
--- /dev/null
+++ b/src/components/LeftPanelOverlay/ThumbnailMoreOptionsPopupSmall.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import selectors from 'selectors';
+import FlyoutMenu from 'components/FlyoutMenu/FlyoutMenu';
+import PageAdditionalControls from 'components/PageManipulationOverlay/PageAdditionalControls';
+import PageManipulationControls from '../PageManipulationOverlay/PageManipulationControls';
+import DataElements from 'constants/dataElement';
+
+const ThumbnailMoreOptionsPopupSmall = () => {
+ const selectedPageIndexes = useSelector((state) => selectors.getSelectedThumbnailPageIndexes(state));
+
+ return (
+
+ i + 1)}
+ warn
+ />
+ i + 1)}
+ warn
+ />
+
+ );
+};
+
+export default ThumbnailMoreOptionsPopupSmall;
diff --git a/src/components/LinkAnnotationPopup/LinkAnnotationPopup.js b/src/components/LinkAnnotationPopup/LinkAnnotationPopup.js
new file mode 100644
index 0000000000..e5639f5d9f
--- /dev/null
+++ b/src/components/LinkAnnotationPopup/LinkAnnotationPopup.js
@@ -0,0 +1,71 @@
+import ActionButton from 'components/ActionButton';
+import classNames from 'classnames';
+import DataElements from 'constants/dataElement';
+import PropTypes from 'prop-types';
+import React from 'react';
+
+import './LinkAnnotationPopup.scss';
+
+const propTypes = {
+ handleUnLink: PropTypes.func,
+ isAnnotation: PropTypes.bool,
+ isMobileDevice: PropTypes.bool,
+ linkText: PropTypes.string,
+ handleOnMouseEnter: PropTypes.func,
+ handleOnMouseLeave: PropTypes.func,
+ handleMouseMove: PropTypes.func,
+};
+
+const LinkAnnotationPopup = ({
+ handleUnLink,
+ isAnnotation,
+ isMobileDevice,
+ linkText,
+ handleOnMouseEnter,
+ handleOnMouseLeave,
+ handleMouseMove
+}) => {
+ const renderContents = () => (
+
+ {linkText && (
+ <>
+
+ {linkText}
+
+
+ >
+ )}
+
+
+ );
+
+ if (isMobileDevice || !isAnnotation) {
+ return null;
+ }
+
+ return (
+
+ {renderContents()}
+
+ );
+};
+
+LinkAnnotationPopup.propTypes = propTypes;
+
+export default LinkAnnotationPopup;
diff --git a/src/components/LinkAnnotationPopup/LinkAnnotationPopup.scss b/src/components/LinkAnnotationPopup/LinkAnnotationPopup.scss
new file mode 100644
index 0000000000..c09a744bbe
--- /dev/null
+++ b/src/components/LinkAnnotationPopup/LinkAnnotationPopup.scss
@@ -0,0 +1,52 @@
+@import '../../constants/styles.scss';
+@import '../../constants/popup';
+
+.LinkAnnotationPopupContainer {
+ @extend %popup;
+ border-radius: 4px;
+ box-shadow: 0 0 3px 0 var(--document-box-shadow);
+ background: var(--component-background);
+}
+
+.LinkAnnotationPopup {
+ &.is-horizontal {
+ .contents {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 8px;
+
+ .link-annot-input {
+ margin: 8px 0 8px 8px;
+ color: #485056;
+ font-style: normal;
+ font-weight: 400;
+ line-height: normal;
+ /* top right bottom left */
+ border: none;
+ width: fit-content;
+ max-width: 240px;
+ cursor: pointer;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 3;
+ line-clamp: 3;
+ -webkit-box-orient: vertical;
+ box-sizing: border-box;
+ word-break: break-all;
+ }
+
+ .divider {
+ width: 1px;
+ height: 20px;
+ background: var(--divider);
+ flex-shrink: 0;
+ }
+
+ .main-menu-button {
+ margin: 4px 8px 4px 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/LinkAnnotationPopup/LinkAnnotationPopup.stories.js b/src/components/LinkAnnotationPopup/LinkAnnotationPopup.stories.js
new file mode 100644
index 0000000000..8065cd5f77
--- /dev/null
+++ b/src/components/LinkAnnotationPopup/LinkAnnotationPopup.stories.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import LinkAnnotationPopup from './LinkAnnotationPopup';
+import { configureStore } from '@reduxjs/toolkit';
+import { Provider } from 'react-redux';
+
+const noop = () => { };
+
+export default {
+ title: 'Components/LinkAnnotationPopup',
+ component: LinkAnnotationPopup,
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ annotationPopup: [
+ ],
+ activeDocumentViewerKey: 1,
+ },
+};
+
+export const Basic = () => {
+ const props = {
+ linkText: 'https://www.Apryse.com',
+ handleUnLink: noop,
+ handleOnMouseEnter: noop,
+ handleOnMouseLeave: noop,
+ handleMouseMove: noop,
+ isAnnotation: true,
+ isMobileDevice: false,
+ };
+ return (
+
initialState })}>
+
+
+
+
+ );
+};
diff --git a/src/components/LinkAnnotationPopup/index.js b/src/components/LinkAnnotationPopup/index.js
new file mode 100644
index 0000000000..a60b394985
--- /dev/null
+++ b/src/components/LinkAnnotationPopup/index.js
@@ -0,0 +1,3 @@
+import LinkAnnotationPopupContainer from './LinkAnnotationPopupContainer';
+
+export default LinkAnnotationPopupContainer;
\ No newline at end of file
diff --git a/src/components/LinkModal/LinkModal.spec.js b/src/components/LinkModal/LinkModal.spec.js
new file mode 100644
index 0000000000..5ebc24e6a3
--- /dev/null
+++ b/src/components/LinkModal/LinkModal.spec.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import { NoURLInput } from './LinkModal.stories';
+import core from 'core';
+
+core.addEventListener = jest.fn();
+
+describe('LinkModal', () => {
+ describe('Component', () => {
+ it('Story should not throw any errors', () => {
+ expect(() => {
+ render(
);
+ }).not.toThrow();
+ });
+ });
+});
diff --git a/src/components/LinkModal/LinkModal.stories.js b/src/components/LinkModal/LinkModal.stories.js
new file mode 100644
index 0000000000..61a3896bac
--- /dev/null
+++ b/src/components/LinkModal/LinkModal.stories.js
@@ -0,0 +1,54 @@
+import React, { useState } from 'react';
+import { configureStore } from '@reduxjs/toolkit';
+import { Provider as ReduxProvider } from 'react-redux';
+import LinkModal from './LinkModal';
+import core from 'core';
+
+export default {
+ title: 'Components/LinkModal',
+ component: LinkModal,
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ openElements: {
+ 'linkModal': true,
+ },
+ currentPage: 1,
+ selectedTab: 'notesPanel',
+ tab: {
+ linkModal: 'URLPanelButton'
+ },
+ customElementOverrides: {},
+ pageLabels: []
+ },
+ document: {
+ totalPages: 1
+ }
+};
+const store = configureStore({
+ reducer: () => initialState
+});
+
+export function NoURLInput() {
+ core.getDocumentViewer = () => ({
+ getAnnotationManager: () => ({
+ isAnnotationSelected: () => true
+ }),
+ getSelectedText: () => 'selected text'
+ });
+
+ core.getSelectedAnnotations = () => ({});
+
+ return (
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/LogoBar/LogoBar.js b/src/components/LogoBar/LogoBar.js
new file mode 100644
index 0000000000..f28018de26
--- /dev/null
+++ b/src/components/LogoBar/LogoBar.js
@@ -0,0 +1,42 @@
+import './LogoBar.scss';
+import React from 'react';
+import selectors from 'selectors';
+import DataElements from 'constants/dataElement';
+import { useSelector } from 'react-redux';
+import packageConfig from '../../../package.json';
+import Button from '../Button';
+import { isMobileSize } from 'src/helpers/getDeviceSize';
+
+const LogoBar = () => {
+ const [
+ isDisabled,
+ ] = useSelector((state) => [
+ selectors.isElementDisabled(state, DataElements.LOGO_BAR),
+ ]);
+
+ const logoText = isMobileSize() ? 'Apryse' : 'Powered by Apryse';
+ const versionText = isMobileSize() ? packageConfig.version : `Version ${packageConfig.version}`;
+ const apryseURL = 'https://apryse.com/products/webviewer';
+ const apryseRedirect = () => {
+ window.top.location.href = apryseURL;
+ };
+
+ return isDisabled ? null : (
+
+ );
+};
+
+export default LogoBar;
diff --git a/src/components/LogoBar/LogoBar.scss b/src/components/LogoBar/LogoBar.scss
new file mode 100644
index 0000000000..e203b2202c
--- /dev/null
+++ b/src/components/LogoBar/LogoBar.scss
@@ -0,0 +1,38 @@
+@import '../../constants/styles';
+
+.LogoBar {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 4px 16px;
+ width: 100%;
+ height: $logo-bar-height;
+ background: var(--gray-0);
+ color: #747C84;
+ font-size: 12px;
+ line-height: 16px;
+
+ .logo-container {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ }
+
+ .logo-button {
+ width: 100%;
+ gap: 4px;
+ color: #747C84;
+ font-size: 12px !important;
+
+ .Icon {
+ width: 14px;
+ height: 14px;
+ }
+ }
+
+ .version {
+ text-decoration: none;
+ color: #747C84;
+ }
+}
\ No newline at end of file
diff --git a/src/components/LogoBar/LogoBar.stories.js b/src/components/LogoBar/LogoBar.stories.js
new file mode 100644
index 0000000000..e90c79b544
--- /dev/null
+++ b/src/components/LogoBar/LogoBar.stories.js
@@ -0,0 +1,29 @@
+import React from 'react';
+import { configureStore } from '@reduxjs/toolkit';
+import LogoBar from './LogoBar';
+
+import { Provider } from 'react-redux';
+
+export default {
+ title: 'Components/LogoBar',
+ component: LogoBar,
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ },
+};
+
+const store = configureStore({
+ reducer: () => initialState
+});
+
+export const LogoBarComponent = () => (
+
+
+
+
+
+);
\ No newline at end of file
diff --git a/src/components/LogoBar/index.js b/src/components/LogoBar/index.js
new file mode 100644
index 0000000000..561567d51e
--- /dev/null
+++ b/src/components/LogoBar/index.js
@@ -0,0 +1,3 @@
+import LogoBar from './LogoBar';
+
+export default LogoBar;
diff --git a/src/components/ModularComponents/BottomHeader/BottomHeader.scss b/src/components/ModularComponents/BottomHeader/BottomHeader.scss
new file mode 100644
index 0000000000..ac3c7ba51c
--- /dev/null
+++ b/src/components/ModularComponents/BottomHeader/BottomHeader.scss
@@ -0,0 +1,23 @@
+.bottom-headers-wrapper {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ pointer-events: none;
+}
+
+.BottomHeader {
+ display: inline-block;
+ width: 100%;
+ pointer-events: auto;
+}
+
+.BottomHeader.closed {
+ display: none;
+}
+
+.BottomHeader.stroke {
+ border-top-width: 1px;
+ border-top-style: solid;
+ border-top-color: var(--gray-5);
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/BottomHeader/index.js b/src/components/ModularComponents/BottomHeader/index.js
new file mode 100644
index 0000000000..e4d6606372
--- /dev/null
+++ b/src/components/ModularComponents/BottomHeader/index.js
@@ -0,0 +1,3 @@
+import BottomHeader from './BottomHeaderContainer';
+
+export default BottomHeader;
\ No newline at end of file
diff --git a/src/components/ModularComponents/CustomButton/CustomButton.js b/src/components/ModularComponents/CustomButton/CustomButton.js
new file mode 100644
index 0000000000..029a7407c6
--- /dev/null
+++ b/src/components/ModularComponents/CustomButton/CustomButton.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import '../../Button/Button.scss';
+import './CustomButton.scss';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import Button from 'components/Button';
+import { PLACEMENT } from 'constants/customizationVariables';
+
+const CustomButton = (props) => {
+ const { title, dataElement, label, img, onClick, disabled, className, preset, headerPlacement, ariaLabel } = props;
+ let forceTooltipPosition;
+ if ([PLACEMENT.LEFT, PLACEMENT.RIGHT].includes(headerPlacement)) {
+ forceTooltipPosition = headerPlacement === PLACEMENT.LEFT ? PLACEMENT.RIGHT : PLACEMENT.LEFT;
+ } else if ([PLACEMENT.TOP, PLACEMENT.BOTTOM].includes(headerPlacement)) {
+ forceTooltipPosition = headerPlacement === PLACEMENT.TOP ? PLACEMENT.BOTTOM : PLACEMENT.TOP;
+ }
+ return (
+
+ );
+};
+
+CustomButton.propTypes = {
+ dataElement: PropTypes.string,
+ title: PropTypes.string,
+ label: PropTypes.string,
+ img: PropTypes.string,
+ onClick: PropTypes.func,
+ disabled: PropTypes.bool,
+};
+
+export default CustomButton;
diff --git a/src/components/ModularComponents/CustomButton/CustomButton.scss b/src/components/ModularComponents/CustomButton/CustomButton.scss
new file mode 100644
index 0000000000..3a5f9f65b6
--- /dev/null
+++ b/src/components/ModularComponents/CustomButton/CustomButton.scss
@@ -0,0 +1,46 @@
+@import '../../../constants/styles';
+
+.CustomButton {
+ padding: 5px;
+ width: fit-content;
+
+ &:hover {
+ background-color: var(--view-header-button-hover);
+ }
+}
+
+.confirm-button {
+ background-color: var(--primary-button);
+ border: 1px solid var(--primary-button);
+ color: var(--primary-button-text);
+ padding: 7px 14px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ border-radius: 5px;
+ height: 30px;
+ cursor: pointer;
+
+ &:hover {
+ background: var(--primary-button-hover) !important;
+ border: 1px solid var(--primary-button-hover) !important;
+ border-radius: 5px !important;
+ }
+}
+
+.cancel-button {
+ color: var(--secondary-button-text);
+ background-color: transparent;
+ padding: 7px 14px;
+ width: -webkit-fit-content;
+ width: -moz-fit-content;
+ width: fit-content;
+ border-radius: 5px;
+ height: 30px;
+ cursor: pointer;
+
+ &:hover {
+ color: var(--secondary-button-hover);
+ background: transparent;
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/CustomButton/CustomButton.stories.js b/src/components/ModularComponents/CustomButton/CustomButton.stories.js
new file mode 100644
index 0000000000..a099cfae2a
--- /dev/null
+++ b/src/components/ModularComponents/CustomButton/CustomButton.stories.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import CustomButton from './CustomButton';
+import initialState from 'src/redux/initialState';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+
+export default {
+ title: 'Components/CustomButton',
+ component: CustomButton,
+};
+
+const store = configureStore({
+ reducer: () => initialState
+});
+
+const BasicComponent = (props) => {
+ return (
+
+
+
+ );
+};
+
+export const DefaultButton = BasicComponent.bind({});
+DefaultButton.args = {
+ dataElement: 'button-data-element',
+ title: 'Button title',
+ disabled: false,
+ label: 'Click',
+ img: 'icon-save',
+ onClick: () => {
+ alert('Clicked!');
+ }
+};
+
+export const ConfirmButton = BasicComponent.bind({});
+ConfirmButton.args = {
+ dataElement: 'button-data-element',
+ title: 'Apply Fields',
+ label: 'Apply Fields',
+ preset: 'confirm',
+ onClick: () => {
+ alert('Apply Fields button clicked!');
+ }
+};
+
+export const CancelButton = BasicComponent.bind({});
+CancelButton.args = {
+ dataElement: 'button-data-element',
+ title: 'Cancel',
+ label: 'Cancel',
+ preset: 'cancel',
+ onClick: () => {
+ alert('Cancel button clicked!');
+ }
+};
\ No newline at end of file
diff --git a/src/components/ModularComponents/CustomButton/index.js b/src/components/ModularComponents/CustomButton/index.js
new file mode 100644
index 0000000000..e5f8cd2a41
--- /dev/null
+++ b/src/components/ModularComponents/CustomButton/index.js
@@ -0,0 +1,3 @@
+import CustomButton from './CustomButton';
+
+export default CustomButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/Divider/Divider.js b/src/components/ModularComponents/Divider/Divider.js
new file mode 100644
index 0000000000..c7b9aa1e30
--- /dev/null
+++ b/src/components/ModularComponents/Divider/Divider.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import classNames from 'classnames';
+import './Divider.scss';
+
+const Divider = ({ headerDirection }) => {
+ const className = classNames('Divider', `${headerDirection || 'column'}`);
+
+ return (
+
+ );
+};
+
+export default Divider;
\ No newline at end of file
diff --git a/src/components/ModularComponents/Divider/Divider.scss b/src/components/ModularComponents/Divider/Divider.scss
new file mode 100644
index 0000000000..03f39f4228
--- /dev/null
+++ b/src/components/ModularComponents/Divider/Divider.scss
@@ -0,0 +1,15 @@
+.Divider {
+ background: var(--gray-5);
+
+ &.row {
+ width: 1px;
+ height: auto;
+ margin: 4px 0px;
+ }
+
+ &.column {
+ width: auto;
+ height: 1px;
+ margin: 0px 4px;
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/Divider/index.js b/src/components/ModularComponents/Divider/index.js
new file mode 100644
index 0000000000..7909d0ef6b
--- /dev/null
+++ b/src/components/ModularComponents/Divider/index.js
@@ -0,0 +1,3 @@
+import Divider from './Divider';
+
+export default Divider;
\ No newline at end of file
diff --git a/src/components/ModularComponents/FlexDropdown/FlexDropdown.js b/src/components/ModularComponents/FlexDropdown/FlexDropdown.js
new file mode 100644
index 0000000000..62a510a650
--- /dev/null
+++ b/src/components/ModularComponents/FlexDropdown/FlexDropdown.js
@@ -0,0 +1,12 @@
+import React from 'react';
+import Dropdown from 'components/Dropdown';
+
+import './FlexDropdown.scss';
+
+const FlexDropdown = (props) => {
+ return (
+
+ );
+};
+
+export default FlexDropdown;
\ No newline at end of file
diff --git a/src/components/ModularComponents/FlexDropdown/FlexDropdown.scss b/src/components/ModularComponents/FlexDropdown/FlexDropdown.scss
new file mode 100644
index 0000000000..94b98f1f51
--- /dev/null
+++ b/src/components/ModularComponents/FlexDropdown/FlexDropdown.scss
@@ -0,0 +1,51 @@
+@import '../../../constants/styles';
+
+.FlexDropdown {
+ &__wrapper {
+ position: relative;
+ }
+
+ .Dropdown__items {
+ z-index: $headers-z-index;
+ }
+
+ .Dropdown__item-object {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ }
+
+ &.column {
+ height: auto;
+ padding: 0;
+ .picked-option {
+ flex-direction: column;
+ gap: 4px;
+ padding: 4px;
+ }
+ .Dropdown__items {
+ z-index: $headers-z-index;
+ width: 100%;
+ padding: 0;
+ gap: 12px;
+ }
+ .Dropdown__item {
+ min-height: 28px;
+ height: 100%;
+ padding: 4px;
+ }
+ .Dropdown__item-object {
+ display: flex;
+ flex-direction: column;
+ max-width: 100%;
+ flex: 1;
+ }
+ }
+
+ .Dropdown__item-text {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/FlexDropdown/index.js b/src/components/ModularComponents/FlexDropdown/index.js
new file mode 100644
index 0000000000..4d12ec3a2d
--- /dev/null
+++ b/src/components/ModularComponents/FlexDropdown/index.js
@@ -0,0 +1,3 @@
+import FlexDropdown from './FlexDropdown';
+
+export default FlexDropdown;
\ No newline at end of file
diff --git a/src/components/ModularComponents/FloatingHeader/FloatingHeader.scss b/src/components/ModularComponents/FloatingHeader/FloatingHeader.scss
new file mode 100644
index 0000000000..c556e61fed
--- /dev/null
+++ b/src/components/ModularComponents/FloatingHeader/FloatingHeader.scss
@@ -0,0 +1,117 @@
+@import '../../../constants/styles.scss';
+
+.FloatingHeaderContainer {
+ transition: all .3s ease-in-out;
+ position: absolute;
+ width: 100%;
+ box-sizing: border-box;
+ display: flex;
+ flex-direction: row;
+ align-items: baseline;
+ min-height: $top-bar-height;
+ padding: 24px;
+ z-index: $headers-z-index;
+ pointer-events: none;
+
+ &.bottom {
+ position: relative;
+ }
+
+ &.left {
+ width: $left-header-width;
+ padding: 24px 0px 24px 24px;
+ }
+
+ &.right {
+ width: $right-header-width;
+ align-items: end;
+ }
+
+ &.vertical {
+ flex-direction: column;
+ }
+
+ .FloatSection {
+ display: flex;
+ flex: 1;
+
+ &.vertical {
+ flex-direction: column;
+ }
+
+ // Maybe we'll need these maybe we wont
+ // &.start__top,
+ // &.start__bottom {
+ // // align-self: flex-start;
+ // }
+
+ // &.start__left,
+ // &.start__right {
+ // // align-self: flex-start;
+ // }
+
+ &.center__top,
+ &.center__bottom {
+ justify-content: center;
+ }
+
+ &.center__left,
+ &.center__right {
+ justify-content: center;
+ }
+
+ &.end__top,
+ &.end__bottom {
+ justify-content: flex-end;
+ }
+
+ &.end__left,
+ &.end__right {
+ justify-content: flex-end;
+ }
+ }
+}
+
+.FloatingHeader {
+ border-radius: 4px;
+ pointer-events: auto;
+ transition: opacity .2s ease;
+
+ // customizable styles
+ padding: 8px 12px;
+ background: var(--gray-0);
+
+ &.opacity-full {
+ opacity: 1;
+ }
+
+ &.opacity-low {
+ opacity: 0.5;
+
+ &.isVisible {
+ opacity: 1;
+ }
+ }
+
+ &.opacity-none {
+ opacity: 0;
+
+ &.isVisible {
+ opacity: 1;
+ }
+ }
+
+ &.opacity-mode-dynamic:hover {
+ opacity: 1;
+ }
+}
+
+.FloatingHeader.stroke {
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(--gray-5);
+}
+
+.FloatingHeader.VerticalHeader {
+ padding: 12px 8px;
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/FloatingHeader/FloatingHeaderContainer.js b/src/components/ModularComponents/FloatingHeader/FloatingHeaderContainer.js
new file mode 100644
index 0000000000..9e95a0ae35
--- /dev/null
+++ b/src/components/ModularComponents/FloatingHeader/FloatingHeaderContainer.js
@@ -0,0 +1,120 @@
+import React, { useMemo } from 'react';
+import useFloatingHeaderSelectors from 'hooks/useFloatingHeaderSelectors';
+import FloatingHeader from './FloatingHeader';
+import './FloatingHeader.scss';
+import classNames from 'classnames';
+import { PLACEMENT, POSITION, DEFAULT_GAP } from 'src/constants/customizationVariables';
+
+const FloatSection = ({ position, isVertical, children, gap = DEFAULT_GAP }) => {
+ const className = classNames('FloatSection', position, { 'vertical': isVertical });
+ return (
+
+ {children}
+
+ );
+};
+
+const FloatingHeaderContainer = React.forwardRef((props, ref) => {
+ const { floatingHeaders, placement } = props;
+ const isHorizontalHeader = [PLACEMENT.TOP, PLACEMENT.BOTTOM].includes(placement);
+ const selectors = useFloatingHeaderSelectors();
+
+ const style = useMemo(() => computeFloatContainerStyle({
+ ...selectors,
+ isHorizontalHeader,
+ placement,
+ }), [selectors, isHorizontalHeader, placement]);
+
+ const renderHeaders = (headers, positionPrefix) => (
+
+ {headers.map((header) => )}
+
+ );
+
+ return (
+
+ {renderHeaders(floatingHeaders.filter((h) => h.position === POSITION.START), POSITION.START)}
+ {renderHeaders(floatingHeaders.filter((h) => h.position === POSITION.CENTER), POSITION.CENTER)}
+ {renderHeaders(floatingHeaders.filter((h) => h.position === POSITION.END), POSITION.END)}
+
+ );
+});
+
+FloatingHeaderContainer.displayName = 'FloatingHeaderContainer';
+
+function computeFloatContainerStyle(params) {
+ const {
+ isLeftPanelOpen,
+ leftPanelWidth,
+ isRightPanelOpen,
+ rightPanelWidth,
+ leftHeaderWidth,
+ rightHeaderWidth,
+ isHorizontalHeader,
+ topFloatingContainerHeight,
+ bottomFloatingContainerHeight,
+ topStartFloatingHeaders,
+ bottomStartFloatingHeaders,
+ topHeadersHeight,
+ bottomHeadersHeight,
+ bottomEndFloatingHeaders,
+ topEndFloatingHeaders,
+ placement
+ } = params;
+
+ const styles = {};
+ const verticalHeaderWidth = rightHeaderWidth + leftHeaderWidth;
+ let panelsWidth = 0;
+ let leftOffset = leftHeaderWidth;
+
+ if (isLeftPanelOpen) {
+ panelsWidth += leftPanelWidth;
+ leftOffset += leftPanelWidth;
+ }
+ if (isRightPanelOpen) {
+ panelsWidth += rightPanelWidth;
+ }
+
+ if (leftOffset !== 0) {
+ styles.transform = `translate(${leftOffset}px, 0px)`;
+ }
+ if (placement === PLACEMENT.RIGHT) {
+ styles.transform = 'translate(-48px, 0px)';
+ }
+ if (isHorizontalHeader && (panelsWidth || verticalHeaderWidth)) {
+ styles.width = `calc(100% - ${panelsWidth + verticalHeaderWidth}px)`;
+ }
+ if (!isHorizontalHeader) {
+ // if it is the left float header, and there are no top start floating headers, then we can take the full height
+ // otherwise the height must accotun for the floating header container
+ let topFloatingHeaderOffset = 0;
+ let bottomFloatingHeaderOffset = 0;
+
+ if (placement === PLACEMENT.LEFT) {
+ topFloatingHeaderOffset = topStartFloatingHeaders.length === 0 ? 0 : topFloatingContainerHeight;
+ bottomFloatingHeaderOffset = bottomStartFloatingHeaders.length === 0 ? 0 : bottomFloatingContainerHeight;
+ }
+
+ if (placement === PLACEMENT.RIGHT) {
+ topFloatingHeaderOffset = topEndFloatingHeaders.length === 0 ? 0 : topFloatingContainerHeight;
+ bottomFloatingHeaderOffset = bottomEndFloatingHeaders.length === 0 ? 0 : bottomFloatingContainerHeight;
+ }
+
+ styles.height = `calc(100% - ${topHeadersHeight + bottomHeadersHeight + topFloatingHeaderOffset + bottomFloatingHeaderOffset}px)`;
+ if (topFloatingHeaderOffset) {
+ styles.marginTop = `${topFloatingContainerHeight}px`;
+ styles.paddingTop = '0px';
+ }
+ if (bottomFloatingHeaderOffset) {
+ styles.paddingBottom = '0px';
+ }
+ }
+
+ return styles;
+}
+
+export default FloatingHeaderContainer;
\ No newline at end of file
diff --git a/src/components/ModularComponents/FloatingHeader/Stories/HeadersInApp.stories.js b/src/components/ModularComponents/FloatingHeader/Stories/HeadersInApp.stories.js
new file mode 100644
index 0000000000..b86672fa3c
--- /dev/null
+++ b/src/components/ModularComponents/FloatingHeader/Stories/HeadersInApp.stories.js
@@ -0,0 +1,233 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import App from 'components/App';
+import initialState from 'src/redux/initialState';
+import rootReducer from 'reducers/rootReducer';
+import {
+ defaultLeftHeader,
+ defaultTopHeader,
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+ secondFloatStartLeftHeader,
+ floatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultRightHeader,
+ secondFloatStartRightHeader,
+ floatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultBottomHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+ floatStarTopHeaderStatic,
+ floatCenterTopHeaderDynamic,
+ floatEndTopHeaderNone,
+ mockModularComponents,
+} from '../../Helpers/mockHeaders';
+
+export default {
+ title: 'ModularComponents/FloatingHeader/App',
+ component: App,
+};
+
+
+const noop = () => { };
+
+const MockApp = ({ initialState }) => {
+ return (
+
getDefaultMiddleware({ serializableCheck: false })
+ })}>
+
+
+ );
+};
+
+const Template = (args) => {
+ const stateWithHeaders = {
+ ...initialState,
+ viewer: {
+ ...initialState.viewer,
+ modularHeaders: args.headers,
+ modularComponents: mockModularComponents,
+ openElements: {},
+ },
+ featureFlags: {
+ customizableUI: true,
+ },
+ };
+ return
;
+};
+
+function createTemplate(headers) {
+ const template = Template.bind({});
+ template.args = { headers };
+ template.parameters = {
+ layout: 'fullscreen',
+ chromatic: { disableSnapshot: true }
+ };
+ return template;
+}
+
+export const TopAndLeftHeaders = createTemplate({
+ defaultLeftHeader,
+ secondFloatStartLeftHeader,
+ floatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultTopHeader,
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+});
+
+export const TopCenterAndLeftHeaders = createTemplate({
+ defaultLeftHeader,
+ secondFloatStartLeftHeader,
+ floatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultTopHeader,
+ floatCenterHeader,
+ floatEndHeader,
+});
+
+export const TopAndRightHeaders = createTemplate({
+ defaultRightHeader,
+ secondFloatStartRightHeader,
+ floatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultTopHeader,
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+});
+
+export const TopCenterAndRightHeaders = createTemplate({
+ defaultRightHeader,
+ secondFloatStartRightHeader,
+ floatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultTopHeader,
+ floatCenterHeader,
+});
+
+export const BottomAndLeftHeaders = createTemplate({
+ defaultLeftHeader,
+ secondFloatStartLeftHeader,
+ floatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultBottomHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+});
+
+export const BottomCenterAndLeftHeaders = createTemplate({
+ defaultLeftHeader,
+ secondFloatStartLeftHeader,
+ floatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+});
+
+export const BottomAndRightHeaders = createTemplate({
+ defaultRightHeader,
+ secondFloatStartRightHeader,
+ floatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultBottomHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+});
+
+export const BottomCenterAndRightHeaders = createTemplate({
+ defaultRightHeader,
+ secondFloatStartRightHeader,
+ floatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultBottomHeader,
+ floatCenterBottomHeader,
+});
+
+export const TopAndBottomHeaders = createTemplate({
+ defaultBottomHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+ defaultTopHeader,
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+});
+
+export const FloatingOnAllSides = createTemplate({
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+ floatStartLeftHeader,
+ secondFloatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ floatStartRightHeader,
+ secondFloatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+});
+
+export const FloatiesWithOpacityLevels = createTemplate({
+ floatStarTopHeaderStatic,
+ floatCenterTopHeaderDynamic,
+ floatEndTopHeaderNone,
+});
+
+export const AllHeaders = createTemplate({
+ defaultTopHeader,
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+ defaultLeftHeader,
+ floatStartLeftHeader,
+ secondFloatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultRightHeader,
+ floatStartRightHeader,
+ secondFloatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultBottomHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+});
\ No newline at end of file
diff --git a/src/components/ModularComponents/FloatingHeader/index.js b/src/components/ModularComponents/FloatingHeader/index.js
new file mode 100644
index 0000000000..6f3f435969
--- /dev/null
+++ b/src/components/ModularComponents/FloatingHeader/index.js
@@ -0,0 +1,3 @@
+import FloatingHeaderContainer from './FloatingHeaderContainer';
+
+export default FloatingHeaderContainer;
\ No newline at end of file
diff --git a/src/components/ModularComponents/GenericOutlinesPanel/GenericOutlinesPanel.stories.js b/src/components/ModularComponents/GenericOutlinesPanel/GenericOutlinesPanel.stories.js
index 974a922073..7328dccd98 100644
--- a/src/components/ModularComponents/GenericOutlinesPanel/GenericOutlinesPanel.stories.js
+++ b/src/components/ModularComponents/GenericOutlinesPanel/GenericOutlinesPanel.stories.js
@@ -30,7 +30,7 @@ export const Editable = () => {
},
panelWidths: { 'outlines-panel': DEFAULT_NOTES_PANEL_WIDTH },
isInDesktopOnlyMode: true,
- modularHeaders: []
+ modularHeaders: {}
},
document: {
outlines: getDefaultOutlines(),
@@ -63,7 +63,7 @@ export const NonEditable = () => {
},
panelWidths: { 'outlines-panel': DEFAULT_NOTES_PANEL_WIDTH },
isInDesktopOnlyMode: true,
- modularHeaders: []
+ modularHeaders: {}
},
document: {
outlines: getDefaultOutlines(),
@@ -98,7 +98,7 @@ export const Expanded = () => {
},
panelWidths: { 'outlines-panel': DEFAULT_NOTES_PANEL_WIDTH },
isInDesktopOnlyMode: true,
- modularHeaders: []
+ modularHeaders: {}
},
document: {
outlines: getDefaultOutlines(),
@@ -132,7 +132,7 @@ export const NoOutlines = () => {
},
panelWidths: { 'outlines-panel': DEFAULT_NOTES_PANEL_WIDTH },
isInDesktopOnlyMode: true,
- modularHeaders: []
+ modularHeaders: {}
},
document: {
outlines: [],
diff --git a/src/components/ModularComponents/GroupedItems/GroupedItems.scss b/src/components/ModularComponents/GroupedItems/GroupedItems.scss
new file mode 100644
index 0000000000..e68163fef2
--- /dev/null
+++ b/src/components/ModularComponents/GroupedItems/GroupedItems.scss
@@ -0,0 +1,3 @@
+.GroupedItems {
+ display: flex;
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/GroupedItems/index.js b/src/components/ModularComponents/GroupedItems/index.js
new file mode 100644
index 0000000000..bf4e4bb82f
--- /dev/null
+++ b/src/components/ModularComponents/GroupedItems/index.js
@@ -0,0 +1,3 @@
+import GroupedItems from './GroupedItems';
+
+export default GroupedItems;
\ No newline at end of file
diff --git a/src/components/ModularComponents/Helpers/menuItems.js b/src/components/ModularComponents/Helpers/menuItems.js
new file mode 100644
index 0000000000..7120298b93
--- /dev/null
+++ b/src/components/ModularComponents/Helpers/menuItems.js
@@ -0,0 +1,129 @@
+import React from 'react';
+import DataElements from 'constants/dataElement';
+import { PRESET_BUTTON_TYPES } from 'constants/customizationVariables';
+import ActionButton from 'components/ActionButton';
+
+export const menuItems = {
+ [PRESET_BUTTON_TYPES.UNDO]: {
+ dataElement: 'undoButton',
+ presetDataElement: DataElements.UNDO_PRESET_BUTTON,
+ icon: 'icon-operation-undo',
+ label: 'action.undo',
+ title: 'action.undo',
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.REDO]: {
+ dataElement: 'redoButton',
+ presetDataElement: DataElements.REDO_PRESET_BUTTON,
+ icon: 'icon-operation-redo',
+ label: 'action.redo',
+ title: 'action.redo',
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.FORM_FIELD_EDIT]: {
+ dataElement: 'formFieldEditButton',
+ presetDataElement: DataElements.FORM_FIELD_EDIT_PRESET_BUTTON,
+ icon: 'ic-fill-and-sign',
+ label: 'action.formFieldEditMode',
+ title: 'action.formFieldEditMode',
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.CONTENT_EDIT]: {
+ dataElement: 'contentEditButton',
+ presetDataElement: DataElements.CONTENT_EDIT_PRESET_BUTTON,
+ icon: 'icon-content-edit',
+ label: 'action.contentEditMode',
+ title: 'action.contentEditMode',
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.NEW_DOCUMENT]: {
+ dataElement: DataElements.NEW_DOCUMENT_BUTTON,
+ presetDataElement: DataElements.NEW_DOCUMENT_PRESET_BUTTON,
+ icon: 'icon-plus-sign',
+ label: 'action.newDocument',
+ title: 'action.newDocument',
+ isActive: false,
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.FILE_PICKER]: {
+ dataElement: DataElements.FILE_PICKER_BUTTON,
+ presetDataElement: DataElements.FILE_PICKER_PRESET_BUTTON,
+ icon: 'icon-header-file-picker-line',
+ label: 'action.openFile',
+ title: 'action.openFile',
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.DOWNLOAD]: {
+ dataElement: DataElements.DOWNLOAD_BUTTON,
+ presetDataElement: DataElements.DOWNLOAD_PRESET_BUTTON,
+ icon: 'icon-download',
+ label: 'action.download',
+ title: 'action.download',
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.SAVE_AS]: {
+ dataElement: DataElements.SAVE_AS_BUTTON,
+ presetDataElement: DataElements.SAVE_AS_PRESET_BUTTON,
+ icon: 'icon-save',
+ label: 'saveModal.saveAs',
+ title: 'saveModal.saveAs',
+ isActive: false,
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.PRINT]: {
+ dataElement: DataElements.PRINT_BUTTON,
+ presetDataElement: DataElements.PRINT_PRESET_BUTTON,
+ icon: 'icon-header-print-line',
+ label: 'action.print',
+ title: 'action.print',
+ isActive: false,
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.CREATE_PORTFOLIO]: {
+ dataElement: DataElements.CREATE_PORTFOLIO_BUTTON,
+ presetDataElement: DataElements.CREATE_PORTFOLIO_PRESET_BUTTON,
+ icon: 'icon-pdf-portfolio',
+ label: 'portfolio.createPDFPortfolio',
+ title: 'portfolio.createPDFPortfolio',
+ isActive: false,
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.SETTINGS]: {
+ dataElement: DataElements.SETTINGS_BUTTON,
+ presetDataElement: DataElements.SETTINGS_PRESET_BUTTON,
+ icon: 'icon-header-settings-line',
+ label: 'option.settings.settings',
+ title: 'option.settings.settings',
+ isActive: false,
+ hidden: false,
+ },
+ [PRESET_BUTTON_TYPES.FULLSCREEN]: {
+ dataElement: DataElements.FULLSCREEN_BUTTON,
+ presetDataElement: DataElements.FULLSCREEN_PRESET_BUTTON,
+ icon: 'icon-header-full-screen',
+ label: 'action.enterFullscreen',
+ title: 'action.enterFullscreen',
+ hidden: false,
+ },
+};
+
+export const getPresetButtonDOM = (buttonType, isDisabled, onClick, isFullScreen) => {
+ const { dataElement, presetDataElement } = menuItems[buttonType];
+ let { icon, title } = menuItems[buttonType];
+
+ if (buttonType === PRESET_BUTTON_TYPES.FULLSCREEN) {
+ icon = isFullScreen ? 'icon-header-full-screen-exit' : 'icon-header-full-screen';
+ title = isFullScreen ? 'action.exitFullscreen' : 'action.enterFullscreen';
+ }
+
+ return (
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/ModularComponents/Helpers/mockHeaders.js b/src/components/ModularComponents/Helpers/mockHeaders.js
new file mode 100644
index 0000000000..4ff660e6e1
--- /dev/null
+++ b/src/components/ModularComponents/Helpers/mockHeaders.js
@@ -0,0 +1,301 @@
+/* eslint-disable no-alert */
+const baseButton = {
+ dataElement: 'button',
+ onClick: () => alert('Added'),
+ disabled: false,
+ title: 'Button 1',
+ label: 'Add',
+ type: 'customButton'
+};
+
+const divider = {
+ type: 'divider',
+ dataElement: 'divider-1',
+};
+
+const leftPanelToggle = {
+ dataElement: 'leftPanelToggle',
+ toggleElement: 'leftPanel',
+ disabled: false,
+ title: 'Left Panel',
+ img: 'icon-header-sidebar-line',
+ type: 'toggleButton',
+};
+
+const notesPanelToggle = {
+ dataElement: 'notesPanelToggle',
+ toggleElement: 'notesPanel',
+ disabled: false,
+ title: 'Notes Panel',
+ img: 'icon-header-chat-line',
+ type: 'toggleButton',
+};
+
+// Handy mock buttons
+const button1 = { ...baseButton, dataElement: 'button1', label: 'Button 1' };
+const button2 = { ...baseButton, dataElement: 'button2', label: 'Button 2' };
+const button3 = { ...baseButton, dataElement: 'button3', label: 'Button 3' };
+const button4 = { ...baseButton, dataElement: 'button4', label: 'Button 4' };
+const button5 = { ...baseButton, dataElement: 'button5', label: 'Button 5' };
+const button6 = { ...baseButton, dataElement: 'button6', label: 'Button 6' };
+const button7 = { ...baseButton, dataElement: 'button7', label: 'Button 7' };
+const button8 = { ...baseButton, dataElement: 'button8', label: 'Button 8' };
+const button9 = { ...baseButton, dataElement: 'button9', label: 'Button 9' };
+
+// These are our headers
+const defaultTopHeader = {
+ dataElement: 'defaultHeader',
+ placement: 'top',
+ gap: 20,
+ items: ['button1', 'button2', 'divider', 'button3'],
+};
+
+const floatStartHeader = {
+ dataElement: 'floatStartHeader',
+ placement: 'top',
+ float: true,
+ position: 'start',
+ items: ['button1', 'button2'],
+ gap: 20
+};
+
+const secondFloatStartHeader = {
+ dataElement: 'floatStartHeader-2',
+ placement: 'top',
+ float: true,
+ position: 'start',
+ items: ['button3', 'button4'],
+ gap: 20
+};
+
+const floatCenterHeader = {
+ dataElement: 'floatCenterHeader',
+ placement: 'top',
+ float: true,
+ position: 'center',
+ items: ['button5', 'divider', 'button6'],
+ gap: 20
+};
+
+const floatEndHeader = {
+ dataElement: 'floatEndHeader',
+ placement: 'top',
+ float: true,
+ position: 'end',
+ items: ['button7', 'divider', 'button8', 'button9'],
+ gap: 20
+};
+
+const defaultLeftHeader = {
+ dataElement: 'defaultHeader',
+ placement: 'left',
+ gap: 20,
+ items: ['button1', 'button2', 'divider', 'button3'],
+};
+
+const floatStartLeftHeader = {
+ dataElement: 'floatStartLeftHeader',
+ placement: 'left',
+ float: true,
+ position: 'start',
+ items: ['button3', 'button4', 'leftPanelToggle'],
+ gap: 20
+};
+
+const secondFloatStartLeftHeader = {
+ dataElement: 'secondFloatLeftBottomHeader',
+ placement: 'left',
+ float: true,
+ position: 'start',
+ items: ['button5', 'button6'],
+ gap: 20
+};
+
+const floatCenterLeftHeader = {
+ dataElement: 'floatCenterLeftHeader',
+ placement: 'left',
+ float: true,
+ position: 'center',
+ items: ['button1', 'button2'],
+ gap: 20
+};
+
+const floatEndLeftHeader = {
+ dataElement: 'floatEndLeftHeader',
+ placement: 'left',
+ float: true,
+ position: 'end',
+ items: ['button7', 'button8', 'divider', 'button9'],
+ gap: 20
+};
+
+const defaultRightHeader = {
+ dataElement: 'defaultHeader',
+ placement: 'right',
+ gap: 20,
+ items: ['button1', 'button2', 'divider', 'button3'],
+};
+
+const floatStartRightHeader = {
+ dataElement: 'floatStartRightHeader',
+ placement: 'right',
+ float: true,
+ position: 'start',
+ items: ['button3', 'button4', 'notesPanelToggle'],
+ gap: 20
+};
+
+const secondFloatStartRightHeader = {
+ dataElement: 'secondFloatRightBottomHeader',
+ placement: 'right',
+ float: true,
+ position: 'start',
+ items: ['button5', 'button6'],
+ gap: 20
+};
+
+const floatCenterRightHeader = {
+ dataElement: 'floatCenterRightHeader',
+ placement: 'right',
+ float: true,
+ position: 'center',
+ items: ['button1', 'button2'],
+ gap: 20
+};
+
+const floatEndRightHeader = {
+ dataElement: 'floatEndRightHeader',
+ placement: 'right',
+ float: true,
+ position: 'end',
+ items: ['button7', 'button8', 'divider', 'button9'],
+ gap: 20
+};
+
+const defaultBottomHeader = {
+ dataElement: 'defaultBottomHeader',
+ placement: 'bottom',
+ gap: 20,
+ items: ['button1', 'button2', 'divider', 'button3'],
+ getDimensionTotal: () => {
+ return 32;
+ }
+};
+
+const floatStartBottomHeader = {
+ dataElement: 'floatStartBottomHeader',
+ placement: 'bottom',
+ float: true,
+ position: 'start',
+ items: ['button3', 'button4'],
+ gap: 20
+};
+
+const secondFloatStartBottomHeader = {
+ dataElement: 'secondFloatStartBottomHeader',
+ placement: 'bottom',
+ float: true,
+ position: 'start',
+ items: ['button5', 'button6'],
+ gap: 20
+};
+
+const floatCenterBottomHeader = {
+ dataElement: 'floatCenterBottomHeader',
+ placement: 'bottom',
+ float: true,
+ position: 'center',
+ items: ['button1', 'button2'],
+ gap: 20
+};
+
+const floatEndBottomHeader = {
+ dataElement: 'floatEndBottomHeader',
+ placement: 'bottom',
+ float: true,
+ position: 'end',
+ items: ['button7', 'button8', 'divider', 'button9'],
+ gap: 20
+};
+
+const floatStarTopHeaderStatic = {
+ dataElement: 'floatStarTopHeaderStatic',
+ placement: 'top',
+ float: true,
+ position: 'start',
+ items: ['button1', 'button2'],
+ opacityMode: 'static',
+ opacity: 'full',
+};
+
+const floatCenterTopHeaderDynamic = {
+ dataElement: 'floatStarTopHeaderDynamic',
+ placement: 'top',
+ float: true,
+ position: 'center',
+ items: ['button1', 'button2'],
+ opacityMode: 'dynamic',
+ opacity: 'low',
+};
+
+const floatEndTopHeaderNone = {
+ dataElement: 'floatStarTopHeaderNone',
+ placement: 'top',
+ float: true,
+ position: 'end',
+ items: ['button1', 'button2'],
+ opacityMode: 'dynamic',
+ opacity: 'none',
+};
+
+const mockModularComponents = {
+ 'button1': button1,
+ 'button2': button2,
+ 'button3': button3,
+ 'button4': button4,
+ 'button5': button5,
+ 'button6': button6,
+ 'button7': button7,
+ 'button8': button8,
+ 'button9': button9,
+ 'divider': divider,
+ 'leftPanelToggle': leftPanelToggle,
+ 'notesPanelToggle': notesPanelToggle,
+};
+
+export {
+ button1,
+ button2,
+ button3,
+ button4,
+ button5,
+ button6,
+ button7,
+ button8,
+ button9,
+ defaultTopHeader,
+ floatStartHeader,
+ secondFloatStartHeader,
+ floatCenterHeader,
+ floatEndHeader,
+ defaultLeftHeader,
+ floatStartLeftHeader,
+ secondFloatStartLeftHeader,
+ floatCenterLeftHeader,
+ floatEndLeftHeader,
+ defaultRightHeader,
+ floatStartRightHeader,
+ secondFloatStartRightHeader,
+ floatCenterRightHeader,
+ floatEndRightHeader,
+ defaultBottomHeader,
+ floatStartBottomHeader,
+ secondFloatStartBottomHeader,
+ floatCenterBottomHeader,
+ floatEndBottomHeader,
+ floatStarTopHeaderStatic,
+ floatCenterTopHeaderDynamic,
+ floatEndTopHeaderNone,
+ // Modular stuff
+ mockModularComponents,
+};
\ No newline at end of file
diff --git a/src/components/ModularComponents/Helpers/validation-helper.js b/src/components/ModularComponents/Helpers/validation-helper.js
new file mode 100644
index 0000000000..44c9e533f9
--- /dev/null
+++ b/src/components/ModularComponents/Helpers/validation-helper.js
@@ -0,0 +1,26 @@
+import { JUSTIFY_CONTENT } from 'constants/customizationVariables';
+
+export const isJustifyContentValid = (justifyContent) => {
+ const validJustifications = Object.values(JUSTIFY_CONTENT);
+ if (!validJustifications.includes(justifyContent)) {
+ console.warn(`${justifyContent} is not a valid value for justifyContent. Please use one of the following: ${validJustifications}`);
+ return false;
+ }
+ return true;
+};
+
+export const isGapValid = (gap) => {
+ if (isNaN(gap) || gap < 0) {
+ console.warn(`${gap} is not a valid value for gap. Please use a number, which represents the gap between items in pixels.`);
+ return false;
+ }
+ return true;
+};
+
+export const isGrowValid = (grow) => {
+ if (isNaN(grow) || grow < 0) {
+ console.warn(`${grow} is not a valid value for grow. Please use a number, which represents the flex-grow property of item.`);
+ return false;
+ }
+ return true;
+};
\ No newline at end of file
diff --git a/src/components/ModularComponents/InnerItem/index.js b/src/components/ModularComponents/InnerItem/index.js
new file mode 100644
index 0000000000..9549d3edc3
--- /dev/null
+++ b/src/components/ModularComponents/InnerItem/index.js
@@ -0,0 +1,3 @@
+import InnerItem from './InnerItem';
+
+export default InnerItem;
\ No newline at end of file
diff --git a/src/components/ModularComponents/LeftHeader/LeftHeader.scss b/src/components/ModularComponents/LeftHeader/LeftHeader.scss
new file mode 100644
index 0000000000..313ddc7cfe
--- /dev/null
+++ b/src/components/ModularComponents/LeftHeader/LeftHeader.scss
@@ -0,0 +1,20 @@
+@import '../../../constants/styles.scss';
+
+.LeftHeader {
+ height: 100%;
+ width: $left-header-width;
+ min-width: fit-content;
+ padding: 12px 8px;
+}
+
+.LeftHeader.closed {
+ position: fixed;
+ left: 0;
+ display: none;
+}
+
+.LeftHeader.stroke {
+ border-right-width: 1px;
+ border-right-style: solid;
+ border-right-color: var(--gray-5);
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/LeftHeader/index.js b/src/components/ModularComponents/LeftHeader/index.js
new file mode 100644
index 0000000000..aa454c14b8
--- /dev/null
+++ b/src/components/ModularComponents/LeftHeader/index.js
@@ -0,0 +1,3 @@
+import LeftHeader from './LeftHeaderContainer';
+
+export default LeftHeader;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PageControls/PageControls.js b/src/components/ModularComponents/PageControls/PageControls.js
new file mode 100644
index 0000000000..c2f0f1a6dd
--- /dev/null
+++ b/src/components/ModularComponents/PageControls/PageControls.js
@@ -0,0 +1,119 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import CustomButton from '../CustomButton';
+import ToggleElementButton from '../ToggleElementButton';
+import { isMobileSize } from 'helpers/getDeviceSize';
+import { DIRECTION } from 'constants/customizationVariables';
+import { useTranslation } from 'react-i18next';
+import './PageControls.scss';
+
+function PageControls(props) {
+ const {
+ size,
+ dataElement,
+ onFlyoutToggle,
+ leftChevron,
+ rightChevron,
+ currentPage,
+ totalPages,
+ elementRef,
+ headerDirection,
+ onBlur,
+ onFocus,
+ onClick,
+ onChange,
+ onSubmit,
+ isFocused,
+ input,
+ inputRef,
+ allowPageNavigation,
+ } = props;
+
+ const { t } = useTranslation();
+ const isMobile = isMobileSize();
+ let inputWidth = 0;
+ if (input) {
+ inputWidth = 26 + input.length * (isMobile ? 10 : 7);
+ }
+
+ const style = { width: inputWidth };
+ if (headerDirection === DIRECTION.COLUMN) {
+ style.minHeight = 32;
+ }
+
+ return (
+
+ {size === 0 && <>
+
+
+
+
{totalPages}
+
+ >}
+ {size === 1 &&
+
+ }
+
+
+
+
+ );
+}
+
+PageControls.propTypes = {
+ size: PropTypes.number,
+ dataElement: PropTypes.string.isRequired,
+ onFlyoutToggle: PropTypes.func,
+ leftChevron: PropTypes.object,
+ rightChevron: PropTypes.object,
+ currentPage: PropTypes.number,
+ totalPages: PropTypes.number,
+ elementRef: PropTypes.any,
+ headerDirection: PropTypes.string,
+ onBlur: PropTypes.func,
+ onFocus: PropTypes.func,
+ onClick: PropTypes.func,
+ onChange: PropTypes.func,
+ onSubmit: PropTypes.func,
+ isFocused: PropTypes.bool,
+ input: PropTypes.string,
+ inputRef: PropTypes.any,
+ allowPageNavigation: PropTypes.bool,
+};
+
+export default PageControls;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PageControls/PageControls.scss b/src/components/ModularComponents/PageControls/PageControls.scss
new file mode 100644
index 0000000000..8da2251226
--- /dev/null
+++ b/src/components/ModularComponents/PageControls/PageControls.scss
@@ -0,0 +1,74 @@
+@import '../../../constants/styles';
+@import '../../../constants/overlay';
+
+.PageControlsWrapper {
+ display: flex;
+ gap: 8px;
+ flex-direction: row;
+ justify-content: flex-start;
+ flex-grow: 0;
+
+ .total-page {
+ margin: auto;
+ }
+
+ .paddingTop {
+ padding-top: 4px;
+ }
+
+ .paddingLeft {
+ padding-left: 4px;
+ }
+
+ form {
+ height: 100%;
+ }
+
+ form input {
+ height: 100%;
+ text-align: center;
+ font-family: Lato, sans-serif;
+ font-weight: 700;
+ background: var(--gray-0);
+ cursor: pointer;
+ outline: none;
+ border-radius: 3px;
+ min-width: 32px;
+ }
+}
+
+.pageNavFlyoutMenu {
+ .page-nav-display .flyout-item-label>* {
+ margin: auto;
+ }
+
+ form input {
+ font-size: 13px !important;
+ width: 17px;
+ font-family: Lato, sans-serif;
+ cursor: pointer;
+ outline: none;
+ margin: 0px 6px;
+ padding: 0;
+ text-align: center;
+ background: transparent;
+
+ min-width: 26px;
+ min-height: 22px;
+ }
+
+ form input:not(:focus) {
+ font-weight: 700;
+ border: none;
+ border-bottom: 1px solid var(--icon-color);
+ border-radius: 0;
+ }
+
+ form input:focus {
+ height: 100%;
+ background: var(--gray-0);
+ border-radius: 3px;
+ border: 1px solid var(--border);
+ min-height: 26px;
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/PageControls/PageControls.spec.js b/src/components/ModularComponents/PageControls/PageControls.spec.js
new file mode 100644
index 0000000000..63ac8dac5c
--- /dev/null
+++ b/src/components/ModularComponents/PageControls/PageControls.spec.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import PageControls from './PageControls';
+import core from 'core';
+
+const PageControlWithRedux = withProviders(PageControls);
+
+const props = {
+ dataElement: 'page-controls-container',
+ size: 0,
+ leftChevron: {
+ dataElement: 'leftChevronBtn',
+ title: 'action.pagePrev',
+ label: null,
+ img: 'icon-chevron-up',
+ type: 'customButton',
+ disabled: false,
+ ariaLabel: 'action.pagePrev',
+ onClick: jest.fn(),
+ },
+ rightChevron: {
+ dataElement: 'rightChevronBtn',
+ title: 'action.pageNext',
+ label: null,
+ img: 'icon-chevron-right',
+ type: 'customButton',
+ disabled: false,
+ ariaLabel: 'action.pageNext',
+ onClick: jest.fn(),
+ },
+ input: '7',
+ totalPages: 11,
+ onChange: jest.fn(),
+};
+
+describe('Page Controls Container component', () => {
+ beforeEach(() => {
+ const documentViewer = core.setDocumentViewer(1, new window.Core.DocumentViewer());
+ documentViewer.doc = new window.Core.Document('dummy', 'pdf');
+ });
+
+ it('Should be able to find input and check input value', () => {
+ render(
);
+ const input = screen.getByRole('textbox');
+ expect(input.value).toEqual(props.input);
+ });
+
+ it('Should be able to type into input of Page Controls', () => {
+ render(
);
+ const input = screen.getByRole('textbox');
+ userEvent.type(input, '8');
+ expect(input.value).toEqual(props.input);
+ });
+
+ it('Should call onClick on left/right button on Page Controls component', () => {
+ render(
);
+ const leftBtn = screen.getByRole('button', { name: 'action.pagePrev' });
+ const rightBtn = screen.getByRole('button', { name: 'action.pageNext' });
+ expect(leftBtn).toBeInTheDocument();
+ expect(rightBtn).toBeInTheDocument();
+ fireEvent.click(leftBtn);
+ fireEvent.click(rightBtn);
+ expect(props.leftChevron.onClick).toHaveBeenCalledTimes(1);
+ expect(props.rightChevron.onClick).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/ModularComponents/PageControls/PageControlsFlyout.js b/src/components/ModularComponents/PageControls/PageControlsFlyout.js
new file mode 100644
index 0000000000..406e6ef3d1
--- /dev/null
+++ b/src/components/ModularComponents/PageControls/PageControlsFlyout.js
@@ -0,0 +1,57 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+function PageControlsFlyout(props) {
+ const {
+ onSubmit,
+ onChange,
+ input,
+ totalPages,
+ inputWidth,
+ } = props;
+ const [isFocused, setIsFocused] = useState(false);
+
+ const onBlur = () => {
+ setIsFocused(false);
+ props.onBlur();
+ };
+
+ const onFocus = () => {
+ setIsFocused(true);
+ props.onFocus();
+ };
+
+ const style = {};
+ if (isFocused) {
+ style.width = inputWidth;
+ } else {
+ style.width = inputWidth - 10;
+ }
+
+ return (
+
+
{'Pages: '}
+
+
{` of ${totalPages}`}
+
+ );
+}
+
+PageControlsFlyout.propTypes = {
+ onSubmit: PropTypes.func,
+ onChange: PropTypes.func,
+ onFocus: PropTypes.func,
+ onBlur: PropTypes.func,
+ input: PropTypes.string,
+ totalPages: PropTypes.number,
+ inputWidth: PropTypes.number,
+};
+
+export default PageControlsFlyout;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PageControls/index.js b/src/components/ModularComponents/PageControls/index.js
new file mode 100644
index 0000000000..df6986a39e
--- /dev/null
+++ b/src/components/ModularComponents/PageControls/index.js
@@ -0,0 +1,3 @@
+import PageControls from './PageControlsContainer';
+
+export default PageControls;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/PresetButton.js b/src/components/ModularComponents/PresetButton/PresetButton.js
new file mode 100644
index 0000000000..d0ca16cbd8
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/PresetButton.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { PRESET_BUTTON_TYPES } from 'constants/customizationVariables';
+import './PresetButton.scss';
+import NewDocumentButton from './buttons/NewDocument';
+import FilePickerButton from './buttons/FilePicker';
+import UndoButton from './buttons/Undo';
+import RedoButton from './buttons/Redo';
+import DownloadButton from './buttons/Download';
+import FullScreenButton from './buttons/FullScreen';
+import SaveAsButton from './buttons/SaveAs';
+import PrintButton from './buttons/Print';
+import NewPortfolioButton from './buttons/NewPortfolio';
+import SettingsButton from './buttons/Settings';
+import FormFieldEditButton from './buttons/FormFieldEdit';
+import ContentEditButton from './buttons/ContentEdit';
+
+const PresetButton = (props) => {
+ const { buttonType } = props;
+
+ switch (buttonType) {
+ case PRESET_BUTTON_TYPES.UNDO:
+ return
;
+ case PRESET_BUTTON_TYPES.REDO:
+ return
;
+ case PRESET_BUTTON_TYPES.NEW_DOCUMENT:
+ return
;
+ case PRESET_BUTTON_TYPES.FILE_PICKER:
+ return
;
+ case PRESET_BUTTON_TYPES.DOWNLOAD:
+ return
;
+ case PRESET_BUTTON_TYPES.FULLSCREEN:
+ return
;
+ case PRESET_BUTTON_TYPES.SAVE_AS:
+ return
;
+ case PRESET_BUTTON_TYPES.PRINT:
+ return
;
+ case PRESET_BUTTON_TYPES.CREATE_PORTFOLIO:
+ return
;
+ case PRESET_BUTTON_TYPES.SETTINGS:
+ return
;
+ case PRESET_BUTTON_TYPES.FORM_FIELD_EDIT:
+ return
;
+ case PRESET_BUTTON_TYPES.CONTENT_EDIT:
+ return
;
+ default:
+ console.warn(`${buttonType} is not a valid item type.`);
+ return null;
+ }
+};
+
+PresetButton.propTypes = {
+ buttonType: PropTypes.string.isRequired
+};
+
+export default PresetButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/PresetButton.scss b/src/components/ModularComponents/PresetButton/PresetButton.scss
new file mode 100644
index 0000000000..b0df6a3610
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/PresetButton.scss
@@ -0,0 +1,19 @@
+@import '../../../../src/constants/styles';
+
+.PresetButton {
+ &:hover {
+ background: var(--tools-button-hover);
+ }
+
+ &.formFieldEditButton, &.contentEditButton {
+ &.active {
+ background: var(--tools-button-active);
+ color: var(--view-header-icon-active-fill);
+ cursor: default;
+
+ .Icon {
+ color: var(--view-header-icon-active-fill)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/PresetButton.stories.js b/src/components/ModularComponents/PresetButton/PresetButton.stories.js
new file mode 100644
index 0000000000..44494bd676
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/PresetButton.stories.js
@@ -0,0 +1,74 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import initialState from 'src/redux/initialState';
+import PresetButton from './PresetButton';
+import { PRESET_BUTTON_TYPES } from 'src/constants/customizationVariables';
+
+export default {
+ title: 'ModularComponents/PresetButton',
+ component: PresetButton,
+};
+
+initialState.viewer.activeDocumentViewerKey = 1;
+const store = configureStore({ reducer: () => initialState });
+
+const prepareButtonStory = (buttonType) => {
+ const props = {
+ buttonType: buttonType,
+ };
+
+ return (
+
+
+
+ );
+};
+
+export function UndoButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.UNDO);
+}
+
+export function RedoButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.REDO);
+}
+
+export function NewDocumentButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.NEW_DOCUMENT);
+}
+
+export function FilePickerButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.FILE_PICKER);
+}
+
+export function DownloadButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.DOWNLOAD);
+}
+
+export function FullscreenButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.FULLSCREEN);
+}
+
+export function SaveAsButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.SAVE_AS);
+}
+
+export function PrintButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.PRINT);
+}
+
+export function CreatePortfolioButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.CREATE_PORTFOLIO);
+}
+
+export function SettingsButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.SETTINGS);
+}
+
+export function FormFieldEditButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.FORM_FIELD_EDIT);
+}
+
+export function ContentEditButton() {
+ return prepareButtonStory(PRESET_BUTTON_TYPES.CONTENT_EDIT);
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/buttons/FullScreen.js b/src/components/ModularComponents/PresetButton/buttons/FullScreen.js
new file mode 100644
index 0000000000..031f514502
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/buttons/FullScreen.js
@@ -0,0 +1,43 @@
+import { useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
+import selectors from 'selectors';
+import { useTranslation } from 'react-i18next';
+import toggleFullscreen from 'helpers/toggleFullscreen';
+import { innerItemToFlyoutItem } from 'src/helpers/itemToFlyoutHelper';
+import { getPresetButtonDOM } from '../../Helpers/menuItems';
+import { PRESET_BUTTON_TYPES } from 'src/constants/customizationVariables';
+
+/**
+ * A button that toggles fullscreen mode.
+ * @name fullscreenButton
+ * @memberof UI.Components.PresetButton
+ */
+const FullScreenButton = (props) => {
+ const { isFlyoutItem, iconDOMElement } = props;
+ const { t } = useTranslation();
+ const [
+ isFullScreen,
+ ] = useSelector(
+ (state) => [
+ selectors.isFullScreen(state),
+ ],
+ );
+
+ return (
+ isFlyoutItem ?
+ innerItemToFlyoutItem({
+ isDisabled: false,
+ icon: iconDOMElement,
+ label: isFullScreen ? t('action.exitFullscreen') : t('action.enterFullscreen'),
+ }, toggleFullscreen)
+ :
+ getPresetButtonDOM(PRESET_BUTTON_TYPES.FULLSCREEN, false, toggleFullscreen, isFullScreen)
+ );
+};
+
+FullScreenButton.propTypes = {
+ isFlyoutItem: PropTypes.bool,
+ iconDOMElement: PropTypes.object,
+};
+
+export default FullScreenButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/buttons/Print.js b/src/components/ModularComponents/PresetButton/buttons/Print.js
new file mode 100644
index 0000000000..17530ff243
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/buttons/Print.js
@@ -0,0 +1,62 @@
+import { useDispatch, shallowEqual, useSelector } from 'react-redux';
+import PropTypes from 'prop-types';
+import { getPresetButtonDOM, menuItems } from '../../Helpers/menuItems';
+import { print } from 'helpers/print';
+import selectors from 'selectors';
+import core from 'core';
+import { innerItemToFlyoutItem } from 'helpers/itemToFlyoutHelper';
+import { useTranslation } from 'react-i18next';
+import { PRESET_BUTTON_TYPES } from 'constants/customizationVariables';
+
+/**
+ * A button that prints the document.
+ * @name printButton
+ * @memberof UI.Components.PresetButton
+ */
+const PrintButton = (props) => {
+ const { isFlyoutItem, iconDOMElement } = props;
+ const { label } = menuItems.printButton;
+ const dispatch = useDispatch();
+ const { t } = useTranslation();
+
+ const [
+ isEmbedPrintSupported,
+ sortStrategy,
+ colorMap,
+ timezone,
+ ] = useSelector(
+ (state) => [
+ selectors.isEmbedPrintSupported(state),
+ selectors.getSortStrategy(state),
+ selectors.getColorMap(state),
+ selectors.getTimezone(state),
+ ],
+ shallowEqual,
+ );
+
+ const handlePrintButtonClick = () => {
+ print(dispatch, isEmbedPrintSupported, sortStrategy, colorMap, { isGrayscale: core.getDocumentViewer().isGrayscaleModeEnabled(), timezone });
+ };
+
+ return (
+ isFlyoutItem ?
+ innerItemToFlyoutItem({
+ isDisabled: false,
+ icon: iconDOMElement,
+ label: t(label),
+ }, handlePrintButtonClick)
+ :
+ getPresetButtonDOM(
+ PRESET_BUTTON_TYPES.PRINT,
+ false,
+ handlePrintButtonClick
+ )
+ );
+};
+
+PrintButton.propTypes = {
+ isFlyoutItem: PropTypes.bool,
+ iconDOMElement: PropTypes.object,
+};
+
+export default PrintButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/buttons/Redo.js b/src/components/ModularComponents/PresetButton/buttons/Redo.js
new file mode 100644
index 0000000000..5c4b2b2103
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/buttons/Redo.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import selectors from 'selectors';
+import PropTypes from 'prop-types';
+import ActionButton from 'components/ActionButton';
+import { menuItems } from '../../Helpers/menuItems';
+import core from 'core';
+
+/**
+ * A button that performs the redo action.
+ * @name redoButton
+ * @memberof UI.Components.PresetButton
+ */
+const RedoButton = (props) => {
+ const { isFlyoutItem, iconDOMElement } = props;
+ const { label, presetDataElement, icon, title } = menuItems.redoButton;
+ const activeDocumentViewerKey = useSelector((state) => selectors.getActiveDocumentViewerKey(state));
+ const { t } = useTranslation();
+
+ const handleClick = () => {
+ core.redo(activeDocumentViewerKey);
+ };
+
+ const onKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ handleClick();
+ }
+ };
+
+ return (
+ isFlyoutItem ?
+ (
+
+
+ {iconDOMElement}
+ {label &&
{t(label)}
}
+
+
+ )
+ : (
+
!state.viewer.canRedo[state.viewer.activeDocumentViewerKey]}
+ />
+ )
+ );
+};
+
+RedoButton.propTypes = {
+ isFlyoutItem: PropTypes.bool,
+ iconDOMElement: PropTypes.object,
+};
+
+export default RedoButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/buttons/Settings.js b/src/components/ModularComponents/PresetButton/buttons/Settings.js
new file mode 100644
index 0000000000..005196fcb2
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/buttons/Settings.js
@@ -0,0 +1,42 @@
+import { useDispatch } from 'react-redux';
+import actions from 'actions';
+import PropTypes from 'prop-types';
+import { getPresetButtonDOM, menuItems } from '../../Helpers/menuItems';
+import DataElements from 'constants/dataElement';
+import { PRESET_BUTTON_TYPES } from 'constants/customizationVariables';
+import { innerItemToFlyoutItem } from 'helpers/itemToFlyoutHelper';
+import { useTranslation } from 'react-i18next';
+
+/**
+ * A button that opens the settings modal.
+ * @name settingsButton
+ * @memberof UI.Components.PresetButton
+ */
+const SettingsButton = (props) => {
+ const { isFlyoutItem, iconDOMElement } = props;
+ const { label } = menuItems.settingsButton;
+ const dispatch = useDispatch();
+ const { t } = useTranslation();
+
+ const handleSettingsButtonClick = () => {
+ dispatch(actions.openElement(DataElements.SETTINGS_MODAL));
+ };
+
+ return (
+ isFlyoutItem ?
+ innerItemToFlyoutItem({
+ isDisabled: false,
+ icon: iconDOMElement,
+ label: t(label),
+ }, handleSettingsButtonClick)
+ :
+ getPresetButtonDOM(PRESET_BUTTON_TYPES.SETTINGS, false, handleSettingsButtonClick)
+ );
+};
+
+SettingsButton.propTypes = {
+ isFlyoutItem: PropTypes.bool,
+ iconDOMElement: PropTypes.object,
+};
+
+export default SettingsButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/buttons/Undo.js b/src/components/ModularComponents/PresetButton/buttons/Undo.js
new file mode 100644
index 0000000000..07ca5b8200
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/buttons/Undo.js
@@ -0,0 +1,61 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import selectors from 'selectors';
+import PropTypes from 'prop-types';
+import ActionButton from 'components/ActionButton';
+import { menuItems } from '../../Helpers/menuItems';
+import core from 'core';
+
+/**
+ * A button that performs the undo action.
+ * @name undoButton
+ * @memberof UI.Components.PresetButton
+ */
+const UndoButton = (props) => {
+ const { isFlyoutItem, iconDOMElement } = props;
+ const { label, presetDataElement, icon, title } = menuItems.undoButton;
+ const activeDocumentViewerKey = useSelector((state) => selectors.getActiveDocumentViewerKey(state));
+ const { t } = useTranslation();
+
+ const handleClick = () => {
+ core.undo(activeDocumentViewerKey);
+ };
+
+ const onKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ handleClick();
+ }
+ };
+
+ return (
+ isFlyoutItem ?
+ (
+
+
+ {iconDOMElement}
+ {label &&
{t(label)}
}
+
+
+ )
+ : (
+ !state.viewer.canUndo[state.viewer.activeDocumentViewerKey]}
+ />
+ )
+ );
+};
+
+UndoButton.propTypes = {
+ isFlyoutItem: PropTypes.bool,
+ iconDOMElement: PropTypes.object,
+};
+
+export default UndoButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/PresetButton/index.js b/src/components/ModularComponents/PresetButton/index.js
new file mode 100644
index 0000000000..204e7d4a6d
--- /dev/null
+++ b/src/components/ModularComponents/PresetButton/index.js
@@ -0,0 +1,3 @@
+import PresetButton from './PresetButton';
+
+export default PresetButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/RibbonGroup/index.js b/src/components/ModularComponents/RibbonGroup/index.js
new file mode 100644
index 0000000000..7301b1f53d
--- /dev/null
+++ b/src/components/ModularComponents/RibbonGroup/index.js
@@ -0,0 +1,3 @@
+import RibbonGroup from './RibbonGroup';
+
+export default RibbonGroup;
\ No newline at end of file
diff --git a/src/components/ModularComponents/RibbonItem/RibbonItem.scss b/src/components/ModularComponents/RibbonItem/RibbonItem.scss
new file mode 100644
index 0000000000..98e531dd7c
--- /dev/null
+++ b/src/components/ModularComponents/RibbonItem/RibbonItem.scss
@@ -0,0 +1,57 @@
+.RibbonItem {
+ padding: 0;
+ border: none;
+ background-color: transparent;
+ cursor: pointer;
+ color: var(--faded-text);
+ white-space: nowrap;
+ height: auto;
+
+ .Button {
+ width: 100%;
+ padding: 8px;
+ column-gap: 8px;
+ row-gap: 4px;
+ color: var(--faded-text);
+ &:hover {
+ background-color: var(--blue-1);
+ }
+ &.active {
+ color: var(--ribbon-active-color);
+ background-color: transparent;
+ .Icon {
+ color: var(--ribbon-active-color);
+ }
+ }
+ }
+
+ &:not(.vertical) .Button {
+ &.active {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+ border-bottom: 2px solid var(--ribbon-active-color);
+ }
+ }
+
+ &.vertical {
+ white-space: wrap;
+ .Button {
+ flex-direction: column;
+ height: 100%;
+ &.active {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0;
+ border-left: 2px solid var(--ribbon-active-color);
+ }
+ }
+ }
+
+ &.vertical:not(.left) .Button {
+ &.active {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0;
+ border-left: none;
+ border-right: 2px solid var(--ribbon-active-color);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/RibbonItem/index.js b/src/components/ModularComponents/RibbonItem/index.js
new file mode 100644
index 0000000000..a44f180ef1
--- /dev/null
+++ b/src/components/ModularComponents/RibbonItem/index.js
@@ -0,0 +1,3 @@
+import RibbonItem from './RibbonItem';
+
+export default RibbonItem;
\ No newline at end of file
diff --git a/src/components/ModularComponents/RibbonOverflowFlyout/RibbonOverflowFlyout.js b/src/components/ModularComponents/RibbonOverflowFlyout/RibbonOverflowFlyout.js
new file mode 100644
index 0000000000..c21fce7d1f
--- /dev/null
+++ b/src/components/ModularComponents/RibbonOverflowFlyout/RibbonOverflowFlyout.js
@@ -0,0 +1,22 @@
+import { useEffect } from 'react';
+import { useDispatch } from 'react-redux';
+import actions from 'actions';
+import './RibbonOverflowFlyout.scss';
+
+const RibbonOverflowFlyout = (props) => {
+ const dispatch = useDispatch();
+ const { items } = props;
+
+ useEffect(() => {
+ const RibbonOverflowFlyout = {
+ dataElement: 'RibbonOverflowFlyout',
+ className: 'RibbonOverflowFlyout',
+ items: items || [],
+ };
+ dispatch(actions.addFlyout(RibbonOverflowFlyout));
+ }, []);
+
+ return null;
+};
+
+export default RibbonOverflowFlyout;
diff --git a/src/components/ModularComponents/RibbonOverflowFlyout/RibbonOverflowFlyout.scss b/src/components/ModularComponents/RibbonOverflowFlyout/RibbonOverflowFlyout.scss
new file mode 100644
index 0000000000..58f7019b2a
--- /dev/null
+++ b/src/components/ModularComponents/RibbonOverflowFlyout/RibbonOverflowFlyout.scss
@@ -0,0 +1,11 @@
+.Flyout .RibbonOverflowFlyout {
+ .RibbonItem {
+ width: 100%;
+ .Button {
+ justify-content: left;
+ &:hover {
+ background-color: transparent;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/RibbonOverflowFlyout/index.js b/src/components/ModularComponents/RibbonOverflowFlyout/index.js
new file mode 100644
index 0000000000..cade412da3
--- /dev/null
+++ b/src/components/ModularComponents/RibbonOverflowFlyout/index.js
@@ -0,0 +1,3 @@
+import RibbonOverflowFlyout from './RibbonOverflowFlyout';
+
+export default RibbonOverflowFlyout;
\ No newline at end of file
diff --git a/src/components/ModularComponents/RightHeader/RightHeader.scss b/src/components/ModularComponents/RightHeader/RightHeader.scss
new file mode 100644
index 0000000000..ff7d1e55df
--- /dev/null
+++ b/src/components/ModularComponents/RightHeader/RightHeader.scss
@@ -0,0 +1,19 @@
+@import '../../../constants/styles';
+
+.RightHeader {
+ height: 100%;
+ width: $right-header-width;
+ min-width: fit-content;
+ z-index: 63;
+ padding: 12px 8px;
+}
+
+.RightHeader.closed {
+ display: none;
+}
+
+.RightHeader.stroke {
+ border-left-width: 1px;
+ border-left-style: solid;
+ border-left-color: var(--gray-5);
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/RightHeader/index.js b/src/components/ModularComponents/RightHeader/index.js
new file mode 100644
index 0000000000..48ec4457e1
--- /dev/null
+++ b/src/components/ModularComponents/RightHeader/index.js
@@ -0,0 +1,3 @@
+import RightHeader from './RightHeaderContainer';
+
+export default RightHeader;
\ No newline at end of file
diff --git a/src/components/ModularComponents/StatefulButton/StatefulButton.js b/src/components/ModularComponents/StatefulButton/StatefulButton.js
new file mode 100644
index 0000000000..0333abe22f
--- /dev/null
+++ b/src/components/ModularComponents/StatefulButton/StatefulButton.js
@@ -0,0 +1,91 @@
+import React, { useEffect } from 'react';
+import '../../Button/Button.scss';
+import './StatefulButton.scss';
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import Button from 'components/Button';
+
+const StatefulButton = (props) => {
+ const { dataElement, disabled, mount, unmount, states } = props;
+ const [, updateState] = React.useState();
+ const forceUpdate = React.useCallback(() => updateState({}), []);
+
+ const [activeState, setActiveState] = React.useState(props.initialState);
+
+ useEffect(() => {
+ if (mount) {
+ mount(update);
+ }
+ return function() {
+ if (unmount) {
+ unmount();
+ }
+ };
+ });
+
+ const update = (newState) => {
+ if (newState) {
+ setActiveState(newState);
+ } else {
+ forceUpdate();
+ }
+ };
+
+ const onClick = () => {
+ const { dispatch } = props;
+
+ states[activeState].onClick(
+ update,
+ states[activeState],
+ dispatch,
+ );
+ };
+
+ const { title, img, getContent, isActive } = states[activeState];
+ const content = getContent ? getContent(states[activeState]) : '';
+ const className = [
+ 'StatefulButton',
+ states[activeState].className ? states[activeState].className : '',
+ ].join(' ').trim();
+
+ return (
+
+ );
+};
+
+StatefulButton.propTypes = {
+ initialState: PropTypes.string.isRequired,
+ mount: PropTypes.func.isRequired,
+ unmount: PropTypes.func,
+ states: PropTypes.shape({
+ activeState: PropTypes.shape({
+ img: PropTypes.string,
+ label: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired,
+ getContent: PropTypes.func.isRequired,
+ }),
+ AnotherState: PropTypes.shape({
+ img: PropTypes.string,
+ label: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ title: PropTypes.string.isRequired,
+ getContent: PropTypes.func.isRequired,
+ }),
+ }),
+};
+
+export default React.memo(StatefulButton);
diff --git a/src/components/ModularComponents/StatefulButton/StatefulButton.scss b/src/components/ModularComponents/StatefulButton/StatefulButton.scss
new file mode 100644
index 0000000000..1700e53217
--- /dev/null
+++ b/src/components/ModularComponents/StatefulButton/StatefulButton.scss
@@ -0,0 +1,7 @@
+@import '../../../constants/styles';
+
+.StatefulButton {
+ padding: 5px;
+ width: fit-content;
+ background-color: var(--gray-4);
+}
\ No newline at end of file
diff --git a/src/components/ModularComponents/StatefulButton/StatefulButton.stories.js b/src/components/ModularComponents/StatefulButton/StatefulButton.stories.js
new file mode 100644
index 0000000000..b628d65961
--- /dev/null
+++ b/src/components/ModularComponents/StatefulButton/StatefulButton.stories.js
@@ -0,0 +1,75 @@
+import React from 'react';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+import StatefulButtonComponent from './StatefulButton';
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ openElements: [],
+ }
+};
+function rootReducer(state = initialState) {
+ return state;
+}
+
+const store = createStore(rootReducer);
+
+const BasicComponent = (props) => {
+ return (
+
+
+
+ );
+};
+
+export default {
+ title: 'ModularComponents/StatefulButton',
+ component: StatefulButtonComponent,
+};
+
+
+export const StatefulButtonCounter = BasicComponent.bind({});
+StatefulButtonCounter.args = {
+ type: 'statefulButton',
+ dataElement: 'countButton',
+ initialState: 'Count',
+ states: {
+ Count: {
+ number: 3,
+ getContent: (activeState) => {
+ return activeState.number;
+ },
+ onClick: (update, activeState) => {
+ activeState.number += 1;
+ update();
+ }
+ }
+ },
+ mount: () => {},
+};
+
+export const StatefulButtonStates = BasicComponent.bind({});
+StatefulButtonStates.args = {
+ type: 'statefulButton',
+ dataElement: 'singlePageBtn',
+ initialState: 'SinglePage',
+ states: {
+ SinglePage: {
+ img: 'icon-header-page-manipulation-page-layout-single-page-line',
+ onClick: (update) => {
+ update('DoublePage');
+ },
+ title: 'Single Page',
+ },
+ DoublePage: {
+ img: 'icon-header-page-manipulation-page-layout-double-page-line',
+ onClick: (update) => {
+ update('SinglePage');
+ },
+ title: 'Single Page',
+ },
+ },
+ mount: () => {},
+};
diff --git a/src/components/ModularComponents/StatefulButton/index.js b/src/components/ModularComponents/StatefulButton/index.js
new file mode 100644
index 0000000000..a340a837d2
--- /dev/null
+++ b/src/components/ModularComponents/StatefulButton/index.js
@@ -0,0 +1,3 @@
+import StatefulButton from './StatefulButton';
+
+export default StatefulButton;
\ No newline at end of file
diff --git a/src/components/ModularComponents/TabPanel/index.js b/src/components/ModularComponents/TabPanel/index.js
new file mode 100644
index 0000000000..b5620b9e57
--- /dev/null
+++ b/src/components/ModularComponents/TabPanel/index.js
@@ -0,0 +1,3 @@
+import TabPanel from './TabPanel';
+
+export default TabPanel;
\ No newline at end of file
diff --git a/src/components/ModularComponents/TopHeader/index.js b/src/components/ModularComponents/TopHeader/index.js
new file mode 100644
index 0000000000..b3e5f5206d
--- /dev/null
+++ b/src/components/ModularComponents/TopHeader/index.js
@@ -0,0 +1,3 @@
+import TopHeader from './TopHeaderContainer';
+
+export default TopHeader;
\ No newline at end of file
diff --git a/src/components/ModularComponents/ViewControls/ViewControlsFlyout.js b/src/components/ModularComponents/ViewControls/ViewControlsFlyout.js
new file mode 100644
index 0000000000..97252b4007
--- /dev/null
+++ b/src/components/ModularComponents/ViewControls/ViewControlsFlyout.js
@@ -0,0 +1,227 @@
+import displayModeObjects from 'constants/displayModeObjects';
+import core from 'core';
+import { useLayoutEffect } from 'react';
+import { useSelector, useStore, useDispatch } from 'react-redux';
+import selectors from 'selectors';
+import { enterReaderMode, exitReaderMode } from 'helpers/readerMode';
+import actions from 'actions';
+import toggleFullscreen from 'helpers/toggleFullscreen';
+import { isIE11, isIOS, isIOSFullScreenSupported } from 'helpers/device';
+import DataElements from 'constants/dataElement';
+
+const ViewControlsFlyout = () => {
+ const store = useStore();
+ const dispatch = useDispatch();
+ const [
+ totalPages,
+ displayMode,
+ isDisabled,
+ isReaderMode,
+ isMultiViewerMode,
+ isFullScreen,
+ activeDocumentViewerKey,
+ isMultiTab,
+ isMultiViewerModeAvailable,
+ currentFlyout
+ ] = useSelector((state) => [
+ selectors.getTotalPages(state),
+ selectors.getDisplayMode(state),
+ selectors.isElementDisabled(state, 'viewControlsFlyout'),
+ selectors.isReaderMode(state),
+ selectors.isMultiViewerMode(state),
+ selectors.isFullScreen(state),
+ selectors.getActiveDocumentViewerKey(state),
+ selectors.getIsMultiTab(state),
+ selectors.getIsMultiViewerModeAvailable(state),
+ selectors.getFlyout(state, 'viewControlsFlyout')
+ ]);
+
+ const totalPageThreshold = 1000;
+ let isPageTransitionEnabled = totalPages < totalPageThreshold;
+
+ useLayoutEffect(() => {
+ const viewControlsFlyout = {
+ dataElement: 'viewControlsFlyout',
+ className: 'ViewControlsFlyout',
+ items: getViewControlsFlyoutItems()
+ };
+
+ if (!currentFlyout) {
+ dispatch(actions.addFlyout(viewControlsFlyout));
+ } else {
+ dispatch(actions.updateFlyout(viewControlsFlyout.dataElement, viewControlsFlyout));
+ }
+ }, [isFullScreen, isMultiViewerModeAvailable, isMultiViewerMode, displayMode]);
+
+ if (isDisabled) {
+ return;
+ }
+
+ const documentViewer = core.getDocumentViewer();
+ const displayModeManager = documentViewer?.getDisplayModeManager();
+ if (displayModeManager?.isVirtualDisplayEnabled()) {
+ isPageTransitionEnabled = true;
+ }
+
+ const showReaderButton = core.isFullPDFEnabled() && core.getDocument()?.getType() === 'pdf';
+ const showCompareButton = !isIE11 && !isMultiTab && isMultiViewerModeAvailable;
+ const toggleCompareMode = () => {
+ store.dispatch(actions.setIsMultiViewerMode(!isMultiViewerMode));
+ };
+
+ const handleClick = (pageTransition, layout) => {
+ const setDisplayMode = () => {
+ const displayModeObject = displayModeObjects.find(
+ (obj) => obj.pageTransition === pageTransition && obj.layout === layout,
+ );
+ core.setDisplayMode(displayModeObject.displayMode);
+ };
+
+ if (isReaderMode) {
+ exitReaderMode(store);
+ setTimeout(() => {
+ setDisplayMode();
+ });
+ } else {
+ setDisplayMode();
+ }
+ };
+
+ const handleReaderModeClick = () => {
+ if (isReaderMode) {
+ return;
+ }
+ enterReaderMode(store);
+ };
+
+ let pageTransition;
+ let layout;
+
+ const displayModeObject = displayModeObjects.find((obj) => obj.displayMode === displayMode);
+ if (displayModeObject) {
+ pageTransition = displayModeObject.pageTransition;
+ layout = displayModeObject.layout;
+ }
+
+ const getViewControlsFlyoutItems = () => {
+ let viewControlsFlyoutItems = [];
+
+ const continuousPageTransitionButton = {
+ icon: 'icon-header-page-manipulation-page-transition-continuous-page-line',
+ label: 'option.pageTransition.continuous',
+ title: 'option.pageTransition.continuous',
+ onClick: () => handleClick('continuous', layout),
+ dataElement: 'continuousPageTransitionButton',
+ isActive: pageTransition === 'continuous' && !isReaderMode
+ };
+ const defaultPageTransitionButton = {
+ icon: 'icon-header-page-manipulation-page-transition-page-by-page-line',
+ label: 'option.pageTransition.default',
+ title: 'option.pageTransition.default',
+ onClick: () => handleClick('default', layout),
+ dataElement: 'defaultPageTransitionButton',
+ isActive: pageTransition === 'default' && !isReaderMode
+ };
+ const readerPageTransitionButton = {
+ icon: 'icon-header-page-manipulation-page-transition-reader',
+ label: 'option.pageTransition.reader',
+ title: 'option.pageTransition.reader',
+ onClick: () => handleReaderModeClick(),
+ dataElement: 'readerPageTransitionButton',
+ isActive: isReaderMode
+ };
+ const rotateClockwiseButton = {
+ icon: 'icon-header-page-manipulation-page-rotation-clockwise-line',
+ label: 'action.rotateClockwise',
+ title: 'action.rotateClockwise',
+ onClick: () => core.rotateClockwise(activeDocumentViewerKey),
+ dataElement: 'rotateClockwiseButton'
+ };
+ const rotateCounterClockwiseButton = {
+ icon: 'icon-header-page-manipulation-page-rotation-clockwise-line',
+ label: 'action.rotateCounterClockwise',
+ title: 'action.rotateCounterClockwise',
+ onClick: () => core.rotateCounterClockwise(activeDocumentViewerKey),
+ dataElement: 'rotateCounterClockwiseButton'
+ };
+ const singleLayoutButton = {
+ icon: 'icon-header-page-manipulation-page-layout-single-page-line',
+ label: 'option.layout.single',
+ title: 'option.layout.single',
+ onClick: () => handleClick(pageTransition, 'single'),
+ dataElement: 'singleLayoutButton',
+ isActive: layout === 'single'
+ };
+ const doubleLayoutButton = {
+ icon: 'icon-header-page-manipulation-page-layout-double-page-line',
+ label: 'option.layout.double',
+ title: 'option.layout.double',
+ onClick: () => handleClick(pageTransition, 'double'),
+ dataElement: 'doubleLayoutButton',
+ isActive: layout === 'double'
+ };
+ const coverLayoutButton = {
+ icon: 'icon-header-page-manipulation-page-layout-cover-line',
+ label: 'option.layout.cover',
+ title: 'option.layout.cover',
+ onClick: () => handleClick(pageTransition, 'cover'),
+ dataElement: 'coverLayoutButton',
+ isActive: layout === 'cover'
+ };
+
+ const divider = 'divider';
+
+ if (isPageTransitionEnabled) {
+ viewControlsFlyoutItems.push('option.displayMode.pageTransition');
+ viewControlsFlyoutItems = [...viewControlsFlyoutItems, continuousPageTransitionButton, defaultPageTransitionButton];
+
+ if (showReaderButton) {
+ viewControlsFlyoutItems.push(readerPageTransitionButton);
+ }
+ if (!isReaderMode) {
+ viewControlsFlyoutItems.push(divider);
+ }
+ }
+ if (!isReaderMode) {
+ viewControlsFlyoutItems = [...viewControlsFlyoutItems,
+ 'action.rotate',
+ rotateClockwiseButton,
+ rotateCounterClockwiseButton,
+ divider,
+ 'option.displayMode.layout',
+ singleLayoutButton,
+ doubleLayoutButton,
+ coverLayoutButton
+ ];
+ }
+ if (showCompareButton) {
+ const toggleCompareModeButton = {
+ icon: 'icon-header-compare',
+ label: 'action.comparePages',
+ title: 'action.comparePages',
+ onClick: toggleCompareMode,
+ dataElement: 'toggleCompareModeButton',
+ isActive: isMultiViewerMode
+ };
+ viewControlsFlyoutItems.push(toggleCompareModeButton);
+ }
+
+ if (!isIOS || isIOSFullScreenSupported) {
+ const fullScreenButton = {
+ icon: isFullScreen ? 'icon-header-full-screen-exit' : 'icon-header-full-screen',
+ label: isFullScreen ? 'action.exitFullscreen' : 'action.enterFullscreen',
+ title: isFullScreen ? 'action.exitFullscreen' : 'action.enterFullscreen',
+ onClick: toggleFullscreen,
+ dataElement: DataElements.FULLSCREEN_BUTTON
+ };
+ viewControlsFlyoutItems.push(divider);
+ viewControlsFlyoutItems.push(fullScreenButton);
+ }
+
+ return viewControlsFlyoutItems;
+ };
+
+ return null;
+};
+
+export default ViewControlsFlyout;
diff --git a/src/components/ModularComponents/ViewControls/ViewControlsToggleButton.js b/src/components/ModularComponents/ViewControls/ViewControlsToggleButton.js
new file mode 100644
index 0000000000..3142668655
--- /dev/null
+++ b/src/components/ModularComponents/ViewControls/ViewControlsToggleButton.js
@@ -0,0 +1,19 @@
+import React from 'react';
+import ToggleElementButton from '../ToggleElementButton';
+
+const ViewControlsToggleButton = () => {
+ return (
+
+
+
+ );
+};
+
+export default ViewControlsToggleButton;
diff --git a/src/components/ModularComponents/ViewControls/index.js b/src/components/ModularComponents/ViewControls/index.js
new file mode 100644
index 0000000000..59927601c3
--- /dev/null
+++ b/src/components/ModularComponents/ViewControls/index.js
@@ -0,0 +1,3 @@
+import ViewControls from './ViewControlsToggleButton';
+
+export default ViewControls;
\ No newline at end of file
diff --git a/src/components/ModularComponents/ZoomControls/ZoomControls.spec.js b/src/components/ModularComponents/ZoomControls/ZoomControls.spec.js
new file mode 100644
index 0000000000..db6e5590ca
--- /dev/null
+++ b/src/components/ModularComponents/ZoomControls/ZoomControls.spec.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import ZoomControls from './ZoomControls';
+import core from 'core';
+
+const ZoomControlWithRedux = withProviders(ZoomControls);
+
+const props = {
+ setZoomHandler: jest.fn(),
+ zoomValue: '100',
+ zoomTo: jest.fn(),
+ onZoomInClicked: jest.fn(),
+ onZoomOutClicked: jest.fn(),
+ isZoomFlyoutMenuActive: false,
+ isActive: true,
+ dataElement: 'zoom-container',
+ size: 0,
+};
+
+describe('Zoom Container component', () => {
+ beforeEach(() => {
+ const documentViewer = core.setDocumentViewer(1, new window.Core.DocumentViewer());
+ documentViewer.doc = new window.Core.Document('dummy', 'pdf');
+ });
+
+ it('it renders the zoomvalue correctly', () => {
+ render();
+ const input = screen.getByRole('textbox');
+ expect(input.value).toEqual(props.zoomValue);
+ });
+
+ it('it ignores invalid values that you input', () => {
+ render();
+ const input = screen.getByRole('textbox');
+ userEvent.type(input, 'zoom');
+ expect(input.value).toEqual(props.zoomValue);
+ });
+
+ it('Should execute zoomIn/zoomOut when zoom in/out button is clicked', async () => {
+ render();
+ const zoomInButton = screen.getByRole('button', { name: 'Zoom in' });
+ const zoomOutButton = screen.getByRole('button', { name: 'Zoom out' });
+ expect(zoomInButton).toBeInTheDocument();
+ fireEvent.click(zoomInButton);
+ expect(props.onZoomInClicked).toHaveBeenCalledTimes(1);
+ fireEvent.click(zoomOutButton);
+ expect(props.onZoomOutClicked).toHaveBeenCalledTimes(1);
+ });
+
+ it('it renders the zoomvalue correctly', () => {
+ render();
+ const input = screen.getByRole('textbox');
+ userEvent.type(input, '66');
+ fireEvent.keyDown(input, { key: 'Enter', code: 'Enter' });
+ expect(props.zoomTo).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/components/ModularComponents/ZoomControls/index.js b/src/components/ModularComponents/ZoomControls/index.js
new file mode 100644
index 0000000000..96f7b1a7b6
--- /dev/null
+++ b/src/components/ModularComponents/ZoomControls/index.js
@@ -0,0 +1,3 @@
+import ZoomControls from './ZoomControlsContainer';
+
+export default ZoomControls;
diff --git a/src/components/ModularHeaderItems/index.js b/src/components/ModularHeaderItems/index.js
new file mode 100644
index 0000000000..178f651f00
--- /dev/null
+++ b/src/components/ModularHeaderItems/index.js
@@ -0,0 +1,3 @@
+import ModularHeaderItems from './ModularHeaderItems';
+
+export default ModularHeaderItems;
\ No newline at end of file
diff --git a/src/components/MoreOptionsContextMenuPopup/MoreOptionsContextMenuPopup.js b/src/components/MoreOptionsContextMenuPopup/MoreOptionsContextMenuPopup.js
new file mode 100644
index 0000000000..262be70a3d
--- /dev/null
+++ b/src/components/MoreOptionsContextMenuPopup/MoreOptionsContextMenuPopup.js
@@ -0,0 +1,143 @@
+import React, { useState, useRef, useEffect, useCallback } from 'react';
+import { useTranslation } from 'react-i18next';
+import { createPortal } from 'react-dom';
+import PropTypes from 'prop-types';
+
+import DataElementWrapper from 'components/DataElementWrapper';
+
+import useOnClickOutside from 'hooks/useOnClickOutside';
+import getOverlayPositionBasedOn from 'helpers/getOverlayPositionBasedOn';
+
+import './MoreOptionsContextMenuPopup.scss';
+import Button from '../Button';
+import getRootNode from 'helpers/getRootNode';
+
+const propTypes = {
+ type: PropTypes.oneOf(['bookmark', 'outline', 'portfolio']).isRequired,
+ anchorButton: PropTypes.string.isRequired,
+ shouldDisplayDeleteButton: PropTypes.bool,
+ onClosePopup: PropTypes.func.isRequired,
+ onRenameClick: PropTypes.func,
+ onSetDestinationClick: PropTypes.func,
+ onDownloadClick: PropTypes.func,
+ onDeleteClick: PropTypes.func,
+ onOpenClick: PropTypes.func,
+};
+
+const MoreOptionsContextMenuPopup = ({
+ type,
+ anchorButton,
+ shouldDisplayDeleteButton,
+ onClosePopup,
+ onRenameClick,
+ onSetDestinationClick,
+ onDownloadClick,
+ onDeleteClick,
+ onOpenClick,
+}) => {
+ const [t] = useTranslation();
+ const containerRef = useRef(null);
+ const [position, setPosition] = useState({ left: -100, right: 'auto', top: 'auto' });
+
+ const Portal = ({ children, position }) => {
+ const mount = getRootNode().querySelector('#outline-edit-popup-portal');
+ mount.style.position = 'absolute';
+ mount.style.top = position.top === 'auto' ? position.top : `${position.top}px`;
+ mount.style.left = position.left === 'auto' ? position.left : `${position.left}px`;
+ mount.style.right = position.right === 'auto' ? position.right : `${position.right}px`;
+ mount.style.zIndex = 999;
+
+ return createPortal(children, mount);
+ };
+
+ useEffect(() => {
+ const position = getOverlayPositionBasedOn(anchorButton, containerRef);
+ setPosition(position);
+ }, [anchorButton]);
+
+ const onClickOutside = useCallback((e) => {
+ if (!containerRef?.current.contains(e.target)) {
+ onClosePopup();
+ }
+ });
+
+ useOnClickOutside(containerRef, onClickOutside);
+
+ return (
+
+
+ {type === 'portfolio' && onOpenClick &&
+
+
+ );
+};
+
+MoreOptionsContextMenuPopup.propTypes = propTypes;
+
+export default MoreOptionsContextMenuPopup;
diff --git a/src/components/MoreOptionsContextMenuPopup/MoreOptionsContextMenuPopup.scss b/src/components/MoreOptionsContextMenuPopup/MoreOptionsContextMenuPopup.scss
new file mode 100644
index 0000000000..ec949b4f3a
--- /dev/null
+++ b/src/components/MoreOptionsContextMenuPopup/MoreOptionsContextMenuPopup.scss
@@ -0,0 +1,29 @@
+.more-options-context-menu-popup {
+ padding-top: var(--padding-small);
+ padding-bottom: var(--padding-small);
+
+ background-color: var(--component-background);
+ box-shadow: 0px 0px 3px var(--document-box-shadow);
+ border-radius: 4px;
+
+ .option-button {
+ justify-content: flex-start;
+ width: 100%;
+ padding: var(--padding-small) var(--padding-medium);
+ border-radius: 0;
+
+ &:not(:first-child) {
+ margin-top: var(--padding-small);
+ }
+
+ &:hover {
+ background-color: var(--tools-header-background);
+ }
+
+ .Icon {
+ width: 20px;
+ height: auto;
+ margin-right: 10px;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/MoreOptionsContextMenuPopup/index.js b/src/components/MoreOptionsContextMenuPopup/index.js
new file mode 100644
index 0000000000..1351cedcfe
--- /dev/null
+++ b/src/components/MoreOptionsContextMenuPopup/index.js
@@ -0,0 +1,3 @@
+import MoreOptionsContextMenuPopup from './MoreOptionsContextMenuPopup';
+
+export default MoreOptionsContextMenuPopup;
diff --git a/src/components/MultiTabEmptyPage/MultiTabEmptyPage.scss b/src/components/MultiTabEmptyPage/MultiTabEmptyPage.scss
new file mode 100644
index 0000000000..fc77503b32
--- /dev/null
+++ b/src/components/MultiTabEmptyPage/MultiTabEmptyPage.scss
@@ -0,0 +1,13 @@
+.MultiTabEmptyPage {
+ width: 100%;
+
+ &.closed {
+ display: none;
+ }
+
+ .empty-page-body,
+ .image-signature {
+ height: 100%;
+ width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/src/components/MultiTabEmptyPage/index.js b/src/components/MultiTabEmptyPage/index.js
new file mode 100644
index 0000000000..50b46342da
--- /dev/null
+++ b/src/components/MultiTabEmptyPage/index.js
@@ -0,0 +1,3 @@
+import MultiTabEmptyPage from './MultiTabEmptyPage';
+
+export default MultiTabEmptyPage;
\ No newline at end of file
diff --git a/src/components/MultiViewer/ComparePanel/ChangeListItem/ChangeListItem.js b/src/components/MultiViewer/ComparePanel/ChangeListItem/ChangeListItem.js
new file mode 100644
index 0000000000..d63a1781c7
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ChangeListItem/ChangeListItem.js
@@ -0,0 +1,74 @@
+import React, { useCallback } from 'react';
+import './ChangeListItem.scss';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+import Icon from 'components/Icon';
+import core from 'core';
+import selectors from 'selectors/index';
+import { useSelector, useDispatch } from 'react-redux';
+import actions from 'actions';
+import { setIsScrolledByClickingChangeItem } from 'helpers/multiViewerHelper';
+
+const propTypes = {
+ oldText: PropTypes.string,
+ newText: PropTypes.string,
+ oldCount: PropTypes.number,
+ newCount: PropTypes.number,
+ type: PropTypes.string,
+ old: PropTypes.object,
+ new: PropTypes.object,
+};
+
+const ChangeListItem = (props) => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const [syncViewer] = useSelector((state) => [
+ selectors.getSyncViewer(state),
+ ]);
+
+ const onClickItem = useCallback(() => {
+ setIsScrolledByClickingChangeItem(true);
+ if (props.old && props.new && props.old.getPageNumber() !== props.new.getPageNumber()) {
+ dispatch(actions.setSyncViewer(null));
+ }
+ for (const annotation of [props.old, props.new]) {
+ const viewerNumber = annotation === props.old ? 1 : 2;
+ if (!annotation) {
+ continue;
+ }
+ core.getDocumentViewer(viewerNumber).getAnnotationManager().deselectAllAnnotations();
+ core.jumpToAnnotation(annotation, viewerNumber);
+ core.getDocumentViewer(viewerNumber).getAnnotationManager().selectAnnotation(annotation);
+ }
+ }, [syncViewer]);
+
+ return (
+
+
+
+
+
+
{t('multiViewer.comparePanel.change')}
+
{props.type}
+ {props.old &&
+
+
{t('multiViewer.comparePanel.old')}
+
-{props.oldCount}
+
+
{props.oldText}
+
}
+ {props.new &&
+
+
{t('multiViewer.comparePanel.new')}
+
+{props.newCount}
+
+
{props.newText}
+
}
+
+
+ );
+};
+
+ChangeListItem.propTypes = propTypes;
+
+export default ChangeListItem;
diff --git a/src/components/MultiViewer/ComparePanel/ChangeListItem/ChangeListItem.scss b/src/components/MultiViewer/ComparePanel/ChangeListItem/ChangeListItem.scss
new file mode 100644
index 0000000000..928e98a843
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ChangeListItem/ChangeListItem.scss
@@ -0,0 +1,73 @@
+@import "../../../../constants/styles";
+
+.ChangeListItem {
+ display: flex;
+ flex-direction: row;
+ border-radius: 4px;
+ align-self: stretch;
+ filter: drop-shadow(0px 0px 4px var(--gray-7));
+ background-color: var(--component-background);
+ font-family: var(--font-family);
+
+ .icon-change{
+ padding: 14px 0 14px 14px;
+
+ height: 100%;
+ .Icon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+
+ .container-right {
+ padding: 12px;
+ width: calc(100% - 14px - 24px); // Subtract icon padding + icon width
+ display: flex;
+ flex-direction: column;
+ overflow-wrap: break-word;
+
+ .title {
+ font-size: var(--font-size-medium);
+ }
+
+ .type {
+ font-size: var(--font-size-small);
+ color: var(--gray-7);
+ text-transform: capitalize;
+ }
+
+ .value-container {
+ padding: 12px;
+ margin-top: 10px;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ border-radius: 4px;
+ align-self: stretch;
+ color: var(--gray-8);
+ font-size: var(--font-size-medium);
+ font-weight: var(--font-weight-medium);
+
+ &.old {
+ .value-title {
+ color: #CF0101;
+ }
+ background-color: rgba(255, 73, 73, 0.1);
+ }
+ &.new {
+ .value-title{
+ color: #006D41;
+ }
+ background-color: rgba(21, 205, 131, 0.1);
+ }
+
+ .value-title {
+ display: flex;
+ justify-content: space-between;
+ font-size: var(--font-size-small);
+ font-weight: var(--font-weight-bold);
+ }
+ }
+ }
+
+}
diff --git a/src/components/MultiViewer/ComparePanel/ChangeListItem/index.js b/src/components/MultiViewer/ComparePanel/ChangeListItem/index.js
new file mode 100644
index 0000000000..522d18d3f0
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ChangeListItem/index.js
@@ -0,0 +1,3 @@
+import ChangeListItem from './ChangeListItem';
+
+export default ChangeListItem;
\ No newline at end of file
diff --git a/src/components/MultiViewer/ComparePanel/ComparePanel.js b/src/components/MultiViewer/ComparePanel/ComparePanel.js
new file mode 100644
index 0000000000..02abebad6e
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ComparePanel.js
@@ -0,0 +1,305 @@
+import React, { useEffect, useRef, useState } from 'react';
+import './ComparePanel.scss';
+import DataElementWrapper from 'components/DataElementWrapper';
+import classNames from 'classnames';
+import { useSelector, useDispatch } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import selectors from 'selectors';
+import { isMobileSize } from 'helpers/getDeviceSize';
+import Events from 'constants/events';
+import ChangeListItem from 'components/MultiViewer/ComparePanel/ChangeListItem';
+import core from 'core';
+import throttle from 'lodash/throttle';
+import { SYNC_MODES } from 'constants/multiViewerContants';
+import multiViewerHelper from 'helpers/multiViewerHelper';
+import fireEvent from 'helpers/fireEvent';
+import actions from 'actions';
+import DataElements from 'src/constants/dataElement';
+import { panelMinWidth } from 'constants/panel';
+
+const specialChar = /([!@#$%^&*()+=\[\]\\';,./{}|":<>?~_-])/gm;
+
+const ComparePanel = ({
+ dataElement = 'comparePanel',
+ // For storybook tests
+ initialChangeListData = {},
+ initialTotalChanges = 0,
+}) => {
+ const dispatch = useDispatch();
+ const { t } = useTranslation();
+ const isMobile = isMobileSize();
+ const [
+ isOpen,
+ currentWidth,
+ isInDesktopOnlyMode,
+ isComparisonOverlayEnabled,
+ multiViewerSyncScrollMode,
+ ] = useSelector((state) => [
+ selectors.isElementOpen(state, dataElement),
+ selectors.getComparePanelWidth(state),
+ selectors.isInDesktopOnlyMode(state),
+ selectors.getIsComparisonOverlayEnabled(state),
+ selectors.getMultiViewerSyncScrollMode(state),
+ ]);
+ const isCustom = dataElement !== 'comparePanel';
+ const panelWidth = !isCustom ? currentWidth : panelMinWidth - 32;
+ const [searchValue, setSearchValue] = React.useState('');
+ const style = !isInDesktopOnlyMode && isMobile ? {} : { width: `${panelWidth}px`, minWidth: `${panelWidth}px` };
+ const changeListData = useRef(initialChangeListData);
+ const [totalChanges, setTotalChanges] = useState(initialTotalChanges);
+ const [filteredListData, setFilteredListData] = useState(initialChangeListData);
+ const filterfuncRef = useRef(
+ throttle(
+ (searchValue) => {
+ if (!changeListData.current) {
+ return [];
+ }
+ if (!searchValue) {
+ setFilteredListData(changeListData.current);
+ return;
+ }
+ searchValue = searchValue.replace(specialChar, '\\$&');
+ const keys = Object.keys(changeListData.current);
+ const newData = {};
+ for (const key of keys) {
+ for (const item of changeListData.current[key]) {
+ if (
+ item.oldText?.toLowerCase().match(searchValue.toLowerCase()) ||
+ item.newText?.toLowerCase().match(searchValue.toLowerCase())
+ ) {
+ if (!newData[key]) {
+ newData[key] = [];
+ }
+ newData[key].push(item);
+ }
+ }
+ }
+ setFilteredListData(newData);
+ },
+ 100,
+ { leading: true },
+ ),
+ );
+
+ useEffect(() => {
+ const updatePanelItems = (doc1Annotations, doc2Annotations, diffCount) => {
+ dispatch(actions.setIsCompareStarted(true));
+ dispatch(actions.enableElement('comparePanelToggleButton'));
+ dispatch(actions.openElement(DataElements.LOADING_MODAL));
+ const annotMap = {};
+ if (multiViewerSyncScrollMode === SYNC_MODES.SKIP_UNMATCHED) {
+ const doc1AnnotationsClone = [...doc1Annotations];
+ const doc2AnnotationsClone = [...doc2Annotations];
+ const annotationMatches1 = [];
+ const annotationMatches2 = [];
+ while (doc1AnnotationsClone.length || doc2AnnotationsClone.length) {
+ const annotation = doc1AnnotationsClone[0] || doc2AnnotationsClone[0];
+ const ids = [annotation.getCustomData('TextDiffID')];
+ const pageNumbers1 = [];
+ const pageNumbers2 = [];
+ const side1Annotations = [];
+ const side2Annotations = [];
+ const checkIfAnnotionMatches1 = (annotation) => ids.includes(annotation.getCustomData('TextDiffID')) || pageNumbers1.includes(annotation.PageNumber);
+ const checkIfAnnotionMatches2 = (annotation) => ids.includes(annotation.getCustomData('TextDiffID')) || pageNumbers2.includes(annotation.PageNumber);
+ let added = false;
+ do {
+ added = false;
+ for (const annotation of doc1AnnotationsClone) {
+ if (checkIfAnnotionMatches1(annotation)) {
+ const annotId = annotation.getCustomData('TextDiffID');
+ const annotPageNumber = annotation.PageNumber;
+ !ids.includes(annotId) && ids.push(annotId);
+ !pageNumbers1.includes(annotPageNumber) && pageNumbers1.push(annotation.PageNumber);
+ doc1AnnotationsClone.splice(doc1AnnotationsClone.indexOf(annotation), 1);
+ side1Annotations.push(annotation);
+ added = true;
+ }
+ }
+ for (const annotation of doc2AnnotationsClone) {
+ if (checkIfAnnotionMatches2(annotation)) {
+ const annotId = annotation.getCustomData('TextDiffID');
+ const annotPageNumber = annotation.PageNumber;
+ !ids.includes(annotId) && ids.push(annotId);
+ !pageNumbers2.includes(annotPageNumber) && pageNumbers2.push(annotation.PageNumber);
+ doc2AnnotationsClone.splice(doc2AnnotationsClone.indexOf(annotation), 1);
+ side2Annotations.push(annotation);
+ added = true;
+ }
+ }
+ } while (added);
+ if (side1Annotations.length && side2Annotations.length) {
+ annotationMatches1.push(side1Annotations);
+ annotationMatches2.push(side2Annotations);
+ }
+ }
+
+ const matchedPages = { 1: {}, 2: {} };
+ for (const i in annotationMatches1) {
+ const doc1Annotations = annotationMatches1[i];
+ const doc2Annotations = annotationMatches2[i];
+ const doc1Pages = Array.from(new Set(doc1Annotations.map((annotation) => annotation.PageNumber)));
+ const doc2Pages = Array.from(new Set(doc2Annotations.map((annotation) => annotation.PageNumber)));
+ for (const pageNumber of doc1Pages) {
+ matchedPages[1][pageNumber] = {
+ otherSidePages: doc2Pages,
+ thisSidePages: doc1Pages,
+ };
+ }
+ for (const pageNumber of doc2Pages) {
+ matchedPages[2][pageNumber] = {
+ otherSidePages: doc1Pages,
+ thisSidePages: doc2Pages,
+ };
+ }
+ }
+ multiViewerHelper.matchedPages = matchedPages;
+ }
+ const matchedIds = [];
+ for (const index in doc1Annotations) {
+ const annotation = doc1Annotations[index];
+ const type = annotation.getCustomData('TextDiffType');
+ const id = annotation.getCustomData('TextDiffID');
+ const otherAnnotations = doc2Annotations.filter((annotation) => annotation?.getCustomData('TextDiffID') === id);
+ if (!otherAnnotations.length) {
+ continue;
+ }
+ if (!matchedIds.includes(id)) {
+ matchedIds.push(id);
+ }
+ if (!annotMap[annotation.PageNumber]) {
+ annotMap[annotation.PageNumber] = [];
+ }
+ annotMap[annotation.PageNumber].push({
+ new: otherAnnotations[0],
+ newText: otherAnnotations[0]?.Author,
+ newCount: otherAnnotations[0]?.Author?.length,
+ old: annotation,
+ oldText: annotation?.Author,
+ oldCount: annotation?.Author?.length,
+ type: `${t('multiViewer.comparePanel.textContent')} - ${t(`multiViewer.comparePanel.${type}`)}`,
+ });
+ if (otherAnnotations.length > 1) {
+ for (const i in otherAnnotations) {
+ if (i === '0') {
+ continue;
+ }
+ annotMap[annotation.PageNumber].push({
+ new: otherAnnotations[i],
+ newText: otherAnnotations[i]?.Author,
+ newCount: otherAnnotations[i]?.Author?.length,
+ old: annotation,
+ oldText: annotation?.Author,
+ oldCount: annotation?.Author?.length,
+ type: `${t('multiViewer.comparePanel.textContent')} - ${t(`multiViewer.comparePanel.${type}`)}`,
+ });
+ }
+ }
+ }
+ const unmatchedAnnotations1 = doc1Annotations.filter((annotation) => !matchedIds.includes(annotation.getCustomData('TextDiffID')));
+ const unmatchedAnnotations2 = doc2Annotations.filter((annotation) => !matchedIds.includes(annotation.getCustomData('TextDiffID')));
+ for (const annotation of unmatchedAnnotations1) {
+ if (!annotMap[annotation.PageNumber]) {
+ annotMap[annotation.PageNumber] = [];
+ }
+ annotMap[annotation.PageNumber].push({
+ old: annotation,
+ oldText: annotation?.Author,
+ oldCount: annotation?.Author?.length,
+ type: `${t('multiViewer.comparePanel.textContent')} - ${t(`multiViewer.comparePanel.${annotation.getCustomData('TextDiffType')}`)}`,
+ });
+ }
+ for (const annotation of unmatchedAnnotations2) {
+ if (!annotMap[annotation.PageNumber]) {
+ annotMap[annotation.PageNumber] = [];
+ }
+ annotMap[annotation.PageNumber].push({
+ new: annotation,
+ newText: annotation?.Author,
+ newCount: annotation?.Author?.length,
+ type: `${t('multiViewer.comparePanel.textContent')} - ${t(`multiViewer.comparePanel.${annotation.getCustomData('TextDiffType')}`)}`,
+ });
+ }
+ for (const pageNumber of Object.keys(annotMap)) {
+ annotMap[pageNumber] = annotMap[pageNumber].sort((a, b) => {
+ if (a?.new?.PageNumber && b?.new?.PageNumber && a?.new?.PageNumber !== b?.new?.PageNumber) {
+ return a?.new?.PageNumber - b?.new?.PageNumber;
+ }
+ const aY = a?.new?.Y || a?.old?.Y;
+ const bY = b?.new?.Y || b?.old?.Y;
+ if (aY === bY) {
+ return (a?.new?.X || a?.old?.X) - (b?.new?.X || b?.old?.X);
+ }
+ return aY - bY;
+ });
+ }
+ dispatch(actions.closeElement(DataElements.LOADING_MODAL));
+ fireEvent(Events.COMPARE_ANNOTATIONS_LOADED, { annotMap, diffCount });
+ if (!isComparisonOverlayEnabled) {
+ core.hideAnnotations(core.getSemanticDiffAnnotations(1), 1);
+ core.hideAnnotations(core.getSemanticDiffAnnotations(2), 2);
+ }
+ setTotalChanges(diffCount);
+ changeListData.current = annotMap;
+ setFilteredListData(annotMap);
+ };
+ const resetPanelItems = () => {
+ setTotalChanges(0);
+ changeListData.current = {};
+ };
+ core.addEventListener(Events.COMPARE_ANNOTATIONS_LOADED, updatePanelItems, undefined, 1);
+ core.addEventListener('documentUnloaded', resetPanelItems, undefined, 1);
+ core.addEventListener('documentUnloaded', resetPanelItems, undefined, 2);
+ return () => {
+ core.removeEventListener(Events.COMPARE_ANNOTATIONS_LOADED, updatePanelItems, 1);
+ core.removeEventListener('documentUnloaded', resetPanelItems, 1);
+ core.removeEventListener('documentUnloaded', resetPanelItems, 2);
+ };
+ }, []);
+
+ const renderPageItem = (pageNum) => {
+ const changeListItems = filteredListData[pageNum];
+ return
+ {t('multiViewer.comparePanel.page')} {pageNum}
+ {changeListItems.map(((props) => ))}
+ ;
+ };
+
+ const onSearchInputChange = (e) => {
+ const newValue = e.target.value;
+ setSearchValue(newValue);
+ filterfuncRef.current(newValue);
+ };
+
+ const shouldRenderItems = !!(totalChanges && filteredListData && Object.keys(filteredListData).length);
+ return (
+
+
+
+
+
+
+ {t('multiViewer.comparePanel.changesList')} ({totalChanges})
+
+
+ {shouldRenderItems && Object.keys(filteredListData).map((key) => renderPageItem(key))}
+
+
+
+ );
+};
+
+export default ComparePanel;
diff --git a/src/components/MultiViewer/ComparePanel/ComparePanel.scss b/src/components/MultiViewer/ComparePanel/ComparePanel.scss
new file mode 100644
index 0000000000..09ad6fc44f
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ComparePanel.scss
@@ -0,0 +1,62 @@
+@import "../../../constants/styles";
+
+$padding-main: 0px;
+$padding-inner: 16px;
+
+.ComparePanel {
+ padding: $padding-main;
+ height: 100%;
+
+ .input-container {
+ margin: $padding-inner;
+ display: flex;
+ position: relative;
+ box-sizing: border-box;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ height: 28px;
+ align-items: center;
+ justify-content: flex-end;
+ color: var(--text-color);
+ padding: 6px 2px 6px 6px;
+ background: var(--gray-0);
+
+ input {
+ width: 100%;
+ padding: 6px 26px 6px 6px;
+ height: 20px;
+ border: none;
+ background: transparent;
+ }
+ }
+
+ .changeListContainer{
+ height: calc(100% - 28px);
+ .changeListTitle {
+ padding: 8px $padding-inner 2px;
+ font-size: var(--font-size-medium);
+ font-family: var(--font-family);
+ font-weight: var(--font-weight-bold);
+ color: var(--gray-8);
+ span {
+ color: var(--gray-7)
+ }
+ }
+
+ .changeList {
+ padding: $padding-inner;
+ height: calc(100% - 43px);
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+
+ .page-number {
+ margin-top: 10px;
+ font-size: var(--font-size-default);
+ font-family: var(--font-family);
+ color: var(--gray-7);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/MultiViewer/ComparePanel/ComparePanel.spec.js b/src/components/MultiViewer/ComparePanel/ComparePanel.spec.js
new file mode 100644
index 0000000000..666561faea
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ComparePanel.spec.js
@@ -0,0 +1,60 @@
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+import React from 'react';
+import { render } from '@testing-library/react';
+import ComparePanel from './ComparePanel';
+import core from 'core';
+
+// mock initial state.
+// UI Buttons are redux connected, and they need a state or the
+// tests will error out
+const initialState = {
+ viewer: {
+ syncViewer: null,
+ openElements: {
+ comparePanel: true
+ },
+ panelWidths: {
+ comparePanel: 330,
+ },
+ isInDesktopOnlyMode: false,
+ disabledElements: {},
+ }
+};
+
+function rootReducer(state = initialState, action) { // eslint-disable-line no-unused-vars
+ return state;
+}
+
+const store = createStore(rootReducer);
+const ComparePanelWithRedux = (props) => (
+
+
+
+);
+
+const noop = () => {
+};
+
+jest.mock('core', () => ({
+ addEventListener: noop,
+ removeEventListener: noop,
+ jumpToAnnotation: noop,
+ getDocumentViewer: () => ({
+ getAnnotationManager: () => ({
+ deselectAllAnnotations: noop,
+ selectAnnotation: noop,
+ })
+ })
+}));
+describe('ComparePanel component', () => {
+ it('Should render correctly', () => {
+ const { container } = render();
+ const searchBar = container.querySelector('.input-container > input');
+ const changeList = container.querySelector('.changeList');
+ const changeCountTitle = container.querySelector('.changeListTitle');
+ expect(searchBar).toBeInTheDocument();
+ expect(changeList).toBeInTheDocument();
+ expect(changeCountTitle).toBeInTheDocument();
+ });
+});
diff --git a/src/components/MultiViewer/ComparePanel/ComparePanel.stories.js b/src/components/MultiViewer/ComparePanel/ComparePanel.stories.js
new file mode 100644
index 0000000000..705f734724
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/ComparePanel.stories.js
@@ -0,0 +1,118 @@
+import React from 'react';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import ComparePanel from './ComparePanel';
+import rootReducer from 'reducers/rootReducer';
+
+export default {
+ title: 'ModularComponents/ComparePanel',
+ component: ComparePanel,
+};
+
+const store = configureStore({
+ reducer: rootReducer,
+});
+
+export const Empty = () => {
+ return (
+
+
+
+ );
+};
+
+const mockItems = {
+ '1': [
+ {
+ 'new': {
+ 'Id': '24ba68bd-4a45-4c52-b80f-b543ac5a0336'
+ },
+ 'newText': 'Important Factors ',
+ 'newCount': 18,
+ 'old': {
+ 'Id': '5e45eea5-d63d-4328-811f-410ee769fe30'
+ },
+ 'oldText': '',
+ 'oldCount': 0,
+ 'type': 'Text Content - insert'
+ },
+ {
+ 'new': {
+ 'Id': '99c5eacc-325c-4b40-be0c-3eb71d20430e'
+ },
+ 'newText': ' you',
+ 'newCount': 4,
+ 'old': {
+ 'Id': 'fa896655-c4e2-4133-8cda-63d15d6038d3'
+ },
+ 'oldText': '',
+ 'oldCount': 0,
+ 'type': 'Text Content - insert'
+ }
+ ],
+ '2': [
+ {
+ 'new': {
+ 'Id': 'fe9079d4-e139-44c1-afbc-48de7483efae'
+ },
+ 'newText': 'into',
+ 'newCount': 4,
+ 'old': {
+ 'Id': '9608d6a2-d690-47d2-aef0-350d7ba90602'
+ },
+ 'oldText': 'intoadsf',
+ 'oldCount': 8,
+ 'type': 'Text Content - edit'
+ },
+ {
+ 'new': {
+ 'Id': '4629e9e5-126e-4925-aa63-c69a103d3ace'
+ },
+ 'newText': 'This',
+ 'newCount': 4,
+ 'old': {
+ 'Id': '341415d1-d4f7-4ac9-ad04-3d4ce60cdd34'
+ },
+ 'oldText': 'Thasdfis',
+ 'oldCount': 8,
+ 'type': 'Text Content - edit'
+ }
+ ],
+ '3': [
+ {
+ 'new': {
+ 'Id': '93875dd2-8eaf-416d-a01b-b9cb807ad9a3'
+ },
+ 'newText': 'of thing',
+ 'newCount': 9,
+ 'old': {
+ 'Id': 'f0e5be8a-11c0-4b25-9fc4-833a9175875e'
+ },
+ 'oldText': '3',
+ 'oldCount': 1,
+ 'type': 'Text Content - edit'
+ },
+ {
+ 'new': {
+ 'Id': '790fc686-4a83-4654-b74f-4f911614fe11'
+ },
+ 'newText': ' initially on the main platforms preferred by your users. But later if you wish to expand, the library does ',
+ 'newCount': 108,
+ 'old': {
+ 'Id': 'c2ecc98b-3ef5-4935-838e-fc9dfc626537'
+ },
+ 'oldText': 'y does ',
+ 'oldCount': 7,
+ 'type': 'Text Content - edit'
+ }
+ ],
+};
+const mockItemCount = 6;
+
+export const Populated = () => {
+ return (
+
+
+
+ );
+};
diff --git a/src/components/MultiViewer/ComparePanel/index.js b/src/components/MultiViewer/ComparePanel/index.js
new file mode 100644
index 0000000000..67d9ee7f39
--- /dev/null
+++ b/src/components/MultiViewer/ComparePanel/index.js
@@ -0,0 +1,3 @@
+import ComparePanel from './ComparePanel';
+
+export default ComparePanel;
\ No newline at end of file
diff --git a/src/components/MultiViewer/CompareZoomOverlay/CompareZoomOverlay.js b/src/components/MultiViewer/CompareZoomOverlay/CompareZoomOverlay.js
new file mode 100644
index 0000000000..00386de815
--- /dev/null
+++ b/src/components/MultiViewer/CompareZoomOverlay/CompareZoomOverlay.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import FlyoutMenu from 'components/FlyoutMenu/FlyoutMenu';
+import ZoomOverlay from 'components/ZoomOverlay/ZoomOverlay';
+import { useSelector, useDispatch } from 'react-redux';
+import { fitToWidth, fitToPage, zoomTo } from 'helpers/zoom';
+import selectors from 'selectors';
+import actions from 'actions';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+
+const propTypes = {
+ zoom1: PropTypes.number,
+ zoom2: PropTypes.number,
+};
+
+const CompareZoomOverlay = ({
+ zoom1,
+ zoom2,
+}) => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+
+ const onClickMarqueeZoom = (documentViewerKey) => () => {
+ dispatch(actions.closeElements([`zoomOverlay${documentViewerKey}`]));
+ };
+ const onClickZoomLevelOption = (documentViewerKey) => (zoomLevel) => {
+ zoomTo(zoomLevel, true, documentViewerKey);
+ dispatch(actions.closeElements([`zoomOverlay${documentViewerKey}`]));
+ };
+ const onClickMarqueeZoom1 = onClickMarqueeZoom(1);
+ const onClickMarqueeZoom2 = onClickMarqueeZoom(2);
+ const onClickZoomLevelOption1 = onClickZoomLevelOption(1);
+ const onClickZoomLevelOption2 = onClickZoomLevelOption(2);
+ const onFitToWidth1 = () => fitToWidth(1);
+ const onFitToWidth2 = () => fitToWidth(2);
+ const onFitToPage1 = () => fitToPage(1);
+ const onFitToPage2 = () => fitToPage(2);
+
+ return (
+ <>
+
+ selectors.isElementDisabled(state, 'marqueeToolButton'))}
+ fitToWidth={onFitToWidth1}
+ fitToPage={onFitToPage1}
+ onClickZoomLevelOption={onClickZoomLevelOption1}
+ onClickMarqueeZoom={onClickMarqueeZoom1}
+ />
+
+
+ selectors.isElementDisabled(state, 'marqueeToolButton'))}
+ fitToWidth={onFitToWidth2}
+ fitToPage={onFitToPage2}
+ onClickZoomLevelOption={onClickZoomLevelOption2}
+ onClickMarqueeZoom={onClickMarqueeZoom2}
+ />
+
+ >
+ );
+};
+
+CompareZoomOverlay.propTypes = propTypes;
+
+export default CompareZoomOverlay;
diff --git a/src/components/MultiViewer/CompareZoomOverlay/index.js b/src/components/MultiViewer/CompareZoomOverlay/index.js
new file mode 100644
index 0000000000..134e7c8a5f
--- /dev/null
+++ b/src/components/MultiViewer/CompareZoomOverlay/index.js
@@ -0,0 +1,3 @@
+import CompareZoomOverlay from './CompareZoomOverlay';
+
+export default CompareZoomOverlay;
\ No newline at end of file
diff --git a/src/components/MultiViewer/ComparisonButton/ComparisonButton.js b/src/components/MultiViewer/ComparisonButton/ComparisonButton.js
new file mode 100644
index 0000000000..a86d7a4789
--- /dev/null
+++ b/src/components/MultiViewer/ComparisonButton/ComparisonButton.js
@@ -0,0 +1,103 @@
+import React, { useCallback, useState, useEffect } from 'react';
+import './ComparisonButton.scss';
+import { useSelector, useDispatch } from 'react-redux';
+import selectors from 'selectors';
+import actions from 'actions';
+import Choice from 'components/Choice';
+import DataElements from 'src/constants/dataElement';
+import core from 'core';
+import { useTranslation } from 'react-i18next';
+
+const ComparisonButton = () => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const [disabled, setDisabled] = useState(true);
+ const [isCompareStarted, isComparisonOverlayEnabled] = useSelector((state) => [
+ selectors.isCompareStarted(state),
+ selectors.getIsComparisonOverlayEnabled(state),
+ ]);
+ const semanticDiffAnnotations = core.getSemanticDiffAnnotations();
+
+ useEffect(() => {
+ const checkDisabled = () => {
+ const documentsLoaded = core.getDocument(1) && core.getDocument(2);
+ const document1IsValidType = core.getDocument(1)?.getType() === 'pdf' ||
+ (core.getDocument(1)?.getType() === 'webviewerServer' && !core.getDocument(1)?.isWebViewerServerDocument());
+ const document2IsValidType = core.getDocument(2)?.getType() === 'pdf' ||
+ (core.getDocument(2)?.getType() === 'webviewerServer' && !core.getDocument(2)?.isWebViewerServerDocument());
+ if (documentsLoaded && document1IsValidType && document2IsValidType) {
+ setDisabled(false);
+ } else {
+ setDisabled(true);
+ }
+ };
+ const unLoaded = () => {
+ setDisabled(true);
+ dispatch(actions.setIsCompareStarted(false));
+ dispatch(actions.disableElement('comparePanelToggleButton'));
+ dispatch(actions.closeElement('comparePanel'));
+ };
+ checkDisabled();
+ core.addEventListener('documentLoaded', checkDisabled, undefined, 1);
+ core.addEventListener('documentLoaded', checkDisabled, undefined, 2);
+ core.addEventListener('documentUnloaded', unLoaded, undefined, 1);
+ core.addEventListener('documentUnloaded', unLoaded, undefined, 2);
+
+ if (!semanticDiffAnnotations.length && isComparisonOverlayEnabled) {
+ dispatch(actions.setIsComparisonOverlayEnabled(false));
+ }
+
+ if (semanticDiffAnnotations.length && !isComparisonOverlayEnabled) {
+ dispatch(actions.setIsComparisonOverlayEnabled(true));
+ }
+
+ return () => {
+ core.removeEventListener('documentLoaded', checkDisabled, undefined, 1);
+ core.removeEventListener('documentLoaded', checkDisabled, undefined, 2);
+ core.removeEventListener('documentUnloaded', unLoaded, undefined, 1);
+ core.removeEventListener('documentUnloaded', unLoaded, undefined, 2);
+ };
+ }, [semanticDiffAnnotations]);
+
+ const startComparison = useCallback(() => {
+ const [documentViewer, documentViewer2] = core.getDocumentViewers();
+ const shouldDiff = documentViewer?.getDocument() && documentViewer2?.getDocument();
+ if (shouldDiff) {
+ dispatch(actions.setIsCompareStarted(true));
+ dispatch(actions.enableElement('comparePanelToggleButton'));
+ dispatch(actions.openElement(DataElements.LOADING_MODAL));
+ documentViewer.startSemanticDiff(documentViewer2).catch((error) => {
+ console.error(error);
+ dispatch(actions.closeElement(DataElements.LOADING_MODAL));
+ });
+ }
+ }, []);
+
+ const toggleComparisonOverlay = async () => {
+ const enable = !isComparisonOverlayEnabled;
+ const [documentViewerOne, documentViewerTwo] = core.getDocumentViewers();
+ if (enable) {
+ await documentViewerOne.startSemanticDiff(documentViewerTwo);
+ } else {
+ await documentViewerOne.stopSemanticDiff();
+ }
+ dispatch(actions.setIsComparisonOverlayEnabled(enable));
+ };
+
+ return (
+
+ {!isCompareStarted ?
+ {t('action.startComparison')}
+ :
+
+ }
+
+ );
+};
+
+export default ComparisonButton;
diff --git a/src/components/MultiViewer/ComparisonButton/ComparisonButton.scss b/src/components/MultiViewer/ComparisonButton/ComparisonButton.scss
new file mode 100644
index 0000000000..123009c8a4
--- /dev/null
+++ b/src/components/MultiViewer/ComparisonButton/ComparisonButton.scss
@@ -0,0 +1,36 @@
+@import '../../../constants/styles';
+
+.ComparisonButton {
+ display: flex;
+ align-items: center;
+ font-size: var(--font-size-medium);
+
+ button {
+ @include button-reset;
+ background: var(--primary-button);
+ border-radius: 4px;
+ display: flex;
+ align-items: center;
+ padding: 8px 16px;
+ justify-content: center;
+ position: relative;
+ color: var(--primary-button-text);
+ cursor: pointer;
+
+ @include mobile {
+ font-size: 13px;
+ }
+
+ &:hover {
+ background: var(--primary-button-hover);
+ &:disabled {
+ background: var(--primary-button);
+ }
+ }
+
+ &:disabled {
+ opacity: 0.8;
+ cursor: not-allowed;
+ }
+ }
+}
diff --git a/src/components/MultiViewer/ComparisonButton/index.js b/src/components/MultiViewer/ComparisonButton/index.js
new file mode 100644
index 0000000000..8dd89c8e24
--- /dev/null
+++ b/src/components/MultiViewer/ComparisonButton/index.js
@@ -0,0 +1,3 @@
+import ComparisonButton from './ComparisonButton';
+
+export default ComparisonButton;
\ No newline at end of file
diff --git a/src/components/MultiViewer/DocumentContainer/DocumentContainer.scss b/src/components/MultiViewer/DocumentContainer/DocumentContainer.scss
new file mode 100644
index 0000000000..4b25ab223b
--- /dev/null
+++ b/src/components/MultiViewer/DocumentContainer/DocumentContainer.scss
@@ -0,0 +1,58 @@
+@import '../../../constants/styles';
+
+.MultiViewer {
+ .DocumentContainer {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: calc(100% - 4px);
+ height: calc(100% - 32px);
+ overflow: overlay;
+ user-select: none;
+
+ @include ie11 {
+ margin-left: 0 !important;
+ width: 100% !important
+ }
+
+ .document {
+ overflow-x: visible;
+ overflow-y: visible;
+ margin: auto; // vertical centering when content is smaller than document container
+ // can't use 'justify-content: center;' due to losing access to content when overflowing
+ // see: https://stackoverflow.com/questions/33454533/cant-scroll-to-top-of-flex-item-that-is-overflowing-container
+ outline: none;
+ -webkit-tap-highlight-color: transparent;
+
+ &.hidden {
+ display: none;
+ }
+
+ .pageSection {
+ &[id*=pageSectionb] {
+ box-shadow: none;
+ }
+
+ .pageContainer {
+ background-color: $document-bg-color;
+ position: relative;
+ box-shadow: $md-shadow1;
+
+ span.link {
+ cursor: pointer;
+ }
+ }
+ }
+
+ textarea.freetext {
+ position: absolute;
+ z-index: 20;
+ border: 0;
+ padding: 0;
+ box-sizing: border-box;
+ resize: none;
+ outline: 1px solid transparent;
+ }
+ }
+ }
+}
diff --git a/src/components/MultiViewer/DocumentContainer/index.js b/src/components/MultiViewer/DocumentContainer/index.js
new file mode 100644
index 0000000000..57c02d0d56
--- /dev/null
+++ b/src/components/MultiViewer/DocumentContainer/index.js
@@ -0,0 +1,3 @@
+import DocumentContainer from './DocumentContainer';
+
+export default DocumentContainer;
\ No newline at end of file
diff --git a/src/components/MultiViewer/DocumentHeader/DocumentHeader.js b/src/components/MultiViewer/DocumentHeader/DocumentHeader.js
new file mode 100644
index 0000000000..736328d4b1
--- /dev/null
+++ b/src/components/MultiViewer/DocumentHeader/DocumentHeader.js
@@ -0,0 +1,72 @@
+import React, { useEffect, useState } from 'react';
+import ToggleZoomOverlay from 'components/ToggleZoomOverlay';
+import PropTypes from 'prop-types';
+import Button from 'components/Button';
+import classNames from 'classnames';
+import core from 'core';
+import { useTranslation } from 'react-i18next';
+import downloadPdf from 'helpers/downloadPdf';
+import { useDispatch, useSelector } from 'react-redux';
+import selectors from 'selectors';
+import DataElements from 'constants/dataElement';
+import actions from 'actions';
+
+import './DocumentHeader.scss';
+
+const propTypes = {
+ documentViewerKey: PropTypes.number.isRequired,
+ docLoaded: PropTypes.bool.isRequired,
+ isSyncing: PropTypes.bool.isRequired,
+};
+
+// Todo Compare: Make stories for this component
+const DocumentHeader = ({
+ documentViewerKey,
+ docLoaded,
+ isSyncing,
+}) => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const [filename, setFileName] = useState('Untitled');
+ const [saveButtonDisabled] = useSelector((state) => [selectors.isElementDisabled(state, DataElements.MULTI_VIEWER_SAVE_DOCUMENT_BUTTON)]);
+
+ useEffect(() => {
+ const stopSyncing = () => dispatch(actions.setSyncViewer(null));
+ const onLoaded = () => setFileName(core.getDocument(documentViewerKey)?.getFilename());
+ const unLoaded = () => setFileName('Untitled');
+ core.addEventListener('documentLoaded', onLoaded, undefined, documentViewerKey);
+ core.addEventListener('documentUnloaded', unLoaded, undefined, documentViewerKey);
+ core.addEventListener('displayModeUpdated', stopSyncing, undefined, documentViewerKey);
+ setFileName(core.getDocument(1)?.getFilename() || 'Untitled');
+ return () => {
+ core.removeEventListener('documentLoaded', onLoaded, documentViewerKey);
+ core.removeEventListener('documentUnloaded', unLoaded, documentViewerKey);
+ core.removeEventListener('displayModeUpdated', stopSyncing, documentViewerKey);
+ };
+ }, [documentViewerKey]);
+
+ const closeDocument = () => core.closeDocument(documentViewerKey);
+ const onClickSync = () => dispatch(actions.setSyncViewer(isSyncing ? null : documentViewerKey));
+ const onSaveDocument = () => downloadPdf(dispatch, undefined, documentViewerKey);
+
+ return (
+
+ );
+};
+
+DocumentHeader.propTypes = propTypes;
+
+export default DocumentHeader;
diff --git a/src/components/MultiViewer/DocumentHeader/DocumentHeader.scss b/src/components/MultiViewer/DocumentHeader/DocumentHeader.scss
new file mode 100644
index 0000000000..cdc8ccc387
--- /dev/null
+++ b/src/components/MultiViewer/DocumentHeader/DocumentHeader.scss
@@ -0,0 +1,48 @@
+@import '../../../constants/styles';
+
+.DocumentHeader {
+ width: 100%;
+ height: fit-content;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 0;
+
+ &.hidden {
+ visibility: hidden;
+ }
+
+ .Button {
+ width: 28px;
+ height: 28px;
+
+ &:hover {
+ background-color: var(--popup-button-hover);
+ }
+
+ &.active{
+ background-color: var(--view-header-button-active);
+ .Icon {
+ color: var(--view-header-icon-active-fill);
+ }
+ }
+ }
+
+ .zoom-overlay {
+ //align-self: flex-start;
+ }
+
+ .file-name {
+ font-family: var(--font-family);
+ font-size: var(--font-size-medium);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .control-buttons {
+ display: flex;
+ gap: 8px;
+ }
+}
diff --git a/src/components/MultiViewer/DocumentHeader/index.js b/src/components/MultiViewer/DocumentHeader/index.js
new file mode 100644
index 0000000000..8429f904c7
--- /dev/null
+++ b/src/components/MultiViewer/DocumentHeader/index.js
@@ -0,0 +1,3 @@
+import DocumentHeader from './DocumentHeader';
+
+export default DocumentHeader;
\ No newline at end of file
diff --git a/src/components/MultiViewer/DropArea/DropArea.js b/src/components/MultiViewer/DropArea/DropArea.js
new file mode 100644
index 0000000000..e56b4c6fa0
--- /dev/null
+++ b/src/components/MultiViewer/DropArea/DropArea.js
@@ -0,0 +1,63 @@
+import React, { useRef } from 'react';
+import selectors from 'selectors';
+import './DropArea.scss';
+import { useTranslation } from 'react-i18next';
+import Icon from 'components/Icon';
+import PropTypes from 'prop-types';
+import loadDocument from 'helpers/loadDocument';
+import { useDispatch, useSelector } from 'react-redux';
+import getHashParameters from 'helpers/getHashParameters';
+
+const propTypes = {
+ documentViewerKey: PropTypes.number.isRequired,
+};
+
+// Todo Compare: Make stories for this component
+const DropArea = ({ documentViewerKey }) => {
+ const { t } = useTranslation();
+ const dispatch = useDispatch();
+ const fileInput = useRef();
+ const [
+ customMultiViewerAcceptedFileFormats,
+ ] = useSelector((state) => [
+ selectors.getCustomMultiViewerAcceptedFileFormats(state),
+ ]);
+
+ const browseFiles = () => fileInput.current.click();
+
+ const onDrop = (e) => {
+ e.preventDefault();
+ const { files } = e.dataTransfer;
+ if (files.length) {
+ loadDocument(dispatch, files[0], {}, documentViewerKey);
+ }
+ };
+ const loadFile = (e) => {
+ e.preventDefault();
+ const { files } = e.target;
+ if (files.length) {
+ loadDocument(dispatch, files[0], {}, documentViewerKey);
+ }
+ };
+
+ const wvServer = !!getHashParameters('webviewerServerURL', null);
+ const acceptFormats = wvServer ? window.Core.SupportedFileFormats.SERVER : window.Core.SupportedFileFormats.CLIENT;
+
+ return (
+ e.preventDefault()} onDrop={onDrop}>
+
+
{t('multiViewer.dragAndDrop')}
+
{t('multiViewer.or')}
+
{t('multiViewer.browse')}
+
`.${format}`,
+ )).join(', ')}
+ />
+
+ );
+};
+
+DropArea.propTypes = propTypes;
+
+export default DropArea;
diff --git a/src/components/MultiViewer/DropArea/DropArea.scss b/src/components/MultiViewer/DropArea/DropArea.scss
new file mode 100644
index 0000000000..8353ce4412
--- /dev/null
+++ b/src/components/MultiViewer/DropArea/DropArea.scss
@@ -0,0 +1,44 @@
+@import '../../../constants/styles';
+
+.DropArea {
+ gap: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+ z-index: 10;
+ top: 0;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ background-color: var(--document-background-color);
+
+ border: 4px dashed #E1E1E3;
+ box-sizing: border-box;
+
+ .Icon {
+ color: var(--gray-7);
+ height: 65px;
+ width: 65px;
+ }
+ .hidden {
+ display: none;
+ }
+
+ button {
+ width: 105px;
+ height: 32px;
+ border-radius: 4px;
+ inset: 1px;
+ color: var(--blue-5);
+ background-color: var(--document-background-color);
+ border: 1px solid var(--blue-5);
+ pointer-events: auto;
+
+ &:hover {
+ background-color: var(--tools-button-hover);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/MultiViewer/DropArea/index.js b/src/components/MultiViewer/DropArea/index.js
new file mode 100644
index 0000000000..c19e0dc81c
--- /dev/null
+++ b/src/components/MultiViewer/DropArea/index.js
@@ -0,0 +1,3 @@
+import DropArea from './DropArea';
+
+export default DropArea;
\ No newline at end of file
diff --git a/src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper.js b/src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper.js
new file mode 100644
index 0000000000..b431a3491a
--- /dev/null
+++ b/src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper.js
@@ -0,0 +1,11 @@
+import React from 'react';
+import './MultiViewerWrapper.scss';
+import { useSelector } from 'react-redux';
+import selectors from 'selectors';
+
+const MultiViewerWrapper = ({ children }) => {
+ const isMultiViewerReady = useSelector((state) => selectors.isMultiViewerReady(state));
+ return isMultiViewerReady ? <>{children}> : null;
+};
+
+export default MultiViewerWrapper;
\ No newline at end of file
diff --git a/src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper.scss b/src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper.scss
new file mode 100644
index 0000000000..7820f5b31d
--- /dev/null
+++ b/src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper.scss
@@ -0,0 +1,3 @@
+.MultiViewerWrapper {
+
+}
\ No newline at end of file
diff --git a/src/components/MultiViewer/MultiViewerWrapper/index.js b/src/components/MultiViewer/MultiViewerWrapper/index.js
new file mode 100644
index 0000000000..c5717b7f47
--- /dev/null
+++ b/src/components/MultiViewer/MultiViewerWrapper/index.js
@@ -0,0 +1,3 @@
+import MultiViewerWrapper from 'src/components/MultiViewer/MultiViewerWrapper/MultiViewerWrapper';
+
+export default MultiViewerWrapper;
\ No newline at end of file
diff --git a/src/components/MultiViewer/index.js b/src/components/MultiViewer/index.js
new file mode 100644
index 0000000000..27038c49a7
--- /dev/null
+++ b/src/components/MultiViewer/index.js
@@ -0,0 +1,3 @@
+import MultiViewer from 'src/components/MultiViewer/MultiViewer';
+
+export default MultiViewer;
\ No newline at end of file
diff --git a/src/components/NoteTextarea/NoteTextarea.stories.js b/src/components/NoteTextarea/NoteTextarea.stories.js
new file mode 100644
index 0000000000..93de470a49
--- /dev/null
+++ b/src/components/NoteTextarea/NoteTextarea.stories.js
@@ -0,0 +1,77 @@
+import React, { useRef } from 'react';
+import NoteContext from '../Note/Context';
+import NoteTextarea from './NoteTextarea';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+
+export default {
+ title: 'Components/NotesPanel/NoteTextarea',
+ component: NoteTextarea,
+};
+
+function handleStateChange(newValue) {
+ // eslint-disable-next-line no-console
+}
+
+const context = {
+ pendingEditTextMap: {},
+ pendingReplyMap: {},
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ openElements: { audioPlaybackPopup: true },
+ customElementOverrides: {},
+ userData: [
+ {
+ value: 'John Doe',
+ id: 'johndoe@gmail.com',
+ email: 'johndoe@gmail.com',
+ },
+ {
+ value: 'Jane Doe',
+ id: 'janedoe@gmail.com',
+ email: 'janedoe@gmail.com'
+ },
+ {
+ value: 'Jane Doe',
+ id: 'janedoejanedoejanedoejanedoe@gmail.com',
+ email: 'janedoejanedoejanedoejanedoe@gmail.com'
+ },
+ ]
+ }
+};
+
+function rootReducer(state = initialState, action) {
+ return state;
+}
+
+const store = createStore(rootReducer);
+
+const props = {
+ value: 'test',
+ onChange: handleStateChange,
+ onSubmit: () => console.log('onSubmit'),
+ onBlur: () => console.log('onBlur'),
+ onFocus: () => console.log('onFocus')
+};
+
+export const Basic = () => {
+ const textareaRef = useRef(null);
+
+ return (
+
+
+ {
+ textareaRef.current = el;
+ }
+ }
+ />
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/NotesPanel/ReplyAttachmentPicker.js b/src/components/NotesPanel/ReplyAttachmentPicker.js
new file mode 100644
index 0000000000..ee3bf62f12
--- /dev/null
+++ b/src/components/NotesPanel/ReplyAttachmentPicker.js
@@ -0,0 +1,38 @@
+import React from 'react';
+import { useSelector } from 'react-redux';
+import selectors from 'selectors';
+
+const ReplyAttachmentPicker = ({ annotationId, addAttachments }) => {
+ const replyAttachmentHandler = useSelector((state) => selectors.getReplyAttachmentHandler(state));
+
+ const onChange = async (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ let attachment = file;
+ if (replyAttachmentHandler) {
+ const url = await replyAttachmentHandler(file);
+ attachment = {
+ url,
+ name: file.name,
+ size: file.size,
+ type: file.type
+ };
+ }
+ addAttachments(annotationId, [attachment]);
+ }
+ };
+
+ return (
+ {
+ e.target.value = '';
+ }}
+ />
+ );
+};
+
+export default ReplyAttachmentPicker;
diff --git a/src/components/NotesPanelHeader/index.js b/src/components/NotesPanelHeader/index.js
new file mode 100644
index 0000000000..8a772a9429
--- /dev/null
+++ b/src/components/NotesPanelHeader/index.js
@@ -0,0 +1,3 @@
+import NotesPanelHeader from './NotesPanelHeader';
+
+export default NotesPanelHeader;
\ No newline at end of file
diff --git a/src/components/OfficeEditorCreateTablePopup/OfficeEditorCreateTablePopup.scss b/src/components/OfficeEditorCreateTablePopup/OfficeEditorCreateTablePopup.scss
new file mode 100644
index 0000000000..8c2c4b6a13
--- /dev/null
+++ b/src/components/OfficeEditorCreateTablePopup/OfficeEditorCreateTablePopup.scss
@@ -0,0 +1,26 @@
+.office-editor-create-table {
+ margin: 8px;
+ width: 120px;
+ height: 150px;
+ cursor: pointer;
+
+ table, td {
+ border: 1px solid var(--gray-8);
+ border-collapse: collapse;
+ }
+
+ td {
+ width: 12px;
+ height: 12px;
+
+ &.selected-cell {
+ background-color: var(--oe-table-dropdown-highlight);
+ }
+ }
+
+ .create-table-rows-columns {
+ display: block;
+ text-align: center;
+ margin: 10px auto;
+ }
+}
diff --git a/src/components/OfficeEditorCreateTablePopup/OfficeEditorCreateTablePopup.stories.js b/src/components/OfficeEditorCreateTablePopup/OfficeEditorCreateTablePopup.stories.js
new file mode 100644
index 0000000000..ab70942ea3
--- /dev/null
+++ b/src/components/OfficeEditorCreateTablePopup/OfficeEditorCreateTablePopup.stories.js
@@ -0,0 +1,13 @@
+import React from 'react';
+import OfficeEditorCreateTablePopup from './OfficeEditorCreateTablePopup';
+
+export default {
+ title: 'Components/OfficeEditorCreateTablePopup',
+ component: OfficeEditorCreateTablePopup
+};
+
+export function Basic() {
+ return (
+
+ );
+}
diff --git a/src/components/OfficeEditorCreateTablePopup/index.js b/src/components/OfficeEditorCreateTablePopup/index.js
new file mode 100644
index 0000000000..3bd366ecdf
--- /dev/null
+++ b/src/components/OfficeEditorCreateTablePopup/index.js
@@ -0,0 +1,3 @@
+import OfficeEditorCreateTablePopup from './OfficeEditorCreateTablePopup';
+
+export default OfficeEditorCreateTablePopup;
diff --git a/src/components/OfficeEditorImageFilePickerHandler/OfficeEditorImageFilePickerHandler.js b/src/components/OfficeEditorImageFilePickerHandler/OfficeEditorImageFilePickerHandler.js
new file mode 100644
index 0000000000..2204df4ccd
--- /dev/null
+++ b/src/components/OfficeEditorImageFilePickerHandler/OfficeEditorImageFilePickerHandler.js
@@ -0,0 +1,59 @@
+import React from 'react';
+import actions from 'actions';
+import { useDispatch } from 'react-redux';
+import getRootNode from 'helpers/getRootNode';
+import core from 'core';
+import DataElements from 'constants/dataElement';
+
+import '../FilePickerHandler/FilePickerHandler.scss';
+
+// TODO: Can we accept any other image formats?
+const ACCEPTED_FORMATS = ['jpg', 'jpeg', 'png', 'bmp'].map(
+ (format) => `.${format}`,
+).join(', ');
+
+const toBase64 = (file) => new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.readAsDataURL(file);
+ reader.onload = () => resolve(reader.result);
+ reader.onerror = reject;
+});
+
+const FilePickerHandler = () => {
+ const dispatch = useDispatch();
+
+ const openDocument = async (e) => {
+ const file = e.target.files[0];
+ if (file) {
+ try {
+ dispatch(actions.openElement(DataElements.LOADING_MODAL));
+ const base64 = await toBase64(file);
+ await core.getOfficeEditor().insertImageAtCursor(base64);
+ dispatch(actions.closeElement(DataElements.LOADING_MODAL));
+ } catch (error) {
+ dispatch(actions.closeElement(DataElements.LOADING_MODAL));
+ dispatch(actions.showWarningMessage({
+ title: 'Error',
+ message: error.message,
+ }));
+ }
+ const picker = getRootNode().querySelector('#office-editor-file-picker');
+ if (picker) {
+ picker.value = '';
+ }
+ }
+ };
+
+ return (
+
+
+
+ );
+};
+
+export default FilePickerHandler;
diff --git a/src/components/OfficeEditorImageFilePickerHandler/index.js b/src/components/OfficeEditorImageFilePickerHandler/index.js
new file mode 100644
index 0000000000..9babab9332
--- /dev/null
+++ b/src/components/OfficeEditorImageFilePickerHandler/index.js
@@ -0,0 +1,3 @@
+import OfficeEditorImageFilePickerHandler from './OfficeEditorImageFilePickerHandler';
+
+export default OfficeEditorImageFilePickerHandler;
diff --git a/src/components/OutlineContent/OutlineContent.js b/src/components/OutlineContent/OutlineContent.js
new file mode 100644
index 0000000000..de85a11ef3
--- /dev/null
+++ b/src/components/OutlineContent/OutlineContent.js
@@ -0,0 +1,267 @@
+import React, { useContext, useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import PropTypes from 'prop-types';
+import core from 'core';
+
+import Button from '../Button';
+import MoreOptionsContextMenuPopup from '../MoreOptionsContextMenuPopup';
+import OutlineContext from '../Outline/Context';
+import './OutlineContent.scss';
+
+const propTypes = {
+ text: PropTypes.string.isRequired,
+ outlinePath: PropTypes.string,
+ isAdding: PropTypes.bool,
+ isOutlineRenaming: PropTypes.bool,
+ setOutlineRenaming: PropTypes.func,
+ isOutlineChangingDest: PropTypes.bool,
+ setOutlineChangingDest: PropTypes.func,
+ setIsHovered: PropTypes.func,
+ onCancel: PropTypes.func,
+ textColor: PropTypes.string,
+};
+
+const OutlineContent = ({
+ text,
+ outlinePath,
+ isAdding,
+ isOutlineRenaming,
+ setOutlineRenaming,
+ isOutlineChangingDest,
+ setOutlineChangingDest,
+ setIsHovered,
+ onCancel,
+ textColor,
+}) => {
+ const {
+ currentDestPage,
+ currentDestText,
+ editingOutlines,
+ setEditingOutlines,
+ isMultiSelectMode,
+ isOutlineEditable,
+ addNewOutline,
+ renameOutline,
+ updateOutlineDest,
+ updateOutlines,
+ removeOutlines,
+ } = useContext(OutlineContext);
+
+ const [t] = useTranslation();
+ const TOOL_NAME = 'OutlineDestinationCreateTool';
+
+ const [isDefault, setIsDefault] = useState(false);
+ const [outlineText, setOutlineText] = useState(text);
+ const [isContextMenuOpen, setContextMenuOpen] = useState(false);
+ const inputRef = useRef();
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Enter') {
+ e.stopPropagation();
+ if (isAdding) {
+ onAddOutline();
+ }
+ if (isOutlineRenaming && !isRenameButtonDisabled()) {
+ onRenameOutline();
+ }
+ }
+ if (e.key === 'Escape') {
+ onCancelOutline();
+ }
+ };
+
+ const onAddOutline = () => {
+ addNewOutline(outlineText.trim() === '' ? '' : outlineText);
+ };
+
+ const onRenameOutline = () => {
+ setOutlineRenaming(false);
+ renameOutline(outlinePath, outlineText);
+ };
+
+ const onCancelOutline = () => {
+ updateOutlines();
+ if (isOutlineRenaming) {
+ setOutlineRenaming(false);
+ setOutlineText(text);
+ }
+ if (isOutlineChangingDest) {
+ setOutlineChangingDest(false);
+ }
+ if (isAdding) {
+ onCancel();
+ }
+ };
+
+ const isRenameButtonDisabled = () => {
+ return !outlineText || text === outlineText;
+ };
+
+ useEffect(() => {
+ if (outlineText !== text) {
+ setOutlineText(text);
+ }
+ }, [text]);
+
+ useEffect(() => {
+ if (isAdding || isOutlineRenaming) {
+ inputRef.current.focus();
+ inputRef.current.select();
+ }
+
+ setIsDefault(!isAdding && !isOutlineRenaming && !isOutlineChangingDest);
+ }, [isOutlineRenaming, isOutlineChangingDest]);
+
+ useEffect(() => {
+ const editingOutlinesClone = { ...editingOutlines };
+ const isOutlineEditing = isOutlineRenaming || isOutlineChangingDest;
+ if (isOutlineEditing) {
+ editingOutlinesClone[outlinePath] = (isOutlineEditing);
+ } else {
+ delete editingOutlinesClone[outlinePath];
+ }
+ setEditingOutlines({ ...editingOutlinesClone });
+ }, [isOutlineRenaming, isOutlineChangingDest]);
+
+ useEffect(() => {
+ if (!isAdding) {
+ setIsHovered(isContextMenuOpen);
+ }
+ }, [isContextMenuOpen]);
+
+ const textStyle = {
+ color: textColor || 'auto'
+ };
+
+ return (
+
+ {isAdding &&
+
+ {t('component.newOutlineTitle')}
+
+ }
+
+ {isDefault &&
+ <>
+
{
+ if (isOutlineEditable) {
+ setOutlineRenaming(true);
+ }
+ }}
+ style={textStyle}
+ >
+ {text}
+
+
+ {isOutlineEditable &&
+
{
+ e.stopPropagation();
+ setContextMenuOpen(true);
+ }}
+ />
+ }
+ {isContextMenuOpen &&
+ setContextMenuOpen(false)}
+ onRenameClick={() => {
+ setContextMenuOpen(false);
+ setOutlineRenaming(true);
+ }}
+ onSetDestinationClick={() => {
+ setContextMenuOpen(false);
+ setOutlineChangingDest(true);
+ core.setToolMode(TOOL_NAME);
+ }}
+ onDeleteClick={() => {
+ setContextMenuOpen(false);
+ removeOutlines([outlinePath]);
+ }}
+ />
+ }
+ >
+ }
+
+ {isOutlineChangingDest &&
+
+ {text}
+
+ }
+
+ {(isAdding || isOutlineRenaming) &&
+ setOutlineText(e.target.value)}
+ />
+ }
+
+ {(isAdding || isOutlineChangingDest) &&
+
+ {t('component.destination')}: {t('component.bookmarkPage')} {currentDestPage},
+ “{currentDestText}”
+
+ }
+
+ {(isAdding || isOutlineRenaming || isOutlineChangingDest) &&
+
+
+ {isAdding &&
+
+ }
+ {isOutlineRenaming &&
+
+ }
+ {isOutlineChangingDest &&
+ {
+ setOutlineChangingDest(false);
+ updateOutlineDest(outlinePath);
+ }}
+ />
+ }
+
+ }
+
+ );
+};
+
+OutlineContent.propTypes = propTypes;
+
+export default OutlineContent;
diff --git a/src/components/OutlineContent/OutlineContent.scss b/src/components/OutlineContent/OutlineContent.scss
new file mode 100644
index 0000000000..88577c5f59
--- /dev/null
+++ b/src/components/OutlineContent/OutlineContent.scss
@@ -0,0 +1,13 @@
+.outline-text,
+.outline-destination {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.outline-destination {
+ flex-basis: 100%;
+ font-size: 10px;
+ color: var(--faded-text);
+ margin-top: var(--padding-small);
+}
\ No newline at end of file
diff --git a/src/components/OutlineContent/OutlineContent.spec.js b/src/components/OutlineContent/OutlineContent.spec.js
new file mode 100644
index 0000000000..b6093a0fbb
--- /dev/null
+++ b/src/components/OutlineContent/OutlineContent.spec.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import { render, fireEvent } from '@testing-library/react';
+import { Basic, Renaming, ColoredOutline } from './OutlineContent.stories';
+
+const BasicOutline = withProviders(Basic);
+const RenamingOutline = withProviders(Renaming);
+
+describe('Outline', () => {
+ it('Story should not throw any errors', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('Save button in renaming outline is disabled if text is empty or text is the same as current name', () => {
+ const { container } = render();
+
+ const saveButton = container.querySelector('.bookmark-outline-save-button');
+ expect(saveButton.disabled).toBe(true);
+
+ const textInput = container.querySelector('.bookmark-outline-input');
+ fireEvent.change(textInput, { target: { value: 'new outline' } });
+ expect(saveButton.disabled).toBe(false);
+
+ fireEvent.change(textInput, { target: { value: '' } });
+ expect(saveButton.disabled).toBe(true);
+ });
+
+ it('should set font color if textColor is passed to OutlineContent', () => {
+ const { container } = render();
+
+ const outline = container.querySelector('.bookmark-outline-text');
+ expect(outline.style.color).toBe('rgb(255, 0, 0)');
+ });
+});
diff --git a/src/components/OutlineContent/OutlineContent.stories.js b/src/components/OutlineContent/OutlineContent.stories.js
new file mode 100644
index 0000000000..e92329339b
--- /dev/null
+++ b/src/components/OutlineContent/OutlineContent.stories.js
@@ -0,0 +1,190 @@
+import React from 'react';
+import { legacy_createStore as createStore } from 'redux';
+import { Provider as ReduxProvider } from 'react-redux';
+import OutlineContent from './OutlineContent';
+import OutlineContext from '../Outline/Context';
+import '../LeftPanel/LeftPanel.scss';
+
+const NOOP = () => { };
+
+export default {
+ title: 'Components/OutlineContent',
+ component: OutlineContent,
+};
+
+const reducer = () => {
+ return {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ isOutlineEditingEnabled: true,
+ },
+ document: {
+ outlines: {},
+ },
+ };
+};
+
+export const Basic = () => {
+ return (
+
+
+
+ );
+};
+
+export const Adding = () => {
+ return (
+
+
+
+ );
+};
+
+export const Renaming = () => {
+ return (
+
+
+
+ );
+};
+
+export const ChangingDestination = () => {
+ return (
+
+
+
+ );
+};
+
+export const ColoredOutline = () => {
+ return (
+
+
+
+ );
+};
\ No newline at end of file
diff --git a/src/components/OutlineContent/index.js b/src/components/OutlineContent/index.js
new file mode 100644
index 0000000000..a1e905f50e
--- /dev/null
+++ b/src/components/OutlineContent/index.js
@@ -0,0 +1,3 @@
+import OutlineContent from './OutlineContent';
+
+export default OutlineContent;
diff --git a/src/components/OutlinesPanel/OutlinesDragLayer.js b/src/components/OutlinesPanel/OutlinesDragLayer.js
new file mode 100644
index 0000000000..6eff6e115e
--- /dev/null
+++ b/src/components/OutlinesPanel/OutlinesDragLayer.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import { useDragLayer } from 'react-dnd';
+import { ItemTypes } from 'constants/dnd';
+
+const layerStyles = {
+ position: 'fixed',
+ pointerEvents: 'none',
+ zIndex: 99999,
+ left: 0,
+ top: 0,
+ width: '100%',
+ height: '100%'
+};
+
+const getItemStyles = (initialOffset, currentOffset) => {
+ if (!initialOffset || !currentOffset) {
+ return {
+ display: 'none'
+ };
+ }
+ const { x, y } = currentOffset;
+ const transform = `translate(calc(${x}px - 50%), calc(${y}px - 100%))`;
+ return {
+ transform,
+ WebkitTransform: transform,
+ };
+};
+
+export const OutlinesDragLayer = () => {
+ const {
+ itemType,
+ item,
+ isDragging,
+ initialOffset,
+ currentOffset
+ } = useDragLayer((dragLayerState) => ({
+ itemType: dragLayerState.getItemType(),
+ item: dragLayerState.getItem(),
+ isDragging: dragLayerState.isDragging(),
+ initialOffset: dragLayerState.getInitialSourceClientOffset(),
+ currentOffset: dragLayerState.getClientOffset(),
+ }));
+
+ const renderDragItem = () => {
+ if (!item) {
+ return null;
+ }
+
+ const { dragOutline } = item;
+
+ switch (itemType) {
+ case ItemTypes.OUTLINE:
+ return (
+ <>
+ {dragOutline.getName()}
+ >
+ );
+ default:
+ return null;
+ }
+ };
+
+ if (!isDragging) {
+ return null;
+ }
+
+ return (
+
+
+ {renderDragItem()}
+
+
+ );
+};
diff --git a/src/components/Panel/Panel.stories.js b/src/components/Panel/Panel.stories.js
index df6c061d8c..ac207f171e 100644
--- a/src/components/Panel/Panel.stories.js
+++ b/src/components/Panel/Panel.stories.js
@@ -27,7 +27,7 @@ const initialState = {
panelWidths: { panel: DEFAULT_NOTES_PANEL_WIDTH },
sortStrategy: 'position',
isInDesktopOnlyMode: true,
- modularHeaders: []
+ modularHeaders: {}
}
};
diff --git a/src/components/PortfolioItem/PortfolioItem.scss b/src/components/PortfolioItem/PortfolioItem.scss
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/components/PortfolioItem/index.js b/src/components/PortfolioItem/index.js
new file mode 100644
index 0000000000..8711af14dc
--- /dev/null
+++ b/src/components/PortfolioItem/index.js
@@ -0,0 +1,3 @@
+import PortfolioItem from './PortfolioItem';
+
+export default PortfolioItem;
diff --git a/src/components/PortfolioItemContent/PortfolioItemContent.scss b/src/components/PortfolioItemContent/PortfolioItemContent.scss
index 65fbbe5495..3a2afe8993 100644
--- a/src/components/PortfolioItemContent/PortfolioItemContent.scss
+++ b/src/components/PortfolioItemContent/PortfolioItemContent.scss
@@ -1,5 +1,7 @@
.PortfolioPanel {
.bookmark-outline-single-container {
+ margin-top: 6px;
+ margin-bottom: 6px;
.bookmark-outline-label-row {
align-items: center;
diff --git a/src/components/PortfolioItemContent/index.js b/src/components/PortfolioItemContent/index.js
new file mode 100644
index 0000000000..0765d3b00b
--- /dev/null
+++ b/src/components/PortfolioItemContent/index.js
@@ -0,0 +1,3 @@
+import PortfolioItemContent from './PortfolioItemContent';
+
+export default PortfolioItemContent;
diff --git a/src/components/PortfolioPanel/PortfolioContext.js b/src/components/PortfolioPanel/PortfolioContext.js
new file mode 100644
index 0000000000..484ecdd0b5
--- /dev/null
+++ b/src/components/PortfolioPanel/PortfolioContext.js
@@ -0,0 +1,5 @@
+import React from 'react';
+
+const PortfolioContext = React.createContext();
+
+export default PortfolioContext;
\ No newline at end of file
diff --git a/src/components/PortfolioPanel/PortfolioDragLayer.js b/src/components/PortfolioPanel/PortfolioDragLayer.js
new file mode 100644
index 0000000000..be3ff191b0
--- /dev/null
+++ b/src/components/PortfolioPanel/PortfolioDragLayer.js
@@ -0,0 +1,76 @@
+import React from 'react';
+import { useDragLayer } from 'react-dnd';
+import { ItemTypes } from 'constants/dnd';
+
+const layerStyles = {
+ position: 'fixed',
+ pointerEvents: 'none',
+ zIndex: 99999,
+ left: 0,
+ top: 0,
+ width: '100%',
+ height: '100%'
+};
+
+const getItemStyles = (initialOffset, currentOffset) => {
+ if (!initialOffset || !currentOffset) {
+ return {
+ display: 'none'
+ };
+ }
+ const { x, y } = currentOffset;
+ const transform = `translate(calc(${x}px - 50%), calc(${y}px - 100%))`;
+ return {
+ transform,
+ WebkitTransform: transform,
+ };
+};
+
+export const PortfolioDragLayer = () => {
+ const {
+ itemType,
+ item,
+ isDragging,
+ initialOffset,
+ currentOffset
+ } = useDragLayer((dragLayerState) => ({
+ itemType: dragLayerState.getItemType(),
+ item: dragLayerState.getItem(),
+ isDragging: dragLayerState.isDragging(),
+ initialOffset: dragLayerState.getInitialSourceClientOffset(),
+ currentOffset: dragLayerState.getClientOffset(),
+ }));
+
+ const renderDragItemPreview = () => {
+ if (!item) {
+ return null;
+ }
+
+ const { dragPortfolioItem } = item;
+
+ if (itemType === ItemTypes.PORTFOLIO) {
+ return (
+ <>
+ {dragPortfolioItem.name}
+ >
+ );
+ }
+
+ return null;
+ };
+
+ if (!isDragging) {
+ return null;
+ }
+
+ return (
+
+
+ {renderDragItemPreview()}
+
+
+ );
+};
diff --git a/src/components/PortfolioPanel/PortfolioPanel.scss b/src/components/PortfolioPanel/PortfolioPanel.scss
new file mode 100644
index 0000000000..a4a0d22370
--- /dev/null
+++ b/src/components/PortfolioPanel/PortfolioPanel.scss
@@ -0,0 +1,12 @@
+@import '../../constants/styles';
+@import '../../constants/panel';
+
+.PortfolioPanel {
+ .portfolio-panel-control {
+ display: flex;
+ }
+
+ .bookmark-outline-row {
+ padding-top: 6px;
+ }
+}
\ No newline at end of file
diff --git a/src/components/PortfolioPanel/index.js b/src/components/PortfolioPanel/index.js
new file mode 100644
index 0000000000..33783b3238
--- /dev/null
+++ b/src/components/PortfolioPanel/index.js
@@ -0,0 +1,3 @@
+import PortfolioPanel from './PortfolioPanel';
+
+export default PortfolioPanel;
diff --git a/src/components/ReactSelectCustomArrowIndicator/ReactSelectCustomArrowIndicator.js b/src/components/ReactSelectCustomArrowIndicator/ReactSelectCustomArrowIndicator.js
new file mode 100644
index 0000000000..db31b603bb
--- /dev/null
+++ b/src/components/ReactSelectCustomArrowIndicator/ReactSelectCustomArrowIndicator.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import { components } from 'react-select';
+import Icon from 'components/Icon';
+
+const ReactSelectCustomArrowIndicator = (props) => {
+ const { selectProps } = props;
+ const { menuIsOpen } = selectProps;
+ return (
+
+
+
+ );
+};
+
+export default ReactSelectCustomArrowIndicator;
\ No newline at end of file
diff --git a/src/components/ReactSelectCustomArrowIndicator/index.js b/src/components/ReactSelectCustomArrowIndicator/index.js
new file mode 100644
index 0000000000..1a2e698e7f
--- /dev/null
+++ b/src/components/ReactSelectCustomArrowIndicator/index.js
@@ -0,0 +1,3 @@
+import ReactSelectCustomArrowIndicator from './ReactSelectCustomArrowIndicator';
+
+export default ReactSelectCustomArrowIndicator;
\ No newline at end of file
diff --git a/src/components/ReactSelectWebComponentProvider/index.js b/src/components/ReactSelectWebComponentProvider/index.js
new file mode 100644
index 0000000000..3a1f0e7dc1
--- /dev/null
+++ b/src/components/ReactSelectWebComponentProvider/index.js
@@ -0,0 +1,3 @@
+import ReactSelectWebComponentProvider from './ReactSelectWebComponentProvider';
+
+export default ReactSelectWebComponentProvider;
\ No newline at end of file
diff --git a/src/components/RedactionPanel/RedactionPanel.stories.js b/src/components/RedactionPanel/RedactionPanel.stories.js
index b1f2574537..b5af2f69fb 100644
--- a/src/components/RedactionPanel/RedactionPanel.stories.js
+++ b/src/components/RedactionPanel/RedactionPanel.stories.js
@@ -6,13 +6,20 @@ import RedactionPanelContainerWithProvider from './RedactionPanelContainer';
import RightPanel from 'components/RightPanel';
import { RedactionPanelContext } from './RedactionPanelContext';
import { defaultRedactionTypes, redactionTypeMap } from 'constants/redactionTypes';
+import Panel from 'components/Panel';
const noop = () => { };
export default {
title: 'Components/RedactionPanel',
component: RedactionPanel,
- includeStories: ['EmptyList', 'PanelWithRedactionItems', 'RedactionPanelWithSearch']
+ includeStories: [
+ 'EmptyList', 'PanelWithRedactionItems', 'RedactionPanelWithSearch',
+ 'RedactionLeftGenericPanel',
+ 'RedactionRightGenericPanel',
+ 'RightPanelWithRedactionItems',
+ 'LeftPanelWithRedactionItems',
+ ]
};
export const RedactionContextMock = ({ children, mockContext }) => {
@@ -50,12 +57,14 @@ const initialState = {
openElements: {
header: true,
redactionPanel: true,
+ panel: true,
},
currentLanguage: 'en',
panelWidths: {
redactionPanel: 330,
+ panel: 300,
},
- modularHeaders: [],
+ modularHeaders: {},
modularHeadersHeight: {
topHeaders: 40,
bottomHeaders: 40
@@ -200,7 +209,65 @@ export function PanelWithRedactionItems() {
export function RedactionPanelWithSearch() {
return (
-
+
);
}
+
+
+export function RedactionLeftGenericPanel() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function RedactionRightGenericPanel() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+
+export function RightPanelWithRedactionItems() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export function LeftPanelWithRedactionItems() {
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/RedactionSearchResultGroup/RedactionSearchResultGroup.scss b/src/components/RedactionSearchResultGroup/RedactionSearchResultGroup.scss
index 7c35f384a6..aa3e037d00 100644
--- a/src/components/RedactionSearchResultGroup/RedactionSearchResultGroup.scss
+++ b/src/components/RedactionSearchResultGroup/RedactionSearchResultGroup.scss
@@ -1,7 +1,7 @@
.redaction-search-results-page-number {
display: flex;
align-items: center;
- margin-left: 11px;
+ margin-left: 11px !important;
label {
font-size: 13px;
diff --git a/src/components/ReplyAttachmentList/ReplyAttachmentList.scss b/src/components/ReplyAttachmentList/ReplyAttachmentList.scss
new file mode 100644
index 0000000000..910cd5b46b
--- /dev/null
+++ b/src/components/ReplyAttachmentList/ReplyAttachmentList.scss
@@ -0,0 +1,84 @@
+.reply-attachment-list {
+ display: flex;
+ flex-direction: column;
+ cursor: default;
+
+ .reply-attachment {
+ display: flex;
+ flex-direction: column;
+ background-color: var(--gray-1);
+ border-radius: 4px;
+ cursor: pointer;
+ overflow: hidden;
+
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
+
+ .reply-attachment-preview {
+ width: 100%;
+ max-height: 80px;
+ display: flex;
+ justify-content: center;
+
+ &.dirty {
+ position: relative;
+ margin-bottom: 15px;
+ }
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ object-fit: contain;
+ }
+
+ .reply-attachment-preview-message {
+ font-size: 11px;
+ color: var(--error-text-color);
+ position: absolute;
+ bottom: -15px;
+ left: 10px;
+ }
+ }
+
+ .reply-attachment-info {
+ display: flex;
+ align-items: center;
+ height: 40px;
+ padding: 8px;
+
+ .reply-attachment-icon {
+ height: 24px;
+ min-height: 24px;
+ width: 24px;
+ min-width: 24px;
+ }
+
+ .reply-file-name {
+ height: 16px;
+ width: 100%;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ margin-left: 8px;
+ margin-right: 8px;
+ }
+
+ .attachment-button {
+ height: 24px;
+ min-height: 24px;
+ width: 24px;
+ min-width: 24px;
+
+ &:hover {
+ background-color: var(--blue-1);
+ }
+
+ .Icon {
+ height: 16px;
+ width: 16px;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/components/ReplyAttachmentList/ReplyAttachmentList.stories.js b/src/components/ReplyAttachmentList/ReplyAttachmentList.stories.js
new file mode 100644
index 0000000000..f994ca3e33
--- /dev/null
+++ b/src/components/ReplyAttachmentList/ReplyAttachmentList.stories.js
@@ -0,0 +1,95 @@
+import React from 'react';
+import ReplyAttachmentList from './ReplyAttachmentList';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+
+export default {
+ title: 'Components/ReplyAttachmentList',
+ component: ReplyAttachmentList
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ replyAttachmentPreviewEnabled: true,
+ }
+};
+function rootReducer(state = initialState, action) {
+ return state;
+}
+const store = createStore(rootReducer);
+
+const files = [
+ {
+ name: 'file_1.pdf'
+ },
+ {
+ name: 'file_2.doc'
+ },
+ {
+ name: 'file_3_extra_long_file_name.cad'
+ }
+];
+
+// State 1
+export function DisplayMode() {
+ const props = {
+ files,
+ isEditing: false
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+// State 2
+export function EditMode() {
+ const props = {
+ files,
+ isEditing: true
+ };
+
+ return (
+
+
+
+
+
+ );
+}
+
+const SVG_MIME_TYPE = 'image/svg+xml';
+const svgString = `
+
+
+
+`;
+const svgBlob = new Blob([svgString], { type: SVG_MIME_TYPE });
+const svgFile = new File([svgBlob], 'redirect.svg', { type: SVG_MIME_TYPE });
+
+// State 3
+export function UnsafeSVGAttachment() {
+ const props = {
+ files: [...files, svgFile],
+ isEditing: false
+ };
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/ReplyAttachmentList/index.js b/src/components/ReplyAttachmentList/index.js
new file mode 100644
index 0000000000..4487c18bf9
--- /dev/null
+++ b/src/components/ReplyAttachmentList/index.js
@@ -0,0 +1,3 @@
+import ReplyAttachmentList from './ReplyAttachmentList';
+
+export default ReplyAttachmentList;
\ No newline at end of file
diff --git a/src/components/RichTextStyleEditor/RichTextStyleEditor.stories.js b/src/components/RichTextStyleEditor/RichTextStyleEditor.stories.js
new file mode 100644
index 0000000000..6c095e8388
--- /dev/null
+++ b/src/components/RichTextStyleEditor/RichTextStyleEditor.stories.js
@@ -0,0 +1,117 @@
+import React from 'react';
+import { configureStore } from '@reduxjs/toolkit';
+import initialState from 'src/redux/initialState';
+import { Provider } from 'react-redux';
+import RichTextStyleEditor from './RichTextStyleEditor';
+import Panel from '../Panel';
+import '../StylePicker/StylePicker.scss';
+import { initialTextColors } from 'helpers/initialColorStates';
+
+export default {
+ title: 'Components/RichTextStyleEditor',
+ component: RichTextStyleEditor,
+};
+
+// Mock some state to show the style popups
+const state = {
+ ...initialState,
+ viewer: {
+ openElements: {
+ watermarkPanel: true,
+ stylePopup: true,
+ stylePanel: true,
+ stylePopupTextStyleContainer: true,
+ stylePopupColorsContainer: true,
+ stylePopupLabelTextContainer: true,
+ panel: true,
+ header: true,
+ },
+ selectedScale: undefined,
+ colorMap: {
+ textField: {
+ currentStyleTab: 'StrokeColor',
+ iconColor: 'StrokeColor',
+ }
+ },
+ fonts: ['Helvetica', 'Times New Roman', 'Arimo'],
+ isSnapModeEnabled: false,
+ customElementOverrides: {},
+ panelWidths: { panel: 264 },
+ colors: [
+ '#fdac0f', '#fa9933', '#f34747', '#21905b', '#c531a4',
+ '#e5631a', '#3e5ece', '#dc9814', '#c27727', '#b11c1c',
+ '#13558c', '#76287b', '#347842', '#318f29', '#ffffff',
+ '#cdcdcd', '#9c9c9c', '#696969', '#272727', '#000000'
+ ],
+ textColors: initialTextColors,
+ toolColorOverrides: {},
+ disabledElements: {
+ logoBar: { disabled: true },
+ },
+ sortStrategy: 'position',
+ isInDesktopOnlyMode: true,
+ modularHeaders: {}
+ }
+};
+
+const noop = () => {};
+
+const store = configureStore({
+ reducer: () => state
+});
+
+const BasicComponent = (props) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const Basic = BasicComponent.bind({
+ annotation: '',
+ editor: {},
+ style: {},
+ isFreeTextAutoSize: false,
+ onFreeTextSizeToggle: () => {},
+ onPropertyChange: () => {},
+ onRichTextStyleChange: () => {},
+});
+Basic.args = {
+ currentStyleTab: 'StrokeColor',
+ isInFormBuilderAndNotFreeText: true,
+ style: {
+ 'FillColor': new window.Core.Annotations.Color(212, 211, 211),
+ 'StrokeColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'TextColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'Opacity': null,
+ 'StrokeThickness': 1,
+ 'FontSize': '12pt',
+ 'Style': 'solid'
+ },
+ colorMapKey: 'textField',
+ colorPalette: 'StrokeColor',
+ disableSeparator: true,
+ hideSnapModeCheckbox: true,
+ isFreeText: false,
+ isEllipse: false,
+ isTextStyleContainerActive: true,
+ isLabelTextContainerActive: true,
+ properties: {
+ 'StrokeStyle': 'solid',
+ },
+ isRedaction: false,
+ fonts: ['Helvetica', 'Times New Roman', 'Arimo'],
+ isSnapModeEnabled: false,
+ onSliderChange: noop,
+ onStyleChange: noop,
+ closeElement: noop,
+ openElement: noop,
+ onPropertyChange: noop,
+ onRichTextStyleChange: noop,
+ onLineStyleChange: noop,
+};
diff --git a/src/components/RichTextStyleEditor/index.js b/src/components/RichTextStyleEditor/index.js
new file mode 100644
index 0000000000..54d5259661
--- /dev/null
+++ b/src/components/RichTextStyleEditor/index.js
@@ -0,0 +1,3 @@
+import RichTextStyleEditor from './RichTextStyleEditor';
+
+export default RichTextStyleEditor;
\ No newline at end of file
diff --git a/src/components/RightPanel/RightPanel.js b/src/components/RightPanel/RightPanel.js
index bafca271cf..5b965d5b61 100644
--- a/src/components/RightPanel/RightPanel.js
+++ b/src/components/RightPanel/RightPanel.js
@@ -50,7 +50,10 @@ const RightPanel = ({ children, dataElement, onResize }) => {
const legacyToolsHeaderOpen = isToolsHeaderOpen && currentToolbarGroup !== 'toolbarGroup-View';
const legacyAllHeadersHidden = !isHeaderOpen && !legacyToolsHeaderOpen;
const { customizableUI } = featureFlags;
- const style = {};
+ const style = {
+ // prevent panel from appearing until scss is loaded
+ display: 'none',
+ };
// Calculating its height according to the existing horizontal modular headers
if (customizableUI) {
const horizontalHeadersHeight = topHeadersHeight + bottomHeadersHeight;
diff --git a/src/components/ScaleModal/ScaleCustom.js b/src/components/ScaleModal/ScaleCustom.js
new file mode 100644
index 0000000000..ab1198e133
--- /dev/null
+++ b/src/components/ScaleModal/ScaleCustom.js
@@ -0,0 +1,318 @@
+import React, { useState, useEffect, useRef } from 'react';
+import PropTypes from 'prop-types';
+import { useSelector, shallowEqual } from 'react-redux';
+import { useTranslation } from 'react-i18next';
+import selectors from 'selectors';
+import Tooltip from '../Tooltip';
+import Dropdown from '../Dropdown';
+import {
+ ifFractionalPrecision,
+ hintValues,
+ convertUnit,
+ fractionalUnits,
+ floatRegex,
+ inFractionalRegex,
+ ftInFractionalRegex,
+ ftInDecimalRegex,
+ parseFtInDecimal,
+ parseInFractional,
+ parseFtInFractional
+} from 'constants/measurementScale';
+import classNames from 'classnames';
+
+const Scale = window.Core.Scale;
+
+const ScaleCustomProps = {
+ scale: PropTypes.array,
+ onScaleChange: PropTypes.func,
+ precision: PropTypes.number
+};
+
+function ScaleCustom({ scale, onScaleChange, precision }) {
+ const [measurementUnits] = useSelector((state) => [selectors.getMeasurementUnits(state)], shallowEqual);
+ const [pageValueDisplay, setPageValueDisplay] = useState('');
+ const [worldValueDisplay, setWorldValueDisplay] = useState('');
+ const [isFractionalPrecision, setIsFractionalPrecision] = useState(false);
+ const [pageWarningMessage, setPageWarningMessage] = useState('');
+ const [worldWarningMessage, setWorldWarningMessage] = useState('');
+ const [scaleValueBlurFlag, setScaleValueBlurFlag] = useState(false);
+
+ const pageValueInput = useRef(null);
+ const worldValueInput = useRef(null);
+
+ const [t] = useTranslation();
+
+ const filterFractionalUnits = (units) => units.filter((unit) => fractionalUnits.includes(unit));
+ const unitFromOptions = isFractionalPrecision ? filterFractionalUnits(measurementUnits.from) : measurementUnits.from;
+ const unitToOptions = isFractionalPrecision ? filterFractionalUnits(measurementUnits.to) : measurementUnits.to;
+
+ // If our scale has a unit that is not in the current 'from' measurement units, change it
+ // to the first unit in the list.
+ useEffect(() => {
+ if (!unitFromOptions.includes(scale[0][1])) {
+ onScaleUnitChange(unitFromOptions[0], true);
+ }
+ }, [scale[0][1]]);
+
+ // If our scale has a unit that is not in the current 'to' measurement units, change it
+ // to the first unit in the list. We want to wait until the 'from' unit is valid before
+ // setting the 'to' unit. Otherwise, we will reset the 'from' unit.
+ useEffect(() => {
+ if (unitFromOptions.includes(scale[0][1]) && !unitToOptions.includes(scale[1][1])) {
+ onScaleUnitChange(unitToOptions[0], false);
+ }
+ }, [scale[0][1], scale[1][1]]);
+
+ useEffect(() => {
+ const formatDecimal = (value) => {
+ return value?.toFixed((1 / precision).toString().length - 1);
+ };
+
+ if (scale[0][0] && pageValueInput?.current !== document.activeElement) {
+ if (!isFractionalPrecision) {
+ setPageValueDisplay(formatDecimal(scale[0][0]) || '');
+ } else {
+ setPageValueDisplay(Scale.getFormattedValue(scale[0][0], scale[0][1], precision, false, true) || '');
+ }
+ }
+ if (scale[1][0] && !(worldValueInput && worldValueInput.current === document.activeElement)) {
+ if (!isFractionalPrecision && scale[1][1] !== 'ft-in') {
+ setWorldValueDisplay(formatDecimal(scale[1][0]) || '');
+ } else {
+ setWorldValueDisplay(Scale.getFormattedValue(scale[1][0], scale[1][1], precision, false, true) || '');
+ }
+ }
+ }, [scale, precision, worldValueInput, pageValueInput, isFractionalPrecision, scaleValueBlurFlag]);
+
+ useEffect(() => {
+ setIsFractionalPrecision(ifFractionalPrecision(precision));
+ }, [precision]);
+
+ useEffect(() => {
+ if (isFractionalPrecision) {
+ setPageWarningMessage(hintValues[scale[0][1]]);
+ setWorldWarningMessage(hintValues[scale[1][1]]);
+ } else if (scale[1][1] === 'ft-in') {
+ setPageWarningMessage('');
+ setWorldWarningMessage(hintValues['ft-in decimal']);
+ } else {
+ setPageWarningMessage('');
+ setWorldWarningMessage('');
+ }
+ }, [scale, isFractionalPrecision]);
+
+ // Re-validate invalid world value input when world unit changes
+ useEffect(() => {
+ !isWorldValueValid && onInputValueChange(worldValueInput.current.value, false);
+ }, [scale[1][1]]);
+
+ // Re-validate invalid scale value input when isFractionalPrecision value changes
+ useEffect(() => {
+ if (!isPageValueValid && !isWorldValueValid) {
+ let pageScale = {
+ value: scale[0][0],
+ unit: scale[0][1]
+ };
+ onInputValueChange(pageValueInput.current.value, true, (newScale) => {
+ pageScale = newScale.pageScale;
+ });
+ let worldScale = {
+ value: scale[1][0],
+ unit: scale[1][1]
+ };
+ onInputValueChange(worldValueInput.current.value, false, (newScale) => {
+ worldScale = newScale.worldScale;
+ });
+
+ _onScaleChange(new Scale({ pageScale, worldScale }));
+ } else {
+ !isPageValueValid && onInputValueChange(pageValueInput.current.value, true);
+ !isWorldValueValid && onInputValueChange(worldValueInput.current.value, false);
+ }
+ }, [isFractionalPrecision]);
+
+ const isPageValueValid = !!scale[0][0];
+ const isWorldValueValid = !!scale[1][0];
+
+ const pageValueClass = classNames('scale-input', {
+ 'invalid-value': !isPageValueValid
+ });
+ const worldValueClass = classNames('scale-input', {
+ 'invalid-value': !isWorldValueValid
+ });
+
+ // If scale value is smaller than the current precision, replace it with precision value to prevent 0 value.
+ const _onScaleChange = (newScale) => {
+ const getPrecision = (unit) => (unit === 'ft-in' ? precision / 12 : precision);
+
+ if (newScale.pageScale.value && newScale.pageScale.value < precision) {
+ newScale.pageScale.value = getPrecision(newScale.pageScale.unit);
+ }
+ if (newScale.worldScale.value && newScale.worldScale.value < precision) {
+ newScale.worldScale.value = getPrecision(newScale.worldScale.unit);
+ }
+ onScaleChange(newScale);
+ };
+
+ const onInputValueChange = (value, isPageValue, getNewScale) => {
+ const updateScaleValue = (scaleValue) => {
+ if ((isPageValue && scaleValue !== scale[0][0]) || (!isPageValue && scaleValue !== scale[1][0])) {
+ const newScale = new Scale({
+ pageScale: { value: isPageValue ? scaleValue : scale[0][0], unit: scale[0][1] },
+ worldScale: { value: !isPageValue ? scaleValue : scale[1][0], unit: scale[1][1] }
+ });
+ if (getNewScale) {
+ getNewScale(newScale);
+ } else {
+ _onScaleChange(newScale);
+ }
+ }
+ };
+
+ if (isPageValue) {
+ setPageValueDisplay(value);
+ } else {
+ setWorldValueDisplay(value);
+ }
+ const inputValue = value.trim();
+ if (!isFractionalPrecision) {
+ if (!isPageValue && scale[1][1] === 'ft-in') {
+ if (ftInDecimalRegex.test(inputValue)) {
+ const result = parseFtInDecimal(inputValue);
+ if (result > 0) {
+ updateScaleValue(result);
+ return;
+ }
+ }
+ } else if (floatRegex.test(inputValue)) {
+ const scaleValue = parseFloat(inputValue) || 0;
+ updateScaleValue(scaleValue);
+ return;
+ }
+ } else {
+ const scaleUnit = isPageValue ? scale[0][1] : scale[1][1];
+ if (scaleUnit === 'in') {
+ if (inFractionalRegex.test(inputValue)) {
+ const result = parseInFractional(inputValue);
+ if (result > 0) {
+ updateScaleValue(result);
+ return;
+ }
+ }
+ } else if (scaleUnit === 'ft-in') {
+ if (ftInFractionalRegex.test(inputValue)) {
+ const result = parseFtInFractional(inputValue);
+ if (result > 0) {
+ updateScaleValue(result);
+ return;
+ }
+ }
+ }
+ }
+ updateScaleValue(undefined);
+ };
+
+ const onScaleUnitChange = (newUnit, isPageUnit) => {
+ let newPageScale;
+ if (isPageUnit && newUnit !== scale[0][1]) {
+ newPageScale = {
+ value: scale[0][0] ? convertUnit(scale[0][0], scale[0][1], newUnit) : scale[0][0],
+ unit: newUnit
+ };
+ } else {
+ newPageScale = { value: scale[0][0], unit: scale[0][1] };
+ }
+ let newWorldScale;
+ if (!isPageUnit && newUnit !== scale[1][1]) {
+ newWorldScale = {
+ value: scale[1][0] ? convertUnit(scale[1][0], scale[1][1], newUnit) : scale[1][0],
+ unit: newUnit
+ };
+ } else {
+ newWorldScale = { value: scale[1][0], unit: scale[1][1] };
+ }
+
+ _onScaleChange(new Scale({ pageScale: newPageScale, worldScale: newWorldScale }));
+ };
+
+ const getInputPlaceholder = (isPageValue) => {
+ const unit = isPageValue ? scale[0][1] : scale[1][1];
+ return isFractionalPrecision ? hintValues[unit] : (unit === 'ft-in' ? hintValues['ft-in decimal'] : '');
+ };
+
+ const onInputBlur = () => {
+ setScaleValueBlurFlag((flag) => !flag);
+ };
+
+ return (
+
+
+
+
+
onInputValueChange(e.target.value, true)}
+ placeholder={getInputPlaceholder(true)}
+ ref={pageValueInput}
+ onBlur={onInputBlur}
+ />
+
+
+ onScaleUnitChange(value, true)}
+ currentSelectionKey={scale[0][1]}
+ />
+
+
+
+ {' = '}
+
+
onInputValueChange(e.target.value, false)}
+ placeholder={getInputPlaceholder(false)}
+ ref={worldValueInput}
+ onBlur={onInputBlur}
+ />
+
+
+ onScaleUnitChange(value, false)}
+ currentSelectionKey={scale[1][1]}
+ />
+
+
+
+
+
+
+ {!isPageValueValid && (
+
+ {`${t('option.measurement.scaleModal.incorrectSyntax')} ${pageWarningMessage}`}
+
+ )}
+ {!isWorldValueValid && (
+
+ {`${t('option.measurement.scaleModal.incorrectSyntax')} ${worldWarningMessage}`}
+
+ )}
+
+
+ );
+}
+
+ScaleCustom.propTypes = ScaleCustomProps;
+
+export default ScaleCustom;
diff --git a/src/components/ScaleModal/index.js b/src/components/ScaleModal/index.js
new file mode 100644
index 0000000000..73f40ff610
--- /dev/null
+++ b/src/components/ScaleModal/index.js
@@ -0,0 +1,3 @@
+import ScaleModal from './ScaleModal';
+
+export default ScaleModal;
\ No newline at end of file
diff --git a/src/components/ScaleOverlay/MeasurementDetail.spec.js b/src/components/ScaleOverlay/MeasurementDetail.spec.js
new file mode 100644
index 0000000000..1aaee4fa38
--- /dev/null
+++ b/src/components/ScaleOverlay/MeasurementDetail.spec.js
@@ -0,0 +1,62 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { EllipseScaleOverlay } from './MeasurementDetail.stories';
+
+const noop = () => { };
+
+jest.mock('core', () => ({
+ addEventListener: noop,
+ removeEventListener: noop,
+ jumpToAnnotation: noop,
+ getDocumentViewer: () => ({
+ getAnnotationManager: () => ({
+ deselectAllAnnotations: noop,
+ selectAnnotation: noop,
+ })
+ }),
+ getAnnotationManager: () => ({
+ selectAnnotation: noop,
+ redrawAnnotation: noop,
+ trigger: noop
+ }),
+ getTool: () => ({
+ finish: noop
+ })
+}));
+
+
+describe('MeasurementDetail', () => {
+ it('renders the EllipseScaleOverlay storybook component', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('increases the area when the radius increases', () => {
+ render();
+
+ const radiusInput = screen.getByDisplayValue('1.74');
+ const areaTextElement = screen.getByText('10.00 sq in');
+
+ const beforeText = areaTextElement.textContent;
+ fireEvent.change(radiusInput, { target: { value: '2' } });
+ const afterText = areaTextElement.textContent;
+ const beforeValue = parseFloat(beforeText.substring(0, beforeText.indexOf(' ')));
+ const afterValue = parseFloat(afterText.substring(0, afterText.indexOf(' ')));
+
+ expect(beforeValue).toBeLessThan(afterValue);
+ });
+
+ it('sets area to zero when radius is zero', () => {
+ render();
+
+ const radiusInput = screen.getByDisplayValue('1.74');
+ const areaTextElement = screen.getByText('10.00 sq in');
+
+ fireEvent.change(radiusInput, { target: { value: '0' } });
+ const afterText = areaTextElement.textContent;
+ const afterValue = parseFloat(afterText.substring(0, afterText.indexOf(' ')));
+
+ expect(afterValue).toEqual(0);
+ });
+});
\ No newline at end of file
diff --git a/src/components/ScaleOverlay/ScaleHeader.js b/src/components/ScaleOverlay/ScaleHeader.js
new file mode 100644
index 0000000000..aaffcb4eed
--- /dev/null
+++ b/src/components/ScaleOverlay/ScaleHeader.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import Icon from 'components/Icon';
+import ScaleSelector from './ScaleSelector';
+import PropTypes from 'prop-types';
+import { useTranslation } from 'react-i18next';
+
+const propTypes = {
+ scales: PropTypes.arrayOf(PropTypes.object).isRequired,
+ selectedScales: PropTypes.arrayOf(PropTypes.string).isRequired,
+ onScaleSelected: PropTypes.func.isRequired,
+ onAddingNewScale: PropTypes.func.isRequired,
+};
+
+const ScaleHeader = ({ scales, selectedScales, onScaleSelected, onAddingNewScale }) => {
+ const [t] = useTranslation();
+
+ return (
+
+
+
{t('option.measurementOption.scale')}
+ {scales.length ? (
+
+ ) : (
+
{t('option.measurement.scaleOverlay.addNewScale')}
+ )}
+
+ );
+};
+
+ScaleHeader.propTypes = propTypes;
+export default ScaleHeader;
diff --git a/src/components/SearchPanel/SearchPanel.js b/src/components/SearchPanel/SearchPanel.js
index a9f73d2d66..94c7b0a094 100644
--- a/src/components/SearchPanel/SearchPanel.js
+++ b/src/components/SearchPanel/SearchPanel.js
@@ -20,7 +20,8 @@ const propTypes = {
setActiveResult: PropTypes.func,
isInDesktopOnlyMode: PropTypes.bool,
isProcessingSearchResults: PropTypes.bool,
- activeDocumentViewerKey: PropTypes.number
+ activeDocumentViewerKey: PropTypes.number,
+ isCustomPanel: PropTypes.bool,
};
function noop() { }
@@ -36,7 +37,9 @@ function SearchPanel(props) {
isMobile = false,
isInDesktopOnlyMode,
isProcessingSearchResults,
- activeDocumentViewerKey
+ activeDocumentViewerKey,
+ dataElement = 'searchPanel',
+ isCustomPanel = false,
} = props;
const { t } = useTranslation();
@@ -76,12 +79,15 @@ function SearchPanel(props) {
}, []);
const className = getClassName('Panel SearchPanel', { isOpen });
- const style = !isInDesktopOnlyMode && isMobile ? {} : { width: `${currentWidth}px`, minWidth: `${currentWidth}px` };
+ let style = {};
+ if (!isCustomPanel && (isInDesktopOnlyMode || !isMobile)) {
+ style = { width: `${currentWidth}px`, minWidth: `${currentWidth}px` };
+ }
return (
{!isInDesktopOnlyMode && isMobile &&
diff --git a/src/components/SearchPanel/SearchPanel.stories.js b/src/components/SearchPanel/SearchPanel.stories.js
new file mode 100644
index 0000000000..c5c9dcece5
--- /dev/null
+++ b/src/components/SearchPanel/SearchPanel.stories.js
@@ -0,0 +1,46 @@
+import React from 'react';
+import SearchPanel from './SearchPanelContainer';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import Panel from 'components/Panel';
+
+export default {
+ title: 'ModularComponents/SearchPanel',
+ component: SearchPanel
+};
+
+const initialState = {
+ viewer: {
+ openElements: {
+ panel: true,
+ },
+ disabledElements: {},
+ customElementOverrides: {},
+ tab: {},
+ panelWidths: { panel: 300 },
+ modularHeaders: {},
+ },
+ search: {},
+};
+
+const store = configureStore({ reducer: () => initialState });
+
+export function SearchPanelLeft() {
+ return (
+
+
+
+
+
+ );
+}
+
+export function SearchPanelRight() {
+ return (
+ initialState })}>
+
+
+
+
+ );
+}
diff --git a/src/components/Selector/Selector.js b/src/components/Selector/Selector.js
new file mode 100644
index 0000000000..323102e77d
--- /dev/null
+++ b/src/components/Selector/Selector.js
@@ -0,0 +1,51 @@
+import classNames from 'classnames';
+import PropTypes from 'prop-types';
+import React from 'react';
+import Icon from 'components/Icon';
+import './Selector.scss';
+
+const propTypes = {
+ className: PropTypes.string,
+ items: PropTypes.arrayOf(PropTypes.string).isRequired,
+ selectedItem: PropTypes.string,
+ onItemSelected: PropTypes.func.isRequired,
+ placeHolder: PropTypes.string,
+ selectedItemStyle: PropTypes.object,
+};
+
+const Selector = ({ className, items = [], selectedItem = '', onItemSelected, placeHolder, selectedItemStyle }) => {
+ return (
+
+
+ {!selectedItem && placeHolder ? placeHolder : selectedItem}
+
+
+
+
+ );
+};
+
+Selector.propTypes = propTypes;
+
+export default Selector;
diff --git a/src/components/Selector/Selector.scss b/src/components/Selector/Selector.scss
new file mode 100644
index 0000000000..157cc5fa68
--- /dev/null
+++ b/src/components/Selector/Selector.scss
@@ -0,0 +1,93 @@
+.customSelector {
+ position: relative;
+ .customSelector__selectedItem {
+ height: 2rem;
+ width: 8.5rem;
+ position: relative;
+ background-color: transparent;
+ border: solid 1px;
+ border-color: var(--border);
+ padding: 0 4px 0 8px;
+ color: var(--text-color);
+ font-family: Lato;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 0.8rem;
+ text-align: left;
+ border-radius: 0.3rem;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+
+ & ul {
+ margin: 0;
+ list-style-type: none;
+ position: absolute;
+ width: 10rem;
+ left: 0;
+ top: 0;
+ text-align: left;
+ letter-spacing: 0px;
+ display: none;
+ border-radius: 4px;
+ pointer-events: all;
+ z-index: 1000;
+ background-color: var(--component-background);
+ box-shadow: 0 0 3px 0 var(--document-box-shadow);
+ padding-left: 0px;
+ }
+
+ & li {
+ display: block;
+ height: 2rem;
+ position: relative;
+ font-family: Lato;
+ font-style: normal;
+ font-weight: normal;
+ font-size: 0.8rem;
+ padding-left: 0.5rem;
+ :hover {
+ cursor: pointer;
+ }
+ }
+
+ & li:first-child {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 4px 0 8px;
+ }
+
+ & li:not(:first-child) .options:hover {
+ background-color: var(--blue-1);
+ }
+
+ & li .optionSelected {
+ color: var(--blue-5);
+ }
+
+ & li .options {
+ border: none;
+ background-color: transparent;
+ padding-right: 0.65rem;
+ padding-left: 0.5rem;
+ width: calc(100% + 0.5rem);
+ text-align: left;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ height: 100%;
+ margin-left: -0.5rem;
+ z-index: 1;
+ }
+
+ .customSelector__selectedItem:focus + ul {
+ display: block;
+ pointer-events: all;
+ transform: translateY(0px);
+ }
+
+}
diff --git a/src/components/Selector/Selector.spec.js b/src/components/Selector/Selector.spec.js
new file mode 100644
index 0000000000..9f4f11044b
--- /dev/null
+++ b/src/components/Selector/Selector.spec.js
@@ -0,0 +1,24 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { Basic as BasicStory, Placeholder as PlaceholderStory } from './Selector.stories';
+
+const BasicSelectStory = withI18n(BasicStory);
+
+describe('Selector', () => {
+ it('Basic story should not throw any errors', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+});
+
+const PlaceHolderSelectStory = withI18n(PlaceholderStory);
+
+describe('Placeholder version', () => {
+ it('Should have placeholder text as default item', () => {
+ const { container } = render();
+
+ const selectedItem = container.querySelector('.customSelector__selectedItem');
+ expect(selectedItem).toHaveTextContent('PLACEHOLDER');
+ });
+});
\ No newline at end of file
diff --git a/src/components/Selector/Selector.stories.js b/src/components/Selector/Selector.stories.js
new file mode 100644
index 0000000000..4d72f16456
--- /dev/null
+++ b/src/components/Selector/Selector.stories.js
@@ -0,0 +1,47 @@
+import React, { useState } from 'react';
+import Selector from './Selector';
+
+export default {
+ title: 'Components/Selector',
+ component: Selector,
+};
+
+export function Basic() {
+ const items = ['ITEM 1', 'ITEM 2'];
+ const [selectedItem, setSelectedItem] = useState(items[0]);
+ const placeHolder = 'PLACEHOLDER';
+ const onItemSelected = (item) => {
+ setSelectedItem(item);
+ };
+
+ return (
+
+
+
+ );
+}
+
+export function Placeholder() {
+ const items = ['ITEM 1', 'ITEM 2'];
+ const [selectedItem, setSelectedItem] = useState();
+ const placeHolder = 'PLACEHOLDER';
+ const onItemSelected = (item) => {
+ setSelectedItem(item);
+ };
+
+ return (
+
+
+
+ );
+}
diff --git a/src/components/Selector/index.js b/src/components/Selector/index.js
new file mode 100644
index 0000000000..837acf7044
--- /dev/null
+++ b/src/components/Selector/index.js
@@ -0,0 +1,3 @@
+import Selector from './Selector';
+
+export default Selector;
\ No newline at end of file
diff --git a/src/components/SettingsModal/AdvancedTab.scss b/src/components/SettingsModal/AdvancedTab.scss
new file mode 100644
index 0000000000..51ec77df99
--- /dev/null
+++ b/src/components/SettingsModal/AdvancedTab.scss
@@ -0,0 +1,22 @@
+.setting-item {
+ border: 1px var(--border) solid;
+ padding: 16px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+
+ &:not(:last-child) {
+ border-bottom: 0px
+ }
+
+ .setting-item-info {
+ display: flex;
+ flex-direction: column;
+ margin-right: 18px;
+
+ .setting-item-label {
+ font-weight: 700;
+ margin-bottom: 10px;
+ }
+ }
+}
diff --git a/src/components/SettingsModal/GeneralTab.js b/src/components/SettingsModal/GeneralTab.js
new file mode 100644
index 0000000000..a69b139c49
--- /dev/null
+++ b/src/components/SettingsModal/GeneralTab.js
@@ -0,0 +1,109 @@
+import React from 'react';
+import { useSelector, useDispatch, useStore } from 'react-redux';
+import selectors from 'selectors';
+import actions from 'actions';
+import { useTranslation } from 'react-i18next';
+import { isIE } from 'helpers/device';
+import Languages from 'constants/languages';
+import Theme from 'constants/theme';
+import DataElements from 'constants/dataElement';
+import setLanguage from '../../apis/setLanguage';
+import Dropdown from 'components/Dropdown';
+import Icon from 'components/Icon';
+import Choice from 'components/Choice';
+import DataElementWrapper from 'components/DataElementWrapper';
+import { SearchWrapper } from './SearchWrapper';
+
+import './GeneralTab.scss';
+
+const GeneralTab = () => {
+ const [
+ currentLanguage,
+ activeTheme
+ ] = useSelector((state) => [
+ selectors.getCurrentLanguage(state),
+ selectors.getActiveTheme(state)
+ ]);
+ const [t] = useTranslation();
+ const dispatch = useDispatch();
+ const store = useStore();
+
+ const changeLanguage = (value) => {
+ if (value !== currentLanguage) {
+ setLanguage(store)(value);
+ }
+ };
+
+ const isLightMode = activeTheme === Theme.LIGHT;
+
+ const setTheme = (theme) => {
+ dispatch(actions.setActiveTheme(theme));
+ };
+
+ return (
+ <>
+
+
+ {t('option.settings.language')}
+ item[0]}
+ getDisplayValue={(item) => item[1]}
+ onClickItem={changeLanguage}
+ maxHeight={200}
+ width={336}
+ getCustomItemStyle={() => ({ textAlign: 'left', width: '326px' })}
+ className="language-dropdown"
+ />
+
+
+
+ {!isIE && (
+
+ {t('option.settings.theme')}
+
+
+
+
+ setTheme(Theme.LIGHT)}
+ label={t('option.settings.lightMode')}
+ name="theme_choice"
+ />
+
+
+
+
+
+ setTheme(Theme.DARK)}
+ label={t('option.settings.darkMode')}
+ name="theme_choice"
+ />
+
+
+
+
+ )}
+
+ >
+ );
+};
+
+export default GeneralTab;
diff --git a/src/components/SettingsModal/GeneralTab.scss b/src/components/SettingsModal/GeneralTab.scss
new file mode 100644
index 0000000000..9d85ad9fe0
--- /dev/null
+++ b/src/components/SettingsModal/GeneralTab.scss
@@ -0,0 +1,61 @@
+.language-dropdown {
+ .Dropdown__items {
+ left: 0;
+ width: 336px;
+ }
+}
+
+.theme-options {
+ width: 336px;
+ height: 160px;
+ display: flex;
+ justify-content: space-between;
+
+ .theme-option {
+ width: 160px;
+ height: 160px;
+ display: flex;
+ flex-direction: column;
+
+ .Icon {
+ width: 160px;
+ height: 120px;
+
+ &.light-mode-icon {
+ color: white;
+ }
+
+ &.dark-mode-icon {
+ color: black;
+ }
+
+ svg {
+ border: 1px solid;
+ border-color: var(--border);
+ border-top-left-radius: 4px;
+ border-top-right-radius: 4px;
+ }
+ }
+
+ .theme-choice {
+ height: 100%;
+ border: 1px solid;
+ border-color: var(--border);
+ border-top: 0px;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ display: flex;
+ padding-left: 12px;
+ }
+
+ &.active-theme {
+ .Icon svg {
+ border-color: var(--focus-border);
+ }
+
+ .theme-choice {
+ border-color: var(--focus-border);
+ }
+ }
+ }
+}
diff --git a/src/components/SettingsModal/SearchWrapper.js b/src/components/SettingsModal/SearchWrapper.js
new file mode 100644
index 0000000000..a50fb0179d
--- /dev/null
+++ b/src/components/SettingsModal/SearchWrapper.js
@@ -0,0 +1,13 @@
+import React, { createContext, useContext } from 'react';
+
+export const SearchContext = createContext();
+
+export const SearchWrapper = ({ children, keywords = [] }) => {
+ const searchTerm = useContext(SearchContext).trim();
+
+ return (!searchTerm || keywords.some((keyword) => keyword.toLowerCase().includes(searchTerm.toLowerCase()))) ? (
+ <>
+ {children}
+ >
+ ) : null;
+};
diff --git a/src/components/SettingsModal/index.js b/src/components/SettingsModal/index.js
new file mode 100644
index 0000000000..09a83d8fe7
--- /dev/null
+++ b/src/components/SettingsModal/index.js
@@ -0,0 +1,3 @@
+import SettingsModal from './SettingsModal';
+
+export default SettingsModal;
\ No newline at end of file
diff --git a/src/components/SignatureListPanel/index.js b/src/components/SignatureListPanel/index.js
new file mode 100644
index 0000000000..bdc6bf76e5
--- /dev/null
+++ b/src/components/SignatureListPanel/index.js
@@ -0,0 +1,3 @@
+import SignatureListPanel from './SignatureListPanel';
+
+export default SignatureListPanel;
\ No newline at end of file
diff --git a/src/components/SignatureModal/SavedSignatures/index.js b/src/components/SignatureModal/SavedSignatures/index.js
new file mode 100644
index 0000000000..91ec23ef95
--- /dev/null
+++ b/src/components/SignatureModal/SavedSignatures/index.js
@@ -0,0 +1,3 @@
+import SavedSignatures from './SavedSignatures';
+
+export default SavedSignatures;
\ No newline at end of file
diff --git a/src/components/SignatureModal/SignatureModal.js b/src/components/SignatureModal/SignatureModal.js
index fb12f41490..1978ca1388 100644
--- a/src/components/SignatureModal/SignatureModal.js
+++ b/src/components/SignatureModal/SignatureModal.js
@@ -82,6 +82,7 @@ const SignatureModal = () => {
for (const signatureTool of signatureToolArray) {
signatureTool.clearLocation();
signatureTool.setSignature(null);
+ signatureTool.setInitials(null);
}
dispatch(actions.closeElement(DataElements.SIGNATURE_MODAL));
};
diff --git a/src/components/SignatureStylePopup/SignatureStylePopup.stories.js b/src/components/SignatureStylePopup/SignatureStylePopup.stories.js
index be4bcc81af..617444ca02 100644
--- a/src/components/SignatureStylePopup/SignatureStylePopup.stories.js
+++ b/src/components/SignatureStylePopup/SignatureStylePopup.stories.js
@@ -4,30 +4,13 @@ import SelectedSignatureRow from './SelectedSignatureRow';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import SignatureModes from 'constants/signatureModes';
+import { mockSavedSignatures, mockSavedInitials } from './mockedSignatures';
export default {
title: 'Components/SavedSignaturesOverlay',
component: SignatureStylePopup,
};
-const mockSavedSignatures = [
- {
- imgSrc: 'data:image/png;base64,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'
- },
- {
- imgSrc: 'data:image/png;base64,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'
- }
-];
-
-const mockSavedInitials = [
- {
- imgSrc: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABsCAYAAAAv1f1mAAAAAXNSR0IArs4c6QAADThJREFUeF7tXU1eG7kSL3UznuUkm5isXuYEAycYcoA4cIIHJ4jZ4xknZh84QeAEMeYAJCcYcoLHrCbOJmY5/OLW+6m7TRzTUklqqT+s8hLUaqlU/67vEgP6EQWIAlIKMKINUYAoIKcAAYS4gyigoAABhNiDKEAAIR4gCthRgCSIHd3oqUAoQAAJ5KBpm98p0Bt8eQvA94HDDQd4fXncHcvoQwAhzgmKAi8H03ccYH9505zDngwkBJCg2CPsze4e/bOTsOiqgAqzyaj7uIg6BJCweSaY3e8Ovz5K5nd/AcCz1U1zgL8vR90HfxfjCCDBsEi4G03B8e3uChhsFVKBwevJm+6QJEi4PBLszjFwCOkRx52t8fDxjAASLJuEuXEMHIIqKgOdVKww+SaIXe8Ov2wl3/g7qVqVcb9UtVoQiWyQINglrE2m4Jhz4a16JN05h/PJcfcHdy+pWB74RBzGfM5+EVPHMb8dD59ce3gNTalJgdxb9T8X4CAVS5Poq8Ny3fYVMCj0fHDgh5ejzRPL6emxEhTo/fH5BDh7JZ1CQ61afpZULMPD0NJtASCK2TZJE0Pilhyeq1Yi1lH448AOLkdPzkxeQwAxoJaWbpvPh3lHDF5LQzUp8GLw+YoB2ykabgMOUrE0CS+GmYBDjI948nx8/PSDwStoaAkKKNJIABg/nbzZ7NtMTxJEg2qm4ACA2yjuPJMFnzReSUMMKaCQHqXOggCCHIQeOPjH79OwWRQn/fHw6Y3hGdNwSwqopEdZhwkBRHEoLwZf9hnwd6pzs9VtLXmBHiugQO9o+pckIFhKepANImE34cbl8ztRN7BL4Gg2Jl8MPvcZsLeFqzR06RbNQRJkhSq5uH6vDDSJHB4Ll2GzWa19q1MFBbEkRN3dEkCWKNU7mv4pC/4tDbuNeLJLHipdFvM3ThUUdPUBI4CkLtyvj+bzf9/LfOg/gCNmOxQA9Mf0ujOrg4L842S0WRgP0Z1/MS54gOikROfE+hTFbJ/AYcpifsargoIuY1DBA6SoiP/BkTow9vywSZizKg1zzSxdXcoFDRAMHKmhx5N9sjd02cn/OCRbt7Rbd3UHwQJEI8ZxEcWdfYqG+2d6kzf0jqZnwOC/Rc+UDQqSmzengDJvR4xxLKZNGKDqscLYBZjP2hD5V5+bO8N8+QyCkyAaBTUXk1FXGSCsmol9vO/lYPp+JRA6YwBjFncOmyo1FRFzb+UFwQFERWQA+BTFnZ2mMogroCiNXIAZcDiJNpLzJkkVdcTcPlsXo2lQAEGM8tsoTraaxBTY4dn+X6XHr6gXY87YOIp+uqjzo7E7/OdZMo9EIVRRjblzwzxIFQvzWLn0ndsyblXPoWWpxRbwNQA/izb4RdUfkQJ18H6FriLmMtoHIUEwcPjwflTF7DbvQZ0U2KQcRGOKcbTBLnwHTuswzIOSIBg4QvJYLR985uZO8rY3TLTkTDuzWPxuANg4iuHcB1h6g6noUFLYNzeKk199SzOlBMla2iRvGWePhHidHG+eWhCwtkdQVYLDebTR6depX9dGnKUXpzr+t2goiy8YrNEpWHp/TIfA4c/C91eU3SAFSFHad+4GPGgDQ6FqRECxDl0GT4GSRPucwz4D+I/uc4XjUjVM2Cw/n9vwi8owd5XKrrM/KUCkyWAcrqMNduBDnOosWGeMqtV99ryfoJLO2toyJs2WTZJ9ztluWbAwgDMWs1MTnlF2KFFceOOavlKA9AZTrnjZjAM7NO0x5HrxsvkQN2YQsQ6XtHYFFg78A2PR2eTNk3PV+l4cTXcZA1G0VvCr9uMmB4gi52Wx6uzL0KzIq7qAX93q3iVTretcQvWZz4VUYcLA/81ynzfA2LAIKIj0rzxWJbdBsht5RF8nNREapnIp0xGoV5UlPxc/5gAsN8DhLNronC7slCYY5su7ReMgmlHXGXA+rNvLVVc6glOua+lkC7AAsL6FzZKnt7CLZM4LW4eqrknzSTIUIOLleWq4aMas9JXX6eWqMx3B5wG1ce68f3Hfgdv4fvt1ZTpoAUSsMq8BFo1/Mb3zJorZnonHwgUT1JmO4GL96zhHak8kd/3SbuNUjU/2fAcFi85AGyAZSNLLEE+0vgyc96tSudTZqdV6PdaR0V3sKddCRH9c7AMrfV2qofDktMoKTyOALFbeJJVL7RIU1xD4T0dwwUChzJF5GdkQgP1uvWchUSA5rAIoVgDJpEmagjzW+CJ4U7nQvrkVpSNYH3TAD7oASmbzJoc+VS9rgCzOFs13Wgx0rHLhlYGkWrUBf9r8I9+MVw9qaYCkXq4s8ikM+Mq8XEhlYOUBpTYwY9PWiH/k9FfsK2jtBCBVq1x4ZSB1P9RnrfpGqpwrohAKOJ+xiO9o54Ol3q7Oc5vkSBkVnAGkKpULqaemptL18bvxm+W1Hg/VY+EYAuBDjSDkLIrZc1dhBucA8aly5Y4BUUBT+AutMtCYIxv0gKovmSwouIirAAfhLlaq865Kcb0AxEjlMsjlUqa9UH1Hg9hfvRS17YE7V/Q9qOxkMnpyWIYw3gBiqHKh6fNIARSlsJfhgoqfVSUk6qaU6Aaty6Y/eQeIicoFIEe8sps3BQMrZnH716nVZFx6rL4Zs0nT8QZayur8lQDEROUSRTVx/PPesifC9zVb9sdNT5pSwMe1BZphhlnEkz3T6HtlALlXuTQKsQDgPvqORMu9Ng0zPXwaj9geaZpJdCXxsJxPjrt5lxVzSuom05oa75UDJFW5sttjsfT5GTDWB56OK+qoB0CpJOacVNMTVVQKZu/4d4zleZmApBaAZCqXaCnExxp+bdmRfpqMuqKfE/1aQIEqKwU1ivxmk1H3sQ7ZagNIBhI9xBds5DaiuwJ1zrcRY1SGua8WPtj9L5NRV4v3tQb5prJpwpqJiPS9dpofp0BdLXxyu0T0VfgxqMj0u8E3AiAGdklQl9vgrNf8EUi9jve7WDLpxc7u7RLDbpqNAYg4alXZ7DIrkARpPjC+q9B3oglDUW/dSjOus5u0AExztBoDEExnfMgS5dMI2sFm7V1llYa5Lyo1AiBoH13J7sumEfgiKs17X3FaeOlNXS18bM6ldoDgfXSRbdXY8cKG4KE8o3K16uZbNYFWtQMEsTs+cWAnOkFFlzUATTiYNq8BKUvwbpi7pF2tAEHsjnsjTjeoSMa7S9awn0v10Wtbl5naAIJ1JFkVw7pBRQKJPWO7eFJpT7awZqcWgOS5/FfAoDhVRBHI0UgjAFHAfzHqHrg4cJrDjAKKoGArE0trAYgysgr4FQU6LmECiRljuxitlB4tTSytHCCuOpLkhyEa18lrkw1SClwwSOhzrJv0EOdZKUCwnCtT+0Gaa7PEqaZzhs7ktvtfR+lRKUCwHrq21zHncRTlRT+8wjvtbBms7c+to/SoFCCq+64BoJRvXAMkTnsltZ2ZXa9ffe0dP7wcbYqit1b+KlGxkMCRk44keiDp/Oqy614rT9zDomXSo00pJTKyeAdIrlq9KyqbdV0skwPxWmq4e2hN6YHfWjWlWnqwg6behKxLZO8A6Q2mX6U15R4CR7nhXnjPXUoUDteT4+62LoFonJoCirjU7WTULe4l0CKiegUIyqyefONYnIRiJG44VKk6ezpbNyvXn8UbQHRa2/vMy1HWImTuideTN92hPqlo5CoF1tVztbxPbwDpDaYiiPdSwla3HFjft36KpaVQjMQe9EopvUYfHy8AyaWHsD2KfpWWWmIgiWK2bVqGac9W6/Ekojq3MueqMi8WlohYNUNquH9vorizTe5fPfBiqvO6XUHhXIKoEhGhptwoFCQ1rUuPJZszCvv4AcDaNfNzChCs03adpZZY3lada2sOBNQrUX78ACpVnauimTOAYAVQTTCIlflgFB9R8hyShQ3r+oFxApAcHO8l/Y8a1exNZbSvm/7s6iuLxZXWmW6lAZKnGghwSKKm5peiuDrYonlye+RGko5CSY0rRMuDgYXte9KhHrIhfJ6/6dylAIJ9WaCheimybgLJEhch99GXysI2ZdY6xlsDRAMcjb6SuTf4/EF1j0QTbKY6GGL5nYjd4SQLu+49Yu+3AggSCMwlb7MzOdHM31R7aPYesMMt83/EIxnM9RNeANIWxtKQgrMoTrbHw6fCZgnil3044lcAXNxFXvhbZ6N8dcNWABGTSFSU24gnu6YXJdbJeShI1iivCKOzZo/ktbc7lulkDZCH0Wn+MYr5fhu/tkiHFPQOd4zx2vJ/zC4TkfIo7uyElJZjDRBx6AIk8293OzEkszZJDQv3L4TQ+AEDSBR3HocEDsEnpQDSli+j7jqRzN+1Vy3UJQrNimfpnmnZcQSQJQqmEnF+d1188+76M4gsXy3rHZDstFF9JoCUpcDK8zImWddco1XyLdRmFsEW53wWc37ddvW5DIuQBCmgXg6SMwD4Tfy7LW7rMoxAzxZTgACi4Izs4sefbkIzTAks3ylAACFuIAooKEAAIfYgChBAiAeIAnYUIAliRzd6KhAKEEACOWjaph0F/g+hw+/W4d7l1QAAAABJRU5ErkJggg=='
- },
- {
- imgSrc: 'data:image/png;base64,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'
- }
-];
-
const initialState = {
viewer: {
isInitialsModeEnabled: true,
@@ -76,14 +59,14 @@ const savedInitialsStore = configureStore({
export const SavedInitialsTab = () => (
-
+
);
export const SelectedSignature = () => (
-
+
);
@@ -100,6 +83,6 @@ const initialsModeStore = configureStore({
export const SelectedInitials = () => (
-
+
);
diff --git a/src/components/SignatureStylePopup/mockedSignatures.js b/src/components/SignatureStylePopup/mockedSignatures.js
new file mode 100644
index 0000000000..b943b99509
--- /dev/null
+++ b/src/components/SignatureStylePopup/mockedSignatures.js
@@ -0,0 +1,17 @@
+export const mockSavedSignatures = [
+ {
+ imgSrc: 'data:image/png;base64,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'
+ },
+ {
+ imgSrc: 'data:image/png;base64,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'
+ }
+];
+
+export const mockSavedInitials = [
+ {
+ imgSrc: 'data:image/png;base64,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'
+ },
+ {
+ imgSrc: 'data:image/png;base64,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'
+ }
+];
diff --git a/src/components/SignatureValidationModal/SignatureValidationModal.scss b/src/components/SignatureValidationModal/SignatureValidationModal.scss
index 566593902b..5a6190250a 100644
--- a/src/components/SignatureValidationModal/SignatureValidationModal.scss
+++ b/src/components/SignatureValidationModal/SignatureValidationModal.scss
@@ -75,7 +75,7 @@
div.body > div.section {
margin: 16px 16px;
padding-bottom: 16px;
- border-bottom: 1px solid var(--gray-4);
+ border-bottom: 1px solid var(--gray-5);
}
div.body > div.section:last-child {
diff --git a/src/components/SnippingToolPopup/SnippingToolPopup.js b/src/components/SnippingToolPopup/SnippingToolPopup.js
new file mode 100644
index 0000000000..1cc68936d1
--- /dev/null
+++ b/src/components/SnippingToolPopup/SnippingToolPopup.js
@@ -0,0 +1,155 @@
+import React from 'react';
+import classNames from 'classnames';
+import Icon from 'components/Icon';
+import { useTranslation } from 'react-i18next';
+import { Choice } from '@pdftron/webviewer-react-toolkit';
+import Dropdown from 'components/Dropdown';
+import actions from 'actions';
+import { useDispatch } from 'react-redux';
+import DataElements from 'constants/dataElement';
+
+import './SnippingToolPopup.scss';
+
+const SnippingToolPopup = ({
+ snippingMode,
+ onSnippingModeChange,
+ closeSnippingPopup,
+ applySnipping,
+ isSnipping,
+ isInDesktopOnlyMode,
+ isMobile,
+ shouldShowApplySnippingWarning,
+}) => {
+ const { t } = useTranslation();
+
+ const snippingNames = {
+ 'CLIPBOARD': t('snippingPopUp.clipboard'),
+ 'DOWNLOAD': t('snippingPopUp.download'),
+ 'CROP_AND_REMOVE': t('snippingPopUp.cropAndRemove'),
+ };
+
+ const className = classNames({
+ Popup: true,
+ SnippingToolPopup: true,
+ mobile: isMobile,
+ });
+
+ const handleButtonPressed = (button) => {
+ switch (button) {
+ case 'apply':
+ shouldShowApplySnippingWarning && snippingMode === 'CROP_AND_REMOVE' ? openSnippingConfirmationWarning() : applySnipping();
+ break;
+ case 'cancel':
+ isSnipping ? openSnippingCancellationWarning() : closeSnippingPopup();
+ break;
+ }
+ };
+
+ const dispatch = useDispatch();
+
+ const openSnippingConfirmationWarning = () => {
+ const title = t('snippingPopUp.snippingModal.applyTitle');
+ const message = t('snippingPopUp.snippingModal.applyMessage');
+ const confirmationWarning = {
+ message,
+ title,
+ onConfirm: () => {
+ applySnipping();
+ },
+ };
+ dispatch(actions.showWarningMessage(confirmationWarning));
+ };
+
+ const openSnippingCancellationWarning = () => {
+ const title = t('snippingPopUp.snippingModal.cancelTitle');
+ const message = t('snippingPopUp.snippingModal.cancelMessage');
+ const cancellationWarning = {
+ message,
+ title,
+ onConfirm: () => {
+ closeSnippingPopup();
+ },
+ };
+ dispatch(actions.showWarningMessage(cancellationWarning));
+ };
+
+ if (isMobile && !isInDesktopOnlyMode) {
+ return (
+
+
+
+
+ onSnippingModeChange(Object.keys(snippingNames).find((key) => snippingNames[key] === e))}
+ currentSelectionKey={snippingNames[snippingMode]}
+ />
+
+
handleButtonPressed('apply')}
+ disabled={!isSnipping}
+ >
+ {t('action.apply')}
+
+
+
handleButtonPressed('cancel')}
+ >
+
+
+
+
+ );
+ }
+ return (
+
+
+ {t('snippingPopUp.title')}
+ onSnippingModeChange('CLIPBOARD')}
+ checked={snippingMode === 'CLIPBOARD'}
+ radio
+ />
+ onSnippingModeChange('DOWNLOAD')}
+ checked={snippingMode === 'DOWNLOAD'}
+ radio
+ />
+ onSnippingModeChange('CROP_AND_REMOVE')}
+ checked={snippingMode === 'CROP_AND_REMOVE'}
+ radio
+ />
+
+
+
+ handleButtonPressed('cancel')}>
+ {t('action.cancel')}
+
+ handleButtonPressed('apply')}
+ disabled={!isSnipping}
+ >
+ {t('action.apply')}
+
+
+
+ );
+};
+
+export default SnippingToolPopup;
diff --git a/src/components/SnippingToolPopup/SnippingToolPopup.scss b/src/components/SnippingToolPopup/SnippingToolPopup.scss
new file mode 100644
index 0000000000..7e9acbfaaa
--- /dev/null
+++ b/src/components/SnippingToolPopup/SnippingToolPopup.scss
@@ -0,0 +1,204 @@
+@import '../../constants/styles';
+@import '../../constants/modal';
+@import '../../constants/popup.scss';
+
+.SnippingToolPopup {
+ width: 250px;
+
+ .snipping-section {
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+
+ .ui__choice {
+ margin: 0;
+ }
+
+ .ui__choice:not(:last-of-type) {
+ padding-bottom: 12px;
+ }
+ }
+
+ .menu-title {
+ padding-bottom: 16px;
+ font-weight: bold;
+ }
+
+ .crop-inactive {
+ color: var(--gray-6);
+ }
+
+ .Icon {
+ height: 18px;
+ width: 18px;
+ }
+
+ .divider {
+ border-top: 1px solid var(--divider);
+ width: 100%;
+ }
+
+ .buttons {
+ padding: 12px;
+ text-align: right;
+ font-size: 13px;
+ display: flex;
+ justify-content: space-between;
+ }
+
+ .save-button {
+ color: var(--primary-button-text);
+ padding: 6px 16px;
+ background: var(--primary-button);
+ border-radius: 4px;
+ border: 0;
+ height: 32px;
+ cursor: pointer;
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: auto;
+ }
+ }
+
+ .cancel-button {
+ cursor: pointer;
+ background: none;
+ border: 0;
+ color: var(--secondary-button-text);
+ padding: 6px 16px;
+ margin-right: 4px;
+ height: 32px;
+ &:hover {
+ color: var(--secondary-button-hover);
+ }
+ &:focus {
+ outline: none;
+ }
+ &:disabled {
+ opacity: 0.5;
+ cursor: auto;
+ color: var(--secondary-button-text);
+ }
+ }
+}
+
+.custom-select {
+ flex-grow: 2;
+ max-width: 100%;
+ margin: 4px;
+
+ .customSelector {
+ margin-left: 0;
+ height: 28px;
+ width: 100% !important;
+
+ .customSelector__selectedItem {
+ width: 100%;
+ }
+
+ ul {
+ width: 100%;
+ }
+
+ .customSelector__arrow {
+ height: 18px;
+ width: 18px;
+ }
+ }
+
+ select {
+ height: 28px;
+ width: 100%;
+ }
+}
+
+.SnippingPopupContainer {
+ @extend %popup;
+ border-radius: 4px;
+ box-shadow: 0 0 3px 0 var(--document-box-shadow);
+ background: var(--component-background);
+ top: 0;
+
+ @include mobile {
+ width: 100%;
+ position: fixed;
+ bottom: 0 !important;
+ border-radius: 0;
+ justify-content: start;
+ top: auto;
+
+ .snipping-mobile-section {
+ padding-top: 8px;
+ padding-bottom: 8px;
+ padding-left: 12px;
+ padding-right: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ width: 100%;
+ }
+
+ .SnippingToolPopup {
+ width: 100%;
+ }
+
+ .snipping-mobile-container {
+ display: flex;
+ align-items: center;
+
+ .customSelector {
+ width: 100%;
+ }
+
+ .Dropdown {
+ height: 32px;
+ min-width: 150px;
+ width: 100% !important;
+
+ .arrow {
+ flex: 0 1 auto;
+ }
+
+ .picked-option .picked-option-text {
+ width: 150px;
+ text-align: left;
+ }
+ }
+
+ .Dropdown__items {
+ top: -52px;
+ z-index: 80;
+ width: 100%;
+ }
+
+ .wrapper {
+ z-index: 79;
+ }
+
+ .save-button {
+ margin-left: 6px;
+ min-width: 75px;
+ }
+ }
+ .cancel-button {
+ padding: 0;
+
+ .Icon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+
+ .snipping-selector {
+ width: 100%;
+ display: flex;
+ }
+
+ @media (max-width: 430px) {
+ .snipping-selector {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/src/components/SnippingToolPopup/SnippingToolPopup.spec.js b/src/components/SnippingToolPopup/SnippingToolPopup.spec.js
new file mode 100644
index 0000000000..01edeabec2
--- /dev/null
+++ b/src/components/SnippingToolPopup/SnippingToolPopup.spec.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import { Basic } from './SnippingToolPopup.stories';
+
+const BasicSnippingToolPopupStory = withI18n(Basic);
+
+jest.mock('core');
+
+describe('SnippingToolPopup', () => {
+ describe('Component', () => {
+ it('Story should not throw any errors', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+ });
+});
diff --git a/src/components/SnippingToolPopup/SnippingToolPopup.stories.js b/src/components/SnippingToolPopup/SnippingToolPopup.stories.js
new file mode 100644
index 0000000000..0cf959cec2
--- /dev/null
+++ b/src/components/SnippingToolPopup/SnippingToolPopup.stories.js
@@ -0,0 +1,42 @@
+import React from 'react';
+import SnippingToolPopup from './SnippingToolPopup';
+import { createStore } from 'redux';
+import { Provider } from 'react-redux';
+
+export default {
+ title: 'Components/SnippingToolPopup',
+ component: SnippingToolPopup,
+};
+
+const initialState = {
+ viewer: {
+ disabledElements: {},
+ customElementOverrides: {},
+ },
+};
+
+function rootReducer(state = initialState, action) {
+ return state;
+}
+
+const store = createStore(rootReducer);
+
+const noop = () => {};
+
+const popupProps = {
+ closeSnippingPopup: noop,
+ applySnipping: noop,
+ isSnipping: true,
+ isInDesktopOnlyMode: false,
+ isMobile: false,
+};
+
+export function Basic() {
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/components/SnippingToolPopup/SnippingToolPopupContainer.js b/src/components/SnippingToolPopup/SnippingToolPopupContainer.js
new file mode 100644
index 0000000000..56295559a0
--- /dev/null
+++ b/src/components/SnippingToolPopup/SnippingToolPopupContainer.js
@@ -0,0 +1,200 @@
+import React, { useEffect, useState, useRef, useCallback } from 'react';
+import { useSelector, useDispatch } from 'react-redux';
+import actions from 'actions';
+import selectors from 'selectors';
+import core from 'core';
+import SnippingToolPopup from './SnippingToolPopup';
+import './SnippingToolPopup.scss';
+import Draggable from 'react-draggable';
+import useOnSnippingAnnotationChangedOrSelected from '../../hooks/useOnSnippingAnnotationChangedOrSelected';
+import { isMobileSize } from 'helpers/getDeviceSize';
+import getRootNode from 'helpers/getRootNode';
+import DataElements from 'constants/dataElement';
+
+function SnippingToolPopupContainer() {
+ const snippingToolName = window.Core.Tools.ToolNames['SNIPPING'];
+ const snippingCreateTool = core.getTool(snippingToolName);
+ const [
+ isOpen,
+ isInDesktopOnlyMode,
+ shouldShowApplySnippingWarning,
+ ] = useSelector((state) => [
+ selectors.getActiveToolName(state) === snippingToolName && selectors.isElementOpen(state, DataElements.SNIPPING_TOOL_POPUP),
+ selectors.isInDesktopOnlyMode(state),
+ selectors.shouldShowApplySnippingWarning(state),
+ ]);
+ const dispatch = useDispatch();
+ const [isSnipping, setIsSnipping] = useState(snippingCreateTool.getIsSnipping());
+
+ const elementsToClose = ['leftPanel', 'searchPanel', 'notesPanel', 'redactionPanel', 'textEditingPanel'];
+
+ const openSnippingPopup = () => {
+ dispatch(actions.openElement(DataElements.SNIPPING_TOOL_POPUP));
+ // eslint-disable-next-line no-undef
+ dispatch(actions.closeElements(elementsToClose));
+ setIsSnipping(snippingCreateTool.getIsSnipping());
+ };
+
+ useEffect(() => {
+ const handleSnippingCancellation = () => setIsSnipping(false);
+
+ const handleToolModeChange = (newTool, oldTool) => {
+ if (newTool instanceof Core.Tools.SnippingCreateTool) { // eslint-disable-line no-undef
+ newTool.addEventListener(window.Core.Tools.SnippingCreateTool.Events['SNIPPING_CANCELLED'], handleSnippingCancellation);
+ openSnippingPopup();
+ } else if (oldTool instanceof Core.Tools.SnippingCreateTool) { // eslint-disable-line no-undef
+ newTool.removeEventListener(window.Core.Tools.SnippingCreateTool.Events['SNIPPING_CANCELLED'], handleSnippingCancellation);
+ setIsSnipping(false);
+ snippingCreateTool.reset();
+ reenableHeader();
+ }
+ };
+
+ core.addEventListener('toolModeUpdated', handleToolModeChange);
+
+ return () => {
+ core.removeEventListener('toolModeUpdated', handleToolModeChange);
+ };
+ });
+
+ const disableHeader = () => {
+ const header = getRootNode().querySelector('[data-element=header]');
+ if (header) {
+ header.style.pointerEvents = 'none';
+ header.style.opacity = '0.5';
+ }
+
+ const toolsHeader = getRootNode().querySelector('[data-element=toolsHeader]');
+ if (toolsHeader) {
+ toolsHeader.style.pointerEvents = 'none';
+ toolsHeader.style.opacity = '0.5';
+ }
+ };
+
+ const reenableHeader = () => {
+ const header = getRootNode().querySelector('[data-element=header]');
+ if (header) {
+ header.style.pointerEvents = '';
+ header.style.opacity = '1';
+ }
+
+ const toolsHeader = getRootNode().querySelector('[data-element=toolsHeader]');
+ if (toolsHeader) {
+ toolsHeader.style.pointerEvents = '';
+ toolsHeader.style.opacity = '1';
+ }
+ };
+
+ const snippingAnnotation = useOnSnippingAnnotationChangedOrSelected(openSnippingPopup);
+
+ // re-enable other tools and panels while not snipping
+ useEffect(() => {
+ if (!isSnipping) {
+ reenableHeader();
+ } else {
+ disableHeader();
+ }
+ }, [isSnipping]);
+
+ const [snippingMode, setSnippingMode] = useState(null);
+
+ useEffect(() => {
+ snippingCreateTool.setSnippingMode('CLIPBOARD');
+ setSnippingMode('CLIPBOARD');
+ }, []);
+
+ const onSnippingModeChange = (option) => {
+ snippingCreateTool.setSnippingMode(option);
+ setSnippingMode(option);
+ };
+
+ const snippingPopupRef = useRef();
+ const DEFAULT_POPUP_WIDTH = 250;
+ const DEFAULT_POPUP_HEIGHT = 200;
+ const documentContainerElement = core.getScrollViewElement();
+ const popupWidth = snippingPopupRef.current?.getBoundingClientRect().width || DEFAULT_POPUP_WIDTH;
+ const popupHeight = snippingPopupRef.current?.getBoundingClientRect().height || DEFAULT_POPUP_HEIGHT;
+ const documentViewer = core.getDocumentViewer(1);
+ // eslint-disable-next-line no-undef
+ const xOffset = documentViewer.getViewerElement()?.getBoundingClientRect().right || 0;
+
+ const getSnippingPopupOffset = () => {
+ const offset = {
+ x: xOffset + 35,
+ y: documentContainerElement?.offsetTop + 10,
+ };
+ if (snippingAnnotation && snippingPopupRef?.current) {
+ offset.x = Math.min(offset.x, documentContainerElement.offsetWidth - popupWidth);
+ }
+ return offset;
+ };
+
+ const getSnippingPopupBounds = () => {
+ const bounds = {
+ top: 0,
+ bottom: documentContainerElement.offsetHeight - popupHeight,
+ left: 0 - getSnippingPopupOffset()['x'],
+ right: documentContainerElement.offsetWidth - getSnippingPopupOffset()['x'] - popupWidth,
+ };
+ return bounds;
+ };
+
+ const closeAndReset = () => {
+ snippingCreateTool.reset();
+ dispatch(actions.closeElement(DataElements.SNIPPING_TOOL_POPUP));
+ reenableHeader();
+ core.setToolMode(window.Core.Tools.ToolNames.SNIPPING);
+ };
+
+ const closeSnippingPopup = useCallback(() => {
+ closeAndReset();
+ }, []);
+
+ // disable/enable the 'apply' button when snipping
+ useEffect(() => {
+ setIsSnipping(snippingCreateTool.getIsSnipping());
+ }, [snippingAnnotation]);
+
+ const applySnipping = async () => {
+ await snippingCreateTool.applySnipping();
+ snippingCreateTool.reset();
+ reenableHeader();
+ };
+
+ const props = {
+ snippingMode,
+ onSnippingModeChange,
+ closeSnippingPopup,
+ applySnipping,
+ isSnipping,
+ isInDesktopOnlyMode,
+ shouldShowApplySnippingWarning,
+ };
+
+ const isMobile = isMobileSize();
+
+ if (isOpen && core.getDocument()) {
+ if (isMobile && !isInDesktopOnlyMode) {
+ // disable draggable on mobile devices
+ return (
+
+
+
+ );
+ }
+ return (
+
+
+
+
+
+ );
+ }
+ return null;
+}
+
+export default SnippingToolPopupContainer;
diff --git a/src/components/SnippingToolPopup/index.js b/src/components/SnippingToolPopup/index.js
new file mode 100644
index 0000000000..c95f3125e3
--- /dev/null
+++ b/src/components/SnippingToolPopup/index.js
@@ -0,0 +1,3 @@
+import SnippingToolPopup from './SnippingToolPopupContainer';
+
+export default SnippingToolPopup;
\ No newline at end of file
diff --git a/src/components/StylePicker/ColorPicker/index.js b/src/components/StylePicker/ColorPicker/index.js
new file mode 100644
index 0000000000..60951c677e
--- /dev/null
+++ b/src/components/StylePicker/ColorPicker/index.js
@@ -0,0 +1,3 @@
+import ColorPicker from './ColorPicker';
+
+export default ColorPicker;
\ No newline at end of file
diff --git a/src/components/StylePicker/index.js b/src/components/StylePicker/index.js
new file mode 100644
index 0000000000..8114839de3
--- /dev/null
+++ b/src/components/StylePicker/index.js
@@ -0,0 +1,3 @@
+import StylePicker from './StylePicker';
+
+export default StylePicker;
\ No newline at end of file
diff --git a/src/components/StylePopup/StylePopup.stories.js b/src/components/StylePopup/StylePopup.stories.js
new file mode 100644
index 0000000000..ce01b333a5
--- /dev/null
+++ b/src/components/StylePopup/StylePopup.stories.js
@@ -0,0 +1,213 @@
+import React from 'react';
+import { configureStore } from '@reduxjs/toolkit';
+import initialState from 'src/redux/initialState';
+import { Provider } from 'react-redux';
+import StylePopup from './StylePopup';
+import core from 'core';
+
+export default {
+ title: 'Components/StylePopup',
+ component: StylePopup,
+};
+
+// Mock some state to show the style popups
+const state = {
+ ...initialState,
+ viewer: {
+ openElements: {
+ watermarkPanel: true,
+ stylePopup: true,
+ stylePopupTextStyleContainer: true,
+ stylePopupColorsContainer: true,
+ stylePopupLabelTextContainer: true
+ },
+ disabledElements: {},
+ selectedScale: undefined,
+ colorMap: {
+ textField: {
+ currentStyleTab: 'StrokeColor',
+ iconColor: 'StrokeColor',
+ }
+ },
+ fonts: ['Helvetica', 'Times New Roman', 'Arimo'],
+ isSnapModeEnabled: false,
+ customElementOverrides: {}
+ }
+};
+
+const noop = () => {};
+
+const store = configureStore({
+ reducer: () => state
+});
+
+const BasicComponent = (props) => {
+ core.getFormFieldCreationManager = () => ({
+ isInFormFieldCreationMode: () => true,
+ });
+
+ return (
+
+
+
+ );
+};
+
+export const StylePopupInFormBuilder = BasicComponent.bind({});
+StylePopupInFormBuilder.args = {
+ currentStyleTab: 'StrokeColor',
+ isInFormBuilderAndNotFreeText: true,
+ style: {
+ 'FillColor': new window.Core.Annotations.Color(212, 211, 211),
+ 'StrokeColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'TextColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'Opacity': null,
+ 'StrokeThickness': 1,
+ 'FontSize': '12pt',
+ 'Style': 'solid'
+ },
+ colorMapKey: 'textField',
+ colorPalette: 'StrokeColor',
+ disableSeparator: true,
+ hideSnapModeCheckbox: true,
+ isFreeText: false,
+ isEllipse: false,
+ isTextStyleContainerActive: true,
+ isLabelTextContainerActive: true,
+ properties: {
+ 'StrokeStyle': 'solid',
+ },
+ isRedaction: false,
+ fonts: ['Helvetica', 'Times New Roman', 'Arimo'],
+ isSnapModeEnabled: false,
+ onSliderChange: noop,
+ onStyleChange: noop,
+ closeElement: noop,
+ openElement: noop,
+ onPropertyChange: noop,
+ onRichTextStyleChange: noop,
+ onLineStyleChange: noop,
+};
+
+export const StylePopupForRedactionToolInHeaderItem = () => {
+ const props = {
+ currentStyleTab: 'TextColor',
+ isInFormBuilderAndNotFreeText: false,
+ style: {
+ 'FillColor': new window.Core.Annotations.Color(212, 211, 211),
+ 'StrokeColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'TextColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'Opacity': null,
+ 'StrokeThickness': 1,
+ 'FontSize': '12pt',
+ 'Style': 'solid'
+ },
+ colorMapKey: 'textField',
+ colorPalette: 'TextColor',
+ disableSeparator: true,
+ hideSnapModeCheckbox: true,
+ isFreeText: false,
+ isEllipse: false,
+ isTextStyleContainerActive: true,
+ isLabelTextContainerActive: true,
+ properties: {
+ 'StrokeStyle': 'solid',
+ },
+ isRedaction: true,
+ fonts: ['Helvetica', 'Times New Roman', 'Arimo'],
+ isSnapModeEnabled: false,
+ onSliderChange: noop,
+ onStyleChange: noop,
+ closeElement: noop,
+ openElement: noop,
+ onPropertyChange: noop,
+ onRichTextStyleChange: noop,
+ onLineStyleChange: noop,
+ };
+
+ const stateForTextTab = {
+ ...state,
+ viewer: {
+ ...state.viewer,
+ colorMap: {
+ textField: {
+ currentStyleTab: 'TextColor',
+ iconColor: 'TextColor',
+ }
+ },
+ }
+ };
+
+ const store = configureStore({
+ reducer: () => stateForTextTab
+ });
+
+ return (
+
+
+
+
+
+ );
+};
+
+export const StylePopupForFreeTextToolInHeaderItem = () => {
+ const props = {
+ currentStyleTab: 'TextColor',
+ isInFormBuilderAndNotFreeText: false,
+ style: {
+ 'FillColor': new window.Core.Annotations.Color(212, 211, 211),
+ 'StrokeColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'TextColor': new window.Core.Annotations.Color(0, 0, 0),
+ 'Opacity': 1,
+ 'StrokeThickness': 1,
+ 'FontSize': '12pt',
+ 'Style': 'solid'
+ },
+ colorMapKey: 'textField',
+ colorPalette: 'TextColor',
+ disableSeparator: true,
+ hideSnapModeCheckbox: true,
+ isFreeText: true,
+ isEllipse: false,
+ isTextStyleContainerActive: true,
+ isLabelTextContainerActive: false,
+ properties: {
+ 'StrokeStyle': 'solid',
+ },
+ fonts: ['Helvetica', 'Times New Roman', 'Arimo'],
+ isSnapModeEnabled: false,
+ onSliderChange: noop,
+ onStyleChange: noop,
+ closeElement: noop,
+ openElement: noop,
+ onPropertyChange: noop,
+ onRichTextStyleChange: noop,
+ onLineStyleChange: noop,
+ };
+
+ const stateForTextTab = {
+ ...state,
+ viewer: {
+ ...state.viewer,
+ colorMap: {
+ textField: {
+ currentStyleTab: 'TextColor',
+ iconColor: 'TextColor',
+ }
+ },
+ }
+ };
+
+ const store = configureStore({
+ reducer: () => stateForTextTab
+ });
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/src/components/TextEditingPanel/TextEditingPanel.scss b/src/components/TextEditingPanel/TextEditingPanel.scss
new file mode 100644
index 0000000000..ae3ed0c28f
--- /dev/null
+++ b/src/components/TextEditingPanel/TextEditingPanel.scss
@@ -0,0 +1,152 @@
+@import '../../constants/styles';
+@import '../../constants/panel';
+
+.TextEditingPanel {
+ padding: 16px 16px 0px 16px;
+ display: flex;
+ flex-direction: column;
+
+ .text-editing-panel-text-style-picker {
+ margin-top: 16px;
+ }
+
+ .text-editing-panel-section {
+ .text-editing-panel-heading {
+ font-size: var(--font-size-default);
+ font-weight: 700;
+ }
+
+ .text-editing-panel-menu-items {
+ margin-top: 16px;
+ margin-bottom: 16px;
+
+ .text-editing-panel-menu-items-buttons {
+ display: flex;
+ gap: 8px;
+ }
+
+ .text-editing-panel-menu-items-buttons.undo-redo {
+ gap: 12px;
+ }
+
+ // align font family dropdown items right
+ .Dropdown__items {
+ right: auto;
+ }
+
+ .FontSizeDropdown .Dropdown__items {
+ right: 0;
+ }
+
+ .Dropdown__items .Dropdown__item {
+ font-size: var(--font-size-default);
+ font-family: var(--font-family);
+ }
+
+ .Dropdown .picked-option .picked-option-text {
+ font-family: var(--font-family);
+ }
+
+ .inactive {
+ opacity: 0.5;
+ pointer-Events: none;
+ }
+ }
+
+ .top-panel {
+ margin-top: 0px;
+ }
+
+ .icon-grid .row {
+ padding-top: 12px;
+ }
+
+ .link-section {
+ margin-top: 12px;
+ }
+
+ .text-editing-row {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ gap: 8px;
+
+ width: 100%;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ .Button .Icon {
+ width: 32px;
+ height: 32px;
+ }
+
+ .color-picker-container {
+ width: 100%;
+ }
+
+ .ColorPalette {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: left;
+ align-content: flex-start;
+ gap: 8px;
+ }
+ }
+
+ .custom-colors-pallete {
+ .cell-container {
+ flex: none;
+ }
+ }
+
+ .custom-colors-section {
+ margin-top: 12px;
+ }
+
+ .addToCustomButton {
+ width: 24px;
+ height: 24px;
+ padding-top: 8px;
+ align-self: center;
+ }
+
+ .text-editing-panel-color-palette {
+ display: flex;
+ margin-bottom: 16px;
+ }
+
+ .opacity-slider {
+ @include ie11 {
+ align-items: stretch;
+ }
+ }
+ }
+
+ @include mobile {
+ width: 100%;
+ min-width: 100%;
+ padding-top: 0px;
+
+ .icon-grid .text-horizontal-alignment {
+ float: none;
+ }
+
+ .close-container {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ height: 64px;
+
+ width: 100%;
+ padding-right: 12px;
+
+ .close-icon-container {
+ cursor: pointer;
+ .close-icon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+ }
+}
+
diff --git a/src/components/TextEditingPanel/TextEditingPanel.spec.js b/src/components/TextEditingPanel/TextEditingPanel.spec.js
new file mode 100644
index 0000000000..5d6bf69fba
--- /dev/null
+++ b/src/components/TextEditingPanel/TextEditingPanel.spec.js
@@ -0,0 +1,121 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { TextEditingUndoRedo as TextEditUndoRedoStory } from './TextEditingPanel.stories';
+import TextEditingPanel from './TextEditingPanel';
+import core from 'core';
+
+const TestTextEditingPanel = withProviders(TextEditingPanel);
+
+const noop = () => { };
+
+const mockProps = {
+ handlePropertyChange: noop,
+ handleTextFormatChange: noop,
+ handleColorChange: noop,
+ format: {
+ bold: false,
+ italic: false,
+ underline: false,
+ },
+ undoRedoProperties: undefined,
+};
+
+core.getContentEditManager = () => ({
+ isInContentEditMode: () => true,
+});
+
+describe('TextEditingPanel', () => {
+ it('Undo/redo story should render without errors', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('should render without undo/redo buttons', () => {
+ render();
+ const undoButton = screen.queryByRole('Undo', { name: 'Undo' });
+ const redoButton = screen.queryByRole('Redo', { name: 'Redo' });
+ expect(undoButton).not.toBeInTheDocument();
+ expect(redoButton).not.toBeInTheDocument();
+ });
+
+ it('should render with undo/redo buttons disabled', () => {
+ mockProps.undoRedoProperties = {
+ canUndo: false,
+ canRedo: false
+ };
+
+ render();
+ const undoButton = screen.getByRole('button', { name: 'Undo' });
+ const redoButton = screen.getByRole('button', { name: 'Redo' });
+ expect(undoButton.disabled).toBe(true);
+ expect(redoButton.disabled).toBe(true);
+ });
+
+ it('should render with undo/redo buttons enabled', () => {
+ mockProps.undoRedoProperties = {
+ canUndo: true,
+ canRedo: true
+ };
+
+ render();
+ const undoButton = screen.getByRole('button', { name: 'Undo' });
+ const redoButton = screen.getByRole('button', { name: 'Redo' });
+ expect(undoButton.disabled).toBe(false);
+ expect(redoButton.disabled).toBe(false);
+ });
+
+ it('should render with undo enabled but redo disabled', () => {
+ mockProps.undoRedoProperties = {
+ canUndo: true,
+ canRedo: false
+ };
+
+ render();
+ const undoButton = screen.getByRole('button', { name: 'Undo' });
+ const redoButton = screen.getByRole('button', { name: 'Redo' });
+ expect(undoButton.disabled).toBe(false);
+ expect(redoButton.disabled).toBe(true);
+ });
+
+ it('should render with undo disabled but redo enabled', () => {
+ mockProps.undoRedoProperties = {
+ canUndo: false,
+ canRedo: true
+ };
+
+ render();
+ const undoButton = screen.getByRole('button', { name: 'Undo' });
+ const redoButton = screen.getByRole('button', { name: 'Redo' });
+ expect(undoButton.disabled).toBe(true);
+ expect(redoButton.disabled).toBe(false);
+ });
+
+ it('should fire undo handler when button is clicked', () => {
+ mockProps.undoRedoProperties = {
+ canUndo: true,
+ handleUndo: jest.fn()
+ };
+
+ render();
+ const undoButton = screen.getByRole('button', { name: 'Undo' });
+ expect(undoButton.disabled).toBe(false);
+
+ undoButton.click();
+ expect(mockProps.undoRedoProperties.handleUndo).toHaveBeenCalled();
+ });
+
+ it('should fire redo handler when button is clicked', () => {
+ mockProps.undoRedoProperties = {
+ canRedo: true,
+ handleRedo: jest.fn()
+ };
+
+ render();
+ const redoButton = screen.getByRole('button', { name: 'Redo' });
+ expect(redoButton.disabled).toBe(false);
+
+ redoButton.click();
+ expect(mockProps.undoRedoProperties.handleRedo).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
diff --git a/src/components/TextEditingPanel/index.js b/src/components/TextEditingPanel/index.js
new file mode 100644
index 0000000000..0547944cee
--- /dev/null
+++ b/src/components/TextEditingPanel/index.js
@@ -0,0 +1,3 @@
+import TextEditingPanel from './TextEditingPanelContainer';
+
+export default TextEditingPanel;
diff --git a/src/components/TextStylePicker/TextStylePicker.spec.js b/src/components/TextStylePicker/TextStylePicker.spec.js
new file mode 100644
index 0000000000..2af910b86e
--- /dev/null
+++ b/src/components/TextStylePicker/TextStylePicker.spec.js
@@ -0,0 +1,58 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import TextStylePicker from './TextStylePicker';
+import { DEBOUNCE_TIME } from '../FontSizeDropdown/FontSizeDropdown';
+import { Provider } from 'react-redux';
+import { configureStore } from '@reduxjs/toolkit';
+import userEvent from '@testing-library/user-event';
+
+jest.mock('core', () => ({
+ getContentEditManager: () => ({
+ isInContentEditMode: () => false,
+ }),
+}));
+
+// mock initial state.
+// UI Buttons are redux connected, and they need a state or the
+const initialState = {
+ viewer: {
+ openElements: {
+ },
+ disabledElements: {},
+ customElementOverrides: {},
+ }
+};
+
+const store = configureStore({
+ reducer: () => initialState
+});
+
+const TextStylePickerWithRedux = (props) => (
+
+
+
+);
+
+
+const noop = () => { };
+
+describe.only('TextStylePicker Component', () => {
+ it('should render without errors', () => {
+ const props = {
+ onPropertyChange: noop
+ };
+ render();
+ });
+
+ it('should render a warning if you enter an invalid font size', async () => {
+ const props = {
+ onPropertyChange: noop
+ };
+ render();
+ const fontSizeInput = screen.getByRole('textbox');
+ userEvent.type(fontSizeInput, '12345');
+ // Assert that a warning exists
+ await new Promise((r) => setTimeout(r, DEBOUNCE_TIME + 5));
+ expect(screen.getByText('Font size must be in the following range: 1 - 512')).toBeInTheDocument();
+ });
+});
diff --git a/src/components/TextStylePicker/TextStylePicker.stories.js b/src/components/TextStylePicker/TextStylePicker.stories.js
new file mode 100644
index 0000000000..b47ce1abeb
--- /dev/null
+++ b/src/components/TextStylePicker/TextStylePicker.stories.js
@@ -0,0 +1,82 @@
+import React from 'react';
+import { configureStore } from '@reduxjs/toolkit';
+import initialState from 'src/redux/initialState';
+import i18n from 'i18next';
+import { I18nextProvider } from 'react-i18next';
+import { Provider } from 'react-redux';
+import TextStylePicker from './TextStylePicker';
+import core from 'core';
+
+export default {
+ title: 'Components/TextStylePicker',
+ component: TextStylePicker,
+};
+
+const noop = () => {};
+
+const state = {
+ ...initialState,
+ viewer: {
+ currentLanguage: 'ja',
+ disabledElements: {},
+ customElementOverrides: {}
+ }
+};
+const store = configureStore({
+ reducer: () => state
+});
+
+const BasicComponent = (props) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const DisabledFontSelectorComponent = (props) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+export const TextStylePickerSection = BasicComponent.bind({});
+TextStylePickerSection.args = {
+ properties: {
+ FontSize: '128'
+ },
+ isRedaction: false,
+ onPropertyChange: noop
+};
+
+export const TextStylePickerFreeTextDisabled = DisabledFontSelectorComponent.bind({});
+TextStylePickerFreeTextDisabled.args = {
+ properties: {
+ FontSize: '128'
+ },
+ isFreeText: true,
+ isFreeTextAutoSize: true,
+ isRedaction: false,
+ onPropertyChange: noop
+};
+
+export const TextStylePickerFreeTextEnabled = DisabledFontSelectorComponent.bind({});
+TextStylePickerFreeTextEnabled.args = {
+ properties: {
+ FontSize: '128'
+ },
+ isFreeText: true,
+ isFreeTextAutoSize: false,
+ isRedaction: false,
+ onPropertyChange: noop
+};
\ No newline at end of file
diff --git a/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.js b/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.js
new file mode 100644
index 0000000000..b262ba0922
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.js
@@ -0,0 +1,16 @@
+import React from 'react';
+import PropertyKeyValuePair from '../PropertyKeyValuePair/PropertyKeyValuePair';
+
+const GeneralValuesSection = (props) => {
+ const { entities } = props;
+
+ const elements = [];
+
+ for (const entity in entities) {
+ elements.push();
+ }
+
+ return {elements}
;
+};
+
+export default GeneralValuesSection;
diff --git a/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.stories.js b/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.stories.js
index ca0f98d39b..23ca96ae95 100644
--- a/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.stories.js
+++ b/src/components/Wv3dPropertiesPanel/GeneralValuesSection/GeneralValuesSection.stories.js
@@ -24,7 +24,7 @@ const initialState = {
panelWidths: {
wv3dPropertiesPanel: 330,
},
- modularHeaders: [],
+ modularHeaders: {},
modularHeadersHeight: {
topHeaders: 40,
bottomHeaders: 40
diff --git a/src/components/Wv3dPropertiesPanel/Group/Group.js b/src/components/Wv3dPropertiesPanel/Group/Group.js
new file mode 100644
index 0000000000..c3e4b9d825
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/Group/Group.js
@@ -0,0 +1,37 @@
+import React, { useState, useMemo } from 'react';
+import Icon from 'components/Icon';
+import PropertyKeyValuePair from '../PropertyKeyValuePair/PropertyKeyValuePair';
+import './Group.scss';
+
+const Group = (props) => {
+ const { name, data, open } = props;
+
+ const [isActive, setIsActive] = useState(open);
+ const downArrow = 'ic_chevron_down_black_24px';
+ const rightArrow = 'ic_chevron_right_black_24px';
+
+ const onClick = () => {
+ setIsActive(!isActive);
+ };
+
+ const elements = useMemo(() => {
+ return Object.entries(data).map((entity) => (
+
+ ));
+ }, [data]);
+
+ return (
+
+ );
+};
+
+export default React.memo(Group);
diff --git a/src/components/Wv3dPropertiesPanel/Group/Group.scss b/src/components/Wv3dPropertiesPanel/Group/Group.scss
new file mode 100644
index 0000000000..d9c1a61bf7
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/Group/Group.scss
@@ -0,0 +1,15 @@
+.group-title {
+ display: flex;
+ align-items: center;
+ margin-bottom: 5px;
+ user-select: all !important;
+}
+
+.dropdown.active {
+ visibility: visible;
+}
+
+.dropdown.inactive {
+ visibility: hidden;
+ display: none;
+}
diff --git a/src/components/Wv3dPropertiesPanel/Group/Group.stories.js b/src/components/Wv3dPropertiesPanel/Group/Group.stories.js
index 948ac7dfd7..20b590332c 100644
--- a/src/components/Wv3dPropertiesPanel/Group/Group.stories.js
+++ b/src/components/Wv3dPropertiesPanel/Group/Group.stories.js
@@ -24,7 +24,7 @@ const initialState = {
panelWidths: {
wv3dPropertiesPanel: 330,
},
- modularHeaders: [],
+ modularHeaders: {},
modularHeadersHeight: {
topHeaders: 40,
bottomHeaders: 40
diff --git a/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.js b/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.js
new file mode 100644
index 0000000000..521d13ec1a
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.js
@@ -0,0 +1,39 @@
+import React from 'react';
+import Group from '../Group/Group';
+
+function addOrderedGroups(orderedGroup, groups) {
+ const orderArray = [];
+ for (const group in orderedGroup) {
+ const groupName = orderedGroup[group];
+
+ if (groupName in groups) {
+ orderArray.push();
+ }
+ }
+
+ return orderArray;
+}
+
+const GroupsContainer = (props) => {
+ const { groups, groupOrder } = props;
+
+ let combinedGroups = [];
+
+ if (groupOrder && groupOrder.length > 0) {
+ combinedGroups = addOrderedGroups(groupOrder, groups);
+
+ for (const group in groups) {
+ if (!groupOrder.includes(group)) {
+ combinedGroups.push();
+ }
+ }
+ } else {
+ for (const group in groups) {
+ combinedGroups.push();
+ }
+ }
+
+ return {combinedGroups}
;
+};
+
+export default GroupsContainer;
diff --git a/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.stories.js b/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.stories.js
index 0fd8b3db72..cf6b73fa5f 100644
--- a/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.stories.js
+++ b/src/components/Wv3dPropertiesPanel/GroupsContainer/GroupsContainer.stories.js
@@ -24,7 +24,7 @@ const initialState = {
panelWidths: {
wv3dPropertiesPanel: 330,
},
- modularHeaders: [],
+ modularHeaders: {},
modularHeadersHeight: {
topHeaders: 40,
bottomHeaders: 40
diff --git a/src/components/Wv3dPropertiesPanel/HeaderTitle/HeaderTitle.js b/src/components/Wv3dPropertiesPanel/HeaderTitle/HeaderTitle.js
new file mode 100644
index 0000000000..8b664b6215
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/HeaderTitle/HeaderTitle.js
@@ -0,0 +1,18 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import './HeaderTitle.scss';
+
+const HeaderTitle = (attributes) => {
+ const { title } = attributes;
+ const { t } = useTranslation();
+
+ return (
+
+ {t('wv3dPropertiesPanel.propertiesHeader')}
+
+ {title}
+
+ );
+};
+
+export default HeaderTitle;
diff --git a/src/components/Wv3dPropertiesPanel/HeaderTitle/HeaderTitle.scss b/src/components/Wv3dPropertiesPanel/HeaderTitle/HeaderTitle.scss
new file mode 100644
index 0000000000..9a7c4c74f2
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/HeaderTitle/HeaderTitle.scss
@@ -0,0 +1,7 @@
+.header-value {
+ color: var(--gray-7);
+}
+
+.header-title {
+ font-size: 16px;
+}
diff --git a/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.js b/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.js
new file mode 100644
index 0000000000..cb2f466444
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.js
@@ -0,0 +1,91 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+
+import GeneralValuesSection from '../GeneralValuesSection/GeneralValuesSection';
+import GroupsContainer from '../GroupsContainer/GroupsContainer';
+import Group from '../Group/Group';
+import HeaderTitle from '../HeaderTitle/HeaderTitle';
+
+function createDataSet(dataMap, propertySet, removeEmptyRows) {
+ const combinedMap = {};
+
+ if (removeEmptyRows) {
+ for (const item in dataMap) {
+ const dataPoint = propertySet[dataMap[item]];
+ if (dataPoint !== undefined && dataPoint !== '') {
+ combinedMap[item] = dataPoint;
+ }
+ }
+ } else {
+ for (const item in dataMap) {
+ combinedMap[item] = propertySet[dataMap[item]];
+ }
+ }
+
+ return combinedMap;
+}
+
+function checkForEmptyKeys(data) {
+ for (const key in data) {
+ const value = data[key];
+ if (value !== undefined && value !== '') {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function generateGroupDataSet(dataMap, propertySet, removeEmptyRows, removeEmptyGroups) {
+ const combinedGroupMap = {};
+
+ if (removeEmptyGroups) {
+ for (const group in dataMap) {
+ const dataset = createDataSet(dataMap[group], propertySet, removeEmptyRows);
+ if (Object.keys(dataset).length > 0) {
+ if (!checkForEmptyKeys(dataset)) {
+ combinedGroupMap[group] = dataset;
+ }
+ }
+ }
+ } else {
+ for (const group in dataMap) {
+ combinedGroupMap[group] = createDataSet(dataMap[group], propertySet, removeEmptyRows);
+ }
+ }
+
+ return combinedGroupMap;
+}
+
+const PropertiesElement = (props) => {
+ const { element, schema } = props;
+
+ const {
+ headerName,
+ defaultValues,
+ groups,
+ groupOrder,
+ removeEmptyRows,
+ removeEmptyGroups,
+ createRawValueGroup,
+ } = schema;
+
+ const { t } = useTranslation();
+
+ const defaultItems = createDataSet(defaultValues, element, removeEmptyRows);
+ const groupsItems = generateGroupDataSet(groups, element, removeEmptyRows, removeEmptyGroups);
+ const name = element[headerName];
+
+ return (
+
+
+
+
+ {createRawValueGroup ? (
+
+ ) : null}
+
+ );
+};
+
+export default PropertiesElement;
diff --git a/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.stories.js b/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.stories.js
index df09df1d00..bd4e731d8a 100644
--- a/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.stories.js
+++ b/src/components/Wv3dPropertiesPanel/PropertiesElement/PropertiesElement.stories.js
@@ -106,7 +106,7 @@ const initialState = {
panelWidths: {
wv3dPropertiesPanel: 330,
},
- modularHeaders: [],
+ modularHeaders: {},
modularHeadersHeight: {
topHeaders: 40,
bottomHeaders: 40
diff --git a/src/components/Wv3dPropertiesPanel/PropertyKeyValuePair/PropertyKeyValuePair.js b/src/components/Wv3dPropertiesPanel/PropertyKeyValuePair/PropertyKeyValuePair.js
new file mode 100644
index 0000000000..c585d70bf4
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/PropertyKeyValuePair/PropertyKeyValuePair.js
@@ -0,0 +1,15 @@
+import React from 'react';
+import './PropertyKeyValuePair.scss';
+
+const PropertyKeyValuePair = (props) => {
+ const { name, value } = props;
+
+ return (
+
+ {name}
+ {value}
+
+ );
+};
+
+export default PropertyKeyValuePair;
diff --git a/src/components/Wv3dPropertiesPanel/PropertyKeyValuePair/PropertyKeyValuePair.scss b/src/components/Wv3dPropertiesPanel/PropertyKeyValuePair/PropertyKeyValuePair.scss
new file mode 100644
index 0000000000..2780e41b17
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/PropertyKeyValuePair/PropertyKeyValuePair.scss
@@ -0,0 +1,25 @@
+.property-pair {
+ margin-left: 24px;
+ display: flex;
+ flex-direction: row;
+ flex-wrap: nowrap;
+ justify-content: flex-start;
+ align-items: center;
+ padding-bottom: 10px;
+ user-select: all !important;
+}
+
+.property-key {
+ flex-basis: 140px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.property-value {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ flex-basis: 300px;
+ padding-left: 20px;
+}
diff --git a/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.js b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.js
new file mode 100644
index 0000000000..b4bf9da7a2
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.js
@@ -0,0 +1,53 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import Icon from 'components/Icon';
+import DataElementWrapper from '../DataElementWrapper';
+import { v4 as uuidv4 } from 'uuid';
+import './Wv3dPropertiesPanel.scss';
+
+import PropertiesElement from './PropertiesElement/PropertiesElement';
+
+const Wv3dPropertiesPanel = (props) => {
+ const { currentWidth, isInDesktopOnlyMode, isMobile = false, closeWv3dPropertiesPanel, schema, modelData } = props;
+
+ const { t } = useTranslation();
+ const style = !isInDesktopOnlyMode && isMobile ? {} : { width: `${currentWidth}px`, minWidth: `${currentWidth}px` };
+
+ const renderMobileCloseButton = () => {
+ return (
+
+ );
+ };
+
+ let propertiesCollection = modelData.map((element) => (
+
+ ));
+
+ const emptyPanelPlaceholder = () => {
+ return (
+
+
+
+
+
{t('wv3dPropertiesPanel.emptyPanelMessage')}
+
+ );
+ };
+
+ if (modelData.length < 1) {
+ propertiesCollection = emptyPanelPlaceholder();
+ }
+
+ return (
+
+ {!isInDesktopOnlyMode && isMobile && renderMobileCloseButton()}
+ {propertiesCollection}
+
+ );
+};
+
+export default Wv3dPropertiesPanel;
diff --git a/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.scss b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.scss
new file mode 100644
index 0000000000..5488f9b3b1
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.scss
@@ -0,0 +1,64 @@
+@import '../../constants/styles';
+@import '../../constants/panel';
+
+.wv3d-properties-panel {
+ padding: 16px 0px 0px 16px;
+ display: flex;
+ flex-direction: column;
+ overflow-y: auto;
+ user-select: all !important;
+
+ .no-selections {
+ flex-direction: column;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .empty-icon {
+ width: 100px;
+ height: 100px;
+
+ svg {
+ width: 100px;
+ height: 100px;
+ }
+
+ * {
+ fill: var(--gray-5);
+ color: var(--gray-5);
+ }
+ }
+
+ .empty-text {
+ margin-top: 4px;
+ padding-left: 4px;
+ padding-right: 4px;
+ width: 68%;
+ text-align: center;
+ }
+ }
+
+ @include mobile {
+ width: 100%;
+ min-width: 100%;
+ padding-top: 0px;
+
+ .close-container {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ height: 64px;
+
+ width: 100%;
+ padding-right: 12px;
+
+ .close-icon-container {
+ cursor: pointer;
+ .close-icon {
+ width: 24px;
+ height: 24px;
+ }
+ }
+ }
+ }
+}
diff --git a/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.spec.js b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.spec.js
new file mode 100644
index 0000000000..337fa72f85
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.spec.js
@@ -0,0 +1,322 @@
+import React from 'react';
+import { render } from '@testing-library/react';
+import Wv3dPropertiesPanel from './Wv3dPropertiesPanel';
+
+import {
+ DefaultStandard,
+ GroupOrderSpecified,
+ MultiplePropertiesElements,
+ EmptyPanel,
+ RemoveEmptyRows,
+ RemoveEmptyGroups,
+ RemoveRawValues,
+} from './Wv3dPropertiesPanel.stories';
+
+const sampleData = [
+ {
+ ConnectedFrom: '2274843',
+ ConnectedTo: '2274847',
+ ContainedInStructure: '2258156',
+ Declares: '',
+ Decomposes: '',
+ Description: 'ÿ',
+ ExtendToStructure: 'T',
+ FillsVoids: '',
+ GlobalId: '3_YR89Qiz6UgyQsBcI$FJz',
+ GrossFootprintArea: '317.638889',
+ GrossSideArea: '4879.727431',
+ GrossVolume: '8132.879051',
+ HasAssignments: '',
+ HasAssociations: '2260414',
+ HasContext: '',
+ HasCoverings: '',
+ HasOpenings: '',
+ HasProjections: '',
+ Height: '25.604167',
+ InterferesElements: '',
+ IsConnectionRealization: '',
+ IsDeclaredBy: '',
+ IsDefinedBy: '29092,29099',
+ IsExternal: 'T',
+ IsInterferedByElements: '',
+ IsNestedBy: '',
+ IsTypedBy: '2266845',
+ Length: '190.583333',
+ LoadBearing: 'F',
+ Name: 'Basic Wall:Reinforced Concrete - 1\'-8":117463',
+ Nests: '',
+ ObjectPlacement: '29040',
+ ObjectType: 'Basic Wall:Reinforced Concrete - 1\'-8":118691',
+ OwnerHistory: '42',
+ PredefinedType: 'NOTDEFINED',
+ ProvidesBoundaries: '',
+ Reference: 'Basic Wall:Reinforced Concrete - 1\'-8"',
+ ReferencedBy: '',
+ ReferencedInStructures: '',
+ Representation: '29077',
+ Tag: '117463',
+ Width: '1.666667',
+ handle: '29081',
+ },
+];
+
+const sampleSchema = {
+ headerName: 'Name',
+ defaultValues: {},
+ groups: {},
+ groupOrder: [],
+ removeEmptyRows: false,
+ removeEmptyGroups: false,
+ createRawValueGroup: false,
+};
+
+describe('Wv3dPropertiesPanel', () => {
+ it('The Header is populated with a title', () => {
+ render(
+ ,
+ );
+
+ const name = sampleSchema['headerName'];
+ const expectedHeader = sampleData[0][name];
+ const res = document.body.getElementsByClassName('header-value');
+
+ expect(res[0].innerHTML).toContain(expectedHeader);
+ });
+
+ it('Default Values are generated', () => {
+ sampleSchema.defaultValues = {
+ 'GrossVolume': 'GrossVolume',
+ 'OwnerHistory': 'OwnerHistory',
+ };
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('GrossVolume')).toBeInTheDocument();
+ expect(getByText(sampleData[0]['GrossVolume'])).toBeInTheDocument();
+
+ expect(getByText('OwnerHistory')).toBeInTheDocument();
+ expect(getByText(sampleData[0]['OwnerHistory'])).toBeInTheDocument();
+ });
+
+ it('Groups are created succesfully', async () => {
+ sampleSchema.defaultValues = {};
+ sampleSchema.groups = {
+ TestGroup: {
+ 'GrossVolume': 'GrossVolume',
+ 'OwnerHistory': 'OwnerHistory',
+ },
+ };
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('GrossVolume')).toBeInTheDocument();
+ expect(getByText(sampleData[0]['GrossVolume'])).toBeInTheDocument();
+
+ expect(getByText('OwnerHistory')).toBeInTheDocument();
+ expect(getByText(sampleData[0]['OwnerHistory'])).toBeInTheDocument();
+
+ expect(getByText('TestGroup')).toBeInTheDocument();
+ });
+
+ it('Empty Values are removed when removeEmptyRows is true', () => {
+ sampleSchema.defaultValues = { 'GrossVolume': 'GrossVolume', 'OwnerHistory': 'OwnerHistory', 'EmptyTest': '' };
+ sampleSchema.removeEmptyRows = true;
+ sampleSchema.groups = {};
+
+ const { getByText, queryByText } = render(
+ ,
+ );
+
+ expect(getByText('GrossVolume')).toBeInTheDocument();
+ expect(getByText(sampleData[0]['GrossVolume'])).toBeInTheDocument();
+
+ expect(getByText('OwnerHistory')).toBeInTheDocument();
+ expect(getByText(sampleData[0]['OwnerHistory'])).toBeInTheDocument();
+
+ expect(queryByText('EmptyTest')).toBe(null);
+ });
+
+ it('Empty Groups are removed when removeEmptyGroups is true', () => {
+ sampleSchema.defaultValues = {};
+ sampleSchema.groups = {
+ TestGroup: {
+ 'GrossVolume': 'GrossVolume',
+ 'OwnerHistory': 'OwnerHistory',
+ 'EmptyTest': '',
+ },
+ TestGroup2: {
+ 'EmptyTest': '',
+ 'EmptyTest2': '',
+ 'EmptyTest3': '',
+ },
+ };
+ sampleSchema.removeEmptyRows = true;
+ sampleSchema.removeEmptyGroups = true;
+
+ const { getByText, queryByText } = render(
+ ,
+ );
+
+ expect(getByText('TestGroup')).toBeInTheDocument();
+ expect(queryByText('TestGroup2')).toBe(null);
+ });
+
+ it('Groups are ordered correctly', () => {
+ sampleSchema.defaultValues = {};
+ sampleSchema.groups = {
+ TestGroup: {
+ 'GrossVolume': 'GrossVolume',
+ 'OwnerHistory': 'OwnerHistory',
+ 'EmptyTest': '',
+ },
+ TestGroup2: {
+ 'EmptyTest': '',
+ 'EmptyTest2': '',
+ 'EmptyTest3': '',
+ },
+ TestGroup3: {
+ 'EmptyTest': '',
+ 'EmptyTest2': '',
+ 'EmptyTest3': '',
+ },
+ };
+
+ sampleSchema.groupOrder = ['TestGroup2', 'TestGroup3'];
+ sampleSchema.removeEmptyRows = false;
+ sampleSchema.removeEmptyGroups = false;
+
+ const { asFragment } = render(
+ ,
+ );
+
+ const fragment = asFragment();
+ const groupContainer = fragment.querySelector('[data-element="groupsContainer"]');
+
+ expect(groupContainer.children.length).toBe(3);
+ expect(groupContainer.children[0]).toHaveTextContent('TestGroup2');
+ expect(groupContainer.children[1]).toHaveTextContent('TestGroup3');
+ expect(groupContainer.children[2]).toHaveTextContent('TestGroup');
+ });
+
+ it('Raw Value Group was created successfully', () => {
+ sampleSchema.createRawValueGroup = true;
+
+ const { getByText } = render(
+ ,
+ );
+
+ expect(getByText('All')).toBeInTheDocument();
+ });
+
+ it('Raw Value Group should not be in the document', () => {
+ sampleSchema.createRawValueGroup = false;
+
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('All')).toBe(null);
+ });
+
+ it('renders the storybook component with defaults correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('renders the storybook component with ordered groups correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('renders the storybook component with empty rows removed correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('renders the storybook component with empty groups removed correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('renders the storybook component with the raw values section removed correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('renders the storybook component with multiple elements correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+
+ it('renders the storybook component with zero elements correctly', () => {
+ expect(() => {
+ render();
+ }).not.toThrow();
+ });
+});
diff --git a/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.stories.js b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.stories.js
index 4484ba570e..3a897e67e3 100644
--- a/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.stories.js
+++ b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanel.stories.js
@@ -201,7 +201,7 @@ const initialState = {
panelWidths: {
wv3dPropertiesPanel: 330,
},
- modularHeaders: [],
+ modularHeaders: {},
modularHeadersHeight: {
topHeaders: 40,
bottomHeaders: 40
diff --git a/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanelContainer.js b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanelContainer.js
new file mode 100644
index 0000000000..af740bdf12
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/Wv3dPropertiesPanelContainer.js
@@ -0,0 +1,57 @@
+import React, { useEffect, useState } from 'react';
+import Wv3dPropertiesPanel from './Wv3dPropertiesPanel';
+import { useSelector, shallowEqual, useDispatch } from 'react-redux';
+import selectors from 'selectors';
+import actions from 'actions';
+
+import { isMobileSize } from 'helpers/getDeviceSize';
+
+const Wv3dPropertiesPanelContainer = () => {
+ const [isOpen, isDisabled, wv3dPropertiesPanelWidth, isInDesktopOnlyMode, modelData, schema] = useSelector(
+ (state) => [
+ selectors.isElementOpen(state, 'wv3dPropertiesPanel'),
+ selectors.isElementDisabled(state, 'wv3dPropertiesPanel'),
+ selectors.getWv3dPropertiesPanelWidth(state),
+ selectors.isInDesktopOnlyMode(state),
+ selectors.getWv3dPropertiesPanelModelData(state),
+ selectors.getWv3dPropertiesPanelSchema(state),
+ ],
+ shallowEqual,
+ );
+
+ const isMobile = isMobileSize();
+
+ const dispatch = useDispatch();
+
+ const closeWv3dPropertiesPanel = () => {
+ dispatch(actions.closeElement('wv3dPropertiesPanel'));
+ };
+
+ const [renderNull, setRenderNull] = useState(false);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ setRenderNull(!isOpen);
+ }, 500);
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, [isOpen]);
+
+ if (isDisabled || (!isOpen && renderNull)) {
+ return null;
+ }
+
+ return (
+
+ );
+};
+
+export default Wv3dPropertiesPanelContainer;
diff --git a/src/components/Wv3dPropertiesPanel/index.js b/src/components/Wv3dPropertiesPanel/index.js
new file mode 100644
index 0000000000..c0e4c60388
--- /dev/null
+++ b/src/components/Wv3dPropertiesPanel/index.js
@@ -0,0 +1,3 @@
+import Wv3dPropertiesPanel from './Wv3dPropertiesPanelContainer';
+
+export default Wv3dPropertiesPanel;
diff --git a/src/constants/defaultToolsWithInlineCommentOnAnnotationSelected.js b/src/constants/defaultToolsWithInlineCommentOnAnnotationSelected.js
new file mode 100644
index 0000000000..516ca89263
--- /dev/null
+++ b/src/constants/defaultToolsWithInlineCommentOnAnnotationSelected.js
@@ -0,0 +1,10 @@
+// default annotations that have inline comment enabled on select
+export default [
+ window.Core.Annotations.TextUnderlineAnnotation,
+ window.Core.Annotations.TextHighlightAnnotation,
+ window.Core.Annotations.FreeTextAnnotation,
+ window.Core.Annotations.CaretAnnotation,
+ window.Core.Annotations.StickyAnnotation,
+ window.Core.Annotations.TextSquigglyAnnotation,
+ window.Core.Annotations.TextStrikeoutAnnotation,
+];
\ No newline at end of file
diff --git a/src/constants/featureFlags.js b/src/constants/featureFlags.js
new file mode 100644
index 0000000000..d49647551d
--- /dev/null
+++ b/src/constants/featureFlags.js
@@ -0,0 +1,11 @@
+/**
+ * Contains string enums for WebViewer feature flags
+ * @name UI.FeatureFlags
+ * @property {string} CUSTOMIZABLE_UI Feature flag for the new customizable UI
+ * @ignore
+ */
+const FEATURE_FLAGS = {
+ CUSTOMIZABLE_UI: 'customizableUI',
+};
+
+export default FEATURE_FLAGS;
\ No newline at end of file
diff --git a/src/constants/highContrastDark.scss b/src/constants/highContrastDark.scss
index 9ff92de340..297aa3aadd 100644
--- a/src/constants/highContrastDark.scss
+++ b/src/constants/highContrastDark.scss
@@ -100,4 +100,6 @@
--outline-color: var(--blue-7);
--outline-hover: var(--blue-3);
+
+ --oe-table-dropdown-highlight: var(--blue-5);
}
diff --git a/src/constants/highContrastLight.scss b/src/constants/highContrastLight.scss
index be1b459121..5bcf4c3b76 100644
--- a/src/constants/highContrastLight.scss
+++ b/src/constants/highContrastLight.scss
@@ -103,4 +103,6 @@
--outline-hover: var(--blue-1);
--preset-background: var(--gray-1);
+
+ --oe-table-dropdown-highlight: var(--blue-4);
}
diff --git a/src/constants/languages.js b/src/constants/languages.js
new file mode 100644
index 0000000000..e4681ca970
--- /dev/null
+++ b/src/constants/languages.js
@@ -0,0 +1,72 @@
+// The values in this array should match the language codes of the json files inside the i18n folder
+const Languages = [
+ ['en', 'English'],
+ ['el', 'Ελληνικά'],
+ ['de', 'Deutsch'],
+ ['es', 'Español'],
+ ['fr', 'Français'],
+ ['hu', 'Magyar'],
+ ['it', 'Italiano'],
+ ['ja', '日本語'],
+ ['ko', '한국어'],
+ ['nl', 'Nederlands'],
+ ['pt_br', 'Português'],
+ ['pl', 'Polski'],
+ ['uk', 'українська'],
+ ['ru', 'Pусский'],
+ ['ro', 'Romanian'],
+ ['sv', 'Svenska'],
+ ['tr', 'Türk'],
+ ['th', 'ไทย'],
+ ['vi', 'Tiếng Việt'],
+ ['ms', 'Melayu'],
+ ['hi', 'हिन्दी'],
+ ['bn', 'বাংলা'],
+ ['zh_cn', '简体中文'],
+ ['zh_tw', '繁體中文'],
+ ['cs', 'česky, čeština'],
+ ['id', 'Bahasa Indonesia']
+];
+
+/**
+ * Contains string enums for the default languages found in WebViewer.
+ * @name UI.Languages
+ * @property {string} EN English (en)
+ * @property {string} CS česky, čeština (cs)
+ * @property {string} EL Ελληνικά (el)
+ * @property {string} DE Deutsch (de)
+ * @property {string} ES Español (es)
+ * @property {string} FR Français (fr)
+ * @property {string} HU Magyar (hu)
+ * @property {string} IT Italiano (it)
+ * @property {string} JA 日本語 (ja)
+ * @property {string} KO 한국어 (ko)
+ * @property {string} NL Nederlands (nl)
+ * @property {string} PT_BR Português (pt_br)
+ * @property {string} PL Polski (pl)
+ * @property {string} UK українська (uk)
+ * @property {string} RU Pусский (ru)
+ * @property {string} RO Romanian (ro)
+ * @property {string} SV Svenska (sv)
+ * @property {string} TR Türk (tr)
+ * @property {string} TH ไทย (th)
+ * @property {string} VI Tiếng Việt (vi)
+ * @property {string} ID Bahasa Indonesia (id)
+ * @property {string} MS Melayu (ms)
+ * @property {string} HI हिन्दी (hi)
+ * @property {string} BN বাংলা (bn)
+ * @property {string} ZH_CN 简体中文 (zh_cn)
+ * @property {string} ZH_TW 繁體中文 (zh_tw)
+ * @example
+ WebViewer(...).then(function(instance) {
+ instance.UI.setLanguage(instance.UI.Languages.FR);
+ });
+ */
+
+export const languageEnum = Languages.reduce((acc, pair) => {
+ const code = pair[0];
+ acc[code.toUpperCase()] = code;
+ return acc;
+}, {});
+
+export default Languages;
diff --git a/src/constants/measurementScale.js b/src/constants/measurementScale.js
new file mode 100644
index 0000000000..bef86c3b5c
--- /dev/null
+++ b/src/constants/measurementScale.js
@@ -0,0 +1,150 @@
+const Scale = window.Core.Scale;
+
+export const PresetMeasurementSystems = {
+ METRIC: 'metric',
+ IMPERIAL: 'imperial'
+};
+
+const metricPreset = [
+ ['1:10', new Scale([[1, 'mm'], [10, 'mm']])],
+ ['1:20', new Scale([[1, 'mm'], [20, 'mm']])],
+ ['1:50', new Scale([[1, 'mm'], [50, 'mm']])],
+ ['1:100', new Scale([[1, 'mm'], [100, 'mm']])],
+ ['1:200', new Scale([[1, 'mm'], [200, 'mm']])],
+ ['1:500', new Scale([[1, 'mm'], [500, 'mm']])],
+ ['1:1000', new Scale([[1, 'mm'], [1000, 'mm']])]
+];
+const imperialPreset = [
+ ['1/16"=1\'-0"', new Scale([[1 / 16, 'in'], [1, 'ft-in']])],
+ ['3/32"=1\'-0"', new Scale([[3 / 32, 'in'], [1, 'ft-in']])],
+ ['1/8"=1\'-0"', new Scale([[1 / 8, 'in'], [1, 'ft-in']])],
+ ['3/16"=1\'-0"', new Scale([[3 / 16, 'in'], [1, 'ft-in']])],
+ ['1/4"=1\'-0"', new Scale([[1 / 4, 'in'], [1, 'ft-in']])],
+ ['3/8"=1\'-0"', new Scale([[3 / 8, 'in'], [1, 'ft-in']])],
+ ['1/2"=1\'-0"', new Scale([[1 / 2, 'in'], [1, 'ft-in']])],
+ ['3/4"=1\'-0"', new Scale([[3 / 4, 'in'], [1, 'ft-in']])],
+ ['1"=1\'-0"', new Scale([[1, 'in'], [1, 'ft-in']])]
+];
+
+export const getMeasurementScalePreset = () => ({
+ [PresetMeasurementSystems.METRIC]: metricPreset,
+ [PresetMeasurementSystems.IMPERIAL]: imperialPreset
+});
+
+const decimalPrecisions = [
+ ['0.1', 0.1],
+ ['0.01', 0.01],
+ ['0.001', 0.001],
+ ['0.0001', 0.0001]
+];
+const fractionalPrecisions = [
+ ['1/8', 0.125],
+ ['1/16', 0.0625],
+ ['1/32', 0.03125],
+ ['1/64', 0.015625]
+];
+export const PrecisionType = {
+ DECIMAL: 'decimal',
+ FRACTIONAL: 'fractional'
+};
+export const precisionOptions = {
+ [PrecisionType.DECIMAL]: decimalPrecisions,
+ [PrecisionType.FRACTIONAL]: fractionalPrecisions
+};
+
+export const precisionFractions = {
+ 0.125: '1/8',
+ 0.0625: '1/16',
+ 0.03125: '1/32',
+ 0.015625: '1/64'
+};
+
+export const numberRegex = /^\d*(\.\d*)?$/;
+export const fractionRegex = /^\d*(\s\d\/\d*)$/;
+export const pureFractionRegex = /^(\d\/\d*)*$/;
+export const floatRegex = /^(\d+)?(\.)?(\d+)?$/;
+export const inFractionalRegex = /^((\d+) )?((\d+)\/)?(\d+)"$/;
+export const ftInFractionalRegex = /^((\d+)'-)?((\d+) )?((\d+)\/)?(\d+)"$/;
+export const ftInDecimalRegex = /^((\d+)ft-)?(((\d+).)?(\d+))in$/;
+
+export const parseFtInDecimal = (valueStr) => {
+ const matches = valueStr.match(ftInDecimalRegex);
+ let sum = 0;
+ sum += matches[2] ? Number(matches[2]) : 0;
+ if (matches[3] && Number(matches[3])) {
+ sum += (Number(matches[3]) / 12);
+ }
+ return sum;
+};
+export const parseInFractional = (valueStr) => {
+ const matches = valueStr.match(inFractionalRegex);
+ let sum = 0;
+ sum += matches[2] ? Number(matches[2]) : 0;
+ if (matches[5] && Number(matches[5])) {
+ if (matches[4] && Number(matches[4])) {
+ sum += (Number(matches[4]) / Number(matches[5]));
+ } else {
+ sum += Number(matches[5]);
+ }
+ }
+ return sum;
+};
+export const parseFtInFractional = (valueStr) => {
+ const matches = valueStr.match(ftInFractionalRegex);
+ let sum = 0;
+ sum += matches[2] ? Number(matches[2]) : 0;
+ sum += matches[4] ? Number(matches[4]) / 12 : 0;
+ if (matches[7] && Number(matches[7])) {
+ if (matches[6] && Number(matches[6])) {
+ sum += (Number(matches[6]) / Number(matches[7])) / 12;
+ } else {
+ sum += Number(matches[7]) / 12;
+ }
+ }
+ return sum;
+};
+
+export const fractionalUnits = ['in', 'ft-in'];
+export const metricUnits = ['mm', 'cm', 'm', 'km'];
+
+export const ifFractionalPrecision = (precision) => fractionalPrecisions.map((item) => item[0]).includes(precision) || fractionalPrecisions.map((item) => item[1]).includes(precision);
+
+export const hintValues = {
+ 'in': 'eg. 1 1/2"',
+ 'ft-in': 'eg. 1\'-1 1/2"',
+ 'ft-in decimal': 'eg. 1ft-10.5in'
+};
+
+// the base unit is cm
+const unitConversion = {
+ 'mm': 0.1,
+ 'cm': 1,
+ 'm': 100,
+ 'km': 100000,
+ 'mi': 160394,
+ 'yd': 91.44,
+ 'ft': 30.48,
+ 'in': 2.54,
+ 'ft\'': 30.48,
+ 'in"': 2.54,
+ 'pt': 0.0352778,
+ 'ft-in': 30.48
+};
+
+export const convertUnit = (value, unit, newUnit) => {
+ return value * unitConversion[unit] / unitConversion[newUnit];
+};
+
+export const scalePresetPrecision = {
+ [imperialPreset[0][0]]: fractionalPrecisions[1],
+ [imperialPreset[1][0]]: fractionalPrecisions[2],
+ [imperialPreset[2][0]]: fractionalPrecisions[0],
+ [imperialPreset[3][0]]: fractionalPrecisions[1],
+ [imperialPreset[4][0]]: fractionalPrecisions[0],
+ [imperialPreset[5][0]]: fractionalPrecisions[0],
+ [imperialPreset[6][0]]: fractionalPrecisions[0],
+ [imperialPreset[7][0]]: fractionalPrecisions[0],
+ [imperialPreset[8][0]]: fractionalPrecisions[0]
+};
+
+export const initialScale = new Scale({ pageScale: { value: 1, unit: 'in' }, worldScale: { value: 1, unit: 'in' } });
diff --git a/src/constants/measurementTypes.js b/src/constants/measurementTypes.js
new file mode 100644
index 0000000000..2ef30b46ff
--- /dev/null
+++ b/src/constants/measurementTypes.js
@@ -0,0 +1,9 @@
+export const measurementTypeTranslationMap = {
+ distanceMeasurement: 'option.measurementOverlay.distanceMeasurement',
+ perimeterMeasurement: 'option.measurementOverlay.perimeterMeasurement',
+ areaMeasurement: 'option.measurementOverlay.areaMeasurement',
+ rectangularAreaMeasurement: 'option.measurementOverlay.areaMeasurement',
+ cloudyRectangularAreaMeasurement: 'option.measurementOverlay.areaMeasurement',
+ ellipseMeasurement: 'option.measurementOverlay.areaMeasurement',
+ arcMeasurement: 'option.measurementOverlay.arcMeasurement',
+};
\ No newline at end of file
diff --git a/src/constants/multiViewerContants.js b/src/constants/multiViewerContants.js
new file mode 100644
index 0000000000..c0c1865cb2
--- /dev/null
+++ b/src/constants/multiViewerContants.js
@@ -0,0 +1,7 @@
+export const DISABLED_TOOL_GROUPS = ['toolbarGroup-Edit', 'toolbarGroup-Forms', 'toolbarGroup-EditText'];
+export const DISABLED_TOOLS_KEYWORDS = ['Content', 'AddParagraphTool', 'FormField', 'Crop'];
+
+export const SYNC_MODES = {
+ 'SYNC': 'SYNC',
+ 'SKIP_UNMATCHED': 'SKIP_UNMATCHED',
+};
diff --git a/src/constants/officeEditorFonts.js b/src/constants/officeEditorFonts.js
new file mode 100644
index 0000000000..d53ebdfe48
--- /dev/null
+++ b/src/constants/officeEditorFonts.js
@@ -0,0 +1,170 @@
+export const availableFontFaces = [
+ 'Arial',
+ 'Arial Black',
+ 'Arial Narrow',
+ 'Arial Rounded MT Bold',
+ 'Baskerville Old Face',
+ 'Bookman Old Style',
+ 'Bookshelf Symbol 7',
+ 'Brush Script MT',
+ 'Calibri',
+ 'Calibri Light',
+ 'Cambria',
+ 'Cambria Math',
+ 'Century',
+ 'Century Schoolbook',
+ 'Comic Sans MS',
+ 'Consolas',
+ 'Cooper Black',
+ 'Copperplate Gothic Light',
+ 'Courier',
+ 'Courier New',
+ 'Garamond',
+ 'Georgia',
+ 'Gill Sans MT',
+ 'Gill Sans MT Condensed',
+ 'Helvetica',
+ 'Lucida Console',
+ 'MS Outlook',
+ 'Malgun Gothic',
+ 'Meiryo',
+ 'monospace',
+ 'Myriad Pro',
+ 'sans-serif',
+ 'serif',
+ 'SimSun',
+ 'Symbol',
+ 'Tahoma',
+ 'Tahoma Bold',
+ 'Times New Roman',
+ 'Trebuchet MS',
+ 'Verdana',
+];
+
+export const cssFontValues = {
+ 'Arial': {
+ fontFamily: 'Arial, Helvetica, sans-serif',
+ },
+ 'Arial Black': {
+ fontFamily: '"Arial Black", Gadget, sans-serif',
+ },
+ 'Arial Italic': {
+ fontFamily: 'Arial, Helvetica, sans-serif',
+ fontStyle: 'italic',
+ },
+ 'Arial Narrow': {
+ fontFamily: '"Arial Narrow", sans-serif',
+ },
+ 'Arial Rounded MT Bold': {
+ fontFamily: '"Arial Rounded MT Bold", sans-serif',
+ },
+ 'Baskerville Old Face': {
+ fontFamily: '"Baskerville Old Face", "Book Antiqua", Palatino, serif',
+ },
+ 'Bookman Old Style': {
+ fontFamily: '"Bookman Old Style", serif',
+ },
+ 'Bookshelf Symbol 7': {
+ fontFamily: '"Bookshelf Symbol 7", sans-serif',
+ },
+ 'Brush Script MT': {
+ fontFamily: '"Brush Script MT", cursive',
+ },
+ 'Calibri': {
+ fontFamily: 'Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif',
+ },
+ 'Calibri Light': {
+ fontFamily: 'Calibri, Candara, Segoe, "Segoe UI", Optima, Arial, sans-serif',
+ },
+ 'Cambria': {
+ fontFamily: 'Cambria, Georgia, serif',
+ },
+ 'Cambria Math': {
+ fontFamily: '"Cambria Math", serif',
+ },
+ 'Century': {
+ fontFamily: 'Century, sans-serif',
+ },
+ 'Century Schoolbook': {
+ fontFamily: '"Century Schoolbook", serif',
+ },
+ 'Comic Sans MS': {
+ fontFamily: '"Comic Sans MS", cursive, sans-serif',
+ },
+ 'Consolas': {
+ fontFamily: 'Consolas, monaco, monospace',
+ },
+ 'Consolas Italic': {
+ fontFamily: 'Consolas, monaco, monospace',
+ fontStyle: 'italic',
+ },
+ 'Cooper Black': {
+ fontFamily: '"Cooper Black", sans-serif',
+ },
+ 'Copperplate Gothic Light': {
+ fontFamily: '"Copperplate Gothic Light", sans-serif',
+ },
+ 'Courier': {
+ fontFamily: 'Courier, monospace',
+ },
+ 'Courier New': {
+ fontFamily: '"Courier New", Courier, monospace',
+ },
+ 'Garamond': {
+ fontFamily: 'Garamond, serif',
+ },
+ 'Georgia': {
+ fontFamily: 'Georgia, serif',
+ },
+ 'Gill Sans MT': {
+ fontFamily: '"Gill Sans MT", sans-serif',
+ },
+ 'Gill Sans MT Condensed': {
+ fontFamily: '"Gill Sans MT Condensed", sans-serif',
+ },
+ 'Helvetica': {
+ fontFamily: 'Helvetica, Arial, sans-serif',
+ },
+ 'Lucida Console': {
+ fontFamily: '"Lucida Console", Monaco, monospace',
+ },
+ 'MS Outlook': {
+ fontFamily: '"MS Outlook", sans-serif',
+ },
+ 'Malgun Gothic': {
+ fontFamily: '"Malgun Gothic", sans-serif',
+ },
+ 'Meiryo': {
+ fontFamily: 'Meiryo, sans-serif',
+ },
+ 'monospace': {
+ fontFamily: 'monospace',
+ },
+ 'Myriad Pro': {
+ fontFamily: '"Myriad Pro", Myriad, sans-serif',
+ },
+ 'sans-serif': {
+ fontFamily: 'sans-serif',
+ },
+ 'serif': {
+ fontFamily: 'serif',
+ },
+ 'Symbol': {
+ fontFamily: 'Symbol, sans-serif',
+ },
+ 'SimSun': {
+ fontFamily: '"SimSun", sans-serif',
+ },
+ 'Tahoma': {
+ fontFamily: 'Tahoma, Geneva, sans-serif',
+ },
+ 'Times New Roman': {
+ fontFamily: '"Times New Roman", Times, serif',
+ },
+ 'Trebuchet MS': {
+ fontFamily: '"Trebuchet MS", Helvetica, sans-serif',
+ },
+ 'Verdana': {
+ fontFamily: 'Verdana, Geneva, sans-serif',
+ },
+};
diff --git a/src/constants/pageNumberPlaceholder.js b/src/constants/pageNumberPlaceholder.js
new file mode 100644
index 0000000000..c597b8f901
--- /dev/null
+++ b/src/constants/pageNumberPlaceholder.js
@@ -0,0 +1 @@
+export default '1, 3, 5-10';
\ No newline at end of file
diff --git a/src/constants/presetNewPageDimensions.js b/src/constants/presetNewPageDimensions.js
new file mode 100644
index 0000000000..5535946921
--- /dev/null
+++ b/src/constants/presetNewPageDimensions.js
@@ -0,0 +1,14 @@
+export default {
+ 'Letter': {
+ 'height': 11,
+ 'width': 8.5,
+ },
+ 'Half letter': {
+ 'height': 5.5,
+ 'width': 8.5,
+ },
+ 'Junior legal': {
+ 'height': 5,
+ 'width': 8,
+ },
+};
\ No newline at end of file
diff --git a/src/constants/signatureModes.js b/src/constants/signatureModes.js
new file mode 100644
index 0000000000..a28b91c554
--- /dev/null
+++ b/src/constants/signatureModes.js
@@ -0,0 +1,6 @@
+const SignatureModes = {
+ FULL_SIGNATURE: window.Core.Tools.SignatureCreateTool.SignatureTypes.FULL_SIGNATURE,
+ INITIALS: window.Core.Tools.SignatureCreateTool.SignatureTypes.INITIALS,
+};
+
+export default SignatureModes;
\ No newline at end of file
diff --git a/src/constants/webFonts.js b/src/constants/webFonts.js
new file mode 100644
index 0000000000..09788bd9a5
--- /dev/null
+++ b/src/constants/webFonts.js
@@ -0,0 +1,14 @@
+// web fonts from https://www.pdftron.com/webfonts/v2/fonts.json
+// that support bold, italic, and bold-italic
+// and can be used in content editing
+export default [
+ 'Arimo',
+ 'Caladea',
+ 'Carlito',
+ 'Cousine',
+ 'Liberation Serif',
+ 'Open Sans',
+ 'Roboto',
+ 'Roboto Mono',
+ 'Tinos',
+];
diff --git a/src/core/createAndApplyScale.js b/src/core/createAndApplyScale.js
new file mode 100644
index 0000000000..800947737b
--- /dev/null
+++ b/src/core/createAndApplyScale.js
@@ -0,0 +1,8 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.MeasurementManager.html#createScale__anchor
+ */
+export default (scale, applyTo, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).getMeasurementManager().createAndApplyScale({ scale, applyTo });
+};
diff --git a/src/core/deleteScale.js b/src/core/deleteScale.js
new file mode 100644
index 0000000000..df4f9cc2d3
--- /dev/null
+++ b/src/core/deleteScale.js
@@ -0,0 +1,8 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.MeasurementManager.html#deleteScale__anchor
+ */
+export default (scale, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).getMeasurementManager().deleteScale(scale);
+};
diff --git a/src/core/deselectAnnotations.js b/src/core/deselectAnnotations.js
new file mode 100644
index 0000000000..7bf77bbf79
--- /dev/null
+++ b/src/core/deselectAnnotations.js
@@ -0,0 +1,10 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.AnnotationManager.html#deselectAnnotations__anchor
+ * @fires annotationSelected on AnnotationManager
+ * @see https://docs.apryse.com/api/web/Core.AnnotationManager.html#event:annotationSelected__anchor
+ */
+export default (annotations, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).getAnnotationManager().deselectAnnotations(annotations);
+};
diff --git a/src/core/documentViewers.js b/src/core/documentViewers.js
new file mode 100644
index 0000000000..10ee9e51a4
--- /dev/null
+++ b/src/core/documentViewers.js
@@ -0,0 +1,18 @@
+const documentViewerMap = new Map();
+
+export const setDocumentViewer = (number, documentViewer) => {
+ documentViewerMap.set(number, documentViewer);
+ return documentViewer;
+};
+
+export const deleteDocumentViewer = (number) => {
+ documentViewerMap.delete(number);
+};
+
+export const getDocumentViewer = (number = 1) => {
+ return documentViewerMap.get(number);
+};
+
+export const getDocumentViewers = () => {
+ return Array.from(documentViewerMap.values());
+};
\ No newline at end of file
diff --git a/src/core/enableAnnotationNumbering.js b/src/core/enableAnnotationNumbering.js
new file mode 100644
index 0000000000..e05e8dc68c
--- /dev/null
+++ b/src/core/enableAnnotationNumbering.js
@@ -0,0 +1,8 @@
+import getAnnotationManager from './getAnnotationManager';
+
+/**
+ * https://docs.apryse.com/api/web/Core.AnnotationManager.html#enableAnnotationNumbering__anchor
+ */
+export default (documentViewerKey = 1) => {
+ getAnnotationManager(documentViewerKey).enableAnnotationNumbering();
+};
\ No newline at end of file
diff --git a/src/core/getAllowedFileExtensions.js b/src/core/getAllowedFileExtensions.js
new file mode 100644
index 0000000000..f87656ffeb
--- /dev/null
+++ b/src/core/getAllowedFileExtensions.js
@@ -0,0 +1,8 @@
+/**
+ * https://docs.apryse.com/api/web/Core.html#.getAllowedFileExtensions__anchor
+ */
+export default () => {
+ return window.Core.getAllowedFileExtensions().length > 0 ?
+ window.Core.getAllowedFileExtensions().map((format) => `.${format}`,).join(', ') :
+ window.Core.SupportedFileFormats.CLIENT.map((format) => `.${format}`,).join(', ');
+};
diff --git a/src/core/getContentEditManager.js b/src/core/getContentEditManager.js
new file mode 100644
index 0000000000..9a7e2438a0
--- /dev/null
+++ b/src/core/getContentEditManager.js
@@ -0,0 +1,3 @@
+import core from 'core';
+
+export default (pageNumber, documentViewerKey = 1) => core.getDocumentViewer(documentViewerKey).getContentEditManager();
diff --git a/src/core/getOfficeEditor.js b/src/core/getOfficeEditor.js
new file mode 100644
index 0000000000..1d53fcdc17
--- /dev/null
+++ b/src/core/getOfficeEditor.js
@@ -0,0 +1,3 @@
+import core from 'core';
+
+export default (documentViewerKey = 1) => core.getDocumentViewer(documentViewerKey).getDocument().getOfficeEditor();
diff --git a/src/core/getResultCode.js b/src/core/getResultCode.js
new file mode 100644
index 0000000000..8a6dd62c4a
--- /dev/null
+++ b/src/core/getResultCode.js
@@ -0,0 +1 @@
+export default () => window.Core.Search.ResultCode;
diff --git a/src/core/getScalePrecision.js b/src/core/getScalePrecision.js
new file mode 100644
index 0000000000..713cf553f7
--- /dev/null
+++ b/src/core/getScalePrecision.js
@@ -0,0 +1,8 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.MeasurementManager.html#getScalePrecision__anchor
+ */
+export default (scale, documentViewerKey = 1) => {
+ return core.getDocumentViewer(documentViewerKey).getMeasurementManager().getScalePrecision(scale);
+};
diff --git a/src/core/getScales.js b/src/core/getScales.js
new file mode 100644
index 0000000000..61fe650045
--- /dev/null
+++ b/src/core/getScales.js
@@ -0,0 +1,8 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.MeasurementManager.html#getScales__anchor
+ */
+export default (documentViewerKey = 1) => {
+ return core.getDocumentViewer(documentViewerKey).getMeasurementManager().getScales();
+};
diff --git a/src/core/getSemanticDiffAnnotations.js b/src/core/getSemanticDiffAnnotations.js
new file mode 100644
index 0000000000..6a3e498b78
--- /dev/null
+++ b/src/core/getSemanticDiffAnnotations.js
@@ -0,0 +1,6 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.AnnotationManager.html#getSemanticDiffAnnotations__anchor
+ */
+export default (documentViewerKey = 1) => core.getDocumentViewer(documentViewerKey).getAnnotationManager().getSemanticDiffAnnotations();
\ No newline at end of file
diff --git a/src/core/getToolsFromAllDocumentViewers.js b/src/core/getToolsFromAllDocumentViewers.js
new file mode 100644
index 0000000000..8087d59535
--- /dev/null
+++ b/src/core/getToolsFromAllDocumentViewers.js
@@ -0,0 +1,6 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.DocumentViewer.html#getTool__anchor
+ */
+export default (toolName) => core.getDocumentViewers().map((documentViewer) => documentViewer.getTool(toolName));
diff --git a/src/core/isSearchResultEqual.js b/src/core/isSearchResultEqual.js
new file mode 100644
index 0000000000..e3b5c1e2d5
--- /dev/null
+++ b/src/core/isSearchResultEqual.js
@@ -0,0 +1 @@
+export default (resultA, resultB) => window.Core.Search.isSearchResultEqual(resultA, resultB);
diff --git a/src/core/loadBlankOfficeEditorDocument.js b/src/core/loadBlankOfficeEditorDocument.js
new file mode 100644
index 0000000000..2ad4f2ad33
--- /dev/null
+++ b/src/core/loadBlankOfficeEditorDocument.js
@@ -0,0 +1,6 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.DocumentViewer.html#loadBlankOfficeEditorDocument
+ */
+export default (options, documentViewerKey = 1) => core.getDocumentViewer(documentViewerKey).loadBlankOfficeEditorDocument(options);
diff --git a/src/core/replaceScales.js b/src/core/replaceScales.js
new file mode 100644
index 0000000000..83422b43bd
--- /dev/null
+++ b/src/core/replaceScales.js
@@ -0,0 +1,8 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.MeasurementManager.html#replaceScales__anchor
+ */
+export default (originalScales, scale, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).getMeasurementManager().replaceScales(originalScales, scale);
+};
diff --git a/src/core/setBookmarkIconShortcutVisibility.js b/src/core/setBookmarkIconShortcutVisibility.js
new file mode 100644
index 0000000000..6aa7f2c17c
--- /dev/null
+++ b/src/core/setBookmarkIconShortcutVisibility.js
@@ -0,0 +1,9 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.DocumentViewer.html#setBookmarkIconShortcutVisibility__anchor
+ * @see https://docs.apryse.com/api/web/Core.DocumentViewer.html#setBookmarkIconShortcutVisibility__anchor
+ */
+export default (isEnabled, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).setBookmarkIconShortcutVisibility(isEnabled);
+};
diff --git a/src/core/setBookmarkShortcutToggleOffFunction.js b/src/core/setBookmarkShortcutToggleOffFunction.js
new file mode 100644
index 0000000000..257d402db9
--- /dev/null
+++ b/src/core/setBookmarkShortcutToggleOffFunction.js
@@ -0,0 +1,9 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.DocumentViewer.html#setBookmarkShortcutToggleOffFunction__anchor
+ * @see https://docs.apryse.com/api/web/Core.DocumentViewer.html#setBookmarkShortcutToggleOffFunction__anchor
+ */
+export default (callback, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).setBookmarkShortcutToggleOffFunction(callback);
+};
diff --git a/src/core/setBookmarkShortcutToggleOnFunction.js b/src/core/setBookmarkShortcutToggleOnFunction.js
new file mode 100644
index 0000000000..173af37655
--- /dev/null
+++ b/src/core/setBookmarkShortcutToggleOnFunction.js
@@ -0,0 +1,9 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.DocumentViewer.html#setBookmarkShortcutToggleOnFunction__anchor
+ * @see https://docs.apryse.com/api/web/Core.DocumentViewer.html#setBookmarkShortcutToggleOnFunction__anchor
+ */
+export default (callback, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).setBookmarkShortcutToggleOnFunction(callback);
+};
diff --git a/src/core/setUserBookmarks.js b/src/core/setUserBookmarks.js
new file mode 100644
index 0000000000..24dd049e21
--- /dev/null
+++ b/src/core/setUserBookmarks.js
@@ -0,0 +1,9 @@
+import core from 'core';
+
+/**
+ * https://docs.apryse.com/api/web/Core.DocumentViewer.html#setUserBookmarks__anchor
+ * @see https://docs.apryse.com/api/web/Core.DocumentViewer.html#setUserBookmarks__anchor
+ */
+export default (bookmarks, documentViewerKey = 1) => {
+ core.getDocumentViewer(documentViewerKey).setUserBookmarks(bookmarks);
+};
diff --git a/src/event-listeners/onCaretAnnotationAdded.js b/src/event-listeners/onCaretAnnotationAdded.js
new file mode 100644
index 0000000000..836f7204d7
--- /dev/null
+++ b/src/event-listeners/onCaretAnnotationAdded.js
@@ -0,0 +1,29 @@
+import core from 'core';
+import actions from 'actions';
+import selectors from 'selectors';
+import DataElements from 'src/constants/dataElement';
+
+export default ({ dispatch, getState }) => (annotation) => {
+ const state = getState();
+ const isNotesPanelDisabled = selectors.isElementDisabled(state, DataElements.NOTES_PANEL);
+ const isNotesPanelOpen = selectors.isElementOpen(state, DataElements.NOTES_PANEL);
+ const isInlineCommentDisabled = selectors.isElementDisabled(state, DataElements.INLINE_COMMENT_POPUP);
+
+ if (isNotesPanelDisabled) {
+ return;
+ }
+
+ dispatch(actions.closeElement('searchPanel'));
+ dispatch(actions.closeElement(DataElements.REDACTION_PANEL));
+ if (!isInlineCommentDisabled || isNotesPanelOpen) {
+ core.selectAnnotation(annotation);
+ dispatch(actions.triggerNoteEditing());
+ } else {
+ dispatch(actions.openElement(DataElements.NOTES_PANEL));
+ // wait for the notes panel to be fully opened before focusing
+ setTimeout(() => {
+ core.selectAnnotation(annotation);
+ dispatch(actions.triggerNoteEditing());
+ }, 400);
+ }
+};
diff --git a/src/event-listeners/onContentEditModeEnded.js b/src/event-listeners/onContentEditModeEnded.js
index 99814b480c..213007167a 100644
--- a/src/event-listeners/onContentEditModeEnded.js
+++ b/src/event-listeners/onContentEditModeEnded.js
@@ -1,5 +1,13 @@
import actions from 'actions';
+import selectors from 'selectors';
+import core from 'core';
-export default (dispatch) => () => {
+export default (dispatch, store) => () => {
dispatch(actions.enableElements(['thumbnailControl', 'documentControl']));
+ const featureFlags = selectors.getFeatureFlags(store.getState());
+ const { customizableUI } = featureFlags;
+
+ if (customizableUI) {
+ core.setToolMode('AnnotationEdit');
+ }
};
diff --git a/src/event-listeners/onFormFieldCreationModeStarted.js b/src/event-listeners/onFormFieldCreationModeStarted.js
index 929d3dc8e3..19545b7f5a 100644
--- a/src/event-listeners/onFormFieldCreationModeStarted.js
+++ b/src/event-listeners/onFormFieldCreationModeStarted.js
@@ -15,7 +15,6 @@ const formBuilderDefaultDisabledKeys = {
PRINT: 'print',
BOOKMARK: 'bookmark',
SWITCH_PAN: 'switchPan',
- SELECT: 'select',
PAN: 'pan',
ARROW: 'arrow',
CALLOUT: 'callout',
diff --git a/src/event-listeners/onImageContentAdded.js b/src/event-listeners/onImageContentAdded.js
new file mode 100644
index 0000000000..02cc392960
--- /dev/null
+++ b/src/event-listeners/onImageContentAdded.js
@@ -0,0 +1,9 @@
+import core from 'core';
+import actions from 'actions';
+import defaultTool from 'constants/defaultTool';
+
+export default (dispatch) => (annotation) => {
+ core.setToolMode(defaultTool);
+ dispatch(actions.setActiveToolGroup(''));
+ core.selectAnnotation(annotation);
+};
\ No newline at end of file
diff --git a/src/event-listeners/onInitialDeleted.js b/src/event-listeners/onInitialDeleted.js
new file mode 100644
index 0000000000..b6fe9a6c44
--- /dev/null
+++ b/src/event-listeners/onInitialDeleted.js
@@ -0,0 +1,10 @@
+import core from 'core';
+import actions from 'actions';
+import getSignatureDataToStore from 'helpers/getSignatureDataToStore';
+
+export default (dispatch) => async () => {
+ const signatureTool = core.getTool('AnnotationCreateSignature');
+ const savedInitials = signatureTool.getSavedInitials();
+ const newSavedInitials = await getSignatureDataToStore(savedInitials);
+ dispatch(actions.setSavedInitials(newSavedInitials));
+};
diff --git a/src/event-listeners/onInitialSaved.js b/src/event-listeners/onInitialSaved.js
new file mode 100644
index 0000000000..a1296c41a3
--- /dev/null
+++ b/src/event-listeners/onInitialSaved.js
@@ -0,0 +1,22 @@
+import core from 'core';
+import selectors from 'selectors';
+import actions from 'actions';
+import getSignatureDataToStore from 'helpers/getSignatureDataToStore';
+
+export default (dispatch, store) => async () => {
+ const signatureTool = core.getTool('AnnotationCreateSignature');
+ let savedInitials = signatureTool.getSavedInitials();
+ const maxSignaturesCount = selectors.getMaxSignaturesCount(store.getState());
+ const numberOfInitialsToRemove = savedInitials.length - maxSignaturesCount;
+
+ if (numberOfInitialsToRemove > 0) {
+ // to keep the UI sync with the signatures saved in the tool
+ for (let i = 0; i < numberOfInitialsToRemove; i++) {
+ signatureTool.deleteSavedInitials(0);
+ }
+ }
+
+ savedInitials = signatureTool.getSavedInitials();
+ const initialsToStore = await getSignatureDataToStore(savedInitials);
+ dispatch(actions.setSavedInitials(initialsToStore));
+};
\ No newline at end of file
diff --git a/src/event-listeners/onLayersUpdated.js b/src/event-listeners/onLayersUpdated.js
index da98b2f6d2..edc3c4d549 100644
--- a/src/event-listeners/onLayersUpdated.js
+++ b/src/event-listeners/onLayersUpdated.js
@@ -1,10 +1,10 @@
import actions from 'actions';
-import equal from 'fast-deep-equal';
+import _isEqual from 'lodash/isEqual';
import setUIPropertiesForLayers from 'helpers/setUIPropertiesForLayers';
export default (newOCGLayers, currentOCGLayers, dispatch) => {
- const isEqual = equal(newOCGLayers, currentOCGLayers);
- if (!isEqual) {
+ const layersEqual = _isEqual(newOCGLayers, currentOCGLayers);
+ if (!layersEqual) {
const layersToSet = setUIPropertiesForLayers(newOCGLayers);
dispatch(actions.setLayers(layersToSet));
}
diff --git a/src/event-listeners/onSignatureSaved.js b/src/event-listeners/onSignatureSaved.js
index cbe46d890f..20275a6026 100644
--- a/src/event-listeners/onSignatureSaved.js
+++ b/src/event-listeners/onSignatureSaved.js
@@ -18,5 +18,7 @@ export default (dispatch, store) => async () => {
savedSignatures = signatureTool.getSavedSignatures();
const signaturesToStore = await getSignatureDataToStore(savedSignatures);
+ // get the last element of the array (LIFO) and set it as active so it can be shown in the new signature list panel
+ dispatch(actions.setSelectedDisplayedSignatureIndex(signaturesToStore.length - 1));
dispatch(actions.setSavedSignatures(signaturesToStore));
};
\ No newline at end of file
diff --git a/src/helpers/checkFeaturesToEnable.js b/src/helpers/checkFeaturesToEnable.js
new file mode 100644
index 0000000000..277981c571
--- /dev/null
+++ b/src/helpers/checkFeaturesToEnable.js
@@ -0,0 +1,38 @@
+import { mapKeyToToolNames, annotationMapKeys } from 'constants/map';
+import Feature from 'constants/feature';
+import { getInstanceNode } from './getRootNode';
+import DataElements from 'constants/dataElement';
+
+const checkFeaturesToEnable = (componentsMap) => {
+ const keys = Object.keys(componentsMap);
+ const measurementTools = [
+ mapKeyToToolNames(annotationMapKeys.DISTANCE_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.PERIMETER_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.ARC_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.RECTANGULAR_AREA_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.CLOUDY_RECTANGULAR_AREA_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.AREA_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.ELLIPSE_MEASUREMENT),
+ mapKeyToToolNames(annotationMapKeys.COUNT_MEASUREMENT),
+ ].flat();
+ const contentEditTools = mapKeyToToolNames(annotationMapKeys.CONTENT_EDIT_TOOL);
+ const redactTools = mapKeyToToolNames(annotationMapKeys.REDACTION);
+ const instance = getInstanceNode().instance;
+
+ for (let index = 0, len = keys.length; index < len; index++) {
+ const element = componentsMap[keys[index]];
+ if (element.dataElement === DataElements.FILE_PICKER_BUTTON) {
+ instance.UI.enableFilePicker();
+ } else if (element.dataElement === DataElements.CREATE_PORTFOLIO) {
+ instance.UI.enableFeatures(Feature.Portfolio);
+ } else if (redactTools.indexOf(element.toolName) > -1) {
+ instance.UI.enableRedaction();
+ } else if (measurementTools.indexOf(element.toolName) > -1) {
+ instance.UI.enableMeasurement();
+ } else if (contentEditTools.indexOf(element.toolName) > -1) {
+ instance.UI.enableFeatures(Feature.ContentEdit);
+ }
+ }
+};
+
+export default checkFeaturesToEnable;
\ No newline at end of file
diff --git a/src/helpers/checkFeaturesToEnable.spec.js b/src/helpers/checkFeaturesToEnable.spec.js
new file mode 100644
index 0000000000..31e58825e7
--- /dev/null
+++ b/src/helpers/checkFeaturesToEnable.spec.js
@@ -0,0 +1,48 @@
+import checkFeaturesToEnable from './checkFeaturesToEnable';
+import { getInstanceNode } from './getRootNode';
+
+jest.mock('./getRootNode', () => {
+ const original = jest.requireActual('./getRootNode'); // Step 2.
+
+ return {
+ ...original,
+ getInstanceNode: jest.fn(() => {})
+ };
+});
+
+describe('checkFeaturesToEnable', () => {
+ it('Should call checkFeaturesToEnable functions', () => {
+ const mockMethod = jest.fn();
+ const mockMethod2 = jest.fn();
+ const mockMethod3 = jest.fn();
+ const mockMethod4 = jest.fn();
+ getInstanceNode.mockImplementation(() => {
+ return {
+ instance: {
+ UI: {
+ enableFeatures: mockMethod,
+ enableFilePicker: mockMethod2,
+ enableRedaction: mockMethod3,
+ enableMeasurement: mockMethod4,
+ }
+ }
+ };
+ });
+ const instance = getInstanceNode().instance;
+ checkFeaturesToEnable({
+ arcMeasurementToolButton: {
+ toolName: 'AnnotationCreateArcMeasurement',
+ dataElement: 'arcMeasurementToolButton',
+ },
+ contentEditButton: {
+ toolName: 'ContentEditTool',
+ dataElement: 'contentEditButton',
+ }
+ });
+
+ expect(instance.UI.enableFeatures).toHaveBeenCalledTimes(1);
+ expect(instance.UI.enableFilePicker).not.toBeCalled();
+ expect(instance.UI.enableMeasurement).toHaveBeenCalledTimes(1);
+ expect(instance.UI.enableRedaction).not.toBeCalled();
+ });
+});
\ No newline at end of file
diff --git a/src/helpers/clickTracker.js b/src/helpers/clickTracker.js
new file mode 100644
index 0000000000..ef215d72db
--- /dev/null
+++ b/src/helpers/clickTracker.js
@@ -0,0 +1,11 @@
+export const ClickedItemTypes = {
+ BUTTON: 'button',
+};
+
+let clickMiddleWare;
+
+export const setClickMiddleWare = (middleware) => {
+ clickMiddleWare = middleware;
+};
+
+export const getClickMiddleWare = () => clickMiddleWare;
diff --git a/src/helpers/getAngleInRadians.js b/src/helpers/getAngleInRadians.js
new file mode 100644
index 0000000000..1a20add5b5
--- /dev/null
+++ b/src/helpers/getAngleInRadians.js
@@ -0,0 +1,21 @@
+export default (pt1, pt2, pt3) => {
+ let angle;
+
+ if (pt1 && pt2) {
+ if (pt3) {
+ // calculate the angle using Law of cosines
+ const AB = Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
+ const BC = Math.sqrt(Math.pow(pt2.x - pt3.x, 2) + Math.pow(pt2.y - pt3.y, 2));
+ const AC = Math.sqrt(Math.pow(pt3.x - pt1.x, 2) + Math.pow(pt3.y - pt1.y, 2));
+ angle = Math.acos((BC * BC + AB * AB - AC * AC) / (2 * BC * AB));
+ } else {
+ // if there are only two points returns the angle in the plane (in radians) between the positive x-axis and the ray from (0,0) to the point (x,y)
+ angle = Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x);
+ // keep the angle range between 0 and Math.PI / 2
+ angle = Math.abs(angle);
+ angle = angle > Math.PI / 2 ? Math.PI - angle : angle;
+ }
+ }
+
+ return angle;
+};
diff --git a/src/helpers/getDeviceSize.js b/src/helpers/getDeviceSize.js
new file mode 100644
index 0000000000..9e1fbc502b
--- /dev/null
+++ b/src/helpers/getDeviceSize.js
@@ -0,0 +1,55 @@
+import getRootNode from './getRootNode';
+import useMedia from 'hooks/useMedia';
+import { MOBILE_SIZE, TABLET_SIZE } from 'constants/deviceSizes';
+
+export const isMobileSize = () => {
+ if (window.isApryseWebViewerWebComponent) {
+ return getRootNode()?.host.clientWidth <= MOBILE_SIZE;
+ }
+ return useMedia(
+ // Media queries
+ [`(max-width: ${MOBILE_SIZE}px)`],
+ [true],
+ // Default value
+ false,
+ );
+};
+
+export const isTabletSize = () => {
+ if (window.isApryseWebViewerWebComponent) {
+ return getRootNode()?.host.clientWidth > MOBILE_SIZE && getRootNode()?.host.clientWidth <= TABLET_SIZE;
+ }
+ return useMedia(
+ // Media queries
+ [`(min-width: ${MOBILE_SIZE + 1}px) and (max-width: ${TABLET_SIZE}px)`],
+ [true],
+ // Default value
+ false,
+ );
+};
+
+export const isTabletAndMobileSize = () => {
+ if (window.isApryseWebViewerWebComponent) {
+ return getRootNode()?.host.clientWidth <= TABLET_SIZE;
+ }
+ return useMedia(
+ // Media queries
+ [`(max-width: ${TABLET_SIZE}px)`],
+ [true],
+ // Default value
+ false,
+ );
+};
+
+export const isDesktopSize = () => {
+ if (window.isApryseWebViewerWebComponent) {
+ return getRootNode()?.host.clientWidth > TABLET_SIZE;
+ }
+ return useMedia(
+ // Media queries
+ [`(min-width: ${TABLET_SIZE + 1}px)`],
+ [true],
+ // Default value
+ false,
+ );
+};
\ No newline at end of file
diff --git a/src/helpers/getElements.js b/src/helpers/getElements.js
index c42cc6ea55..5d9f558b6d 100644
--- a/src/helpers/getElements.js
+++ b/src/helpers/getElements.js
@@ -12,8 +12,4 @@ export function getAllOpenedModals() {
export function getDatePicker() {
return document.querySelector('[data-element="datePickerContainer"]');
-}
-
-export function getAllPanels(location) {
- return document.querySelectorAll(`.flx-Panel.${location}`);
-}
+}
\ No newline at end of file
diff --git a/src/helpers/getNumberOfDecimalPlaces.js b/src/helpers/getNumberOfDecimalPlaces.js
new file mode 100644
index 0000000000..7469c00e83
--- /dev/null
+++ b/src/helpers/getNumberOfDecimalPlaces.js
@@ -0,0 +1 @@
+export default (precision) => (precision === 1 ? 0 : precision?.toString().split('.')[1].length);
diff --git a/src/helpers/getRootNode.js b/src/helpers/getRootNode.js
index ae37eb3f8e..7b900be470 100644
--- a/src/helpers/getRootNode.js
+++ b/src/helpers/getRootNode.js
@@ -1,5 +1,21 @@
let rootNode;
+function findNestedWebComponents(tagName, root = document) {
+ const elements = [];
+
+ // Check direct children
+ root.querySelectorAll(tagName).forEach((el) => elements.push(el));
+
+ // Check shadow DOMs
+ root.querySelectorAll('*').forEach((el) => {
+ if (el.shadowRoot) {
+ elements.push(...findNestedWebComponents(tagName, el.shadowRoot));
+ }
+ });
+
+ return elements;
+}
+
const getRootNode = () => {
if (!window.isApryseWebViewerWebComponent) {
return document;
@@ -7,7 +23,13 @@ const getRootNode = () => {
if (rootNode) {
return rootNode;
}
- const elementList = document.getElementsByTagName('apryse-webviewer');
+
+ let elementList;
+ elementList = document.getElementsByTagName('apryse-webviewer');
+ if (elementList.length === 0) {
+ elementList = findNestedWebComponents('apryse-webviewer');
+ }
+
if (elementList?.length) {
for (const element of elementList) {
const foundNode = element.shadowRoot;
diff --git a/src/helpers/handleFreeTextAutoSizeToggle.js b/src/helpers/handleFreeTextAutoSizeToggle.js
new file mode 100644
index 0000000000..9fd6420a32
--- /dev/null
+++ b/src/helpers/handleFreeTextAutoSizeToggle.js
@@ -0,0 +1,22 @@
+import core from 'core';
+
+/**
+ * @ignore
+ * handler for auto size font toggle
+ * @param {FreeTextAnnotation} annotation annotation to toggle auto size font
+ * @param {function} setAutoSizeFont function to set auto size font
+ * @param {boolean} isAutoSizeFont current auto size font value
+ */
+export default (annotation, setAutoSizeFont, isAutoSizeFont) => {
+ const freeTextAnnot = annotation;
+ const calculatedFontSize = freeTextAnnot.getCalculatedFontSize();
+ if (isAutoSizeFont) {
+ freeTextAnnot.FontSize = calculatedFontSize;
+ } else {
+ freeTextAnnot.switchToAutoFontSize();
+ }
+
+ setAutoSizeFont(!isAutoSizeFont);
+ core.getAnnotationManager().redrawAnnotation(freeTextAnnot);
+};
+
diff --git a/src/helpers/initialColorStates.js b/src/helpers/initialColorStates.js
new file mode 100644
index 0000000000..fb754c8a4c
--- /dev/null
+++ b/src/helpers/initialColorStates.js
@@ -0,0 +1,50 @@
+const initialColors = [
+ '#e44234',
+ '#ff8d00',
+ '#ffcd45',
+ '#5cc96e',
+ '#25d2d1',
+ '#597ce2',
+ '#c544ce',
+ '#7d2e25',
+ '#a84f1d',
+ '#e99e38',
+ '#347842',
+ '#167e7d',
+ '#354a87',
+ '#76287b',
+ '#ffffff',
+ '#cdcdcd',
+ '#9c9c9c',
+ '#696969',
+ '#272727',
+ '#000000'
+];
+
+const initialTextColors = [
+ '#000000',
+ '#272727',
+ '#696969',
+ '#9c9c9c',
+ '#cdcdcd',
+ '#ffffff',
+ '#7d2e25',
+ '#a84f1d',
+ '#e99e38',
+ '#347842',
+ '#167e7d',
+ '#354a87',
+ '#76287b',
+ '#e44234',
+ '#ff8d00',
+ '#ffcd45',
+ '#5cc96e',
+ '#25d2d1',
+ '#597ce2',
+ '#c544ce'
+];
+
+export {
+ initialColors,
+ initialTextColors,
+};
\ No newline at end of file
diff --git a/src/helpers/multiViewerHelper.js b/src/helpers/multiViewerHelper.js
new file mode 100644
index 0000000000..34503633be
--- /dev/null
+++ b/src/helpers/multiViewerHelper.js
@@ -0,0 +1,14 @@
+const multiViewerHelper = {
+ matchedPages: null,
+ isScrolledByClickingChangeItem: false,
+};
+
+export const setIsScrolledByClickingChangeItem = (value) => {
+ multiViewerHelper.isScrolledByClickingChangeItem = value;
+};
+
+export const getIsScrolledByClickingChangeItem = () => {
+ return multiViewerHelper.isScrolledByClickingChangeItem;
+};
+
+export default multiViewerHelper;
diff --git a/src/helpers/officeEditor.js b/src/helpers/officeEditor.js
new file mode 100644
index 0000000000..b94a760de3
--- /dev/null
+++ b/src/helpers/officeEditor.js
@@ -0,0 +1,6 @@
+import core from 'core';
+import { workerTypes } from 'constants/types';
+
+export function isOfficeEditorMode() {
+ return core.getDocument()?.getType() === workerTypes.OFFICE_EDITOR;
+}
diff --git a/src/helpers/openOfficeEditorFilePicker.js b/src/helpers/openOfficeEditorFilePicker.js
new file mode 100644
index 0000000000..9d3e3e8c1b
--- /dev/null
+++ b/src/helpers/openOfficeEditorFilePicker.js
@@ -0,0 +1,5 @@
+import getRootNode from 'helpers/getRootNode';
+
+export default () => {
+ getRootNode().querySelector('#office-editor-file-picker')?.click();
+};
diff --git a/src/helpers/sanitizeSVG.js b/src/helpers/sanitizeSVG.js
new file mode 100644
index 0000000000..06208e4d7a
--- /dev/null
+++ b/src/helpers/sanitizeSVG.js
@@ -0,0 +1,43 @@
+import DOMPurify from 'dompurify';
+
+const SVG_MIME_TYPE = 'image/svg+xml';
+
+const hasFileSize = (file) => {
+ return file.size !== undefined;
+};
+
+// Taken from https://github.com/mattkrick/sanitize-svg/blob/master/src/sanitizeSVG.ts#L31
+const readAsText = (svg) => {
+ return new Promise((resolve) => {
+ if (!hasFileSize(svg)) {
+ resolve(svg.toString('utf-8'));
+ } else {
+ const fileReader = new FileReader();
+ fileReader.onload = () => resolve(fileReader.result);
+ fileReader.readAsText(svg);
+ }
+ });
+};
+
+export const isSVG = (file) => {
+ return file.type === SVG_MIME_TYPE;
+};
+
+export const sanitizeSVG = async (file) => {
+ const svgText = await readAsText(file);
+ if (!svgText) {
+ return { svg: file };
+ }
+
+ const forbiddenTags = [];
+ DOMPurify.addHook('uponSanitizeElement', (_, hookEvent) => {
+ const { tagName, allowedTags } = hookEvent;
+ if (!allowedTags[tagName]) {
+ forbiddenTags.push(tagName);
+ }
+ });
+
+ const clean = DOMPurify.sanitize(svgText);
+ const svg = new Blob([clean], { type: SVG_MIME_TYPE });
+ return { svg, isDirty: forbiddenTags.length > 0 };
+};
\ No newline at end of file
diff --git a/src/helpers/setEnableAnnotationNumbering.js b/src/helpers/setEnableAnnotationNumbering.js
new file mode 100644
index 0000000000..1233663334
--- /dev/null
+++ b/src/helpers/setEnableAnnotationNumbering.js
@@ -0,0 +1,7 @@
+import core from 'core';
+
+export default (state) => {
+ if (state.viewer.isAnnotationNumberingEnabled) {
+ core.enableAnnotationNumbering();
+ }
+};
\ No newline at end of file
diff --git a/src/helpers/useWindowsDimensions.js b/src/helpers/useWindowsDimensions.js
new file mode 100644
index 0000000000..89f0f7bd84
--- /dev/null
+++ b/src/helpers/useWindowsDimensions.js
@@ -0,0 +1,24 @@
+import { useState, useEffect } from 'react';
+
+function getWindowDimensions() {
+ const { innerWidth: width, innerHeight: height } = window;
+ return {
+ width,
+ height
+ };
+}
+
+export default function useWindowDimensions() {
+ const [windowDimensions, setWindowDimensions] = useState(getWindowDimensions());
+
+ useEffect(() => {
+ function handleResize() {
+ setWindowDimensions(getWindowDimensions());
+ }
+
+ window.addEventListener('resize', handleResize);
+ return () => window.removeEventListener('resize', handleResize);
+ }, []);
+
+ return windowDimensions;
+}
diff --git a/src/hooks/useFloatingHeaderSelectors/index.js b/src/hooks/useFloatingHeaderSelectors/index.js
new file mode 100644
index 0000000000..d197bf916d
--- /dev/null
+++ b/src/hooks/useFloatingHeaderSelectors/index.js
@@ -0,0 +1,3 @@
+import useFloatingHeaderSelectors from './useFloatingHeaderSelectors';
+
+export default useFloatingHeaderSelectors;
\ No newline at end of file
diff --git a/src/hooks/useFloatingHeaderSelectors/useFloatingHeaderSelectors.spec.js b/src/hooks/useFloatingHeaderSelectors/useFloatingHeaderSelectors.spec.js
new file mode 100644
index 0000000000..43f557219c
--- /dev/null
+++ b/src/hooks/useFloatingHeaderSelectors/useFloatingHeaderSelectors.spec.js
@@ -0,0 +1,102 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { useSelector, Provider } from 'react-redux';
+import useFloatingHeaderSelectors from './useFloatingHeaderSelectors';
+import rootReducer from 'reducers/rootReducer';
+import initialState from 'src/redux/initialState';
+import { configureStore } from '@reduxjs/toolkit';
+import React from 'react';
+import { RESIZE_BAR_WIDTH } from 'src/constants/panel';
+// Mocking useSelector
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useSelector: jest.fn(),
+}));
+
+const floatEndBottomHeader = {
+ dataElement: 'floatEndBottomHeader',
+ placement: 'bottom',
+ float: true,
+ position: 'end',
+ items: [],
+};
+
+const floatStartBottomHeader = {
+ dataElement: 'floatStartBottomHeader',
+ placement: 'bottom',
+ float: true,
+ position: 'start',
+ items: [],
+};
+
+const floatStartTopHeader = {
+ dataElement: 'topStartFloatingHeader',
+ placement: 'top',
+ float: true,
+ position: 'start',
+ items: [],
+};
+
+const floatEndTopHeader = {
+ dataElement: 'topEndFloatingHeader',
+ placement: 'top',
+ float: true,
+ position: 'end',
+ items: [],
+};
+
+describe('useFloatingHeaderSelectors hook', () => {
+ it('should return the correct values from the state', () => {
+ const mockState = {
+ ...initialState,
+ viewer: {
+ ...initialState.viewer,
+ openElements: {
+ ...initialState.viewer.openElements,
+ leftPanel: false,
+ notesPanel: false,
+ redactionPanel: true,
+ },
+ modularHeadersWidth: {
+ rightHeader: 23,
+ leftHeader: 48,
+ },
+ floatingContainersDimensions: {
+ topFloatingContainerHeight: 36,
+ bottomFloatingContainerHeight: 28,
+ },
+ modularHeaders: [
+ floatEndBottomHeader,
+ floatStartTopHeader,
+ floatEndTopHeader,
+ floatStartBottomHeader,
+ ],
+ }
+ };
+
+ const store = configureStore({
+ reducer: rootReducer,
+ preloadedState: mockState,
+ });
+
+ useSelector.mockImplementation((callback) => callback(mockState));
+
+ const { result } = renderHook(() => useFloatingHeaderSelectors(), {
+ wrapper: ({ children }) => {children},
+ });
+
+ expect(result.current.isLeftPanelOpen).toBeFalsy();
+ expect(result.current.isRightPanelOpen).toBe(true);
+ expect(result.current.leftPanelWidth).toBe(mockState.viewer.panelWidths.leftPanel + RESIZE_BAR_WIDTH);
+ expect(result.current.rightPanelWidth).toBe(mockState.viewer.panelWidths.redactionPanel);
+ expect(result.current.leftHeaderWidth).toBe(0);
+ expect(result.current.rightHeaderWidth).toBe(0);
+ expect(result.current.topHeadersHeight).toBe(0);
+ expect(result.current.bottomHeadersHeight).toBe(0);
+ expect(result.current.topFloatingContainerHeight).toBe(mockState.viewer.floatingContainersDimensions.topFloatingContainerHeight);
+ expect(result.current.bottomFloatingContainerHeight).toBe(mockState.viewer.floatingContainersDimensions.bottomFloatingContainerHeight);
+ expect(result.current.topStartFloatingHeaders).toEqual([floatStartTopHeader]);
+ expect(result.current.bottomStartFloatingHeaders).toEqual([floatStartBottomHeader]);
+ expect(result.current.bottomEndFloatingHeaders).toEqual([floatEndBottomHeader]);
+ expect(result.current.topEndFloatingHeaders).toEqual([floatEndTopHeader]);
+ });
+});
diff --git a/src/hooks/useOnAnnotationContentOverlayOpen/index.js b/src/hooks/useOnAnnotationContentOverlayOpen/index.js
new file mode 100644
index 0000000000..8d414fb75f
--- /dev/null
+++ b/src/hooks/useOnAnnotationContentOverlayOpen/index.js
@@ -0,0 +1,3 @@
+import useOnAnnotationContentOverlayOpen from './useOnAnnotationContentOverlayOpen';
+
+export default useOnAnnotationContentOverlayOpen;
\ No newline at end of file
diff --git a/src/hooks/useOnAnnotationContentOverlayOpen/useOnAnnotationContentOverlayOpen.js b/src/hooks/useOnAnnotationContentOverlayOpen/useOnAnnotationContentOverlayOpen.js
new file mode 100644
index 0000000000..4accc4344b
--- /dev/null
+++ b/src/hooks/useOnAnnotationContentOverlayOpen/useOnAnnotationContentOverlayOpen.js
@@ -0,0 +1,49 @@
+import { useEffect, useState } from 'react';
+import { useDispatch, useSelector } from 'react-redux';
+import actions from 'actions';
+import selectors from 'selectors';
+import core from 'core';
+import DataElements from 'constants/dataElement';
+
+export default function useOnAnnotationContentOverlayOpen() {
+ // Clients have the option to customize how the tooltip is rendered by passing a handler
+ const customHandler = useSelector((state) => selectors.getAnnotationContentOverlayHandler(state));
+
+ const dispatch = useDispatch();
+ const [annotation, setAnnotation] = useState(null);
+ const [clientXY, setClientXY] = useState({ clientX: 0, clientY: 0 });
+ const isUsingCustomHandler = customHandler !== null;
+
+ useEffect(() => {
+ const viewElement = core.getViewerElement();
+
+ const onMouseHover = (e) => {
+ if (e.buttons !== 0) {
+ return;
+ }
+ let annotation = core.getAnnotationManager().getAnnotationByMouseEvent(e);
+
+ if (annotation && viewElement.contains(e.target)) {
+ // if hovered annot is grouped, pick the "primary" annot to match Adobe's behavior
+ const groupedAnnots = core.getAnnotationManager().getGroupAnnotations(annotation);
+ const ungroupedAnnots = groupedAnnots.filter((annot) => !annot.isGrouped());
+ annotation = ungroupedAnnots.length > 0 ? ungroupedAnnots[0] : annotation;
+
+ const isFreeTextAnnotation = annotation instanceof window.Core.Annotations.FreeTextAnnotation;
+ if (isUsingCustomHandler || !isFreeTextAnnotation) {
+ setClientXY({ clientX: e.clientX, clientY: e.clientY });
+ setAnnotation(annotation);
+ dispatch(actions.openElement(DataElements.ANNOTATION_CONTENT_OVERLAY));
+ }
+ } else {
+ setAnnotation(null);
+ dispatch(actions.closeElement(DataElements.ANNOTATION_CONTENT_OVERLAY));
+ }
+ };
+
+ core.addEventListener('mouseMove', onMouseHover);
+ return () => core.removeEventListener('mouseMove', onMouseHover);
+ }, [annotation, isUsingCustomHandler]);
+
+ return { annotation, clientXY };
+}
\ No newline at end of file
diff --git a/src/hooks/useOnAnnotationCreateSignatureToolMode/index.js b/src/hooks/useOnAnnotationCreateSignatureToolMode/index.js
new file mode 100644
index 0000000000..2264e9c3af
--- /dev/null
+++ b/src/hooks/useOnAnnotationCreateSignatureToolMode/index.js
@@ -0,0 +1,3 @@
+import useOnAnnotationCreateSignatureToolMode from './useOnAnnotationCreateSignatureToolMode';
+
+export default useOnAnnotationCreateSignatureToolMode;
\ No newline at end of file
diff --git a/src/hooks/useOnAnnotationPopupOpen/index.js b/src/hooks/useOnAnnotationPopupOpen/index.js
new file mode 100644
index 0000000000..62fb3ba75c
--- /dev/null
+++ b/src/hooks/useOnAnnotationPopupOpen/index.js
@@ -0,0 +1,3 @@
+import useOnAnnotationPopupOpen from './useOnAnnotationPopupOpen';
+
+export default useOnAnnotationPopupOpen;
\ No newline at end of file
diff --git a/src/hooks/useOnCountMeasurementAnnotationSelected/useOnCountMeasurementAnnotationSelected.js b/src/hooks/useOnCountMeasurementAnnotationSelected/useOnCountMeasurementAnnotationSelected.js
index 1223246222..80e6de8fab 100644
--- a/src/hooks/useOnCountMeasurementAnnotationSelected/useOnCountMeasurementAnnotationSelected.js
+++ b/src/hooks/useOnCountMeasurementAnnotationSelected/useOnCountMeasurementAnnotationSelected.js
@@ -59,10 +59,7 @@ export default function useOnCountMeasurementAnnotationSelected() {
) {
setAnnotation(annotations[0]);
dispatch(actions.openElement(DataElements.MEASUREMENT_OVERLAY));
- } else if (
- action === 'deselected' &&
- !core.isAnnotationSelected(annotation)
- ) {
+ } else if (action === 'deselected' && !core.isAnnotationSelected(annotation)) {
dispatch(actions.closeElement(DataElements.MEASUREMENT_OVERLAY));
}
};
diff --git a/src/hooks/useOnCropAnnotationAdded/index.js b/src/hooks/useOnCropAnnotationAdded/index.js
new file mode 100644
index 0000000000..66ebd9d402
--- /dev/null
+++ b/src/hooks/useOnCropAnnotationAdded/index.js
@@ -0,0 +1,3 @@
+import useOnCropAnnotationAdded from './useOnCropAnnotationAdded';
+
+export default useOnCropAnnotationAdded;
\ No newline at end of file
diff --git a/src/hooks/useOnCropAnnotationAdded/useOnCropAnnotationAdded.js b/src/hooks/useOnCropAnnotationAdded/useOnCropAnnotationAdded.js
new file mode 100644
index 0000000000..ceb8edb931
--- /dev/null
+++ b/src/hooks/useOnCropAnnotationAdded/useOnCropAnnotationAdded.js
@@ -0,0 +1,24 @@
+import { useEffect, useState } from 'react';
+import core from 'core';
+
+export default function useOnCropAnnotationAdded(openDocumentCropPopup) {
+ const [cropAnnotation, setCropAnnotation] = useState(null);
+
+ useEffect(() => {
+ const onAnnotationChanged = (annotations, action) => {
+ const annotation = annotations[0];
+ if (action === 'add' && annotation.Subject === 'Rectangle' && annotation.ToolName === 'CropPage') {
+ setCropAnnotation(annotation);
+ openDocumentCropPopup();
+ }
+ };
+
+ core.addEventListener('annotationChanged', onAnnotationChanged);
+
+ return () => {
+ core.removeEventListener('annotationChanged', onAnnotationChanged);
+ };
+ }, []);
+
+ return cropAnnotation;
+}
\ No newline at end of file
diff --git a/src/hooks/useOnCropAnnotationChangedOrSelected/index.js b/src/hooks/useOnCropAnnotationChangedOrSelected/index.js
new file mode 100644
index 0000000000..91ececbb66
--- /dev/null
+++ b/src/hooks/useOnCropAnnotationChangedOrSelected/index.js
@@ -0,0 +1,3 @@
+import useOnCropAnnotationChangedOrSelected from './useOnCropAnnotationChangedOrSelected';
+
+export default useOnCropAnnotationChangedOrSelected;
\ No newline at end of file
diff --git a/src/hooks/useOnCropAnnotationChangedOrSelected/useOnCropAnnotationChangedOrSelected.js b/src/hooks/useOnCropAnnotationChangedOrSelected/useOnCropAnnotationChangedOrSelected.js
new file mode 100644
index 0000000000..387599b8df
--- /dev/null
+++ b/src/hooks/useOnCropAnnotationChangedOrSelected/useOnCropAnnotationChangedOrSelected.js
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+import core from 'core';
+
+export default function useOnCropAnnotationChangedOrSelected(openDocumentCropPopup) {
+ const [cropAnnotation, setCropAnnotation] = useState(null);
+
+ useEffect(() => {
+ const onAnnotationChanged = (annotations, action) => {
+ const annotation = annotations[0];
+ if (action === 'add' && annotation.ToolName === window.Core.Tools.ToolNames['CROP'] && annotation instanceof window.Core.Annotations.RectangleAnnotation) {
+ setCropAnnotation(annotation);
+ openDocumentCropPopup();
+ }
+ if (action === 'delete' && annotation.ToolName === window.Core.Tools.ToolNames['CROP'] && annotation instanceof window.Core.Annotations.RectangleAnnotation) {
+ setCropAnnotation(null);
+ }
+ };
+
+ const onAnnotationSelected = (annotations, action) => {
+ const annotation = annotations[0];
+ if (action === 'selected' && annotation.ToolName === window.Core.Tools.ToolNames['CROP'] && annotation instanceof window.Core.Annotations.RectangleAnnotation) {
+ setCropAnnotation(annotation);
+ openDocumentCropPopup();
+ }
+ };
+
+ core.addEventListener('annotationChanged', onAnnotationChanged);
+ core.addEventListener('annotationSelected', onAnnotationSelected);
+
+ return () => {
+ core.removeEventListener('annotationChanged', onAnnotationChanged);
+ core.removeEventListener('annotationSelected', onAnnotationSelected);
+ };
+ }, []);
+
+ return cropAnnotation;
+}
diff --git a/src/hooks/useOnFormFieldsChanged/index.js b/src/hooks/useOnFormFieldsChanged/index.js
new file mode 100644
index 0000000000..db652dca01
--- /dev/null
+++ b/src/hooks/useOnFormFieldsChanged/index.js
@@ -0,0 +1,3 @@
+import useOnFormFieldsChanged from './useOnFormFieldsChanged';
+
+export default useOnFormFieldsChanged;
\ No newline at end of file
diff --git a/src/hooks/useOnFormFieldsChanged/useOnFormFieldsChanged.js b/src/hooks/useOnFormFieldsChanged/useOnFormFieldsChanged.js
new file mode 100644
index 0000000000..08a24daf40
--- /dev/null
+++ b/src/hooks/useOnFormFieldsChanged/useOnFormFieldsChanged.js
@@ -0,0 +1,64 @@
+import { useState, useEffect } from 'react';
+import core from 'core';
+
+export default function useOnFormFieldsChanged() {
+ const [formFieldAnnotationsList, setFormFieldAnnotationsList] = useState([]);
+
+ useEffect(() => {
+ const setFormFieldIndicators = () => {
+ let formFieldIndicators = [];
+ const annotations = core.getAnnotationsList();
+ const formFieldCreationManager = core.getFormFieldCreationManager();
+ if (formFieldCreationManager.isInFormFieldCreationMode()) {
+ const formFieldPlaceholders = annotations.filter((annotation) => annotation.isFormFieldPlaceholder());
+ formFieldIndicators = [
+ ...formFieldPlaceholders
+ .reduce(
+ (fieldNameMap, field) => {
+ if (!fieldNameMap.has(field.getCustomData(formFieldCreationManager.getFieldLabels().FIELD_NAME))) {
+ fieldNameMap.set(field.getCustomData(formFieldCreationManager.getFieldLabels().FIELD_NAME), field);
+ }
+ return fieldNameMap;
+ },
+ new Map()
+ ).values(),
+ ];
+ } else {
+ const widgets = annotations.filter(
+ (annotation) => formFieldCreationManager.getShowIndicator(annotation)
+ );
+ formFieldIndicators = [
+ ...widgets
+ .reduce(
+ (fieldNameMap, field) => {
+ if (!fieldNameMap.has(field['fieldName'])) {
+ fieldNameMap.set(field['fieldName'], field);
+ }
+ return fieldNameMap;
+ },
+ new Map()
+ ).values(),
+ ];
+ }
+ setFormFieldAnnotationsList(formFieldIndicators);
+ };
+
+ const onDocumentLoaded = () => {
+ setFormFieldAnnotationsList([]);
+ };
+
+ core.addEventListener('documentLoaded', onDocumentLoaded);
+ core.addEventListener('zoomUpdated', setFormFieldIndicators);
+ core.addEventListener('annotationChanged', setFormFieldIndicators);
+ core.addEventListener('pageNumberUpdated', setFormFieldIndicators);
+
+ return () => {
+ core.removeEventListener('documentLoaded', onDocumentLoaded);
+ core.removeEventListener('zoomUpdated', setFormFieldIndicators);
+ core.removeEventListener('annotationChanged', setFormFieldIndicators);
+ core.removeEventListener('pageNumberUpdated', setFormFieldIndicators);
+ };
+ });
+
+ return formFieldAnnotationsList;
+}
diff --git a/src/hooks/useOnInlineCommentPopupOpen/index.js b/src/hooks/useOnInlineCommentPopupOpen/index.js
new file mode 100644
index 0000000000..664fdf586b
--- /dev/null
+++ b/src/hooks/useOnInlineCommentPopupOpen/index.js
@@ -0,0 +1,3 @@
+import useOnInlineCommentPopupOpen from './useOnInlineCommentPopupOpen';
+
+export default useOnInlineCommentPopupOpen;
\ No newline at end of file
diff --git a/src/hooks/useOnInlineCommentPopupOpen/useOnInlineCommentPopupOpen.js b/src/hooks/useOnInlineCommentPopupOpen/useOnInlineCommentPopupOpen.js
new file mode 100644
index 0000000000..ca8a8b5282
--- /dev/null
+++ b/src/hooks/useOnInlineCommentPopupOpen/useOnInlineCommentPopupOpen.js
@@ -0,0 +1,125 @@
+import { useEffect, useState } from 'react';
+import { useDispatch, useSelector, shallowEqual } from 'react-redux';
+import actions from 'actions';
+import selectors from 'selectors';
+import core from 'core';
+import DataElements from 'constants/dataElement';
+
+export default function useOnInlineCommentPopupOpen() {
+ const [
+ isNotesPanelOpen,
+ notesInLeftPanel,
+ leftPanelOpen,
+ activeLeftPanel,
+ inlineCommentFilter,
+ activeDocumentViewerKey,
+ ] = useSelector(
+ (state) => [
+ selectors.isElementOpen(state, DataElements.NOTES_PANEL),
+ selectors.getNotesInLeftPanel(state),
+ selectors.isElementOpen(state, DataElements.LEFT_PANEL),
+ selectors.getActiveLeftPanel(state),
+ selectors.getInlineCommentFilter(state),
+ selectors.getActiveDocumentViewerKey(state),
+ ],
+ shallowEqual,
+ );
+ const dispatch = useDispatch();
+
+ const [annotation, setAnnotation] = useState(null);
+ const [isFreeTextAnnotationAdded, setFreeTextAnnotationAdded] = useState(false);
+ const { ToolNames } = window.Core.Tools;
+
+ const isNotesPanelOpenOrActive = isNotesPanelOpen || (notesInLeftPanel && leftPanelOpen && activeLeftPanel === 'notesPanel');
+
+ const closeAndReset = () => {
+ dispatch(actions.closeElement(DataElements.INLINE_COMMENT_POPUP));
+ setAnnotation(null);
+ setFreeTextAnnotationAdded(false);
+ };
+
+ const isFreeTextAnnotation = (annot) => {
+ return annot instanceof window.Core.Annotations.FreeTextAnnotation;
+ };
+
+ useEffect(() => {
+ const onAnnotationDoubleClicked = (annot) => {
+ if (isFreeTextAnnotation(annot)) {
+ closeAndReset();
+ }
+ };
+
+ core.addEventListener('annotationDoubleClicked', onAnnotationDoubleClicked, null, activeDocumentViewerKey);
+ return () => core.removeEventListener('annotationDoubleClicked', onAnnotationDoubleClicked, null, activeDocumentViewerKey);
+ }, [activeDocumentViewerKey]);
+
+ useEffect(() => {
+ const onAnnotationSelected = (annotations, action) => {
+ const selectedAnnotationTool = annotations[0].ToolName;
+ const shouldSetCommentingAnnotation =
+ action === 'selected'
+ && annotations.length
+ && !isFreeTextAnnotationAdded
+ && selectedAnnotationTool !== ToolNames.CROP;
+ if (shouldSetCommentingAnnotation) {
+ setAnnotation(annotations[0]);
+ }
+
+ if (action === 'deselected' && annotations.length) {
+ setFreeTextAnnotationAdded(false);
+ if (annotations.some((annot) => annot === annotation)) {
+ closeAndReset();
+ }
+ }
+ };
+
+ core.addEventListener('annotationSelected', onAnnotationSelected, null, activeDocumentViewerKey);
+ return () => {
+ core.removeEventListener('annotationSelected', onAnnotationSelected, null, activeDocumentViewerKey);
+ };
+ }, [annotation, isFreeTextAnnotationAdded, activeDocumentViewerKey]);
+
+ useEffect(() => {
+ setFreeTextAnnotationAdded(false);
+ const onMouseLeftUp = (e) => {
+ // WILL BE TRIGGERED ON MOBILE: happens before annotationSelected
+ // clicking on the selected annotation is considered clicking outside of this component
+ // so this component will close due to useOnClickOutside
+ // this handler is used to make sure that if we click on the selected annotation, this component will show up again
+ const annotUnderMouse = core.getAnnotationByMouseEvent(e, activeDocumentViewerKey);
+
+ if (annotation) {
+ if (!annotUnderMouse) {
+ closeAndReset();
+ }
+
+ if (core.isAnnotationSelected(annotUnderMouse) && annotUnderMouse !== annotation) {
+ setAnnotation(annotUnderMouse);
+ }
+ }
+ };
+
+ const onAnnotationChanged = (annotations, action) => {
+ setFreeTextAnnotationAdded(action === 'add' && isFreeTextAnnotation(annotations[0]));
+ const isCommentingAnnotationSelected = core.isAnnotationSelected(annotation);
+ if (annotation && !isCommentingAnnotationSelected) {
+ closeAndReset();
+ }
+ };
+
+ core.addEventListener('mouseLeftUp', onMouseLeftUp, null, activeDocumentViewerKey);
+ core.addEventListener('annotationChanged', onAnnotationChanged, null, activeDocumentViewerKey);
+ return () => {
+ core.removeEventListener('mouseLeftUp', onMouseLeftUp, null, activeDocumentViewerKey);
+ core.removeEventListener('annotationChanged', onAnnotationChanged, null, activeDocumentViewerKey);
+ };
+ }, [annotation, activeDocumentViewerKey]);
+
+ useEffect(() => {
+ if (!isNotesPanelOpenOrActive && annotation && inlineCommentFilter(annotation)) {
+ dispatch(actions.openElement(DataElements.INLINE_COMMENT_POPUP));
+ }
+ }, [annotation, inlineCommentFilter]);
+
+ return { annotation, closeAndReset };
+}
\ No newline at end of file
diff --git a/src/hooks/useOnLinkAnnotationPopupOpen/index.js b/src/hooks/useOnLinkAnnotationPopupOpen/index.js
new file mode 100644
index 0000000000..3dcf1661e8
--- /dev/null
+++ b/src/hooks/useOnLinkAnnotationPopupOpen/index.js
@@ -0,0 +1,3 @@
+import useOnLinkAnnotationPopupOpen from './useOnLinkAnnotationPopupOpen';
+
+export default useOnLinkAnnotationPopupOpen;
\ No newline at end of file
diff --git a/src/hooks/useOnLinkAnnotationPopupOpen/useOnLinkAnnotationPopupOpen.spec.js b/src/hooks/useOnLinkAnnotationPopupOpen/useOnLinkAnnotationPopupOpen.spec.js
new file mode 100644
index 0000000000..7b8237f80e
--- /dev/null
+++ b/src/hooks/useOnLinkAnnotationPopupOpen/useOnLinkAnnotationPopupOpen.spec.js
@@ -0,0 +1,47 @@
+import React from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import useOnLinkAnnotationPopupOpen from './useOnLinkAnnotationPopupOpen';
+import core from 'core';
+
+jest.mock('core');
+
+const MockComponent = ({ children }) => ({children}
);
+const wrapper = withProviders(MockComponent);
+
+const scrollContainer = {
+ scrollLeft: 100,
+ scrollTop: 100,
+ addEventListener: jest.fn(),
+ removeEventListener: jest.fn(),
+};
+
+describe('useOnLinkAnnotationPopupOpen hook', () => {
+ beforeAll(() => {
+ core.addEventListener = jest.fn();
+ core.getScrollViewElement = jest.fn();
+ core.getScrollViewElement.mockReturnValue(scrollContainer);
+ core.getAnnotationManager = jest.fn().mockReturnValue({
+ getContentEditHistoryManager: jest.fn().mockReturnValue({
+ getAnnotationsByMouseEvent: jest.fn(),
+ }),
+ });
+ });
+
+ it('adds event listeners on mouse move', () => {
+ const { result } = renderHook(() => {
+ return useOnLinkAnnotationPopupOpen();
+ }, { wrapper });
+
+ expect(result.error).toBeUndefined();
+ expect(core.addEventListener).toBeCalledWith('mouseMove', expect.any(Function));
+ });
+
+ it('removes event listeners to mouse move when component is unmounted', () => {
+ const { result, unmount } = renderHook(() => useOnLinkAnnotationPopupOpen(), { wrapper });
+
+ expect(result.error).toBeUndefined();
+ unmount();
+
+ expect(core.removeEventListener).toBeCalledWith('mouseMove', expect.any(Function));
+ });
+});
diff --git a/src/hooks/useOnMeasurementToolOrAnnotationSelected/index.js b/src/hooks/useOnMeasurementToolOrAnnotationSelected/index.js
new file mode 100644
index 0000000000..ec3414a8f9
--- /dev/null
+++ b/src/hooks/useOnMeasurementToolOrAnnotationSelected/index.js
@@ -0,0 +1,3 @@
+import useOnMeasurementToolOrAnnotationSelected from './useOnMeasurementToolOrAnnotationSelected';
+
+export default useOnMeasurementToolOrAnnotationSelected;
\ No newline at end of file
diff --git a/src/hooks/useOnRightClick/index.js b/src/hooks/useOnRightClick/index.js
new file mode 100644
index 0000000000..ce45aa251c
--- /dev/null
+++ b/src/hooks/useOnRightClick/index.js
@@ -0,0 +1,3 @@
+import useOnRightClick from './useOnRightClick';
+
+export default useOnRightClick;
\ No newline at end of file
diff --git a/src/hooks/useOnRightClick/useOnRightClick.js b/src/hooks/useOnRightClick/useOnRightClick.js
new file mode 100644
index 0000000000..f3329aac40
--- /dev/null
+++ b/src/hooks/useOnRightClick/useOnRightClick.js
@@ -0,0 +1,53 @@
+import { useEffect } from 'react';
+import core from 'core';
+import getRootNode from 'helpers/getRootNode';
+import { shallowEqual, useSelector } from 'react-redux';
+import selectors from 'selectors';
+
+export default (handler) => {
+ const [
+ activeDocumentViewerKey,
+ isMultiViewerMode,
+ ] = useSelector(
+ (state) => [
+ selectors.getActiveDocumentViewerKey(state),
+ selectors.isMultiViewerMode(state),
+ ],
+ shallowEqual,
+ );
+
+ useEffect(() => {
+ const listener = (e) => {
+ const { tagName } = e.target;
+ const clickedOnInput = tagName === 'INPUT';
+ const clickedOnTextarea = tagName === 'TEXTAREA';
+ const clickedOnFreeTextarea = !!((
+ e.target.className === 'ql-editor'
+ || e.target.parentNode.className === 'ql-editor'
+ || e.target.parentNode.parentNode.className === 'ql-editor'
+ ));
+
+ const documentContainer =
+ isMultiViewerMode
+ ? getRootNode().querySelector(`#DocumentContainer${activeDocumentViewerKey}`)
+ : getRootNode().querySelector('.DocumentContainer');
+ const clickedOnDocumentContainer = documentContainer.contains(e.target);
+
+ if (
+ clickedOnDocumentContainer &&
+ // when clicking on these two elements we want to display the default context menu so that users can use auto-correction, look up dictionary, etc...
+ !(clickedOnInput || clickedOnTextarea || clickedOnFreeTextarea)
+ ) {
+ e.preventDefault();
+ handler(e);
+ }
+ };
+
+ getRootNode().addEventListener('contextmenu', listener);
+ core.addEventListener('longTap', listener, null, activeDocumentViewerKey);
+ return () => {
+ getRootNode().removeEventListener('contextmenu', listener);
+ core.removeEventListener('longTap', listener, null, activeDocumentViewerKey);
+ };
+ }, [handler, activeDocumentViewerKey, isMultiViewerMode]);
+};
diff --git a/src/hooks/useOnRightClick/useOnRightClick.spec.js b/src/hooks/useOnRightClick/useOnRightClick.spec.js
new file mode 100644
index 0000000000..d1597e94ec
--- /dev/null
+++ b/src/hooks/useOnRightClick/useOnRightClick.spec.js
@@ -0,0 +1,32 @@
+import React from 'react';
+import { renderHook } from '@testing-library/react-hooks';
+import useOnRightClick from './useOnRightClick';
+import core from 'core';
+
+jest.mock('core');
+
+const MockComponent = ({ children }) => ({children}
);
+const wrapper = withProviders(MockComponent);
+
+describe('useOnRightClick hook', () => {
+ it('adds event listeners to longTap on mobile', () => {
+ core.addEventListener = jest.fn();
+
+ const { result } = renderHook(() => useOnRightClick(), { wrapper });
+
+ expect(result.error).toBeUndefined();
+
+ expect(core.addEventListener).toBeCalledWith('longTap', expect.any(Function), null, 1);
+ });
+
+ it('removes event listeners to longTap when component is unmounted', () => {
+ core.removeEventListener = jest.fn();
+
+ const { result, unmount } = renderHook(() => useOnRightClick(), { wrapper });
+
+ expect(result.error).toBeUndefined();
+ unmount();
+
+ expect(core.removeEventListener).toBeCalledWith('longTap', expect.any(Function), null, 1);
+ });
+});
\ No newline at end of file
diff --git a/src/hooks/useOnRightClickAnnotation/index.js b/src/hooks/useOnRightClickAnnotation/index.js
new file mode 100644
index 0000000000..98132ed95f
--- /dev/null
+++ b/src/hooks/useOnRightClickAnnotation/index.js
@@ -0,0 +1,3 @@
+import useOnRightClickAnnotation from './useOnRightClickAnnotation';
+
+export default useOnRightClickAnnotation;
\ No newline at end of file
diff --git a/src/hooks/useOnRightClickAnnotation/useOnRightClickAnnotation.js b/src/hooks/useOnRightClickAnnotation/useOnRightClickAnnotation.js
new file mode 100644
index 0000000000..13b143ca35
--- /dev/null
+++ b/src/hooks/useOnRightClickAnnotation/useOnRightClickAnnotation.js
@@ -0,0 +1,20 @@
+import { useState, useCallback } from 'react';
+import core from 'core';
+import useOnRightClick from 'hooks/useOnRightClick';
+
+export default () => {
+ const [rightClickedAnnotation, setRightClickedAnnotation] = useState(null);
+
+ useOnRightClick(
+ useCallback((e) => {
+ const annotUnderMouse = core.getAnnotationByMouseEvent(e);
+ if (annotUnderMouse && annotUnderMouse.ToolName !== window.Core.Tools.ToolNames.CROP) {
+ if (annotUnderMouse !== rightClickedAnnotation) {
+ setRightClickedAnnotation(annotUnderMouse);
+ }
+ }
+ }, [rightClickedAnnotation])
+ );
+
+ return { rightClickedAnnotation, setRightClickedAnnotation };
+};
diff --git a/src/hooks/useOnSnippingAnnotationChangedOrSelected/index.js b/src/hooks/useOnSnippingAnnotationChangedOrSelected/index.js
new file mode 100644
index 0000000000..30a2f600cb
--- /dev/null
+++ b/src/hooks/useOnSnippingAnnotationChangedOrSelected/index.js
@@ -0,0 +1,3 @@
+import useOnSnippingAnnotationChangedOrSelected from './useOnSnippingAnnotationChangedOrSelected';
+
+export default useOnSnippingAnnotationChangedOrSelected;
\ No newline at end of file
diff --git a/src/hooks/useOnSnippingAnnotationChangedOrSelected/useOnSnippingAnnotationChangedOrSelected.js b/src/hooks/useOnSnippingAnnotationChangedOrSelected/useOnSnippingAnnotationChangedOrSelected.js
new file mode 100644
index 0000000000..cca8767535
--- /dev/null
+++ b/src/hooks/useOnSnippingAnnotationChangedOrSelected/useOnSnippingAnnotationChangedOrSelected.js
@@ -0,0 +1,37 @@
+import { useEffect, useState } from 'react';
+import core from 'core';
+
+export default function useOnCropAnnotationChangedOrSelected(openSnippingPopup) {
+ const [snippingAnnotation, setSnippingAnnotation] = useState(null);
+
+ useEffect(() => {
+ const onAnnotationChanged = (annotations, action) => {
+ const annotation = annotations[0];
+ if (action === 'add' && annotation.ToolName === window.Core.Tools.ToolNames['SNIPPING'] && annotation instanceof window.Core.Annotations.RectangleAnnotation) {
+ setSnippingAnnotation(annotation);
+ openSnippingPopup();
+ }
+ if (action === 'delete' && annotation.ToolName === window.Core.Tools.ToolNames['SNIPPING'] && annotation instanceof window.Core.Annotations.RectangleAnnotation) {
+ setSnippingAnnotation(null);
+ }
+ };
+
+ const onAnnotationSelected = (annotations, action) => {
+ const annotation = annotations[0];
+ if (action === 'selected' && annotation.ToolName === window.Core.Tools.ToolNames['SNIPPING'] && annotation instanceof window.Core.Annotations.RectangleAnnotation) {
+ setSnippingAnnotation(annotation);
+ openSnippingPopup();
+ }
+ };
+
+ core.addEventListener('annotationChanged', onAnnotationChanged);
+ core.addEventListener('annotationSelected', onAnnotationSelected);
+
+ return () => {
+ core.removeEventListener('annotationChanged', onAnnotationChanged);
+ core.removeEventListener('annotationSelected', onAnnotationSelected);
+ };
+ }, []);
+
+ return snippingAnnotation;
+}
diff --git a/src/hooks/useResizeObserver/index.js b/src/hooks/useResizeObserver/index.js
new file mode 100644
index 0000000000..4fa6b2d3ef
--- /dev/null
+++ b/src/hooks/useResizeObserver/index.js
@@ -0,0 +1,3 @@
+import useResizeObserver from './useResizeObserver';
+
+export default useResizeObserver;
\ No newline at end of file
diff --git a/src/hooks/useResizeObserver/useResizeObserver.js b/src/hooks/useResizeObserver/useResizeObserver.js
new file mode 100644
index 0000000000..ba446181da
--- /dev/null
+++ b/src/hooks/useResizeObserver/useResizeObserver.js
@@ -0,0 +1,34 @@
+import { useEffect, useRef, useState } from 'react';
+
+const useResizeObserver = () => {
+ const [dimensions, setDimensions] = useState({ width: null, height: null });
+ const elementRef = useRef(null);
+
+ useEffect(() => {
+ const node = elementRef.current;
+
+ if (node) {
+ const observer = new ResizeObserver((entries) => {
+ for (const entry of entries) {
+ const observedWidth = entry.borderBoxSize[0].inlineSize;
+ const observedHeight = entry.borderBoxSize[0].blockSize;
+ setDimensions({
+ width: observedWidth,
+ height: observedHeight
+ });
+ }
+ });
+
+ observer.observe(node);
+
+ // Cleanup: stop observing when the component unmounts
+ return () => {
+ observer.unobserve(node);
+ };
+ }
+ }, [elementRef.current]);
+
+ return [elementRef, dimensions];
+};
+
+export default useResizeObserver;
diff --git a/src/hooks/useResizeObserver/useResizeObserver.spec.js b/src/hooks/useResizeObserver/useResizeObserver.spec.js
new file mode 100644
index 0000000000..5f52f85886
--- /dev/null
+++ b/src/hooks/useResizeObserver/useResizeObserver.spec.js
@@ -0,0 +1,32 @@
+import { act } from '@testing-library/react-hooks';
+import { render } from '@testing-library/react';
+import React from 'react';
+import useResizeObserver from './useResizeObserver';
+
+// Mocking ResizeObserver as it's not available in Jest's JSDOM
+global.ResizeObserver = class ResizeObserver {
+ constructor(callback) {
+ this.callback = callback;
+ }
+ observe() {
+ this.callback([{ borderBoxSize: [{ inlineSize: 100, blockSize: 200 }] }]);
+ }
+ unobserve() { }
+};
+
+describe('useResizeObserver', () => {
+ it('returns correct dimensions after observing', () => {
+ let dimensions;
+ function DummyComponent() {
+ const [ref, size] = useResizeObserver();
+ dimensions = size;
+ return ;
+ }
+
+ act(() => {
+ render();
+ });
+
+ expect(dimensions).toEqual({ width: 100, height: 200 });
+ });
+});
\ No newline at end of file
diff --git a/src/redux/reducers/digitalSignatureValidationReducer.js b/src/redux/reducers/digitalSignatureValidationReducer.js
new file mode 100644
index 0000000000..08369bc8d7
--- /dev/null
+++ b/src/redux/reducers/digitalSignatureValidationReducer.js
@@ -0,0 +1,46 @@
+export default (initialState) => (state = initialState, action) => {
+ const { type, payload } = action;
+
+ switch (type) {
+ case 'SET_VALIDATION_MODAL_WIDGET_NAME':
+ return {
+ ...state,
+ validationModalWidgetName: payload.validationModalWidgetName,
+ };
+ case 'SET_VERIFICATION_RESULT':
+ return { ...state, verificationResult: payload.result };
+ case 'ADD_TRUSTED_CERTIFICATES':
+ /**
+ * To mimic the behavior of the Core implementation, where certificates
+ * can only be added but not removed, only allow this action to append
+ * to the existing array
+ */
+ return {
+ ...state,
+ certificates: [...state.certificates, ...payload.certificates],
+ };
+ case 'ADD_TRUST_LIST':
+ /**
+ * The Core implementation only allows a single Trust List to be passed
+ * as a parameter, but in order to allow flexibility of future potential
+ * requirements where a developer may want to add multiple Trust Lists,
+ * we are storing an Array of Trust Lists
+ */
+ return {
+ ...state,
+ trustLists: [...state.trustLists, payload.trustList],
+ };
+ case 'SET_IS_REVOCATION_CHECKING_ENABLED':
+ return {
+ ...state,
+ isRevocationCheckingEnabled: payload.isRevocationCheckingEnabled,
+ };
+ case 'SET_REVOCATION_PROXY_PREFIX':
+ return {
+ ...state,
+ revocationProxyPrefix: payload.revocationProxyPrefix,
+ };
+ default:
+ return state;
+ }
+};
diff --git a/src/redux/reducers/featureFlagsReducer.js b/src/redux/reducers/featureFlagsReducer.js
new file mode 100644
index 0000000000..af3ad53f08
--- /dev/null
+++ b/src/redux/reducers/featureFlagsReducer.js
@@ -0,0 +1,12 @@
+export default (initialState) => (state = initialState, action) => {
+ const { type, payload } = action;
+
+ switch (type) {
+ case 'ENABLE_FEATURE_FLAG':
+ return { ...state, [payload.featureFlag]: true };
+ case 'DISABLE_FEATURE_FLAG':
+ return { ...state, [payload.featureFlag]: false };
+ default:
+ return state;
+ }
+};
\ No newline at end of file
diff --git a/src/redux/reducers/wv3dPropertiesPanelReducer.js b/src/redux/reducers/wv3dPropertiesPanelReducer.js
new file mode 100644
index 0000000000..2ae898d19d
--- /dev/null
+++ b/src/redux/reducers/wv3dPropertiesPanelReducer.js
@@ -0,0 +1,26 @@
+export default (initialState) => (state = initialState, action) => {
+ const { type, payload } = action;
+
+ switch (type) {
+ case 'SET_WV3D_PROPERTIES_PANEL_MODEL_DATA': {
+ const { modelData } = payload;
+
+ return {
+ ...state,
+ modelData,
+ };
+ }
+
+ case 'SET_WV3D_PROPERTIES_PANEL_SCHEMA': {
+ const { schema } = payload;
+
+ return {
+ ...state,
+ schema,
+ };
+ }
+
+ default:
+ return state;
+ }
+};
diff --git a/webpack.config.dev.js b/webpack.config.dev.js
index aca52ad8b1..faff432e65 100644
--- a/webpack.config.dev.js
+++ b/webpack.config.dev.js
@@ -6,7 +6,7 @@ module.exports = {
mode: 'development',
devtool: 'cheap-module-eval-source-map',
entry: [
- 'webpack-hot-middleware/client?name=ui&path=/__webpack_hmr',
+ 'webpack-hot-middleware/client?name=ui&path=/__webpack_hmr&noInfo=true',
path.resolve(__dirname, 'src'),
],
output: {
@@ -57,29 +57,53 @@ module.exports = {
loader: 'style-loader',
options: {
insert: function (styleTag) {
- const webComponents = document.getElementsByTagName('apryse-webviewer');
- if (webComponents.length > 0) {
- const clonedStyleTags = [];
- for (let i = 0; i < webComponents.length; i++) {
- const webComponent = webComponents[i];
- if (i === 0) {
- webComponent.shadowRoot.appendChild(styleTag);
- styleTag.onload = function () {
- if (clonedStyleTags.length > 0) {
- clonedStyleTags.forEach((styleNode) => {
- // eslint-disable-next-line no-unsanitized/property
- styleNode.innerHTML = styleTag.innerHTML;
- });
- }
- };
- } else {
- const styleNode = styleTag.cloneNode(true);
- webComponent.shadowRoot.appendChild(styleNode);
- clonedStyleTags.push(styleNode);
+ function findNestedWebComponents(tagName, root = document) {
+ const elements = [];
+
+ // Check direct children
+ root.querySelectorAll(tagName).forEach(el => elements.push(el));
+
+ // Check shadow DOMs
+ root.querySelectorAll('*').forEach(el => {
+ if (el.shadowRoot) {
+ elements.push(...findNestedWebComponents(tagName, el.shadowRoot));
}
- }
- } else {
+ });
+
+ return elements;
+ }
+ // If its the iframe we just append to the document head
+ if (!window.isApryseWebViewerWebComponent) {
document.head.appendChild(styleTag);
+ return;
+ }
+
+ let webComponents;
+ // First we see if the webcomponent is at the document level
+ webComponents = document.getElementsByTagName('apryse-webviewer');
+ // If not, we check have to check if it is nested in another webcomponent
+ if (!webComponents.length) {
+ webComponents = findNestedWebComponents('apryse-webviewer');
+ }
+ // Now we append the style tag to each webcomponent
+ const clonedStyleTags = [];
+ for (let i = 0; i < webComponents.length; i++) {
+ const webComponent = webComponents[i];
+ if (i === 0) {
+ webComponent.shadowRoot.appendChild(styleTag);
+ styleTag.onload = function () {
+ if (clonedStyleTags.length > 0) {
+ clonedStyleTags.forEach((styleNode) => {
+ // eslint-disable-next-line no-unsanitized/property
+ styleNode.innerHTML = styleTag.innerHTML;
+ });
+ }
+ };
+ } else {
+ const styleNode = styleTag.cloneNode(true);
+ webComponent.shadowRoot.appendChild(styleNode);
+ clonedStyleTags.push(styleNode);
+ }
}
},
},
diff --git a/webpack.config.prod.js b/webpack.config.prod.js
index 8c112a59c9..96784a33e6 100644
--- a/webpack.config.prod.js
+++ b/webpack.config.prod.js
@@ -1,7 +1,7 @@
const path = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
// const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-const MiniCssExtractPlugin = require('mini-css-extract-plugin');
+// const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
mode: 'production',
@@ -32,10 +32,10 @@ module.exports = {
to: '../build/configorigin.txt',
},
]),
- new MiniCssExtractPlugin({
- filename: 'style.css',
- chunkFilename: 'chunks/[name].chunk.css'
- }),
+ // new MiniCssExtractPlugin({
+ // filename: 'style.css',
+ // chunkFilename: 'chunks/[name].chunk.css'
+ // }),
// new BundleAnalyzerPlugin()
],
module: {
@@ -45,12 +45,16 @@ module.exports = {
use: {
loader: 'babel-loader',
options: {
+ ignore: [
+ /\/core-js/,
+ ],
+ sourceType: "unambiguous",
presets: [
'@babel/preset-react',
[
'@babel/preset-env',
{
- useBuiltIns: 'entry',
+ useBuiltIns: 'usage',
corejs: 3,
},
],
@@ -66,14 +70,67 @@ module.exports = {
},
},
include: [path.resolve(__dirname, 'src'), path.resolve(__dirname, 'node_modules')],
- exclude: function(modulePath) {
+ exclude: function (modulePath) {
return /node_modules/.test(modulePath) && !/node_modules.+react-dnd/.test(modulePath);
}
},
{
test: /\.scss$/,
use: [
- MiniCssExtractPlugin.loader,
+ {
+ loader: 'style-loader',
+ options: {
+ insert: function (styleTag) {
+ function findNestedWebComponents(tagName, root = document) {
+ const elements = [];
+
+ // Check direct children
+ root.querySelectorAll(tagName).forEach(el => elements.push(el));
+
+ // Check shadow DOMs
+ root.querySelectorAll('*').forEach(el => {
+ if (el.shadowRoot) {
+ elements.push(...findNestedWebComponents(tagName, el.shadowRoot));
+ }
+ });
+
+ return elements;
+ }
+ if (!window.isApryseWebViewerWebComponent) {
+ document.head.appendChild(styleTag);
+ return;
+ }
+
+ let webComponents;
+ // First we see if the webcomponent is at the document level
+ webComponents = document.getElementsByTagName('apryse-webviewer');
+ // If not, we check have to check if it is nested in another webcomponent
+ if (!webComponents.length) {
+ webComponents = findNestedWebComponents('apryse-webviewer');
+ }
+ // Now we append the style tag to each webcomponent
+ const clonedStyleTags = [];
+ for (let i = 0; i < webComponents.length; i++) {
+ const webComponent = webComponents[i];
+ if (i === 0) {
+ webComponent.shadowRoot.appendChild(styleTag);
+ styleTag.onload = function () {
+ if (clonedStyleTags.length > 0) {
+ clonedStyleTags.forEach((styleNode) => {
+ // eslint-disable-next-line no-unsanitized/property
+ styleNode.innerHTML = styleTag.innerHTML;
+ });
+ }
+ };
+ } else {
+ const styleNode = styleTag.cloneNode(true);
+ webComponent.shadowRoot.appendChild(styleNode);
+ clonedStyleTags.push(styleNode);
+ }
+ }
+ },
+ },
+ },
'css-loader',
{
loader: 'postcss-loader',