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

Merge pull request #2452 from weseek/feat/enable-update-link-with-link-editor-modal

Feat/enable update link with link editor modal
yusuketk 5 лет назад
Родитель
Сommit
39c28e74e4

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

@@ -667,7 +667,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
   }
 
   showLinkEditHandler() {
-    this.linkEditModal.current.show(mlu.getSelectedTextInEditor(this.getCodeMirror()));
+    this.linkEditModal.current.show(mlu.getMarkdownLink(this.getCodeMirror()));
   }
 
   showHandsonTableHandler() {

+ 53 - 41
src/client/js/components/PageEditor/LinkEditModal.jsx

@@ -10,11 +10,13 @@ import {
 } from 'reactstrap';
 
 import Preview from './Preview';
-import PagePathAutoComplete from '../PagePathAutoComplete';
 
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 
+import SearchTypeahead from '../SearchTypeahead';
+import Linker from '../../models/Linker';
+
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 class LinkEditModal extends React.PureComponent {
@@ -26,9 +28,9 @@ class LinkEditModal extends React.PureComponent {
       show: false,
       isUseRelativePath: false,
       isUsePermanentLink: false,
-      linkInputValue: '/',
+      linkInputValue: '',
       labelInputValue: '',
-      linkerType: 'mdLink',
+      linkerType: Linker.types.markdownLink,
       markdown: '',
     };
 
@@ -37,9 +39,9 @@ class LinkEditModal extends React.PureComponent {
     this.show = this.show.bind(this);
     this.hide = this.hide.bind(this);
     this.cancel = this.cancel.bind(this);
-    this.inputChangeHandler = this.inputChangeHandler.bind(this);
-    this.submitHandler = this.submitHandler.bind(this);
+    this.handleChangeTypeahead = this.handleChangeTypeahead.bind(this);
     this.handleChangeLabelInput = this.handleChangeLabelInput.bind(this);
+    this.handleChangeLinkInput = this.handleChangeLinkInput.bind(this);
     this.handleSelecteLinkerType = this.handleSelecteLinkerType.bind(this);
     this.toggleIsUseRelativePath = this.toggleIsUseRelativePath.bind(this);
     this.setMarkdown = this.setMarkdown.bind(this);
@@ -56,8 +58,23 @@ class LinkEditModal extends React.PureComponent {
     }
   }
 
-  show(defaultLabelInputValue = '') {
-    this.setState({ show: true, labelInputValue: defaultLabelInputValue });
+  // defaultMarkdownLink is an instance of Linker
+  show(defaultMarkdownLink = null) {
+    // if defaultMarkdownLink is null, set default value in inputs.
+    const { label = '', link = '' } = defaultMarkdownLink;
+    let { type = Linker.types.markdownLink } = defaultMarkdownLink;
+
+    // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
+    if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
+      type = Linker.types.markdownLink;
+    }
+
+    this.setState({
+      show: true,
+      labelInputValue: label,
+      linkInputValue: link,
+      linkerType: type,
+    });
   }
 
   cancel() {
@@ -71,7 +88,7 @@ class LinkEditModal extends React.PureComponent {
   }
 
   toggleIsUseRelativePath() {
-    if (this.state.linkerType === 'growiLink') {
+    if (this.state.linkerType === Linker.types.growiLink) {
       return;
     }
     this.setState({ isUseRelativePath: !this.state.isUseRelativePath });
@@ -91,10 +108,6 @@ class LinkEditModal extends React.PureComponent {
     );
   }
 
-  insertLinkIntoEditor() {
-    // TODO GW-2659
-  }
-
   async setMarkdown(path) {
     let markdown = '';
     try {
@@ -108,12 +121,21 @@ class LinkEditModal extends React.PureComponent {
     this.setState({ markdown });
   }
 
+  handleChangeTypeahead(selected) {
+    const page = selected[0];
+    this.setState({ linkInputValue: page.path });
+  }
+
   handleChangeLabelInput(label) {
     this.setState({ labelInputValue: label });
   }
 
+  handleChangeLinkInput(link) {
+    this.setState({ linkInputValue: link });
+  }
+
   handleSelecteLinkerType(linkerType) {
-    if (this.state.isUseRelativePath && linkerType === 'growiLink') {
+    if (this.state.isUseRelativePath && linkerType === Linker.types.growiLink) {
       this.toggleIsUseRelativePath();
     }
     this.setState({ linkerType });
@@ -129,14 +151,6 @@ class LinkEditModal extends React.PureComponent {
     this.hide();
   }
 
-  inputChangeHandler(inputChangeValue) {
-    this.setState({ linkInputValue: inputChangeValue });
-  }
-
-  submitHandler(submitValue) {
-    this.setState({ linkInputValue: submitValue });
-  }
-
   generateLink() {
     const { pageContainer } = this.props;
     const {
@@ -152,13 +166,13 @@ class LinkEditModal extends React.PureComponent {
       reshapedLink = path.relative(pageContainer.state.path, linkInputValue);
     }
 
-    if (linkerType === 'pukiwikiLink') {
+    if (linkerType === Linker.types.pukiwikiLink) {
       return `[[${labelInputValue}>${reshapedLink}]]`;
     }
-    if (linkerType === 'growiLink') {
+    if (linkerType === Linker.types.growiLink) {
       return `[${reshapedLink}]`;
     }
-    if (linkerType === 'mdLink') {
+    if (linkerType === Linker.types.markdownLink) {
       return `[${labelInputValue}](${reshapedLink})`;
     }
   }
@@ -177,20 +191,18 @@ class LinkEditModal extends React.PureComponent {
                 <div className="form-gorup my-3">
                   <label htmlFor="linkInput">Link</label>
                   <div className="input-group">
-                    <PagePathAutoComplete
-                      onInputChange={this.inputChangeHandler}
-                      onSubmit={this.submitHandler}
+                    <SearchTypeahead
+                      onChange={this.handleChangeTypeahead}
+                      onInputChange={this.handleChangeLinkInput}
+                      inputName="link"
+                      placeholder="Input page path or URL"
+                      keywordOnInit={this.state.linkInputValue}
                     />
                   </div>
                 </div>
                 <div className="form-inline">
                   <div className="custom-control custom-checkbox custom-checkbox-info">
-                    <input
-                      className="custom-control-input"
-                      id="permanentLink"
-                      type="checkbox"
-                      checked={this.state.isUsePamanentLink}
-                    />
+                    <input className="custom-control-input" id="permanentLink" type="checkbox" checked={this.state.isUsePamanentLink} />
                     <label className="custom-control-label" htmlFor="permanentLink" onClick={this.toggleIsUsePamanentLink}>
                       Use permanent link
                     </label>
@@ -208,16 +220,16 @@ class LinkEditModal extends React.PureComponent {
                     <div className="form-group btn-group d-flex" role="group" aria-label="type">
                       <button
                         type="button"
-                        name="mdLink"
-                        className={`btn btn-outline-secondary w-100 ${this.state.linkerType === 'mdLink' && 'active'}`}
+                        name={Linker.types.markdownLink}
+                        className={`btn btn-outline-secondary w-100 ${this.state.linkerType === Linker.types.markdownLink && 'active'}`}
                         onClick={e => this.handleSelecteLinkerType(e.target.name)}
                       >
                         Markdown
                       </button>
                       <button
                         type="button"
-                        name="growiLink"
-                        className={`btn btn-outline-secondary w-100 ${this.state.linkerType === 'growiLink' && 'active'}`}
+                        name={Linker.types.growiLink}
+                        className={`btn btn-outline-secondary w-100 ${this.state.linkerType === Linker.types.growiLink && 'active'}`}
                         onClick={e => this.handleSelecteLinkerType(e.target.name)}
                       >
                         Growi Original
@@ -225,8 +237,8 @@ class LinkEditModal extends React.PureComponent {
                       {this.isApplyPukiwikiLikeLinkerPlugin && (
                         <button
                           type="button"
-                          name="pukiwikiLink"
-                          className={`btn btn-outline-secondary w-100 ${this.state.linkerType === 'pukiwikiLink' && 'active'}`}
+                          name={Linker.types.pukiwikiLink}
+                          className={`btn btn-outline-secondary w-100 ${this.state.linkerType === Linker.types.pukiwikiLink && 'active'}`}
                           onClick={e => this.handleSelecteLinkerType(e.target.name)}
                         >
                           Pukiwiki
@@ -242,7 +254,7 @@ class LinkEditModal extends React.PureComponent {
                         id="label"
                         value={this.state.labelInputValue}
                         onChange={e => this.handleChangeLabelInput(e.target.value)}
-                        disabled={this.state.linkerType === 'growiLink'}
+                        disabled={this.state.linkerType === Linker.types.growiLink}
                       />
                     </div>
                     <div className="form-inline">
@@ -252,7 +264,7 @@ class LinkEditModal extends React.PureComponent {
                           id="relativePath"
                           type="checkbox"
                           checked={this.state.isUseRelativePath}
-                          disabled={this.state.linkerType === 'growiLink'}
+                          disabled={this.state.linkerType === Linker.types.growiLink}
                         />
                         <label className="custom-control-label" htmlFor="relativePath" onClick={this.toggleIsUseRelativePath}>
                           Use relative path

+ 17 - 9
src/client/js/components/PageEditor/MarkdownLinkUtil.js

@@ -1,25 +1,33 @@
+import Linker from '../../models/Linker';
+
 /**
  * Utility for markdown link
  */
 class MarkdownLinkUtil {
 
   constructor() {
-    // https://regex101.com/r/1UuWBJ/8
-    this.linePartOfLink = /(\[+(.*)+\]){1}(\(+(.*)+\)){1}/;
+    this.getMarkdownLink = this.getMarkdownLink.bind(this);
     this.isInLink = this.isInLink.bind(this);
+    this.replaceFocusedMarkdownLinkWithEditor = this.replaceFocusedMarkdownLinkWithEditor.bind(this);
   }
 
-  getSelectedTextInEditor(editor) {
-    return editor.getDoc().getSelection();
-  }
-
-  replaceFocusedMarkdownLinkWithEditor(editor, link) {
-    editor.getDoc().replaceSelection(link);
+  // return an instance of Linker from cursor position or selected text.
+  getMarkdownLink(editor) {
+    if (!this.isInLink(editor)) {
+      return Linker.fromMarkdownString(editor.getDoc().getSelection());
+    }
+    const curPos = editor.getCursor();
+    return Linker.fromLineWithIndex(editor.getDoc().getLine(curPos.line), curPos.ch);
   }
 
   isInLink(editor) {
     const curPos = editor.getCursor();
-    return this.linePartOfLink.test(editor.getDoc().getLine(curPos.line));
+    const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(editor.getDoc().getLine(curPos.line), curPos.ch);
+    return beginningOfLink >= 0 && endOfLink >= 0;
+  }
+
+  replaceFocusedMarkdownLinkWithEditor(editor) {
+    // GW-3023
   }
 
 }

+ 109 - 0
src/client/js/models/Linker.js

@@ -0,0 +1,109 @@
+export default class Linker {
+
+  constructor(type, label, link) {
+    this.type = type;
+    this.label = label;
+    this.link = link;
+    // TODO GW-3074 相対パスを利用しているかの情報も持つようにする
+  }
+
+  static types = {
+    markdownLink: 'mdLink',
+    growiLink: 'growiLink',
+    pukiwikiLink: 'pukiwikiLink',
+  }
+
+  static patterns = {
+    pukiwikiLinkWithLabel: /^\[\[(?<label>.+)>(?<link>.+)\]\]$/, // https://regex101.com/r/2fNmUN/2
+    pukiwikiLinkWithoutLabel: /^\[\[(?<label>.+)\]\]$/, // https://regex101.com/r/S7w5Xu/1
+    growiLink: /^\[(?<label>\/.+)\]$/, // https://regex101.com/r/DJfkYf/3
+    markdownLink: /^\[(?<label>.*)\]\((?<link>.*)\)$/, // https://regex101.com/r/DZCKP3/2
+  }
+
+  // create an instance of Linker from string
+  static fromMarkdownString(str) {
+    // if str doesn't mean a linker, create a link whose label is str
+    let label = str;
+    let link = '';
+    let type = this.types.markdownLink;
+
+    // pukiwiki with separator ">".
+    if (str.match(this.patterns.pukiwikiWithLabel)) {
+      type = this.types.pukiwikiLink;
+      ({ label, link } = str.match(this.patterns.pukiwikiWithLabel).groups);
+    }
+    // pukiwiki without separator ">".
+    else if (str.match(this.patterns.pukiwikiLinkWithoutLabel)) {
+      type = this.types.pukiwikiLink;
+      ({ label } = str.match(this.patterns.pukiwikiLinkWithoutLabel).groups);
+      link = label;
+    }
+    // growi
+    else if (str.match(this.patterns.growiLink)) {
+      type = this.types.growiLink;
+      ({ label } = str.match(this.patterns.growiLink).groups);
+      link = label;
+    }
+    // markdown
+    else if (str.match(this.patterns.markdownLink)) {
+      type = this.types.markdownLink;
+      ({ label, link } = str.match(this.patterns.markdownLink).groups);
+    }
+
+    return new Linker(type, label, link);
+  }
+
+  // create an instance of Linker from text with index
+  static fromLineWithIndex(line, index) {
+    const { beginningOfLink, endOfLink } = this.getBeginningAndEndIndexOfLink(line, index);
+    // if index is in a link, extract it from line
+    let linkStr = '';
+    if (beginningOfLink >= 0 && endOfLink >= 0) {
+      linkStr = line.substring(beginningOfLink, endOfLink);
+    }
+    return this.fromMarkdownString(linkStr);
+  }
+
+  // return beginning and end indexies of link
+  // if index is not in a link, return { beginningOfLink: -1, endOfLink: -1 }
+  static getBeginningAndEndIndexOfLink(line, index) {
+    let beginningOfLink;
+    let endOfLink;
+
+    // pukiwiki link ('[[link]]')
+    [beginningOfLink, endOfLink] = this.getBeginningAndEndIndexWithPrefixAndSuffix(line, index, '[[', ']]');
+
+    // if index is not in a pukiwiki link
+    // growi link ('[/link]')
+    if (beginningOfLink < 0 || endOfLink < 0 || beginningOfLink > index || endOfLink < index) {
+      [beginningOfLink, endOfLink] = this.getBeginningAndEndIndexWithPrefixAndSuffix(line, index, '[/', ']');
+    }
+
+    // and if index is not in a growi link
+    // markdown link ('[label](link)')
+    if (beginningOfLink < 0 || endOfLink < 0 || beginningOfLink > index || endOfLink < index) {
+      [beginningOfLink, endOfLink] = this.getBeginningAndEndIndexWithPrefixAndSuffix(line, index, '[', ')', '](');
+    }
+
+    // and if index is not in a markdown link
+    // return { beginningOfLink: -1, endOfLink: -1 }
+    if (beginningOfLink < 0 || endOfLink < 0 || beginningOfLink > index || endOfLink < index) {
+      [beginningOfLink, endOfLink] = [-1, -1];
+    }
+
+    return { beginningOfLink, endOfLink };
+  }
+
+  // return begin and end indexies as array only when index is between prefix and suffix and link contains containText.
+  static getBeginningAndEndIndexWithPrefixAndSuffix(line, index, prefix, suffix, containText = '') {
+    const beginningIndex = line.lastIndexOf(prefix, index);
+    const IndexOfContainText = line.indexOf(containText, beginningIndex + prefix.length);
+    const endIndex = line.indexOf(suffix, IndexOfContainText + containText.length);
+
+    if (beginningIndex < 0 || IndexOfContainText < 0 || endIndex < 0) {
+      return [-1, -1];
+    }
+    return [beginningIndex, endIndex + suffix.length];
+  }
+
+}