Browse Source

Editor 显示初步完成。

oldj 8 years ago
parent
commit
fd99bfe0af
10 changed files with 10271 additions and 13 deletions
  1. 9842 3
      app/bundle.js
  2. 9 8
      ui/app.js
  3. 50 0
      ui/content/cm_hl.js
  4. 63 0
      ui/content/content.js
  5. 61 0
      ui/content/content.less
  6. 138 0
      ui/content/editor.js
  7. 33 0
      ui/content/editor.less
  8. 72 0
      ui/content/kw.js
  9. 3 1
      ui/package.json
  10. 0 1
      ui/panel/list-item.js

File diff suppressed because it is too large
+ 9842 - 3
app/bundle.js


+ 9 - 8
ui/app.js

@@ -7,7 +7,7 @@
 
 import React from 'react'
 import Panel from './panel/panel'
-//import Content from './content/content'
+import Content from './content/content'
 //import SudoPrompt from './frame/sudo'
 //import EditPrompt from './frame/edit'
 //import PreferencesPrompt from './frame/preferences'
@@ -28,7 +28,8 @@ export default class App extends React.Component {
     Agent.pact('getHosts').then(data => {
       this.setState({
         list: data.list,
-        sys_hosts: data.sys_hosts
+        sys_hosts: data.sys_hosts,
+        current: data.sys_hosts
       })
     })
 
@@ -90,12 +91,12 @@ export default class App extends React.Component {
           setCurrent={this.setCurrent.bind(this)}
           lang={this.state.lang}
         />
-        {/*<Content*/}
-        {/*current={current}*/}
-        {/*readonly={App.isReadOnly(current)}*/}
-        {/*setHostContent={this.setHostContent.bind(this)}*/}
-        {/*lang={this.state.lang}*/}
-        {/*/>*/}
+        <Content
+          current={current}
+          readonly={App.isReadOnly(current)}
+          setHostContent={this.setHostsContent.bind(this)}
+          lang={this.state.lang}
+        />
         {/*<div className="frames">*/}
         {/*<SudoPrompt/>*/}
         {/*<EditPrompt hosts={this.state.hosts}/>*/}

+ 50 - 0
ui/content/cm_hl.js

@@ -0,0 +1,50 @@
+// custom mode
+
+'use strict'
+
+import CodeMirror from 'codemirror'
+
+export default function () {
+
+  CodeMirror.defineMode('hosts', function () {
+    function tokenBase (stream) {
+      if (stream.eatSpace()) return null
+
+      let sol = stream.sol()
+      let ch = stream.next()
+
+      let s = stream.string
+
+      if (ch === '#') {
+        stream.skipToEnd()
+        return 'comment'
+      }
+      if (!s.match(/^\s*([\d\.]+|[\da-f:\.%lo]+)\s+\w/i)) {
+        return 'error'
+      }
+
+      if (sol && ch.match(/[\w\.:%]/)) {
+        stream.eatWhile(/[\w\.:%]/)
+        return 'ip'
+      }
+
+      return null
+    }
+
+    function tokenize (stream, state) {
+      return (state.tokens[0] || tokenBase)(stream, state)
+    }
+
+    return {
+      startState: function () {
+        return {tokens: []}
+      },
+      token: function (stream, state) {
+        return tokenize(stream, state)
+      },
+      lineComment: '#'
+    }
+  })
+
+  //CodeMirror.defineMIME('text/x-host', 'hosts');
+}

+ 63 - 0
ui/content/content.js

@@ -0,0 +1,63 @@
+/**
+ * @author oldj
+ * @blog https://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import classnames from 'classnames'
+import Editor from './editor'
+import './content.less'
+
+export default class Content extends React.Component {
+  constructor (props) {
+    super(props)
+
+    this.state = {
+      is_loading: this.props.current.is_loading
+    }
+  }
+
+  render () {
+    let {current, lang} = this.props
+
+    return (
+      <div id="sh-content">
+        <div className="inform">
+                    <span
+                      className={classnames({
+                        loading: 1,
+                        show: this.state.is_loading
+                      })}
+                    >loading...</span>
+          <i
+            className={classnames({
+              show: current.where === 'remote',
+              iconfont: 1,
+              'icon-earth': 1
+            })}
+            title={lang.remote_hosts}
+          />
+          <i
+            className={classnames({
+              show: this.props.readonly,
+              iconfont: 1,
+              'icon-lock2': 1
+            })}
+            title={lang.readonly}
+          />
+        </div>
+        <div className={classnames({
+          errorMessage: 1,
+          show: !!this.props.current.error
+        })}>{this.props.current.error}</div>
+        <Editor
+          readonly={this.props.readonly}
+          code={this.props.current.content || ''}
+          //setValue={this.setValue.bind(this)}
+        />
+      </div>
+    )
+  }
+}

+ 61 - 0
ui/content/content.less

@@ -0,0 +1,61 @@
+#sh-content {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  left: 240px;
+  height: 100%;
+
+  .inform {
+    position: absolute;
+    z-index: 100;
+    top: 5px;
+    right: 10px;
+    opacity: 0.5;
+    background: #fff;
+
+    i {
+      display: none;
+      color: #666;
+      margin-left: 5px;
+
+      &.show {
+        display: inline-block;
+      }
+    }
+
+    span {
+      display: none;
+
+      &.show {
+        display: inline-block;
+      }
+    }
+  }
+
+  .errorMessage {
+    display: none;
+    position: absolute;
+    z-index: 101;
+    top: 0;
+    left: 0;
+    right: 0;
+    padding: 4px 40px;
+    text-align: center;
+    background: rgba(153, 0, 0, 0.5);
+    color: #fff;
+    transition: 0.5s;
+
+    &.show {
+      display: block;
+    }
+  }
+}
+
+.platform-win32 {
+  #sh-content {
+    .inform {
+      right: 20px;
+    }
+  }
+}

+ 138 - 0
ui/content/editor.js

@@ -0,0 +1,138 @@
+/**
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict'
+
+import React from 'react'
+import CodeMirror from 'codemirror'
+// import '../../../node_modules/codemirror/addon/comment/comment'
+import 'codemirror/addon/comment/comment'
+import classnames from 'classnames'
+import m_kw from './kw'
+import Agent from '../Agent'
+import 'codemirror/lib/codemirror.css'
+import './editor.less'
+
+import modeHosts from './cm_hl'
+modeHosts()
+
+export default class Editor extends React.Component {
+
+  constructor (props) {
+    super(props)
+
+    this.codemirror = null
+
+    this.marks = []
+    this.kw = ''
+
+    this.state = {
+      code: this.props.code
+    }
+
+    Agent.on('search', (kw) => {
+      this.kw = kw
+      this.highlightKeyword()
+    })
+  }
+
+  highlightKeyword () {
+    while (this.marks.length > 0) {
+      this.marks.shift().clear()
+    }
+
+    let code = this.props.code
+    let pos = m_kw.findPositions(this.kw, code) || []
+    // this.codemirror.markText({line: 6, ch: 16}, {line: 6, ch: 22}, {className: 'cm-hl'});
+
+    pos.map((p) => {
+      this.marks.push(this.codemirror.markText(p[0], p[1], {className: 'cm-hl'}))
+    })
+  }
+
+  setValue (v) {
+    //this.props.setValue(v)
+  }
+
+  toComment () {
+    let doc = this.codemirror.getDoc()
+    let cur = doc.getCursor()
+    let line = cur.line
+    let info = doc.lineInfo(line)
+    this.codemirror.toggleComment({
+      line: line,
+      cur: 0
+    }, {
+      line: line,
+      cur: info.text.length
+    })
+  }
+
+  componentDidMount () {
+    // console.log(this.cnt_node, this.cnt_node.value);
+    this.codemirror = CodeMirror.fromTextArea(this.cnt_node, {
+      lineNumbers: true,
+      readOnly: true,
+      mode: 'hosts'
+    })
+
+    this.codemirror.setSize('100%', '100%')
+
+    this.codemirror.on('change', (a) => {
+      let v = a.getDoc().getValue()
+      this.setValue(v)
+    })
+
+    this.codemirror.on('gutterClick', (cm, n) => {
+      if (this.props.readonly === true) return
+
+      let info = cm.lineInfo(n)
+      //cm.setGutterMarker(n, "breakpoints", info.gutterMarkers ? null : makeMarker());
+      let ln = info.text
+      if (/^\s*$/.test(ln)) return
+
+      let new_ln
+      if (/^#/.test(ln)) {
+        new_ln = ln.replace(/^#\s*/, '')
+      } else {
+        new_ln = '# ' + ln
+      }
+      this.codemirror.getDoc()
+        .replaceRange(new_ln, {line: info.line, ch: 0}, {
+          line: info.line,
+          ch: ln.length
+        })
+      //app.caculateHosts();
+    })
+
+    Agent.on('to_comment', () => {
+      this.toComment()
+    })
+  }
+
+  componentWillReceiveProps (next_props) {
+    // console.log(next_props);
+    this.codemirror.getDoc().setValue(next_props.code)
+    this.codemirror.setOption('readOnly', next_props.readonly)
+    setTimeout(() => {
+      this.highlightKeyword()
+    }, 100)
+  }
+
+  render () {
+    return (
+      <div
+        id="sh-editor"
+        className={classnames({
+          readonly: this.props.readonly
+        })}>
+                <textarea
+                  ref={(c) => this.cnt_node = c}
+                  defaultValue={this.props.code || ''}
+                />
+      </div>
+    )
+  }
+}

+ 33 - 0
ui/content/editor.less

@@ -0,0 +1,33 @@
+#sh-editor {
+  height: 100%;
+  font-family: Menlo, "Source Code Pro", Monaco, "Courier New", sans-serif;
+
+  // CodeMirror
+  .cm-s-default .cm-comment {
+    color: #090;
+  }
+  .cm-s-default .cm-ip {
+    color: #00a;
+    font-weight: bold;
+  }
+  .cm-s-default .cm-hl {
+    background: #ff0;
+  }
+  .CodeMirror-gutters {
+    border-right: none;
+    padding-right: 6px;
+  }
+
+  .CodeMirror-linenumber {
+    cursor: pointer;
+  }
+
+  &.readonly .CodeMirror {
+    .CodeMirror-linenumber {
+      cursor: default;
+    }
+    .CodeMirror-cursors {
+      display: none;
+    }
+  }
+}

+ 72 - 0
ui/content/kw.js

@@ -0,0 +1,72 @@
+/**
+ * kw
+ * @author oldj
+ * @blog http://oldj.net
+ */
+
+'use strict';
+
+function kw2re(kw) {
+    // 模糊搜索
+    let r;
+    let m;
+    let flag = [];
+
+    if (kw === '/') {
+        return;
+    } else if ((m = kw.match(/^\/([^\/]+)\/?(\w*)$/))) {
+        if (m[2].indexOf('i') > -1) {
+            flag.push('i');
+        }
+        // if (m[2].indexOf('g') > -1) {
+        flag.push('g');
+        // }
+        try {
+            r = new RegExp(m[1], flag.join(''));
+        } catch (e) {
+        }
+    } else if (kw.indexOf('*') > -1) {
+        try {
+            r = new RegExp(kw.replace(/\*/g, '.*'), 'ig');
+        } catch (e) {
+        }
+    }
+
+    return r;
+}
+
+exports.findPositions = function (kw, code) {
+    if (!kw || kw === '/') return;
+
+    let r = kw2re(kw);
+    if (!r) {
+        try {
+            r = new RegExp(kw
+                    .replace(/([\.\?\*\+\^\$\(\)\-\[\]\{\}])/g, '\\$1')
+                , 'ig');
+        } catch (e) {
+            console.log(e);
+            return;
+        }
+    }
+    let indexes = [];
+
+    let lines = code.split('\n');
+
+    lines.map((ln, idx) => {
+        let match;
+        let max_loop = 30;
+        while (match = r.exec(ln)) {
+            indexes.push([
+                {line: idx, ch: match.index},
+                {line: idx, ch: match.index + match[0].length},
+            ]);
+            max_loop--;
+            if (max_loop < 0) break;
+        }
+    });
+
+    return indexes;
+};
+
+exports.kw2re = kw2re;

+ 3 - 1
ui/package.json

@@ -10,7 +10,9 @@
   "license": "MIT",
   "devDependencies": {
     "classnames": "^2.2.5",
+    "codemirror": "^5.25.0",
     "react": "^15.4.2",
     "react-dom": "^15.4.2"
-  }
+  },
+  "dependencies": {}
 }

+ 0 - 1
ui/panel/list-item.js

@@ -24,7 +24,6 @@ export default class ListItem extends React.Component {
   }
 
   beSelected() {
-    console.log(this.props.data)
     this.props.setCurrent(this.props.data)
   }
 

Some files were not shown because too many files changed in this diff