11/08/2018, 20:04

Thử làm một editor tương tác (phần 2)

Ở bài Thử làm một editor tương tác (Phần 1) mình đã giới thiệu qua tool. Trong bài viết này mình sẽ viết cách làm tool một cách chi tiết nhất có thể. Mã nguồn của tool ở đây: https://github.com/telescreen/ijs Bắt đầu Để có thể có tool này, mình cần: Editor. Nếu có thể hỗ trợ highlight ...

Ở bài Thử làm một editor tương tác (Phần 1) mình đã giới thiệu qua tool. Trong bài viết này mình sẽ viết cách làm tool một cách chi tiết nhất có thể.

Mã nguồn của tool ở đây: https://github.com/telescreen/ijs

Bắt đầu

Để có thể có tool này, mình cần:

  • Editor. Nếu có thể hỗ trợ highlight js thì càng tốt.
  • HTML container có thể canvas
  • Một ít CSS.
  • Mã JS để xử lý

Editor mình hoàn toàn có thể dùng <textarea> bình thường để làm. Tuy vậy việc hỗ trợ code hightlight và các tính năng cơ bản của một editor bình thường thì không hề đơn giản. Nếu viết mã lại từ đầu sẽ rất mệt và tốn công. Sau khi thử tìm hiểu, mình thấy có 2 sản phẩm mã nguồn mở làm được điều mình cần đấy là:

  • Code Mirror
  • ACE

ACE được dùng bởi Github nên có vẻ tin tưởng được. Tuy vậy sau khi tìm hiểu, mình nhận thấy ACE vẫn chưa hỗ trợ ký tự multibytes (tiếng Việt) do vậy mình quyết dịnh dùng CodeMirror.

CSS viết tay cũng được nhưng làm thế không khác nào phát minh lại cái bánh xe. Mình cần CSS giúp mình tạo layout 2 cột đơn giản, vì vậy mình quyết định tìm opensource. Giống như editor có 2 lựa chọn cho CSS:

  • Bootstrap
  • SemanticUI

Mình quyết định chọn SemanticUI vì kipalog.com cũng đang dùng semanticUI :)

Opensource lựa chọn đã xong, tiếp theo là quá trình viết mã.

HTML container với thẻ canvas

Do mình dùng canvas API để vẽ nên một HTML có thẻ <canvas> là điều bắt buộc. Sau một hồi đánh vật với đống tài liệu grid của semantic-ui, mình cũng làm được 1 page có 2 cột với mã như sau:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <link rel="stylesheet" href="codemirror/lib/codemirror.css"></link>
    <link rel="stylesheet" href="codemirror/theme/eclipse.css"></link>
    <link rel="stylesheet" href="semantic-ui/dist/semantic.css"></link>
    <link rel="stylesheet" href="editor.css"></link>      
    <script src="jquery-2.1.4.min.js"></script>
    <script src="codemirror/lib/codemirror.js"></script>      
    <script src="codemirror/mode/javascript/javascript.js"></script>
    <script src="Class.js"></script>
    <script src="editor.js"></script>
  </head>
  <body>
    <div class="ui centered grid" id="codearea">      
      <div class="row"></div>
      <div class="row">
        <div class="seven wide column">
          <textarea id="code"></textarea>
        </div>
        <div class="seven wide column" id="preview">
          <canvas id="render"></canvas>
        </div>
      </div> 
    </div>
  </body>
</html>

Đoạn HTML này đơn giản load các CSS của codemirror và semantic-ui. Mình dùng theme eclipse.css.

Để chia layout 2 cột, mình dùng class ui centered grid của Semantic-UI. Theo tài liệu, grid sẽ chia page ra 16 cột. Để 2 cột editor và preview có độ cao bằng nhau, mình cho nó vào 1 row. Vì mình muốn để khoảng trắng ở 2 bên page cho dễ nhìn, mình quyết định editor và preview có độ rộng là 7 cột.

Giao diện như hiện tại có các vấn đề sau:

  • <canvas> không chiếm hết độ dài và độ rộng của <div id="preview">
  • Code editor hơi bị ngắn. Font code hơi to (cảm nhận cá nhân :)).
  • <div id="preview"> có màu nền khác code editor.

Để giải quyết vấn đề này, mình cho <div id="codearea"> dài ra và overwrite style mặc định của codemirror bằng đoạn mã CSS dưới đây.

.CodeMirror {
    height: auto;
  font-family: Monaco, 'Andale Mono', 'Lucida Console', 'Bitstream Vera Sans Mono', 'Courier New', Courier, monospace;
  font-size: 8pt;
}

#codearea {
    max-height: 500px;
    height: 500px;
}

#preview {
    background: #fff;
    min-height: 500px;
}

#render {
  height: 100%;
  awidth: 100%;
}

Kết quả:

alt text

Nhìn hơi tẻ nhạt nhưng ít nhất mình cũng đã có 2 cột!

Tham khảo: http://semantic-ui.com/collections/grid.html

Code Editor

Để nhúng chức năng editor của CodeMirror, mình dùng JS viết trong file editor.js

  var codeArea = $('#code');
  var editor = CodeMirror.fromTextArea(codeArea[0], {
    indentUnit: 4,
    lineWrapping: true,
    mode: "javascript",
    theme: "eclipse"
  });

Sử dụng CodeMirror có vẻ đơn giản: chỉ cần tìm 1 thẻ textarea và gọi hàm CodeMirror.fromTextArea là xong! Ở đây mình dùng các lựa chọn sau:

  • Indent 4 khoảng trắng.
  • lineWrapping: true: khi từ khoá đến mép editor thay vì ký tự xuống dòng thì toàn bộ từ khoá sẽ xuống dòng.
  • mode: javascript, theme: eclipse

Chạy đoạn code trong editor

Code viết trong editor thực chất là một chuỗi ký tự. Vấn đề là phải chạy chuỗi ký tự đó. Làm thể nào để chạy chuỗi ký tự thì có nhiều cách:

  • Tự viết interpreter để chạy mã JS
  • Gói chuỗi mã vào Function Object và chạy Function đấy.

Cách đầu tiên có vẻ phức tạp và làm mất thời gian... May mắn là Javascript có Function Object và ta có thể tạo Fucntion Object từ chuỗi! Ta chỉ cần lấy chuỗi trong Editor và ném vào Function rồi chạy nó là xong! Dựa vào ý tưởng này ta có đoạn mã sau:

  var Context = Class.extend({
    init: function() {
      this.canvas = document.getElementById("render");
      this.canvas.awidth = this.canvas.offsetWidth;
      this.canvas.height = this.canvas.offsetHeight;
    },
    clearWindow: function() {
      var ctx = this.canvas.getContext("2d");
      ctx.clearRect(0, 0, this.canvas.awidth, this.canvas.height);
    }
  });

  var Executor = Context.extend({
    init: function(code) {
      this.code = code;
      this._super();
    },
    execute: function() {
      try {        
        var f = new Function(this.code);
        f.call(this);
      } catch(err) {
        console.log(err);
      }
    }
  });

  editor.on("change", function(cm, change) {
    var e = new Executor(cm.getValue());
    e.execute();
  });

John Resig huyền thoại đã có bài viết và một đoạn mã hỗ trợ OOP và class trên Javascript rồi, do vậy mình dùng luôn.

Ở đây mình tạo 1 đối tượng gọi là Context. Đối tượng này làm nhiệu vụ tìm và lưu lại DOM của <canvas>. Ngoài ra mình làm thêm hàm clearWindow để xoá trắng toàn bộ màn hình <canvas>. Đối tượng Executor mở rộng lớp Context và do vậy thừa kế các biến global của Context (ở đây là canvas). Đối tượng này nhận và một chuỗi dữ liệu và cung cấp 1 hàm execute để chạy đoạn mã nhận được. Ở đây có một chỗ cần chú ý là lúc chạy đoạn mã được cung cấp, ta cần cung cấp Context chứa biến global (canvas) vì nếu không đoạn mã sẽ không nhận được canvas. Do vậy ta dùng cách gọi f.call(this), để bind "context" Executor mà có canvas vào f.

Chi tiết về context và hàm call trong Javascript, các bạn có thể tham khảo bài viết Javascript context của bạn @studybot

Kết luận

Mình đã mô tả chi tiết đoạn mã editor interactive của mình. Do sử dụng các opensource mà đoạn mã của mình khá ngắn -- một điều hay của opensource.

Trong tương lai mình có các ý tưởng sau cho code editor này:

  • Làm 1 webapp đơn giản lưu đoạn code. Người dùng có thể tham khảo lại đoạn code dùng mã hash.
  • Cho phép upload file lên.

Mình sẽ cố gắng dùng tool này để học đồ hoạ máy tính (mục đích ban đầu của mình :D)

Bonus 1 vài đoạn mã copy được từ Mozilla MDN

Vẽ trái tim

alt text

** Vẽ mặt cười

alt text

PS: Nghịch online tại http://buiha.com/ijs/container.htm

0