Xây dựng Rich Text Editor - Phần 1

Từ DesignMode đến 1 Rich Text Editor đơn giản


Đã có khá nhiều trình soạn thảo văn bản WYSIWYG miễn phí được share trên mạng, nhưng nếu bạn là một web developer đích thực, có thể một lúc nào đó bạn sẽ muốn xây dựng cho riêng mình một Rich Text Editor. Nếu vậy, bài viết này chắc hẳn sẽ đem lại cho bạn một chút gì đó hữu ích.

Thoạt tiên, khi nảy ra ý muốn viết 1 cái RTE, tôi đã nghĩ đến việc sử dụng 1 thẻ DIV để làm khung soạn thảo. Không khó trong việc giả lập con trỏ nhấp nháy, và chuyển các ký tự từ bàn phím vào trong phần tử DIV này. Một ví dụ là chương trình mô phỏng giao diện dòng lệnh của Window mà tôi đặt tên là Web Command Line.

Nhưng rồi sau nhiều cuộc tìm kiếm, tôi đã phát hiện ra 1 điều quan trọng khác giúp cho công việc tự đơn giản hóa đi rất nhiều, đó là : DesignMode.

DesignMode là 1 thuộc tính của đối tượng document, giá trị mặc định của nó là "off". Khi bạn sửa đổi giá trị này thành "on", hồ sơ web trở thành dạng có thể chỉnh sửa - editable, và bạn có thể thao tác với trang web trong cửa sổ như đang làm việc trên MS Word ! Đây chính là bí quyết để xây dựng nên những RTE cho các trình duyệt web.

Bây giờ chúng ta sẽ làm 1 trình soạn thảo WYSIWYG đơn giản, gồm một trường nhập text, và các nút lệnh cho phép định dạng văn bản in đậm - B, in nghiêng - I, gạch dưới - U. Ngoài ra, chúng ta cũng thêm vào 2 nút cho phép chèn hình ảnh và liên kết. Sau cùng là một nút để gỡ bỏ các định dạng. Giao diện của editor trông sẽ như thế này :


Khung soạn thảo là 1 iframe, như cách thường dùng của các RTE. Chúng ta cho nó 1 ID và dùng CSS để thiết lập kích thước và khả năng scroll :

<iframe id="textArea" style="width:500px;height:240px;overflow:auto;"></iframe> 

Để biến vùng iframe này về dạng editable, chúng ta thiết lập giá trị của thuộc tính designMode thành on bằng script sau :


var editor = document.getElementById('textArea').contentWindow.document;
  editor.designMode='On';
  editor.open();
  editor.write('<html><head></head><body></body></html> ');
  editor.close(); 



Đoạn code JavaScript này phải được gọi sau khi iframe đã hiện diện trong trang web.

contentWindow là 1 thuộc tính của các frames, iframes trong hồ sơ HTML.

document.getElementById('textArea').contentWindow cho phép tham chiếu đến đối tượng iframe mà định danh được thiết lập : ID = "textArea".

Trong Mozilla , chúng ta có thể tham chiếu đến 1 đối tượng cửa sổ iframe bằng script :

window.frames["myFrame"] 

Script tương ứng cho IE là :

document.all.myFrame.contentWindow 

Còn script mà chúng ta dùng sẽ thích hợp cho cả IE, Mozilla và Opera.

Như vậy, chúng ta có 1 biến toàn cục editor, để tham chiếu đến đối tượng document của phần tử iframe giả lập khung soạn thảo văn bản. Dòng lệnh tiếp theo thiết lập designMode của nó thành "on". Các lệnh open, write, close chèn vào iframe các tags cơ bản của 1 hồ sơ HTML, những gì nằm giữa <body> và </body> sẽ hiển thị trên khung soạn thảo như phần text mặc định của Editor.

Sau thiết lập designMode = "on", đối tượng document của iframe sẽ tự kích hoạt một số phương thức hỗ trợ thực hiện định dạng phần văn bản được chọn (selected text) bên trong nó. Chúng bao gồm :
  • queryCommandEnabled
  • queryCommandIndeterm
  • queryCommandState
  • queryCommandSupported
  • queryCommandValue

Trong đó, quan trọng hơn cả là 2 phương thức execCommandqueryCommandEnabled.

Syntax của execCommand như sau :

editableDocument.execCommand(sCommand [, bUserInterface] [, vValue])

Nếu không có text được chọn, phương thức này không làm gì cả.

Tham số bắt buộc sCommand là 1 chuỗi tên lệnh định dạng (command identifiers), chẳng hạn "bold", "italic"... QuirksMode liệt kê khá chi tiết những lệnh định dạng này, và cả cách chúng làm việc trong từng trình duyệt :


Tham số thứ hai, bUserInterface, thuộc dạng tùy chọn và mang 1 giá trị boolean (mặc định : false). Trong Mozilla, nếu đặt là true, bạn có thể nhận 1 lỗi (NS_ERROR_NOT_IMPLEMENTED).

Tham số thứ 3, vValue, cũng thuộc dạng tùy chọn. Bạn sẽ phải dùng đến nó trong một số trường hợp mà tham số đầu tiên cần 1 giá trị cụ thể. Chẳng hạn khi muốn định dạng màu chữ, tham số thứ nhất sẽ là "forecolor", và chúng ta cần tham số thứ 3 để cho trình duyệt biết màu gì sẽ được sử dụng. Ví dụ :

editableDocument.execCommand("forecolor", false, "#0000ff"); 

Hoặc khi chèn 1 hình ảnh, tham số thứ nhất sẽ là "insertimage", và tham số thứ 3 cho biết đường dẫn của hình ảnh đó :
editableDocument.execCommand('insertimage', false, imageURL); 

Để biết 1 command có thể thực thi hay không, chúng ta dùng queryCommandEnabled. Phương thức này nhận 1 tham số là tên của command (như : "bold", "italic", "forecolor", "insertimage"...), và trả về 1 giá trị boolean cho biết khả năng thi hành command đó. Thông thường chúng ta kiểm tra command trước khi gọi nó. Script dưới đây kiểm tra command tạo liên kết, nếu có thể thực hiện thì sẽ chèn vào khung soạn thảo 1 HyperLink (aLink) :

if(editableDocument.queryCommandEnabled("createlink")){
   editableDocument.execCommand('createlink', false, aLink);
} 

Bây giờ, chúng ta xem xét các button. Chúng là những hình ảnh mà bạn tùy ý trình bày sao cho hợp lý. Thông thường các button định dạng nằm ngay trên khung soạn thảo như trong ví dụ mẫu này. Các lệnh in đậm, in nghiêng, gạch dưới được liên kết với hàm doFormat qua sự kiện click :

    &lt;img src="bold.gif" onclick="doFormat('bold');"&gt;  
    &lt;img src="italic.gif" onclick="doFormat('italic');"&gt;  
    &lt;img src="underline.gif" onclick="doFormat('underline');"&gt;  

Hàm doFormat như sau :

&nbsp;&nbsp;&nbsp; function doFormat(a,b){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(editor.queryCommandEnabled(a)){
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; if(!b){b=null;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; editor.execCommand(a,false,b);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; }
&nbsp;&nbsp;&nbsp; } 

Như vậy, doFormat nhận vào 2 tham số :

- a : tên của command
- b : giá trị của command

Ở đây, editor là biến toàn cục đã định nghĩa bên ngoài hàm như trình bày phía trên. Khi được gọi, doFormat sử dụng phương thức queryCommandEnabled của editor để kiểm tra khả năng thực thi command a. Sau đó, tiếp tục kiểm tra tham số thứ 2, nếu không thấy thì gán cho nó giá trị null. Nếu có thể thực thi command thì execCommand.

Đối với việc chèn hình ảnh và liên kết, có một chút khác biệt, chúng ta viết 2 hàm addLink và insertImage dành riêng cho nhiệm vụ này. Chúng được gọi theo cách tương tự như 3 nút lệnh trước.

      &lt;img src="image.gif" onclick="insertImage();"&gt;  
      &lt;img src="link.gif" onclick="addLink();"&gt;   

Trong IE, bạn có thể viết :

    editor.execCommand("CreateLink", true); 
Hoặc :

   editor.execCommand("InsertImage", true); 

Trình duyệt này sẽ hiển thị một hộp thoại để người sử dụng nhập vào các tham số thích hợp :





Tuy nhiên tính năng này chưa được Mozilla và Opera hỗ trợ, vì vậy, một số người lập trình Rich Text Editor đã tự xây dựng các kiểu DialogBox riêng để thay thế. Việc đó không khó, nhưng trong bài viết này, chúng ta sẽ đơn giản hóa công việc bằng 1 hộp thoại prompt.


Dưới đây là hàm addLink :

function addLink(){
  var aLink=prompt('Enter or paste a link :', '');
    if(aLink){
        doFormat('CreateLink',  aLink);
    }
} 

Và hàm insertImage :

function insertImage(){
  document.getElementById('textArea').contentWindow.focus();
  var iURL=prompt('Enter or paste a URL :', '');
    if(iURL){
        doFormat('InsertImage',  iURL);
    }
} 

Khi người sử dụng click trên nút Link, một hộp thoại prompt bật ra để họ nhập chuỗi siêu liên kết. Hàm addLink kiểm tra lại chuỗi này, nếu có giá trị thì gọi doFormat với
2 tham số : "CreateLink" - tên command, và aLink - URL đích do người dùng cung cấp.

Khi hàm insertImage được gọi, chúng ta sử dụng dòng lệnh đầu tiên để focus vào vùng soạn thảo. (Điều này là cần thiết cho trình duyệt IE, bạn thử bỏ đi và chạy thử chương trình thì sẽ hiểu tại sao !). Tiếp đó, 1 hộp thoại prompt mở ra yêu cầu người sử dụng nhập vào URL của hình ảnh. Cuối cùng, chúng ta gọi doFormat với command "InsertImage" và giá trị iURL mà người dùng đã nhập.

Nút lệnh Remove Formatting cho phép gỡ bỏ mọi định dạng trên phần text được chọn.

 &lt;img src="removeformatting.gif" onclick="unformat();"&gt;   

Chúng ta viết hàm xử lý như sau :

function unformat(){
    doFormat('removeformat');
    doFormat('unlink');
} 

Không có gì để giải thích nhiều. Command "removeformat" loại bỏ toàn bộ các thiết lập kiểu dáng văn bản. Command "unlink" gỡ bỏ liên kết cho phần văn bản đó.

Cuối cùng, chúng ta có toàn bộ mã HTML và JavaScript như dưới đây :

&lt;h1&gt;Rich Text Editor&lt;/h1&gt;
&lt;table border="0" cellpadding="0" cellspacing="1" bgcolor="#e1f2ff"&gt;
&lt;tr height="20"&gt; 
&lt;td&gt;  
  &lt;img src="bold.gif" title="Bold" onclick="doFormat('bold');"&gt;  
  &lt;img src="italic.gif" title="Italic" onclick="doFormat('italic');"&gt;  
  &lt;img src="underline.gif" title="Underline" onclick="doFormat('underline');"&gt;  
  &lt;img src="image.gif" title="Insert Image" onclick="insertImage();"&gt;  
  &lt;img src="link.gif" title="Hyperlink" onclick="addLink();"&gt;  
  &lt;img src="removeformatting.gif" title="Remove Formatting" onclick="unformat();"&gt;  
 &lt;/td&gt;  
 &lt;/tr&gt;
 &lt;tr&gt;  
 &lt;td align="center" bgcolor="#ffffff"&gt;  
 &lt;iframe id="textArea" style="width:500px;height:240px;overflow:auto;"&gt;&lt;/iframe&gt;  
 &lt;/td&gt;  
 &lt;/tr&gt;  
 &lt;/table&gt;  
 &lt;script type="text/javascript"&gt;  
 var editor = document.getElementById('textArea').contentWindow.document;  
 editor.designMode='On';  
 editor.open();  
 editor.write('&lt;html&gt;&lt;head&gt;&lt;/head&gt;&lt;body&gt;&lt;/body&gt;&lt;/html&gt;');  
 editor.close();  
 function doFormat(a,b){  
    if(editor.queryCommandEnabled(a)){  
        if(!b){b=null;}  
        editor.execCommand(a,false,b);  
    }  
 }  
 function addLink(){  
    var aLink=prompt('Enter or paste a link :', '');  
    if(aLink){  
      doFormat('CreateLink',&amp;nbsp; aLink);  
    }  
 }  
 function insertImage(){  
    document.getElementById('textArea').contentWindow.focus();  
    var aLink=prompt('Enter or paste a URL :', '');  
    if(aLink){  
      doFormat('InsertImage',&amp;nbsp; aLink);  
    }  
 }  
 function unformat(){  
    doFormat('removeformat');  
    doFormat('unlink');  
 }  
 &lt;/script&gt;   

Bây giờ thì Editor của chúng ta đã có thể hoạt động trên Mozilla, IE, Opera và Safari.

Phần sau, chúng ta sẽ tìm hiểu sâu hơn một số command khác để mở rộng tính năng cho editor này, đồng thời viết lại kịch bản điều khiển Editor theo mô hình hướng đối tượng. Hẹn gặp lại.