10/10/2018, 13:24

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

Đã 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 (nó chỉ để chơi thôi chứ không giải quyết công việc gì ).

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 :

PHP Code:
<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 :


PHP Code:
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 :

PHP Code:
window.frames***91;"myFrame"***93; 
Script tương ứng cho IE là :
PHP Code:
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></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 :

PHP Code:
editableDocument.execCommand(sCommand ***91;, bUserInterface***93; ***91;, vValue***93;) 
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 :

http://www.quirksmode.org/dom/execCommand.html

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ụ :

PHP Code:
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 đó :

PHP Code:
editableDocument.execCommand('insertimage'falseimageURL); 
Để 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) :


PHP Code:
if(editableDocument.queryCommandEnabled("createlink")){
   
editableDocument.execCommand('createlink'falseaLink);

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 :

PHP Code:
     <img src="bold.gif" onclick="doFormat('bold');">
      <
img src="italic.gif" onclick="doFormat('italic');">
      <
img src="underline.gif" onclick="doFormat('underline');"
Hàm doFormat như sau :

PHP Code:
    function doFormat(a,b){
      if(
editor.queryCommandEnabled(a)){
        if(!
b){b=null;}
            
editor.execCommand(a,false,b);
      }
    } 
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ì gọi phương thức 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 addLinkinsertImage 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.

PHP Code:
      <img src="image.gif" onclick="insertImage();">
      <
img src="link.gif" onclick="addLink();"
Trong IE, bạn có thể viết :
PHP Code:
    editor.execCommand("CreateLink"true); 
Hoặc :

PHP Code:
   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 :

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

Và hàm insertImage :

PHP Code:
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 [], 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 [] cho phép gỡ bỏ mọi định dạng trên phần text được chọn.

PHP Code:
<img src="removeformatting.gif" onclick="unformat();"
Chúng ta viết hàm xử lý như sau :
PHP Code:
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 :

PHP Code:
    <h1>Rich Text Editor</h1>
     <
table border="0" cellpadding="0" cellspacing="1" bgcolor="#e1f2ff">

        <
tr height="20">
            <
td>
            &
nbsp;
            <
img src="bold.gif" title="Bold" onclick="doFormat('bold');">
            <
img src="italic.gif" title="Italic" onclick="doFormat('italic');">
            <
img src="underline.gif" title="Underline" onclick="doFormat('underline');">
            <
img src="image.gif" title="Insert Image" onclick="insertImage();">
            <
img src="link.gif" title="Hyperlink" onclick="addLink();">
            <
img src="removeformatting.gif" title="Remove Formatting" onclick="unformat();">        
            </
td>

        </
tr>
        <
tr>
            <
td align="center" bgcolor="#ffffff">
    <
iframe id="textArea" style="width:500px;height:240px;overflow:auto;"></iframe>
            </
td>
        </
tr>
    </
table>
    <
script type="text/javascript">
    var 
editor document.getElementById('textArea').contentWindow.document;
        
editor.designMode='On';
      
editor.open();
      
editor.write('<html><head></head><body></body></html>');
      
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',  aLink);
    }
}
function 
insertImage(){
 
document.getElementById('textArea').contentWindow.focus();
  var 
aLink=prompt('Enter or paste a URL :'');
    if(
aLink){
        
doFormat('InsertImage',  aLink);
    }
}
function 
unformat(){
    
doFormat('removeformat');
    
doFormat('unlink');
}
    
</script> 
Bây giờ thì Editor của chúng ta đã có thể hoạt động trên Mozilla, IE, Opera và Safari.

Demo :

http://sacroyant.uni.cc/Examples/Editor/V1/rte.htm

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 kiểu hướng đối tượng. Hẹn gặp lại.
kiem_bo viết 15:29 ngày 10/10/2018
nói gì thì nói đây là thứ tôi đang cần cảm ơn cái nha. Hy vọng bạn đưa thêm những bài có chất lượng như trên. Thích quá đi thôi. Đây giống như món quà năm mới bạn tặng cho anh em đó. TIếp nào
sacroyant viết 15:26 ngày 10/10/2018
Định dạng văn bản tại chỗ :

bold : in đậm text
fontname : font chữ, vValue :tên font. VD : arial, verdana...
fontsize : khổ chữ, vValue : các số từ 1 đến 7.
forecolor : màu chữ. vValue : chuỗi tên hoặc mã màu. VD : #0000ff, navy...
hilitecolor : màu nền (1). vValue : chuỗi tên hoặc mã màu. VD : #0000ff, navy...
italic : làm nghiêng text
subscript : text thấp xuống so với bình thường
superscript : đẩy text lên cao hơn bình thường
underline : gạch dưới text

Định dạng khối văn bản :

heading : Định dạng cho một tiêu đề. vValue : <h1>, <h2>, <h3>, <h4>, <h5>, <h6>.
indent : cho khối văn bản lui vào 1 tab
insertorderedlist : liệt kê theo số thứ tự
insertunorderedlist : liệt kê không đánh số.
justifycenter : căn giữa
justifyfull : dàn đều 2 biên
justifyleft : căn trái
justifyright : căn phải
outdent : cho khối văn bản lui ra 1 tab

Các command khác :

createlink : tạo liên kết. vValue : chuỗi URL
delete : xóa phần selection
inserthorizontalrule : chèn vào 1 phần tử <hr>
inserthtml : chèn vào 1 chuỗi HTML (2)
insertimage : chèn hình ảnh. vValue : đường dẫn đến file ảnh.
removeformat : loại bỏ các định dạng ở phần selection.
unlink : loại bỏ liên kết ở phần selection.

Các command điều khiển editor :

undo : khôi phục lại tình trạng trước khi có 1 thay đổi được ghi nhận (3)
redo : khôi phục lại tình trạng trước khi undo 1 bước.
selectall : chọn toàn bộ nội dung editor.

Danh sách trên chưa đầy đủ, nhưng là những gì cơ bản nhất cho 1 trình soạn thảo văn bản. Chúng là các commands nhận được sự hỗ trợ chung của nhiều trình duyệt.

Nhắc lại cú pháp của execCommand :

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

Trong đó :

editableDocument
: tên biến tham chiếu đến phần tử document của đối tượng iframe được dùng để giả lập khung soạn thảo.
sCommand : chuỗi tên command. Không phân biệt hoa - thường.
bUserInterface : biến tùy chọn hiển thị các DialogBox. Luôn thiết lập là false để tránh lỗi trong Mozilla và Opera.
vValue : giá trị cho command. Nếu command không cần chỉ định giá trị, sử dụng null.

Thông thường vValue là 1 chuỗi, ngoại trừ giá trị cho kích thước chữ có thể để dạng interger, nhưng nói chung vẫn nên đưa về kiểu string.

* Chú thích :

  1. Với IE, cần thay bằng command backcolor.
  2. Chỉ duy nhất Mozilla hỗ trợ command này. Với IE, có thể dùng các command cụ thể cho từng đối tượng muốn chèn, như : InsertInputButton, InsertTextArea, InsertMarquee... Xem thêm tại đây.
  3. Các thay đổi được ghi nhận có thể là bất cứ hành động nào làm khác đi nội dung vốn có trong Editor, chẳng hạn như việc gõ vào, xóa đi, hay đổi màu 1 ký tự... UndoRedo chỉ làm việc hoàn hảo trong Mozilla.
temp2 viết 15:24 ngày 10/10/2018
đề nghị những ai có ý kiến, góp ý, đả kích, khen chê... xung quanh mục tiêu của cái topic này (và cũng của sacroyant) nên hạn chế lại bớt nhằm tránh làm loãng cái topic này ra

chỉ nên post khi thực sự cảm thấy rất cần thiết và nên post các bài mang tính xây dựng cho cái editor, chớ đừng post linh tinh, đây là một topic rất có ý nghĩa, ko cần các spammer/comment/suggestion vô thưởng vô phạt


các chủ nhân của những bài viết vô thưởng vô phạt nên tự delete by yourself đi, coi như là 1 hình thức tự kiểm điểm dzậy. Cho đến khi nào bạn sacroyant tuyên bố kết thúc cái tut này thì mọi người cứ "tự nhiên như người Hà Nội"; còn bây giờ nên để cho nó xúc tích và cô đọng và liền mạch hơn
amida viết 15:27 ngày 10/10/2018
Good job man
1 point cho bài viết
soccerervn viết 15:30 ngày 10/10/2018
Tuyệt vời quá, cảm ơn sacroyant!
greenbeetle viết 15:40 ngày 10/10/2018
Bài viết rất hay ! Thank
kiem_bo viết 15:38 ngày 10/10/2018
một câu hỏi nhỏ nha. Làm cách nào để lấy nội dung những thứ mình đang gõ. Tôi muốn thay <ifframe> bằng <textare> thì mình thay đổi như thế nào
sacroyant viết 15:29 ngày 10/10/2018
Cảm ơn mọi người đã hưởng ứng topic này

Được gửi bởi kiem_bo
một câu hỏi nhỏ nha. Làm cách nào để lấy nội dung những thứ mình đang gõ. Tôi muốn thay <iframe> bằng <textarea> thì mình thay đổi như thế nào
Bài viết này mình nói về cách viết 1 trình soạn thảo WYSIWYG dựa trên chế độ DesignMode của đối tượng hồ sơ HTML. Chỉ với 1 iframe, chúng ta mới có thể chèn vào trang web hiện tại 1 hồ sơ HTML, rồi sử dụng nó ở chế độ DesignMode ! Còn cách lấy nội dung từ trình soạn thảo thì sẽ được đề cập đến sau
cuonphong viết 15:32 ngày 10/10/2018
Thank you very much, mình cũng đang cần cái này
Bài liên quan
0