survey-creator icon indicating copy to clipboard operation
survey-creator copied to clipboard

Markdown Support in SurveyJS Creator - Implement a custom inplace editor / Add custom help text to inplace editors

Open JaneSjs opened this issue 1 year ago • 9 comments

  • 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

JaneSjs avatar Sep 09 '24 09:09 JaneSjs

+1 https://github.com/surveyjs/survey-creator/issues/6081

JaneSjs avatar Nov 15 '24 07:11 JaneSjs

T21262 - Adding Information when using Markdown in Editor https://surveyjs.answerdesk.io/internal/ticket/details/T21262

JaneSjs avatar Dec 30 '24 12:12 JaneSjs

+1 T22936 - for HTML questionType how can I use WYSIWYG HTML editor https://surveyjs.answerdesk.io/internal/ticket/details/T22936

JaneSjs avatar Apr 17 '25 16:04 JaneSjs

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

tsv2013 avatar Apr 24 '25 13:04 tsv2013

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;
}

tsv2013 avatar May 07 '25 12:05 tsv2013

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 />);

tsv2013 avatar May 07 '25 13:05 tsv2013

Researching the issue: https://github.com/surveyjs/survey-creator/issues/6904

JaneSjs avatar May 20 '25 08:05 JaneSjs

Issues when using the RTF Editor as an inplace editor & property grid editor: https://github.com/surveyjs/service/issues/3195.

JaneSjs avatar Jun 04 '25 11:06 JaneSjs

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. Image

It is required to wrap a custom adorner with another base component which renders these actions.

JaneSjs avatar Jun 17 '25 15:06 JaneSjs

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. Image
  • The PG editor overlaps other property grid editors. Image

JaneSjs avatar Sep 11 '25 08:09 JaneSjs