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

Xử lý các command copy, cutpaste


Clipboard là một vùng nhớ tạm trong window, nơi chứa dữ liệu mà chúng ta đã copy hay cut. Các ứng dụng khác nhau vẫn được phép sử dụng chung khối dữ liệu này. Có nghĩa là có thể bạn copy text trong MS Word, sau đó dán lên khung soạn thảo của Google Docs.

Các command "copy", "cut", "paste" cho phép RTE của chúng ta làm điều này. Nhưng vì lý do an toàn, ngoại trừ IE, các trình duyệt khác đều mặc định vô hiệu hóa tính năng trên.

Nếu Java Script cố gọi thử lệnh này trên Mozilla,  Eror Console sẽ trả về lỗi NS_ERROR_DOM_XPCONNECT_ACCESS_DENIED.

Google Docs bắt lỗi này lại và cho ra thông báo :


Nhiều phương pháp đã được đưa ra để khắc phục trở ngại này, đồng thời tránh khỏi việc phải yêu cầu người dùng thay đổi cấu hình trên hệ thống của họ. Một trong những phương pháp khá thú vị là sử dụng Action Script. Do ngôn ngữ này có thể can thiệp sâu vào hệ thống của client nên 1 chương trình flash đã được viết ra để bổ sung khả năng vào nơi mà Java Script bất lực. (Xem chi tiết)

Nhưng nếu bạn từng sử dụng Zoho Writer, bạn sẽ thấy họ vẫn làm cho các lệnh copy, cut và paste hoạt động được trên FireFox. Lần tìm giải pháp trong source code của một ứng dụng web phức tạp như Zoho Writer là một việc không mấy dễ dàng. Chỉ còn cách dựa trên các biểu hiện để phán đoán.

Điều dễ nhận thấy là chúng ta không thể đem paste khối văn bản được copy hay cut từ khung soạn thảo của Zoho Writer vào MS Word, và ngược lại, cũng không thể dùng lệnh paste trên khung soạn thảo của Zoho Writer để chèn vào nó những gì có trong clipboard của windows. Từ đó cho thấy nội dung chỉ được lưu trong 1 biến tạm của kịch bản javascript mà Zoho Writer sử dụng.

Tuy nhiên, cách làm của Zoho Writer chưa hoàn toàn thuyết phục. Điểm hạn chế của nó là không ngăn cản được sự "merge style". Xem hình minh họa bên dưới :


Đầu tiên tôi chọn đoạn text "thuộc tính designMode", nhấn lệnh Copy bên trên toolBar, rồi đặt con trỏ vào cuối dòng tiêu đề - "Viết class cho editor", sau đó nhấn Paste. Kết quả là chuỗi được paste sẽ mang định dạng của chuỗi tiêu đề bài viết mà không giữ nguyên kiểu dáng của nó. Nếu bạn quen dùng MS Word thì điều này thật đáng bực mình.

Chúng ta sẽ giải quyết vấn đề một cách triệt để hơn Zoho. Ý tưởng cơ bản là tạo ra 1 bản sao chính xác của phần text được chọn, lưu nó vào một biến tạm. Biến này đóng vai trò clipboard, được xem như 1 thuộc tính của lớp RichTextEditor.

Với mỗi command, chúng ta sẽ có cách xử lý tương ứng :
  •  copy : tạo bản sao của selection, lưu vào biến tạm.
  •  cut : copy rồi xóa đi phần text selection.
  •  paste : gắn chuỗi trong biến tạm vào lại editor.

Để thực hiện ý tưởng, trước hết hãy tìm hiểu đôi chút về SelectionRange.

Range là một phần nội dung trong hồ sơ HTML, thường liên quan đến phần text được highlight bởi người sử dụng. Đối tượng này mô tả tất cả nội dung giữa điểm đầu và điểm cuối. Với các phương thức và thuộc tính của đối tượng này, chúng ta có thể làm khá nhiều việc với phần văn bản đang ở trạng thái selected. Cùng một thời điểm, có thể tồn tại nhiều đối tượng Range.

Selection là đối tượng chỉ phần text đang trong trạng thái selected và đang được active. Nó cung cấp thông tin về văn bản và toàn bộ các phần tử nằm trong phần text đang được chọn. Không hoàn toàn giống với Range, Selection liên quan đến các đối tượng văn bản, như : từ, câu, đoạn... Trong 1 thời điểm chỉ có thể tồn tại 1 Selection.

Điểm khác biệt giữa Range và Selection khá tinh tế. Lấy 1 minh họa như hình dưới đây cho dễ hiểu :


Nếu bạn tạo 1 Selection từ phần được chọn, đối tượng này sẽ phản ánh chuỗi "ject</i> to modify the document <b>select". Trong khi Range phân tích cấu trúc DOM của hồ sơ và cho biết cụ thể đoạn "ject" nằm trong phần tử <i>, đoạn "select" nằm trong phần tử <b>, và toàn bộ selection cùng thuộc phần tử <BODY>.

Chính vì vậy, việc sử dụng Range cho phép chúng ta không những lấy ra phần text selection mà cả các phần tử chứa nó - các tag HTML, nghĩa là text selection có kèm theo định dạng

Với Opera và Mozilla, bạn có thể tạo Range bằng phương thức createRange của đối tượng document, hoặc phương thức getRangeAt của đối tượng Selection. Khi chọn 1 đoạn text bất kỳ trên trang web, bạn có thể lấy nội dung phần được chọn bằng phương thức getSelection của window. Phương thức này cũng đồng thời tạo ra một đối tượng Selection.

Range có khá nhiều thuộc tính và phương thức. Danh sách đầy đủ bạn có thể xem tại đây. Trong số này chúng ta sẽ cần đến 2 phương thức extractContentscloneContents.

cloneContents trả về 1 bản sao chính xác của range hiện thời, tức là phần text được chọn. extractContents cũng tương tự cloneContents, nhưng loại bỏ range gốc. Kết quả trả về từ 2 phương thức này có dạng 1 đối tượng documentFragment. Tới đây chúng ta rơi vào lãnh địa của XML và DOM.

Vì documentFragment giống như mọi phần tử trong 1 hồ sơ XML, chúng ta sẽ dùng phương thức serializeToString của đối tượng XMLSerializer được các trình duyệt tuân thủ chuẩn W3C hỗ trợ, để chuyển phần hồ sơ XML đó về dạng chuỗi thông thường.

Note :
Ngược lại với serializeToString của đối tượng XMLSerializer là phương thức parseFromString của đối tượng DOMParser, cho phép bạn thao tác trên 1 chuỗi text bình thường như với một tập hợp node trong hồ sơ XML.

Chẳng hạn bạn có chuỗi s='<result>125789</result>', đây không phải là dữ liệu trong 1 hồ sơ XML nên bạn không thể xử lý result như phần tử XML. Và bạn cần parseFromString để làm điều đó.


Trở lại với công việc chính, theo logic chương trình như trên, chúng ta thêm vào class RichTextEditor 1 thuộc tính có tên là clipBoard :

function RichTextEditor(sID, oContain, sDefaultText, iWidth, iHeight){
  this.ID=sID;
  this.content=sDefaultText;
  this.width=(iWidth>300?iWidth:500);
  this.height=(iHeight>80?iHeight:200);
  this.clipBoard='';
   .... 

Và phương thức useClipboard được định nghĩa như dưới đây :

RichTextEditor.prototype.useClipboard=function(command){
  if(isIE){
    this.format(command);
  }   
  else{
    if(command=='cut'||command=='copy'){
      var sel = this.UI.getSelection();
      if(sel==''){return;}
      var range = sel.getRangeAt(0);
      var docFrag = (command=='cut')?range.extractContents():range.cloneContents();
      var xmls = new XMLSerializer();
      this.clipBoard=xmls.serializeToString(docFrag);   
    }
    else if(command=='paste'){
      if(this.clipBoard!=''){
        this.format('inserthtml',this.clipBoard);
      }
    }
  }
  this.UI.focus();
} 


useClipboard kiểm tra xem trình duyệt có phải IE không, nếu là IE thì thực hiện command bình thường. Ngược lại, bắt đầu phân tích command.

Với copycut, chúng ta tạo ra Selection
var sel = this.UI.getSelection(); 

Từ đối tượng Selection này, chúng ta tạo Range bằng phương thức getRangeAt(0). Ở đây, tham số 0 ám chỉ Range đầu tiên trong tập hợp Range.

var range = sel.getRangeAt(0); 

Và clone của nó ở dạng documentFragment. Nếu là cut thì sử dụng extractContents, nếu là copy thì sử dụng cloneContents :

var docFrag = (command=='cut')?range.extractContents():range.cloneContents(); 

Cuối cùng , chúng ta khởi tạo XMLSerializer và chuyển chuỗi vào clipboard của Editor.

      var xmls = new XMLSerializer();
      this.clipBoard=xmls.serializeToString(docFrag); 


Với paste, chúng ta dùng command inserthtml để chèn chuỗi từ clipboard vào editor :

this.format('inserthtml', this.clipBoard); 

Thế là vấn đề đã được giải quyết xong. Bây giờ hãy thêm vào toolbar các lệnh Copy, Cut và Paste :


 <img id="btnCut" src="cut.gif" title="Cut">  
 <img id="btnCopy" src="copy.gif" title="Copy">  
 <img id="btnPaste" src="paste.gif" title="Paste">

Và thêm thiết lập action trong hàm init :

  setActionOnButton('btnCut', 'RTE.useClipboard("cut");');
  setActionOnButton('btnCopy', 'RTE.useClipboard("copy");');
  setActionOnButton('btnPaste', 'RTE.useClipboard("paste");'); 


Như vậy, vấn đề đã được giải quyết tương đối trọn vẹn.