survey-creator
survey-creator copied to clipboard
Markdown Support in SurveyJS Creator - Implement a custom inplace editor / Add custom help text to inplace editors
- A custom text inplace editor T19696 - Communicating the value between question and property https://surveyjs.answerdesk.io/internal/ticket/details/T19696
- Rich Edit: T19547 - Rich Content Editor - Can the output be displayed in Preview? https://surveyjs.answerdesk.io/internal/ticket/details/T19547
- T21022 - Rich Text Support in Creator https://surveyjs.answerdesk.io/internal/ticket/details/T21022
+1 https://github.com/surveyjs/survey-creator/issues/6081
T21262 - Adding Information when using Markdown in Editor https://surveyjs.answerdesk.io/internal/ticket/details/T21262
+1 T22936 - for HTML questionType how can I use WYSIWYG HTML editor https://surveyjs.answerdesk.io/internal/ticket/details/T22936
I've started a plunker with quill - https://plnkr.co/edit/iIjeTLLwKc5eGws8 It still has some issues at this moment:
- string is not updated on design surface
- editor is closed via cross button, but should be closed on lost focus
- multiline text in editor overlaps parent container, but should be scrolled
Updated plunker - https://plnkr.co/edit/0pzc8DKFz8aGJp4p
const converter = markdownit({
html: true
});
class Editor extends React.Component {
constructor (props) {
super(props)
this.quillRef = null; // Quill instance
this.reactQuillRef = null; // ReactQuill component
this.state = { editorHtml: props.html || "" }
}
modules = {
toolbar: [
[{ 'header': '1'}, {'header': '2'}, { 'font': [] }],
[{size: []}],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[{'list': 'ordered'}, {'list': 'bullet'},
{'indent': '-1'}, {'indent': '+1'}],
['link', 'image', 'video'],
['clean']
],
clipboard: {
matchVisual: false,
}
}
formats = [
'header', 'font', 'size',
'bold', 'italic', 'underline', 'strike', 'blockquote',
'list', 'bullet', 'indent',
'link', 'image', 'video'
]
handleChange = (html) => {
// this.setState({ editorHtml: html });
this.props.onChange && this.props.onChange(html);
}
componentDidMount() {
this.attachQuillRefs();
}
componentDidUpdate() {
this.attachQuillRefs();
}
attachQuillRefs = () => {
if (typeof this.reactQuillRef.getEditor !== 'function') return;
this.quillRef = this.reactQuillRef.getEditor();
this.quillRef.focus();
this.quillRef.setSelection(0, Number.MAX_VALUE);
}
onClose = (previousRange, source, editor) => {
this.props.onClose && this.props.onClose();
}
render () {
return (
<div className="svc-quill-inplace-editor-container">
<div className="svc-quill-inplace-editor">
<ReactQuill
ref={(el) => {
this.reactQuillRef = el;
}}
style={{ height: "100%" }}
theme={'snow'}
onChange={this.handleChange}
value={this.state.editorHtml}
modules={this.modules}
formats={this.formats}
placeholder={this.props.placeholder}
/>
</div>
<div className="svc-quill-inplace-editor-close-button" onClick={this.onClose}>X</div>
</div>
)
}
}
let activeEditorComponent = null;
class SurveyCustomStringEditor extends React.Component {
constructor(props) {
super(props);
this.state = { changed: 0, isEditing: false };
this.edit = (event) => {
this.setState({ isEditing: true });
}
this.done = (event) => {
this.setState({ isEditing: false });
activeEditorComponent = null;
}
this.onTextChanged = (html) => {
if(this.locString.text != html) {
this.locString.text = html;
this.locString.onStrChanged();
}
}
}
get locString() {
return this.props.locStr.locStr;
}
get creator() {
return this.props.locStr.creator;
}
get placeholder() {
let locPlaceholder = this.locString.placeholder;
if (typeof locPlaceholder === "function") {
locPlaceholder = locPlaceholder();
}
return locPlaceholder ? SurveyCreatorCore.editorLocalization.getString(locPlaceholder) : "";
}
render() {
if (!this.locString) {
return null;
}
if(this.state.isEditing) {
if(activeEditorComponent && activeEditorComponent != this) {
activeEditorComponent.done();
}
activeEditorComponent = this;
}
return (
<span className="svc-string-editor">
<span className="svc-string-editor__content" onClick={this.edit}>
<div className="svc-string-editor__border svc-string-editor__border--hover"
></div>
{this.locString.text ? <SurveyReact.SurveyLocStringViewer locStr={this.locString} />
: <span className="sv-string-editor"
aria-placeholder={this.placeholder}
dangerouslySetInnerHTML={{__html: this.locString.renderedHtml}}
></span>
}
</span>
{ this.state.isEditing ? <Editor placeholder={this.placeholder} html={this.locString.renderedHtml} onChange={this.onTextChanged} onClose={this.done}/> : null }
</span>
);
}
}
SurveyReact.ReactElementFactory.Instance.registerElement("svc-custom-inplace-string-editor",
(props) => {
return React.createElement(SurveyCustomStringEditor, props);
}
);
const creator = new SurveyCreator.SurveyCreator();
creator.onSurveyInstanceCreated.add((_, options) => {
const survey = options.survey;
if (options.reason === "designer") {
const prevGetRendererForString = survey.getRendererForString;
survey.getRendererForString = (element, name) => {
if (!creator.readOnly && SurveyCreatorCore.isStringEditable(element, name)) {
return "svc-custom-inplace-string-editor";
}
return undefined;
};
survey.onTextMarkdown.add((_1, opts) => {
let str = converter.renderInline(opts.text);
opts.html = str;
});
}
});
creator.JSON = surveyJSON;
function SurveyCreatorRenderComponent() {
return (<SurveyCreator.SurveyCreatorComponent creator={creator} />);
}
const root = ReactDOM.createRoot(document.getElementById("surveyCreatorContainer"));
root.render(<SurveyCreatorRenderComponent />);
.svc-string-editor {
position: relative;
}
.svc-quill-inplace-editor-container {
position: absolute;
top: 20px;
left: 20px;
width: 50vw;
height: 20vh;
background-color: white;
z-index: 100;
}
.svc-quill-inplace-editor {
height: 100%;
}
.svc-quill-inplace-editor-close-button {
position: absolute;
right: -28px;
top: 0px;
width: 24px;
height: 24px;
line-height: 24px;
color: black;
font-size: 16px;
cursor: pointer;
}
.svc-quill-inplace-editor-close-button:hover {
font-weight: bold;
}
.ql-container {
background: #fefcfc;
}
.ql-snow.ql-toolbar {
display: block;
background: #eaecec;
}
.ql-snow .ql-picker {
line-height: 14px;
}
One more example - HTML question design surface editor - https://plnkr.co/edit/BCymRvtWqX72bNpN
class Editor extends React.Component {
constructor (props) {
super(props)
this.quillRef = null; // Quill instance
this.reactQuillRef = null; // ReactQuill component
this.state = { editorHtml: props.html || "" }
}
modules = {
toolbar: [
[{ 'header': '1'}, {'header': '2'}, { 'font': [] }],
[{size: []}],
['bold', 'italic', 'underline', 'strike', 'blockquote'],
[{'list': 'ordered'}, {'list': 'bullet'},
{'indent': '-1'}, {'indent': '+1'}],
['link', 'image', 'video'],
['clean']
],
clipboard: {
matchVisual: false,
}
}
formats = [
'header', 'font', 'size',
'bold', 'italic', 'underline', 'strike', 'blockquote',
'list', 'bullet', 'indent',
'link', 'image', 'video'
]
handleChange = (html) => {
this.setState({ editorHtml: html });
this.props.onChange && this.props.onChange(html);
}
componentDidMount() {
this.attachQuillRefs();
}
componentDidUpdate() {
this.attachQuillRefs();
}
attachQuillRefs = () => {
if (typeof this.reactQuillRef.getEditor !== 'function') return;
this.quillRef = this.reactQuillRef.getEditor();
}
render () {
return (
<div className="svc-quill-editor">
<ReactQuill
ref={(el) => {
this.reactQuillRef = el;
}}
// style={{ height: "100%" }}
theme={'snow'}
onChange={this.handleChange}
value={this.state.editorHtml}
modules={this.modules}
formats={this.formats}
placeholder={this.props.placeholder}
/>
</div>
)
}
}
class QuestionHtmlAdornerComponent extends SurveyCreator.QuestionAdornerComponent {
constructor(props) {
super(props);
this.onHtmlChanged = (html) => {
if(this.question.html != html) {
this.question.html = html;
}
}
}
get question() {
return this.model.surveyElement;
}
get placeholder() {
return "Enter HTML content here";
}
// renderElementPlaceholder() {
renderElementContent() {
return (
<div className="svc-question__html-editor--wrapper">
<Editor placeholder={this.placeholder} html={this.question.html} onChange={this.onHtmlChanged}/>
</div>
);
}
}
SurveyReact.ReactElementFactory.Instance.registerElement(
"svc-html-question",
(props) => {
return React.createElement(QuestionHtmlAdornerComponent, props);
}
);
const creator = new SurveyCreator.SurveyCreator();
creator.onSurveyInstanceCreated.add((_, options) => {
const survey = options.survey;
if (options.reason === "designer") {
survey.onElementWrapperComponentName.add((_2, opt) => {
const compName = opt.componentName;
if (opt.wrapperName === "component" && opt.element.getType() == "html") {
opt.componentName = "svc-html-question";
}
opt.componentName = opt.componentName || compName;
});
}
});
creator.JSON = surveyJSON;
function SurveyCreatorRenderComponent() {
return (<SurveyCreator.SurveyCreatorComponent creator={creator} />);
}
const root = ReactDOM.createRoot(document.getElementById("surveyCreatorContainer"));
root.render(<SurveyCreatorRenderComponent />);
Researching the issue: https://github.com/surveyjs/survey-creator/issues/6904
Issues when using the RTF Editor as an inplace editor & property grid editor: https://github.com/surveyjs/service/issues/3195.
Angular Demo: View CodeSandbox Vue Demo: View CodeSandbox
Simply replacing an adorner with a custom component won't work: the Question Type, Duplicate, Remove actions do not appear.
It is required to wrap a custom adorner with another base component which renders these actions.
React Demo for an RTF inplace editor for an HTML question: View Demo.
Issues
- Text entered within a PG editor doesn't immediatelly sync with the inplace editor.
- The PG editor overlaps other property grid editors.