Markdown Editor (study)

単純なエディターを作ってみた。 React使用。

by Masaki Moriwaki

HTML

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.2/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.2/react-dom.js"></script>
<link rel="stylesheet" href="//cdn.materialdesignicons.com/1.3.41/css/materialdesignicons.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.33/browser.js"></script>
<link rel="stylesheet" href="https://cdn.rawgit.com/twbs/bootstrap/v4-dev/dist/css/bootstrap.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/5.0.1/markdown-it.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.js"></script>
<script src="https://facebook.github.io/react/js/jsfiddle-integration-babel.js"></script>

<div id="editor">
    <!-- This element's contents will be replaced with your component. -->
</div>

CSS

.mdi::before {
    font-size: 24px;
    line-height: 14px;
}
.btn .mdi::before {
    position: relative;
    top: 4px;
}
.btn-sm .mdi::before {
    font-size: 18px;
    top: 3px;
}
.mdi-stack {
    position: relative;
    display: inline-block;
    width: 18px;
    height: 18px;
    line-height: 14px;
    vertical-align: middle;
}
.mdi-stack-1x {
    position: absolute;
    left: 0;
    width: 100%;
    text-align: center;
}

html, body, #editor {
    width: 100%;
    height: 100%;
}
.editor {
    width: 100%;
    height: 100%;
    display: flex;
    flex-direction: column;
}
.editor .btn {
    background-color: transparent;
}
.editor .btn:hover,
.editor .btn:active {
    border-color: #ccc;
}
.editor > header {
    padding: 0.5rem;
}
.editor > header > h2 {
    margin: 0;
    padding: 0;
}
.editor > header > h2 > input,
.editor > .viewport > .markdown > textarea {
    width: 100%;
    border: 0;
}
.editor ::-webkit-input-placeholder {
   color: #ccc;
}
.editor :-moz-placeholder { /* Firefox 18- */
   color: #ccc;  
}
.editor ::-moz-placeholder {  /* Firefox 19+ */
   color: #ccc;  
}
.editor :-ms-input-placeholder {  
   color: #ccc;
}
.editor > header {
    border-bottom: 1px solid #aaa;
}
.editor > .viewport {
    flex-grow: 1;
    display: flex;
    flex-direction: row;
}
.editor > .viewport > .markdown {
    flex: 1 1 50%;
    display: flex;
    flex-direction: column;
    border-right: 1px solid #aaa;
}
.editor > .viewport > .preview {
    flex: 1 1 50%;
    display: flex;
    flex-direction: column;
}
.editor > .viewport > .markdown > header {
    padding: 0.5rem;
}
.editor > .viewport > .markdown > textarea {
    flex-grow: 1;
    resize: none;
    overflow-y: auto;
    padding: 0.5rem;
}
.editor > .viewport > .markdown > footer {
    padding: 0.5rem;
    color: #888;
    border-top: 1px solid #aaa;
}
.editor > .viewport > .preview > article {
    flex-grow: 1;
    overflow-y: auto;
    padding: 0.5rem;
}
.editor > .viewport > .preview > footer {
   ...

JavaScript 1.7

const md = window.markdownit({
  html: true,
  xhmlOut: true,
  breaks: true,
  linkify: true
});

class Editor extends React.Component {
  constructor(props) {
    super(props);
    this.preview = _.debounce(this.preview, 1000).bind(this);

    this.bold = this.decorate.bind(this, '**', '**', '太字');
    this.italic = this.decorate.bind(this, '*', '*', '斜体');
    this.underline = this.decorate.bind(this, '<u>', '</u>', '下線');
    this.strikethrough = this.decorate.bind(this, '~~', '~~', '取り消し線');
  }
  preview() {
    const html = md.render(this.refs.markdown.value);
    this.refs.preview.innerHTML = html;
  }
  decorate(pre = '', post = '', altenate = '') {
    const textarea = this.refs.markdown;
    const selectionStart = textarea.selectionStart;
    const selectionEnd = textarea.selectionEnd;
    const oldText = textarea.value;
    const selection = oldText.substring(selectionStart, selectionEnd) || altenate;
    const lineBreak = /([\r\n]+)/;
    textarea.value = oldText.substring(0, selectionStart) + selection.split(lineBreak).reduce((prev, curr) => (lineBreak.test(curr) ? prev + curr : prev + pre + curr + post), '') + oldText.substring(selectionEnd);
    this.preview();
  }
  render() {
    return (
      <div className="editor">
        <header>
          <h2><input type="text" ref="title" placeholder="タイトル" /></h2>
        </header>
        <div className="viewport">
          <div className="markdown">
            <header>
              <button title="太字" type="button" className="btn btn-sm" onClick={this.bold}>
                <i className="mdi mdi-format-bold"></i>
              </button>
              <button title="斜体" type="button" className="btn btn-sm" onClick={this.italic}>
                <i className="mdi mdi-format-italic"></i>
              </button>
              <button title="下線" type="button" className="btn btn-sm" onClick={this.underline}>
                <i className="mdi mdi-format-underline"></i>
              </button>
             ...