diff --git a/package.json b/package.json index ff7828f9d7..eeb12ba182 100644 --- a/package.json +++ b/package.json @@ -171,6 +171,7 @@ "draft-js-side-toolbar-plugin": "3.0.1", "draftjs-to-html": "0.8.4", "element-closest": "2.0.2", + "embed-video": "2.0.4", "es6-object-assign": "1.1.0", "es6-promise": "2.3.0", "eventlistener": "0.0.1", diff --git a/web/client/components/mapviews/settings/CompactRichTextEditor.jsx b/web/client/components/mapviews/settings/CompactRichTextEditor.jsx new file mode 100644 index 0000000000..5f3acd59bd --- /dev/null +++ b/web/client/components/mapviews/settings/CompactRichTextEditor.jsx @@ -0,0 +1,126 @@ +/* + * Copyright 2022, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; +import 'react-draft-wysiwyg/dist/react-draft-wysiwyg.css'; +import { Editor } from 'react-draft-wysiwyg'; +import embed from 'embed-video'; +import { DEFAULT_FONT_FAMILIES } from '../../../utils/GeoStoryUtils'; + +export const resizeBase64Image = (src, options) => { + return new Promise((resolve, reject) => { + const { + size, + type = 'image/png', + quality = 0.9 + } = options || {}; + const img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = () => { + const { naturalWidth, naturalHeight } = img; + const imgResolution = naturalWidth / naturalHeight; + const width = size; + const height = size / imgResolution; + const canvas = document.createElement('canvas'); + canvas.setAttribute('width', width); + canvas.setAttribute('height', height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0, width, height); + const dataURL = canvas.toDataURL(type, quality); + resolve(dataURL); + }; + img.onerror = (error) => { + reject(error); + }; + img.src = src; + }); +}; + +function CompactRichTextEditor({ + wrapperClassName = 'ms-compact-text-editor', + toolbarOptions, + ...props +}) { + + return ( + <Editor + {...props} + editorStyle={{ minHeight: 200 }} + wrapperClassName={wrapperClassName} + toolbar={{ + options: toolbarOptions || ['fontFamily', 'blockType', 'inline', 'textAlign', 'list', 'link', 'colorPicker', 'remove', 'image', 'embedded'], + image: { + urlEnabled: true, + // upload controlled via props, disabled by default + uploadEnabled: props.uploadEnabled || false, + alignmentEnabled: false, + uploadCallback: (file) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.addEventListener('load', () => { + resizeBase64Image(reader.result, { + size: 500, + type: 'image/jpeg', + quality: 0.8 + }).then((linkBase64) => { + resolve({ data: { link: linkBase64 } }); + }); + }); + if (file) { + reader.readAsDataURL(file); + } else { + reject(); + } + }), + previewImage: true, + inputAccept: 'image/gif,image/jpeg,image/jpg,image/png,image/svg', + alt: props.alt || { present: false, mandatory: false }, + defaultSize: { + height: 'auto', + width: '100%' + } + }, + fontFamily: { + // Setup fonts via props or use default from GeoStories + options: props.fonts || DEFAULT_FONT_FAMILIES + }, + link: { + inDropdown: false, + showOpenOptionOnHover: true, + defaultTargetOption: '_self', + options: ['link', 'unlink'] + }, + blockType: { + inDropdown: true, + options: ['Normal', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'Blockquote', 'Code'] + }, + inline: { + inDropdown: true, + options: ['bold', 'italic', 'underline', 'strikethrough', 'monospace'] + }, + textAlign: { + inDropdown: true + }, + list: { + inDropdown: true + }, + embedded: { + embedCallback: link => { + const detectedSrc = /<iframe.*? src="(.*?)"/.exec(embed(link)); + return (detectedSrc && detectedSrc[1]) || link; + }, + defaultSize: { + height: 'auto', + width: '100%' + } + } + }} + /> + ); +} + +export default CompactRichTextEditor; diff --git a/web/client/components/widgets/builder/wizard/text/TextOptions.jsx b/web/client/components/widgets/builder/wizard/text/TextOptions.jsx index 3300abe79a..f88bec1a0c 100644 --- a/web/client/components/widgets/builder/wizard/text/TextOptions.jsx +++ b/web/client/components/widgets/builder/wizard/text/TextOptions.jsx @@ -1,4 +1,4 @@ -/* +/** * Copyright 2018, GeoSolutions Sas. * All rights reserved. * @@ -6,34 +6,64 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { Col, Form, FormControl, FormGroup } from 'react-bootstrap'; -import ReactQuill from '../../../../../libs/quill/react-quill-suspense'; +import React, { useState } from "react"; +import { Col, Form, FormControl, FormGroup } from "react-bootstrap"; +import localizedProps from "../../../../misc/enhancers/localizedProps"; +import { + htmlToDraftJSEditorState, + draftJSEditorStateToHtml +} from "../../../../../utils/EditorUtils"; -import localizedProps from '../../../../misc/enhancers/localizedProps'; +import withDebounceOnCallback from "../../../../misc/enhancers/withDebounceOnCallback"; +import CompactRichTextEditor from "../../../../mapviews/settings/CompactRichTextEditor"; const TitleInput = localizedProps("placeholder")(FormControl); +const DescriptorEditor = withDebounceOnCallback( + "onEditorStateChange", + "editorState" +)(CompactRichTextEditor); -const Editor = localizedProps("placeholder")(ReactQuill); - -export default ({ data = {}, onChange = () => { }}) => ( - <div> - <Col key="form" xs={12}> - <Form> - <FormGroup controlId="title"> - <Col sm={12}> - <TitleInput style={{ marginBottom: 10 }} placeholder="widgets.builder.wizard.titlePlaceholder" value={data.title} type="text" onChange={e => onChange("title", e.target.value)} /> - </Col> - </FormGroup> - </Form> - </Col> - <Editor modules={{ - toolbar: [ - [{'size': ['small', false, 'large', 'huge'] }, 'bold', 'italic', 'underline', 'blockquote'], - [{'list': 'bullet' }, {'align': [] }], - [{'color': [] }, {'background': [] }, 'clean'], ['image', 'link'] - ] - }} placeholder="widgets.builder.wizard.textPlaceholder" value={data && data.text || ''} onChange={(val) => onChange("text", val)} /> - </div> -); +function TextOptions({ data = {}, onChange = () => {} }) { + const [editorState, setEditorState] = useState( + htmlToDraftJSEditorState(data.text || "") + ); + return ( + <div> + <Col key="form" xs={12}> + <Form> + <FormGroup controlId="title"> + <Col sm={12}> + <TitleInput + style={{ marginBottom: 10 }} + placeholder="widgets.builder.wizard.titlePlaceholder" + value={data.title} + type="text" + onChange={(e) => + onChange("title", e.target.value) + } + /> + </Col> + </FormGroup> + </Form> + </Col> + <DescriptorEditor + editorState={editorState} + onEditorStateChange={(newEditorState) => { + const previousHTML = draftJSEditorStateToHtml(editorState); + const newHTML = draftJSEditorStateToHtml(newEditorState); + if (newHTML !== previousHTML) { + onChange( + "text", + draftJSEditorStateToHtml(newEditorState) + ); + setEditorState(newEditorState); + } + }} + // Array of custom or built in fonts can be set via props + // fonts={["Arial", "Impact", "Roman"]} + /> + </div> + ); +} +export default TextOptions; diff --git a/web/client/themes/default/less/wizard.less b/web/client/themes/default/less/wizard.less index f9e742b4ae..b0c092494a 100644 --- a/web/client/themes/default/less/wizard.less +++ b/web/client/themes/default/less/wizard.less @@ -30,7 +30,7 @@ .ms-wizard { - position: absolute; + // position: absolute; width: 100%; .form-horizontal { .form-group {