Просмотр исходного кода

[draw.io] Implement Save Digram from Page Component.

Koki Oyatsu 6 лет назад
Родитель
Сommit
8cc2767a24

+ 32 - 9
src/client/js/components/Drawio.jsx

@@ -12,6 +12,22 @@ export default class Drawio extends React.Component {
       // viewer.min.js の Resize による Scroll イベントを抑止するために無効化する
       DrawioViewer.useResizeSensor = false;
     }
+
+    this.style = {
+      borderRadius: 3,
+      border: '1px solid #d7d7d7',
+      margin: '20px 0',
+    };
+
+    this.onEdit = this.onEdit.bind(this);
+  }
+
+  onEdit() {
+    if (window.crowi != null) {
+      window.crowi.launchDrawioIFrame('page',
+        this.props.rangeLineNumberOfMarkdown.beginLineNumber,
+        this.props.rangeLineNumberOfMarkdown.endLineNumber);
+    }
   }
 
   componentDidMount() {
@@ -27,14 +43,20 @@ export default class Drawio extends React.Component {
 
   render() {
     return (
-      <div
-        className="drawio"
-        ref={(c) => { this.drawioContainer = c }}
-        onScroll={(event) => {
-          event.preventDefault();
-        }}
-        dangerouslySetInnerHTML={{ __html: this.renderContents() }}
-      >
+      <div className="editable-with-drawio">
+        <button type="button" className="drawio-iframe-trigger btn" onClick={this.onEdit}>
+          <i className="icon-note"></i> Edit
+        </button>
+        <div
+          className="drawio"
+          style={this.style}
+          ref={(c) => { this.drawioContainer = c }}
+          onScroll={(event) => {
+            event.preventDefault();
+          }}
+          dangerouslySetInnerHTML={{ __html: this.renderContents() }}
+        >
+        </div>
       </div>
     );
   }
@@ -43,6 +65,7 @@ export default class Drawio extends React.Component {
 
 Drawio.propTypes = {
   appContainer: PropTypes.object.isRequired,
-  drawioContent: PropTypes.any,
+  drawioContent: PropTypes.any.isRequired,
   isPreview: PropTypes.bool,
+  rangeLineNumberOfMarkdown: PropTypes.object.isRequired,
 };

+ 48 - 0
src/client/js/components/Page.jsx

@@ -11,7 +11,9 @@ import MarkdownTable from '../models/MarkdownTable';
 
 import RevisionRenderer from './Page/RevisionRenderer';
 import HandsontableModal from './PageEditor/HandsontableModal';
+import DrawioIFrame from './PageEditor/DrawioIFrame';
 import mtu from './PageEditor/MarkdownTableUtil';
+import mdu from './PageEditor/MarkdownDrawioUtil';
 
 const logger = loggerFactory('growi:Page');
 
@@ -22,11 +24,13 @@ class Page extends React.Component {
 
     this.state = {
       currentTargetTableArea: null,
+      currentTargetDrawioArea: null,
     };
 
     this.growiRenderer = this.props.appContainer.getRenderer('page');
 
     this.saveHandlerForHandsontableModal = this.saveHandlerForHandsontableModal.bind(this);
+    this.saveHandlerForDrawioIFrame = this.saveHandlerForDrawioIFrame.bind(this);
   }
 
   componentWillMount() {
@@ -45,6 +49,19 @@ class Page extends React.Component {
     this.handsontableModal.show(MarkdownTable.fromMarkdownString(tableLines));
   }
 
+  /**
+   * launch DrawioIFRame with data specified by arguments
+   * @param beginLineNumber
+   * @param endLineNumber
+   */
+  launchDrawioIFrame(beginLineNumber, endLineNumber) {
+    const markdown = this.props.pageContainer.state.markdown;
+    const drawioMarkdownArray = markdown.split(/\r\n|\r|\n/).slice(beginLineNumber, endLineNumber);
+    const drawioData = drawioMarkdownArray.slice(1, drawioMarkdownArray.length - 1).join('\n').trim();
+    this.setState({ currentTargetDrawioArea: { beginLineNumber, endLineNumber } });
+    this.drawioIFrame.show(drawioData);
+  }
+
   async saveHandlerForHandsontableModal(markdownTable) {
     const { pageContainer, editorContainer } = this.props;
     const optionsToSave = editorContainer.getCurrentOptionsToSave();
@@ -75,6 +92,36 @@ class Page extends React.Component {
     }
   }
 
+  async saveHandlerForDrawioIFrame(drawioData) {
+    const { pageContainer, editorContainer } = this.props;
+    const optionsToSave = editorContainer.getCurrentOptionsToSave();
+
+    const newMarkdown = mdu.replaceDrawioInMarkdown(
+      drawioData,
+      this.props.pageContainer.state.markdown,
+      this.state.currentTargetDrawioArea.beginLineNumber,
+      this.state.currentTargetDrawioArea.endLineNumber,
+    );
+
+    try {
+      // disable unsaved warning
+      editorContainer.disableUnsavedWarning();
+
+      // eslint-disable-next-line no-unused-vars
+      const { page, tags } = await pageContainer.save(newMarkdown, optionsToSave);
+      logger.debug('success to save');
+
+      pageContainer.showSuccessToastr();
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      pageContainer.showErrorToastr(error);
+    }
+    finally {
+      this.setState({ currentTargetDrawioArea: null });
+    }
+  }
+
   render() {
     const isMobile = this.props.appContainer.isMobile;
     const { markdown } = this.props.pageContainer.state;
@@ -83,6 +130,7 @@ class Page extends React.Component {
       <div className={isMobile ? 'page-mobile' : ''}>
         <RevisionRenderer growiRenderer={this.growiRenderer} markdown={markdown} />
         <HandsontableModal ref={(c) => { this.handsontableModal = c }} onSave={this.saveHandlerForHandsontableModal} />
+        <DrawioIFrame ref={(c) => { this.drawioIFrame = c }} onSave={this.saveHandlerForDrawioIFrame} />
       </div>
     );
   }

+ 4 - 4
src/client/js/components/PageEditor/CodeMirrorEditor.jsx

@@ -20,7 +20,7 @@ import mtu from './MarkdownTableUtil';
 import mdu from './MarkdownDrawioUtil';
 import HandsontableModal from './HandsontableModal';
 import EditorIcon from './EditorIcon';
-import DrawioModal from './DrawioModal';
+import DrawioIFrame from './DrawioIFrame';
 
 const loadScript = require('simple-load-script');
 const loadCssSync = require('load-css-file');
@@ -651,7 +651,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   showDrawioHandler() {
-    this.drawioModal.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
+    this.drawioIFrame.show(mdu.getMarkdownDrawioMxfile(this.getCodeMirror()));
   }
 
   getNavbarItems() {
@@ -839,8 +839,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
           ref={(c) => { this.handsontableModal = c }}
           onSave={(table) => { return mtu.replaceFocusedMarkdownTableWithEditor(this.getCodeMirror(), table) }}
         />
-        <DrawioModal
-          ref={(c) => { this.drawioModal = c }}
+        <DrawioIFrame
+          ref={(c) => { this.drawioIFrame = c }}
           onSave={(drawioData) => { return mdu.replaceFocusedDrawioWithEditor(this.getCodeMirror(), drawioData) }}
         />
 

+ 95 - 0
src/client/js/components/PageEditor/DrawioIFrame.jsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export default class DrawioIFrame extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      shown: false,
+      drawioMxFile: '',
+      style: {
+        position: 'fixed',
+        zIndex: 9999,
+        top: 0,
+        left: 0,
+        bottom: 0,
+      },
+    };
+
+    this.drawioIFrame = React.createRef();
+
+    this.init = this.init.bind(this);
+    this.cancel = this.cancel.bind(this);
+    this.receiveFromDrawio = this.receiveFromDrawio.bind(this);
+  }
+
+  init(drawioMxFile) {
+    const initDrawioMxFile = drawioMxFile;
+    this.setState(
+      {
+        drawioMxFile: initDrawioMxFile,
+      },
+    );
+  }
+
+  show(drawioMxFile) {
+    this.init(drawioMxFile);
+    const navHeaderHeight = Math.floor(document.querySelector('.navbar-header').getBoundingClientRect().height);
+
+    this.setState({
+      style: Object.assign({}, this.state.style, {
+        top: `${navHeaderHeight}px`,
+        width: document.body.clientWidth,
+        height: document.body.clientHeight - navHeaderHeight,
+      }),
+    });
+
+    window.addEventListener('message', this.receiveFromDrawio);
+    this.setState({ shown: true });
+  }
+
+  hide() {
+    this.setState({
+      shown: false,
+    });
+  }
+
+  cancel() {
+    this.hide();
+  }
+
+  receiveFromDrawio(event) {
+    if (event.data === 'ready') {
+      event.source.postMessage(this.state.drawioMxFile, '*');
+    }
+    else {
+      if (event.data.length > 0) {
+        const parser = new DOMParser();
+        const dom = parser.parseFromString(event.data, 'text/xml');
+        const value = dom.getElementsByTagName('diagram')[0].innerHTML;
+        this.props.onSave(value);
+      }
+
+      window.removeEventListener('message', this.receiveFromDrawio);
+      this.hide();
+    }
+  }
+
+  render() {
+    return (
+      <div>
+        {
+          this.state.shown
+          && <iframe ref={(c) => { this.drawioIFrame = c }} src="https://www.draw.io/?embed=1" style={this.state.style}></iframe>
+        }
+      </div>
+    );
+  }
+
+}
+
+DrawioIFrame.propTypes = {
+  onSave: PropTypes.func,
+};

+ 0 - 111
src/client/js/components/PageEditor/DrawioModal.jsx

@@ -1,111 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import Modal from 'react-bootstrap/es/Modal';
-import Button from 'react-bootstrap/es/Button';
-
-export default class DrawioModal extends React.PureComponent {
-
-  constructor(props) {
-    super(props);
-
-    /*
-     * ## Note ##
-     * Currently, this component try to synchronize the cells data and alignment data of state.markdownTable with these of the HotTable.
-     * However, changes made by the following operations are not synchronized.
-     *
-     * 1. move columns: Alignment changes are synchronized but data changes are not.
-     * 2. move rows: Data changes are not synchronized.
-     * 3. insert columns or rows: Data changes are synchronized but alignment changes are not.
-     * 4. delete columns or rows: Data changes are synchronized but alignment changes are not.
-     *
-     * However, all operations are reflected in the data to be saved because the HotTable data is used when the save method is called.
-     */
-    this.state = {
-      show: false,
-      drawioMxFileOnInit: '',
-      drawioMxFile: '',
-      drawioIFrame: null,
-    };
-
-    this.drawioIFrame = React.createRef();
-
-    this.init = this.init.bind(this);
-    this.cancel = this.cancel.bind(this);
-    this.receiveFromDrawio = this.receiveFromDrawio.bind(this);
-  }
-
-  init(drawioMxFile) {
-    const initDrawioMxFile = drawioMxFile;
-    this.setState(
-      {
-        drawioMxFile: initDrawioMxFile,
-      },
-    );
-  }
-
-  show(drawioMxFile) {
-    this.init(drawioMxFile);
-
-    const iframe = document.createElement('iframe');
-    iframe.src = 'https://www.draw.io/?embed=1&lang=ja&title=FUGA&diagramName=HOGE';
-    iframe.style = 'position: absolute; z-index: 9999; top: 0; left: 0;';
-    iframe.width = document.body.clientWidth;
-    iframe.height = document.body.clientHeight;
-
-    window.addEventListener('message', this.receiveFromDrawio);
-    this.setState({ show: true });
-
-    this.setState(
-      {
-        drawioIFrame: iframe,
-      },
-    );
-
-    document.body.appendChild(iframe);
-  }
-
-  hide() {
-    this.setState({
-      show: false,
-    });
-  }
-
-  cancel() {
-    this.hide();
-  }
-
-  receiveFromDrawio(event) {
-    if (event.data === 'ready') {
-      event.source.postMessage(this.state.drawioMxFile, '*');
-    }
-    else {
-      if (event.data.length > 0) {
-        const parser = new DOMParser();
-        const dom = parser.parseFromString(event.data, 'text/xml');
-        const value = dom.getElementsByTagName('diagram')[0].innerHTML;
-        console.log(value);
-        this.props.onSave(value);
-      }
-
-      // window.removeEventListener('resize', resize);
-      window.removeEventListener('message', this.receiveFromDrawio);
-      document.body.removeChild(this.state.drawioIFrame);
-    }
-  }
-
-  render() {
-    const dialogClassNames = ['handsontable-modal'];
-    if (this.state.isWindowExpanded) {
-      dialogClassNames.push('handsontable-modal-expanded');
-    }
-
-    return (
-      <div />
-    );
-  }
-
-}
-
-DrawioModal.propTypes = {
-  onSave: PropTypes.func,
-};

+ 26 - 0
src/client/js/components/PageEditor/MarkdownDrawioUtil.js

@@ -28,6 +28,32 @@ class MarkdownDrawioUtil {
     editor.getDoc().replaceRange(drawioData.toString(), { line: curPos.line, ch: 0 }, { line: curPos.line, ch: line.length });
   }
 
+  /**
+   * return markdown where the drawioData specified by line number params is replaced to the drawioData specified by drawioData param
+   * @param {string} drawioData
+   * @param {string} markdown
+   * @param beginLineNumber
+   * @param endLineNumber
+   */
+  replaceDrawioInMarkdown(drawioData, markdown, beginLineNumber, endLineNumber) {
+    const splitMarkdown = markdown.split(/\r\n|\r|\n/);
+    const markdownBeforeDrawio = splitMarkdown.slice(0, beginLineNumber);
+    const markdownAfterDrawio = splitMarkdown.slice(endLineNumber);
+
+    let newMarkdown = '';
+    if (markdownBeforeDrawio.length > 0) {
+      newMarkdown += `${markdownBeforeDrawio.join('\n')}\n`;
+      newMarkdown += '::: drawio\n';
+    }
+    newMarkdown += drawioData;
+    if (markdownAfterDrawio.length > 0) {
+      newMarkdown += '\n:::';
+      newMarkdown += `\n${markdownAfterDrawio.join('\n')}`;
+    }
+
+    return newMarkdown;
+  }
+
 }
 
 // singleton pattern

+ 10 - 0
src/client/js/services/AppContainer.js

@@ -294,6 +294,16 @@ export default class AppContainer extends Container {
     targetComponent.launchHandsontableModal(beginLineNumber, endLineNumber);
   }
 
+  launchDrawioIFrame(componentKind, beginLineNumber, endLineNumber) {
+    let targetComponent;
+    switch (componentKind) {
+      case 'page':
+        targetComponent = this.getComponentInstance('Page');
+        break;
+    }
+    targetComponent.launchDrawioIFrame(beginLineNumber, endLineNumber);
+  }
+
   async apiGet(path, params) {
     return this.apiRequest('get', path, { params });
   }

+ 11 - 3
src/client/js/util/interceptor/drawio-interceptor.js

@@ -75,7 +75,14 @@ export class DrawioInterceptor extends BasicInterceptor {
     context.DrawioMap = {};
     Array.from(div.querySelectorAll('.mxgraph')).forEach((element) => {
       const domId = `mxgraph-${this.createRandomStr(8)}`;
-      context.DrawioMap[domId] = element.outerHTML;
+
+      context.DrawioMap[domId] = {
+        rangeLineNumberOfMarkdown: {
+          beginLineNumber: element.parentNode.dataset.beginLineNumberOfMarkdown,
+          endLineNumber: element.parentNode.dataset.endLineNumberOfMarkdown,
+        },
+        contentHtml: element.outerHTML,
+      };
       element.outerHTML = `<div id="${domId}"></div>`;
     });
     context.parsedHTML = div.innerHTML;
@@ -106,13 +113,14 @@ export class DrawioInterceptor extends BasicInterceptor {
   /**
    * @inheritdoc
    */
-  renderReactDOM(drawioContent, elem, isPreview) {
+  renderReactDOM(drawioMapEntry, elem, isPreview) {
     ReactDOM.render(
       // eslint-disable-next-line react/jsx-filename-extension
       <Drawio
         appContainer={this.appContainer}
-        drawioContent={drawioContent}
+        drawioContent={drawioMapEntry.contentHtml}
         isPreview={isPreview}
+        rangeLineNumberOfMarkdown={drawioMapEntry.rangeLineNumberOfMarkdown}
       />,
       elem,
     );

+ 30 - 0
src/client/styles/scss/_page.scss

@@ -8,6 +8,7 @@
    * header
    */
   header {
+
     // the container of h1
     div.title-container {
       padding-right: 5px;
@@ -38,6 +39,7 @@
 
     // change button opacity
     &:hover {
+
       .btn.btn-copy,
       .btn-copy-link,
       .btn.btn-edit,
@@ -114,6 +116,7 @@
 .main-container .main .content-main .revision-history {
   .revision-history-list {
     .revision-history-outer {
+
       // add border-top except of first element
       &:not(:first-of-type) {
         border-top: 1px solid $border;
@@ -206,6 +209,33 @@
   }
 }
 
+/**
+ * for drawio with drawio iframe button
+ */
+.editable-with-drawio {
+  position: relative;
+
+  .drawio-iframe-trigger {
+    position: absolute;
+    top: 11px;
+    right: 10px;
+    z-index: 999;
+    font-size: 16px;
+    line-height: 1;
+    color: $linktext;
+    vertical-align: bottom;
+    background-color: transparent;
+    border: 1px solid $linktext;
+    border-radius: 3px;
+    opacity: 1;
+
+    &:hover {
+      color: $white;
+      background-color: $linktext;
+    }
+  }
+}
+
 /*
  * for Presentation
  */

+ 3 - 3
yarn.lock

@@ -8332,9 +8332,9 @@ markdown-it-blockdiag@^1.0.2:
     utf8-bytes "0.0.1"
 
 markdown-it-drawio-viewer@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.1.1.tgz#3fb622060ff76c21a63e9b4310c98a5c61942422"
-  integrity sha512-+PTPfOQlqHm3bpZgweADJ/KEhXBQFxqEYkYUkLyR2GRQ9iPbrbOmCVhTFsvMhpSEIwRkDflyYAfO4YsmIKioZg==
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/markdown-it-drawio-viewer/-/markdown-it-drawio-viewer-1.1.2.tgz#4ae442545d32bd44bb9f8ca18085315cea1ec692"
+  integrity sha512-YD+wZ4SDipZf1+TeHcUDWhGqN+FaiA+77be1flcfCli1a4cGy3fo8r2QnPRAn+rVEhTpMB6cQsUS5I0KxwHFYQ==
   dependencies:
     markdown-it-fence "0.1.3"
     xmldoc "^1.1.2"