我們將以和 images 的實現方式來實現 Videos。從第一個直覺或許可以使用 HTML 的 <video>
標籤,但我們無法用這種方式來播放 Youtube 的影片,考慮到 Youtube 影片是目前主流看影片的其中一種方式,我們就用 <iframe>
標籤來實現。如果希望多個 Blot 使用相同的標籤,除了 tagName
之外,我們還可以使用 className
,下一個 Tweets 練習會示範這個部分。
另外,我們將支援對寬度與高度,作為未註冊的 Formats。特定於 Embeds 的 Formats 不需要單獨註冊,只要它們與已註冊的 Formats 沒有命名空間的衝突即可。這樣可以運作是因為 Blots 只是將未知的 Foramts 傳遞給其子元素,最終達到葉節點。這也允許不同的 Embeds 以不同的方式處理未註冊的 Formats。例如,我們之前的插入圖片可能會跟我們在這裡的 Videos 以不同的方式來識別和處理寬度格式。
export class VideoBlot extends BlockEmbed { static blotName = 'myVideo'; static tagName = 'iframe'; static create(value: { url: string }) { const node = super.create(); node.setAttribute('src', value.url); // Set non-format related attributes with static values node.setAttribute('frameborder', '0'); node.setAttribute('allowfullscreen', 'true'); return node; } static formats(node: HTMLIFrameElement): Format { let format: Format = {}; if (node.hasAttribute('height')) { format['height'] = node.getAttribute('height')!; } if (node.hasAttribute('width')) { format['width'] = node.getAttribute('width')!; } return format; } static value(node: HTMLImageElement) { return node.getAttribute('src'); } format(name: string, value: number | string) { // Handle unregistered embed formats if (name === 'height' || name === 'width') { if (value) { this['domNode'].setAttribute(name, value); } else { this['domNode'] .removeAttribute(name, value); } } else { super.format(name, value); } }}
新增 VideoBlot 之後,和前面幾次練習一樣,註冊到 Quill,並且加上對應的 Click Event 到 Component:
<button type="button" title="video" id="video-button" (click)="addVideo()"> <i class="fa fa-play"></i> </button>
registerBasicFormatting() { // ... Quill.register(VideoBlot); } addVideo() { const range = this.quillInstance.getSelection(true); this.quillInstance.insertText(range.index, '\n', Quill.sources.USER); this.quillInstance.insertEmbed(range.index + 1, 'myVideo', { url: 'https://www.youtube.com/embed/QHH3iSeDBLo', }); this.quillInstance.formatText(range.index + 1, 1, { height: '170', width: '400', }); this.quillInstance.setSelection( { index: range.index + 2, length: 0 }, Quill.sources.SILENT ); }
點擊按鈕嵌入 Youtube 影片之後,可以看到編輯器的內容加了一個 iframe
標籤:
如果打開 dev tool 使用 getContents
方法來查看編輯器內容,Quill 會回傳 Video 的 Delta 內容像這樣:
{ ops: [{ insert: { video: 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0' }, attributes: { height: '170', width: '400' } }]}
Medium 支援多種嵌入類型,但我們練習就只專注於 Tweets。Tweet Blot 的實現方式與 images 幾乎完全相同。我們利用 Embed Blots 不一定要對應到一個空(void)節點的行為。它可以是任何自定義的節點,Quill 會將其視為一個空節點,而不遍歷其子節點或後代節點。這使我們可以使用一個 <div>
,並讓原生的 Twitter Javascript 函式庫在我們指定的 <div>
容器內運作。
由於我們的根 Scroll Blot 也使用了一個 <div>
,所以我們還指定了一個 className 來消除歧義。需要注意的是,Inline Blots 預設使用 <span>
,而 Block Blots 預設使用 <p>
。因此,如果想讓自定義的 Blots 使用這些標籤,除了指定 tagName
之外,還要帶上一個 className
。
我們使用 Tweet id 作為定義我們 Blot 的值。同樣的,在 Click Event handler 一樣帶入固定值來方便練習。
export class TweetBlot extends BlockEmbed { static blotName = 'myTweet'; static tagName = 'div'; static className = 'tweet'; static create(id: string) { const node = super.create(); node.dataset.id = id; // Allow twitter library to modify our content twttr.widgets.createTweet(id, node); return node; } static value(domNode: HTMLElement) { return domNode.dataset['id']; }}
上面這個範例程式中,如果直接加上 twttr
應該會出現 TypeScript 不認得的錯誤訊息,twttr
是 Twitter platform widgets.js 函式庫提供的,因此我們這邊就先使用 declare
any 來定義它的型別:
declare var twttr: any;
此外,我們還需要加上 Twitter widgets 的 JS script,這邊我們可以利用 Angular 提供的 Renderer2
來插入外部的 script
,當然也可以直接在 index.html
的檔案上加入:
import { CommonModule, DOCUMENT } from '@angular/common';import { AfterViewInit, Component, ElementRef, Inject, OnInit, Renderer2, SecurityContext, ViewChild,} from '@angular/core';// ...Component constructor( // ... private renderer: Renderer2, @Inject(DOCUMENT) private document: Document ) {} ngOnInit(): void { const script = this.renderer.createElement('script'); script.type = 'text/javascript'; script.src = 'https://platform.twitter.com/widgets.js'; script.async = true; script.charset = 'utf-8'; this.renderer.appendChild(this.document.body, script); }
建立好 TweetBlot 之後,我們一樣進行 Quill 註冊以及綁定 click event 對應的按鈕:
<button type="button" title="tweet" id="tweet-button" (click)="addTweet()"> <i class="fa-brands fa-twitter"></i> </button>
使用 Quill Instance 提供的方法取得游標位置,並插入 TweetBlot 的區塊:
registerBasicFormatting() { // ... Quill.register(TweetBlot); } addTweet() { const range = this.quillInstance.getSelection(true); const id = '464454167226904576'; this.quillInstance.insertText(range.index, '\n', Quill.sources.USER); this.quillInstance.insertEmbed( range.index + 1, 'myTweet', id, Quill.sources.USER ); this.quillInstance.setSelection( { index: range.index + 2, length: 0 }, Quill.sources.SILENT ); }
點擊後的效果:
我們從一堆按鈕和只能理解純文本的 Quill 核心開始。通過 Parchment,我們能夠添加粗體、斜體、連結、引用區塊、標題、分隔線、圖片、影片,甚至是 Tweets。所有這些都能在維持一個可預測且一致性的文件實現,這使我們能夠使用 Quill 的 API 來處理這些新的格式和內容。
讓我們為這個範例加上一些最後的潤色。雖然它不能與 Medium 的 UI 相比,但還是盡可能的去貼近它。
最後的效果,當選擇文本時會顯示工具列:
游標換行之後停在最前面的時候顯示插入內容按鈕,點擊之後可以展開內容:
具體的程式碼變更可以參考對應的 commit 紀錄。
終於來到了第 30 天,最後用了這個練習把 Quill 的東西初步都摸過了一遍。今天把剩下的練習像是 Video, Tweets 這些自訂的 Blot 插入,並把整個 UI 改成像是 Medium 的編輯器風格。從中學到不少東西,也因為很久沒碰 Angular,有一些對我來說可能是新的東西也派上用場,目前的實現並不是最好的實現方式,因為官方提供的範例是直接用 jQuery,那我想在 Angular 專案的話,應該要透過 Angular 的生態系統下來實現正確的 UI 操作方式,這個未來可以在持續的探討。同時有機會的話也可以改成 Signal 的版本 XD
今天下午繼續參加週末的 MNH 社群日,遇到很多低等的會想挑戰高階的魔物,想挑戰的心態值得嘉許,但更多的可能是想蹭並獲取材料,對於等級可能剛剛好可以應付魔物的玩家,如果遇到這樣的情況就會很尷尬,因為不一定能夠扛著住,所以現在加入一個組隊之後,都要先觀望一下隊友的等級,才知道這場會不會又整個翻車,畢竟藥水真的不便宜 QQ
文章同步發表於2023 iThome 鐵人賽
]]>接下來的步驟中,我們將實作第一個所謂的「葉子 Blot (Leaf Blot) 」。不同於先前我們練習過的 Blot,這些主要是負責文本格式化—例如定義文字的外觀或調整排列,並實作format()
方法。Leaf Blot 的主要職責則是提供特定的內容,並透過實作 value()
方法來達成。
Leaf Blot 可以是文本 (Text) 型態或嵌入 (Embed) 型態的 Blot。在本例中,我們會實作一個屬於嵌入型態的 Blot,即分隔線 (Divider)。值得注意的是,一旦 Embed Blot 建立,其內含的值將會是不可變的 (Immutable) 。因此,如果你需要變更這個 Blot 的內容,則必須先將其從文本中刪除,再重新插入新的內容。
首先我們新增一個 TS 檔當作 Leaf Blot 的練習,並加入 Divider 的 Blot:
import Quill from 'quill';const BlockEmbed = Quill.import('blots/block/embed');export class DividerBlot extends BlockEmbed { static blotName = 'myDivider'; static tagName = 'hr';}
我們的 click handler 呼叫了 insertEmbed()
方法,這個方法不像 format()
那麼方便可以確定、保存和恢復使用者的選擇區域。因此我們需要自行做一些額外的工作來維護這個選擇區域。此外,當我們嘗試在一個 Block 的中間插入一個 Block Embed 時,Quill 會自動為我們將該 Block 分割開來。為了讓這個行為更為明確,我們會在插入分隔線之前明確地插入一個換行符,以自行分割該 Block。
建立 DividerBlot 之後,回到 Component 註冊 DividerBlot 並新增 addDivider
方法:
registerBasicFormatting() { // ... // Leaf blot Quill.register(DividerBlot);}addDivider() { const range = this.quillInstance.getSelection(true); this.quillInstance.insertText(range.index, '\n', Quill.sources.USER) this.quillInstance.insertEmbed( range.index + 1, 'myDivider', true, Quill.sources.USER ); this.quillInstance.setSelection( { index: range.index + 2, length: 0 }, Quill.sources.SILENT );}
接著將對應的 button 加上事件綁定:
<button type="button" title="divider" id="divider-button" (click)="addDivider()" > <i class="fa fa-minus"></i> </button>
輸入兩行 Hello World 之後,游標停留在第一行的 Hello 後面,並點擊加入分隔線,可以看到 HTML 被強制換行後加入分隔線:
圖片的處理可以使用我們在建立 Link 和 Divider blots 時所學到的概念來新增。我們會使用一個物件作為圖片的值來展示如何被支援的。我們用於插入圖像的 click handler 直接帶入 hardcode 的內容來專注在插入圖片 Blot 的實現。
建立 ImageBlot,分別有 create
以及 value
兩個靜態方法:
export class ImageBlot extends BlockEmbed { static blotName = 'myImage'; static tagName = 'img'; static create(value: { alt: string; url: string }) { const node = super.create(); node.setAttribute('alt', value.alt); node.setAttribute('src', value.url); return node; } static value(node: HTMLImageElement) { return { alt: node.getAttribute('alt'), url: node.getAttribute('src'), }; }}
接著在 Component 加上插入圖片的 handler,並綁定到對應的 button:
<button type="button" title="image" id="image-button" (click)="addImage()"> <i class="fa fa-camera"></i></button>
addImage() { const range = this.quillInstance.getSelection(true); this.quillInstance.insertText(range.index, '\n', Quill.sources.USER); this.quillInstance.insertEmbed( range.index + 1, 'image', { alt: 'Quill Cloud', url: 'https://quilljs.com/0.20/assets/images/cloud.png', }, Quill.sources.USER ); this.quillInstance.setSelection( { index: range.index + 2, length: range.length }, Quill.sources.SILENT ); }
看一下加入圖片後的效果:
今天主要就兩個 Embed Blot 的實現,我們透過繼承 Quill 底下的 Parchment Embed Blot 來建立自定義的 Blot,對於 Quill 的方法及應用有比較深入的理解。 整體的實現上都是與 DOM 去做對應在編輯器中加入內容,因此都會經過 Create()
方法來新增 DOM,如果是簡單的 HTML,沒有太多的加工處理,則直接帶上 blotNmae
和 tagName
即可,按照官網文件的說明,Quill 的確也讓編輯器的內容與結構盡可能的單純易懂。
這個週末是魔物獵人 Now 的社群日,有期間限定的櫻火龍,貌似對拿弓箭的玩家來說是不錯的裝備材料收集,準備好今天的文章之後,等等就要出去晃晃,希望不會太快就把藥水喝完 XD
文章同步發表於2023 iThome 鐵人賽
]]>我們之前提到過,Inline 不貢獻任何格式。這是為 Inline 基礎類別所制定的例外,而不是規則。基本的 block Blot 和區塊級的元素 (Block level element) 的運作方式相同。
要實作粗體和斜體,我們只需要繼承 Inline,設定 blotName 和 tagName,並註冊到 Quill 中即可。有關繼承和靜態方法和變數的內容介紹可以參考 Parchment 的介紹。
import Quill from 'quill';const Inline = Quill.import('blots/inline');export class BoldBlot extends Inline { static blotName = 'myBold'; static tagName = 'strong';}export class ItalicBlot extends Inline { static blotName = 'myItalic'; static tagName = 'em';}
這裡跟著 Medium 的範例使用 Strong
以及 em
標籤,但我們也可以使用 b
和 i
標籤。Quill 將使用 blot 的名稱當作格式名稱,透過註冊我們的 Blot,我們現在可以在新格式上使用 Quill 的完整 API:
ngAfterViewInit(): void { this.registerBasicFormatting(); this.quillInstance = new Quill(this.editorContainer.nativeElement);}registerBasicFormatting() { Quill.register(BoldBlot); Quill.register(ItalicBlot);}insertText() { this.quillInstance.insertText(0, 'Test', { myBold: true });}formatText() { this.quillInstance.formatText(0, 4, 'myItalic', true);}
接著將按鈕的 click
事件加上,這邊為了示範方便,我們直接寫死一個 true
在程式裡面,這樣就會一直是加上格式的操作。在 App 中,我們可以使用 getFormat()
來尋找指定範圍內的文本格式,來決定是否新增或刪除格式。Toolbar 模組因為 Quill 已經實現了,就不在這重新實作。
兩個按鈕都點擊之後的效果如下:
與其他格式(如粗體或斜體)不同,Link 需要存入更多資訊,特別是 URL。這主要影響到「Link blot」的兩個方面:建立和格式檢索。
雖然 URL 通常以字串的型別存入,但也可以用其他方式來表示,例如以一個包含 URL Key value 的物件。這樣做可以允許我們加入其他的 Key/Value 來定義一個連結,提供更多自定義的選項。
新增一個 Link Blot:
export class LinkBlot extends Inline { static blotName = 'myLink'; static tagName = 'a'; static create(value: string) { let node = super.create(); // Sanitize url value if desired node.setAttribute('href', value); // Okay to set other non-format related attributes // These are invisible to Parchment so must be static node.setAttribute('target', '_blank'); return node; } static formats(node: HTMLElement) { // We will only be called with a node already // determined to be a Link blot, so we do // not need to check ourselves return node.getAttribute('href'); }}
Component 加入新的註冊和方法:
registerBasicFormatting() { Quill.register(BoldBlot); Quill.register(ItalicBlot); Quill.register(LinkBlot);}
考慮到安全性,這裡我們可以在 constructor
注入 Angular 提供的 DomSanitizer
“消毒” (sanitize)輸入的 URL 避免 XSS 問題發生:
constructor(private sanitizer: DomSanitizer) {}addLink() { const url = prompt('請輸入 URL'); const safeUrl = this.sanitizer.sanitize(SecurityContext.URL, url); this.quillInstance.format('myLink', safeUrl);}
接著嘗試選取文本內容,並點擊加入連結的按鈕,輸入網址後可以看到效果:
Blockquotes 繼承自 Block,這是基本的 Block Blot(一種自定義的文本塊)。與 Inline blots 不同的是,Block Blots 不能被嵌套。如果對同一範圍的文字套用多個 Block blots,它們不會互相包裹,而是會相互替換。也就是說,新套用的 Block Blot 會取代原有的 Block Blot。
建立 BlockquoteBlot:
const Block = Quill.import('blots/block');export class BlockquoteBlot extends Block { static blotName = 'myBlockquote'; static tagName = 'blockquote';}
註冊 Blot:
Quill.register(BlockquoteBlot);
Header 的實作方式完全相同,只有一處不同:它可以由多個 DOM 元素表示。預設情況下,格式的值將成為 tagName,而不僅僅是 true。我們可以透過擴充 formats() 來自訂,類似於我們對連結所做的那樣:
export class HeaderBlot extends Block { static blotName = 'myHeader'; static tagName = ['H1', 'H2']; static formats(node: HTMLElement) { return HeaderBlot.tagName.indexOf(node.tagName) + 1; }}
為了方便測試,加入 CSS 的部分:
::ng-deep h1, ::ng-deep h2 { margin-top: 0.5em; color: purple;}::ng-deep blockquote { border-left: 4px solid #111; padding-left: 1em;}
最後在 Component 加入 event function,再和 template 的 click
事件綁定:
addBlockquote() { this.quillInstance.format('myBlockquote', true);}addHeader1() { this.quillInstance.format('myHeader', 1);}addHeader2() { this.quillInstance.format('myHeader', 2);}
輸入不同段落的內容後,點擊按鈕試試看套用格式效果,可以看到對應的 HTML 元素也被成功加入了,並且套用了設定好的 CSS Style:
今天嘗試跟著實現自訂的 Inline Blot 和 Block Blot,實際操作過一遍會比較有感覺,官方文件提供的範例是 JavaScript,那我們就直接以 Angular 的專案當作練習,以 Angular 的方式來實現對應的功能。對於自訂的 Blot 內容有進一步的理解,明天再接著練習後面的其他功能。
轉眼間就來到第 28 天了,時間真的過的很快,也平安度過試用期(?)。但對於很多細節和產業的觀念還是持續學習中,白天工作內容的轟炸與考古,晚上則持續學習及寫文章做紀錄,上週的連假則是邊出去旅遊,回到住宿的地方後,繼續準備文章內容,腦袋裝了滿滿的東西。生活的節奏也比以往要快了許多,從進辦公室開始工作,回過神來就快下班了,除了充實,還是充實 XD…
文章同步發表於2023 iThome 鐵人賽
]]>為了提供一致的編輯體驗,我們需要同時具有一致的資料及可預測的行為,然而這兩項是 DOM 都沒有的。現代編輯器的解決方案是維護自己的文件模型來表示其內容。對於 Quill 來說,Parchment 就是這樣的一個解決方案。它在自己的 library 中有組織的架構,並有屬於自己的 API。透過 Parchment,我們就可以自定義 Quill 能夠識別的內容與格式,或者加入全新的格式。
在官網這份指南中,我們將使用 Parchment 和 Quill 提供的基礎模組來複製 Medium 上的編輯器。我們將從 Quill 的最基本架構開始,不涉及任何 Theme,額外的模組或格式。在這個基礎上,Quill 只能理解純文本。但跟著這份指南做到最後,連結,影片甚至推文都能被 Quill 所辨別。
剛開始我們不使用 Quill,而只需要 textarea
及按鈕。並且將按鈕加上 event listener。文件的介紹是使用 jQuery 來實現,但我們就直接在 Angular 專案下來做這個練習囉。另外還需要 Google Fonts 和 Font Awesome 為練習的專案加上一些基本樣式。這些都和 Quill 或 Parchment 沒有直接關係,這部分就快速帶過。首先新增一個練習用的 Component,之後分別將 HTML 以及 CSS 加到 Component。
HTML :
<p>medium-editor works!</p><div #tooltipControls class="tooltip-controls"> <button id="bold-button" (click)="formatBold()"> <i class="fa fa-bold"></i> </button> <button id="italic-button"><i class="fa fa-italic"></i></button> <button id="link-button"><i class="fa fa-link"></i></button> <button id="blockquote-button"><i class="fa fa-quote-right"></i></button> <button id="header-1-button"><i class="fa fa-header"></i><sub>1</sub></button> <button id="header-2-button"><i class="fa fa-header"></i><sub>2</sub></button></div><div class="sidebar-controls"> <button id="image-button"><i class="fa fa-camera"></i></button> <button id="video-button"><i class="fa fa-play"></i></button> <button id="tweet-button"><i class="fa-brands fa-twitter"></i></button> <button id="divider-button"><i class="fa fa-minus"></i></button></div><textarea class="editor-container" placeholder="Tell your story..." #editorContainer></textarea>
CSS:
* { box-sizing: border-box;}.editor-container { display: block; font-family: 'Open Sans', Helvetica, sans-serif; font-size: 1.2em; height: 200px; margin: 0 auto; width: 450px;}.tooltip-controls, .sidebar-controls { text-align: center;} button { background: transparent; border: none; cursor: pointer; display: inline-block; font-size: 18px; padding: 0; height: 32px; width: 32px; text-align: center;}button:active, button:focus { outline: none;}
Component 我們只加了一個 formatBold
方法來和 template 做事件綁定:
import { Component } from '@angular/core';import { CommonModule } from '@angular/common';@Component({ selector: 'app-medium-editor', standalone: true, imports: [CommonModule], templateUrl: './medium-editor.component.html', styleUrls: ['./medium-editor.component.scss'],})export class MediumEditorComponent { formatBold() { alert('click!'); }}
執行 serve
指令之後確認渲染的結果:
接下來,我們將用 Quill 核心取代文字區域,去除主題、格式和無關模組。打開 Dev tool,在編輯器中輸入內容時檢查示範。可以看到 Parchment 文件的 base building block 正在執行中。
HTML 的部分,將剛才加入的 textarea
改成 div
並帶入範本參考變數 (Template Reference Variable) editorContainer
, 例如:
<div class="editor-container" #editorContainer>Tell your story...</div>
由於換成 div
,所以 editor-container
class 也有做了小更動:
.editor-container { border: 1px solid #ccc; font-family: 'Open Sans', Helvetica, sans-serif; font-size: 1.2em; height: 200px; margin: 0 auto; width: 450px;}
存檔重新整理之後,嘗試在編輯區域打字,可以看到 Quill 核心正在執行中:
就像 DOM 一樣,Parchment 文件是一個樹 (tree)。它的節點稱為 Blot,是 DOM 節點的抽象化。已經有一些 blot 已經為我們定義了,例如:Scroll, Block, Inline, Text 以及 Break。當我們輸入文字的時候,Text Blot 會與對應的 DOM 文字節點同步。而 Enter 則會建立一個新的 Block Blot 來處理。在 Parchment 中,可以有子項的 Blot 必須至少有一個子項,因此 Empty Block 會被 Break Blot 填滿。這使得處理樹葉 (leaves) 變得簡單且可預測。所有這一切都組織在 Root Scroll Blot 下。
這時我們無法僅透過輸入文本來觀察 Inline Blot,因為它不會為文件提供有意義的結構或格式。有效的 Quill 文件必須規範 (canonical) 且緊湊 (compact)。只有一棵有效的 DOM 樹可以表示給定的文件,並且該 DOM 樹包含最少數量的節點。
由於 <p><span>Text</span></p>
和 <p>Text</p>
代表著相同的內容, 前者是無效的,Quill 的優化過程之一就是拆開 <span>
. 同樣地,一旦我們加入格式,<p><em>Te</em><em>st</em></p>
和 <p><em><em>Test</em></em></p>
也是無效的,因為它們不是最緊湊的表示方式。
因為這些限制,Quill 無法支援任意 DOM 樹和 HTML 變更。但正如我們將看到的,這種結構提供的一致性和可預測性使我們能夠輕鬆建立豐富的編輯體驗。
今天開始嘗試從無到有實現 Quill 的基本功能,官網文件介紹是使用 jQuery 當作範例,但因為我們主要是在 Angular 的專案上開發,所以範例的部分都融入了 Angular 元件的生命週期,使用起來更貼近實際的開發情況。明天繼續練習 Basic Formatting 以及自訂 blot 的部分。
今天中午吃飯經過南港展覽館,看到人比平常還多就知道這週末有展期了,分別是世界貓咪博覽會,還有攝影器材暨影音創作設備展,台灣戶外用品展,共有三個展覽同步在今天開始,如果是貓奴、有在玩影音創作相關設備或是時常在露營的人,感覺進去錢包就會被榨乾 XD
文章同步發表於2023 iThome 鐵人賽
]]>Attributor 是另一種更輕量的表示格式的方式。與其對應的就是 DOM attribute。就像 DOM 屬性與節點的關係一樣,屬性也屬於 Blot。在 Inline 或 Block blot 上呼叫 formats()
如果有對應的 DOM 節點以及 DOM 節點 attribute
則將回傳其表示的格式。
首先我們來看一下 Attributor 的介面:
class Attributor { attrName: string; keyName: string; scope: Scope; whitelist: string[]; constructor(attrName: string, keyName: string, options: AttributorOptions = {}); add(node: HTMLElement, value: string): boolean; canAdd(node: HTMLElement, value: string): boolean; remove(node: HTMLElement); value(node: HTMLElement);}
需要留意的地方是,自訂的 attributor 是 instance,而不是像 blot 一樣的 class 定義。與 Blot 相似,我們不會想要從頭開始建立,而是希望使用既有的 attributors 實現,例如基礎屬性器 (base Attributor),類別屬性器 (Class Attributor) 或樣式屬性器 (Style Attributor)。另外我們也可以透過原始碼來看 attributor 的實現,其實沒有很複雜。
使用 Attributor 來表示格式:
const width = new Attributor('width', 'width');Quill.register(width);const imageNode = document.createElement('img');width.add(imageNode, '200px');console.log(imageNode.outerHTML); // Will print <img width="200px">const value = width.value(imageNode); // Will return 200pxconsole.log('value', value); width.remove(imageNode);console.log(imageNode.outerHTML) // Will print <img>
可以看到我們直接以 new Attributor()
的方法來新增一個實體化 width
屬性後,以 Quill.register()
註冊 attribute,並且呼叫 add
方法將屬性加到 img
DOM 上。然後可以透過 value()
取得目標 DOM 的 width
,最後使用 remove()
將 width
從 imageNode
刪除。
使用 Class Attributor 的方式來表示格式:
const align = new ClassAttributor('align', 'blot-align');Quill.register(align);const node = document.createElement('div');align.add(node, 'right');console.log(node.outerHTML); // Will print <div class="blot-align-right"></div>
有別於上一個 new Attributor()
,一樣是 new
但後面換成是 ClassAttributor
,帶入指定的 DOM attribute
並自訂一個名稱 blot-align
,一樣註冊後使用。也能呼叫 add()
將自訂的 class attributor 加到目標 DOM。
使用 Style Attributor 的方式來表示格式:
const align = new StyleAttributor('align', 'text-align', { whitelist: ['right', 'center', 'justify'], // Having no value implies left align});Quill.register(align);const node = document.createElement('div');align.add(node, 'right');console.log(node.outerHTML); // Will print <div style="text-align: right;"></div>
這次則是在實體化的時候以 new StyleAttributor
來新增 attributor,一樣是操作 text-align
,但這次加上了 whitelist
來表示合法的參數選項。沒有帶入值則代表 left
置左。在註冊之後呼叫 add()
方法並帶入 DOM 以及 align
的參數選項來套用。
今天探討了 Parchment 的另一塊拼圖,Attributor,提供一個文本格式套用的簡易方式。一開始嘗試練習發現奇怪怎麼會出現找不到的錯誤,看了一下 sourcecode 才發現原來實現的方式已經換了,但 Github 的 repositroy README 還是古早的實現方式。這時只能看原始碼才能知道要怎麼使用了。
我們可以透過 Base Atrributor,Class Atrributor,以及 Style Attributor 來實現不同方式的文本樣式套用,並且 Attributor 也提供了幾個方法例如 add()
,value()
,remove()
等方法取得與操作對應的 blot 來編輯文本樣式。之後再研究看看如何將 attributor 應用到編輯器中來套用文本樣式。
連假後上班的第一天,果然精神不是很好,儘管前一晚已經盡量提早躺平,但起床後還是有沒充滿電的感覺,由於台北住處附近不好停車,所以果斷的把車開回宜蘭停放,所以今天早上是從宜蘭搭車到台北,想說國光客運到南港展覽館離上班地點最近,沒想到七點半到轉運站,要能上車得要等到八點整的班次,到辦公室就都九點了,看來如果是從宜蘭到台北的話還要再更早一點到才行了QQ
文章同步發表於2023 iThome 鐵人賽
]]>在 Angular 專案中,有時候會需要用到第三方套件,為了要能順利的融入 Angular 的世界,我們會需要額外的處理與封裝,讓套件使用體驗可以更 Angular。而使用 ngx-quill 的好處如下:
我們都知道 Angular 使用 data binding 以及 event binding 作為核心特性之一,使用 ngx-quill 可以方便的透過綁定的方式來維護資料的狀態以及編輯器的互動等功能。
Angular 專案中,我們透過 module (目前更推薦使用 standalone component),以及相依性注入 ( Dependency Injection ) 作為管理各種服務和元件的方式。ngx-quill 也是按照 Angular 的模組化及相依性注入的設計模式來建立。使其更容易整合到既有的 Angular App 中。
Angular 有很強大的表單 module,包括了:template-driven forms
以及 reactive Forms
。ngx-quill 可以輕鬆的整合到 Angular 的表單系統中,讓我們能使用 Angular 的驗證、狀態追蹤等功能。
首先我們一樣透過 npm install
來安裝 ngx-quill
:
npm install ngx-quill --savenpm install @types/quill@1.3.10
另外需要注意的是,如果之前的練習有安裝到 @types/quill
的話,版本會是 2.0.11
,這邊我們需要降版到 1.3.10
才不會導致編譯時的類型錯誤。
如果是全新的 Angular 專案,需要將 quill editor 的佈景主題 (theme) CSS Style 加到專案,例如:
要選用 snow
的主題,可以 import CSS 到 styles.scss
:
@import '~quill/dist/quill.snow.css';
也可以把 node_modules/quill/dist/quill.snow.css
加到 angular.json
或 Nx 的 project.json
的 styles
陣列中。
"styles": [ "node_modules/quill/dist/quill.snow.css", "src/styles.scss"],
安裝完畢之後,接著我們要將 ngx-quill
的 module 導入:
import { QuillModule } from 'ngx-quill';@NgModule({ imports: [ QuillModule.forRoot() ],})export class AppModule { }
Import 之後就可以直接在 template 使用這個元件:
<quill-editor></quill-editor>
這時直接 ng serve
就可以看到有基本款的 Quill Editor 了。
配置選項目前我們可以放在兩個地方,一個是在 template 的 component 屬性中,另一個則是在 import QuillConfigModule.forRoot()
的括號中帶入配置選項。
在 template 的 component 屬性加上 quill eidtor 的配置:
<quill-editor [modules]="{ toolbar: [ ['bold', 'italic'], ['link', 'blockquote'] ] }" [theme]="'snow'"></quill-editor>
透過 import QuillConfigModule
帶入配置:
import { QuillConfigModule, QuillModule } from 'ngx-quill';@NgModule({ imports: [ QuillModule.forRoot(), QuillConfigModule.forRoot({ modules: { toolbar: [ ['bold', 'italic'], ['link', 'blockquote'], ], }, }), ],})export class AppModule { }
ngx-quill
也支援 standalone 的功能,可以直接使用 provideQuillConfig
方法進行配置,例如在 main.ts
的 bootstrapApplication
呼叫時,將配置加入到 providers
:
import { provideQuillConfig } from 'ngx-quill/config';bootstrapApplication(AppComponent, { providers: [ provideQuillConfig({ modules: { syntax: true, toolbar: [ ['bold', 'italic'], ['link', 'blockquote'], ], } }) ]});
此時的 AppComponent
對應的 standalone 設定如下:
import { Component } from '@angular/core';import { QuillModule } from 'ngx-quill';@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], standalone: true, imports: [QuillModule],})export class AppComponent {// ...}
有時候我們要確認編輯器的狀態,以根據需求進行像是表單驗證,或是否修改過等相關的操作,這時候可以搭配 Angular Form module 加到 ngx-quill
就能快速的實現表單操作與驗證的需求。例如以下的範例,搭配 import
對應的 FormsModule
或 ReactiveFormsModule
就能使用了:
<!-- Reactive Forms --><form [formGroup]="myForm"> <quill-editor formControlName="editorContent"></quill-editor></form><!-- Template-driven Forms --><quill-editor [(ngModel)]="editorContent" name="editorContent"></quill-editor>
ngx-quill
作為 Quill 的 Angular wrapper,為 Angular 開發者提供了一個更方便、更“Angular化”的方式來使用編輯器。從簡單的安裝配置到與 Angular Forms 的整合,可以省略掉前期的設定流程,直接無痛加入並使用。之後再繼續看 ngx-quill 的其他介紹內容。
連假的最後一天,運氣還不錯,儘管前一天晚上豪大雨,但今天的天氣就陰陰的沒有下雨,是涼爽舒服的,去了傳統藝術中心,這次也待了比較多的時間在裡面度過,跟著導覽員去看各種不同的傳統文化,也看了很帥的霹靂布袋戲人偶,不論什麼時候看,精細的程度都不輸專業的模型,但真的很大尊,家裡空間不夠的收一尊就很極限了 XD 期待下次再來逛逛,會有不同的展覽內容。
KillerCodeMonkey/ngx-quill: Angular (>=2) components for the Quill Rich Text Editor (github.com)
@types/quill - npm Package Health Analysis | Snyk
文章同步發表於2023 iThome 鐵人賽
]]>剪貼簿 ( Clipboard Module ),負責處理 Quill Editor 與外部 App 之間的複製、剪下與貼上的操作。Clipboard module 提供了一組預設的判斷邏輯處理貼上的內容,並且我們能進一步通過加入自定義的匹配器(matcher)來調整或擴充這些預設行為。例如:我們可以讓特定的 HTML 標籤或文字段落在貼上時有特殊的格式或行為。
剪貼簿會通過後序遍歷(post-order)相應的 DOM 樹來處理貼上的 HTML,從而建構所有子樹的 Delta 表示形式。在每個子節點,matcher 函數會被呼叫,並傳入 DOM 節點和到目前為止的 Delta 處理,這樣可以讓 matcher 回傳一個修改過的 Delta。要能操作好 matcher,就需要熟悉和理解 Delta。
將自定義 matcher 新增到 clipboard module。使用 nodeType
的 matcher 會先被呼叫,按照它們被加入的順序,另一個是使用 CSS selector 的 matcher,也是按照被加入的順序。nodeType
可能是 Node.ELEMENT_NODE
或 Node.TEXT_NODE
。
方法:
addMatcher(selector: String, (node: Node, delta: Delta) => Delta)addMatcher(nodeType: Number, (node: Node, delta: Delta) => Delta)
範例:
quill.clipboard.addMatcher(Node.TEXT_NODE, function(node, delta) { return new Delta().insert(node.data);});// Interpret a <b> tag as boldquill.clipboard.addMatcher('B', function(node, delta) { return delta.compose(new Delta().retain(delta.length(), { bold: true }));});
在指定的索引位置將由 HTML 片段表示的內容插入到編輯器中。該片段會被剪貼簿的匹配器解釋,這可能不會產生完全相同的輸入 HTML。如果沒有提供插入索引,則會覆蓋整個編輯器的內容。來源可能是 “user”、”api” 或 “silent”。
不正確的處理 HTML 可能會導致跨站腳本攻擊(XSS),而未能正確清理 (sanitize) 則是引發網站漏洞的主要原因之一。明確的命名這個方法,以確保我們能注意到使用這個方法可能涉及的風險。這個命名方式也遵循了 React 框架的例子,React 也有類似的概念,如 dangerouslySetInnerHTML
用來提醒開發者必須謹慎操作。
方法:
dangerouslyPasteHTML(html: String, source: String = 'api')dangerouslyPasteHTML(index: Number, html: String, source: String = 'api')
範例:
quill.setText('Hello!');quill.clipboard.dangerouslyPasteHTML(5, ' <b>World</b>');// 編輯器的 HTML 文本內容會是 '<p>Hello <strong>World</strong>!</p>';
可以將 matcher
陣列傳遞到剪貼簿的配置選項中。這些將附加在 Quill 內建的 matcher
之後。
var quill = new Quill('#editor', { modules: { clipboard: { matchers: [ ['B', customMatcherA], [Node.TEXT_NODE, customMatcherB] ] } }});
語法高亮模組(Syntax Highlighter Module)在 Quill Editor 中用於增強程式碼區塊內容(Code Block)格式。它會自動檢測並套用語法高亮效果,且依賴於 highlight.js 函式庫來解析和標記程式碼區塊。
我們可以根據需求來配置 highlight.js。不過,Quill 要求 useBR
的選項必須設為 false。
範例:
<!-- 引入 highlight.js 樣式表 --><link href="highlight.js/monokai-sublime.min.css" rel="stylesheet"><!-- 引入 highlight.js 函式庫 --><script href="highlight.js"></script> <script>hljs.configure({ // optionally configure hljs languages: ['javascript', 'ruby', 'python']});var quill = new Quill('#editor', { modules: { syntax: true, // Include syntax module toolbar: [['code-block']] // Include button in toolbar }, theme: 'snow'});</script>
剪貼簿(Clipboard)模組在 Quill Editor 中負責處理與外部應用間的複製、剪下和貼上操作。它提供了一套預設行為來解析貼上的內容,並允許開發者透過自定義 matcher
來進一步調整這些行為。這些 matcher
可以按照它們被加入的順序來進行呼叫,而在貼上 HTML 時,剪貼簿會後序遍歷對應的 DOM 樹來創建一個 Delta 表示形式。
基於安全性考慮,提供了一個 dangerouslyPasteHTML
的API,用在確認安全的操作情境下插入 HTML。剪貼簿模組不僅提供了彈性的自訂方式,也考慮到貼上內容的安全處理,使 Quill 更靈活實用。
儘管昨日發現前一晚停車時刮到了輪拱板金,心情受到一些影響之外,整體的行程還不錯,在金車威士忌酒廠待了一整天,雖然之前也去過幾次,但都沒有導覽員,這次趁著人多時,有導覽員的服務,也學到不少威士忌的一些觀念,今天出發前就發了這篇文章,預計會去傳統藝術中心,今天天氣看起來很不錯,要多喝水避免被太陽曬昏頭了 XD
文章同步發表於2023 iThome 鐵人賽
]]>預設值:1000
設定在指定秒數內更改合併成一個更改紀錄。例如當 delay
設為 0
時,幾乎每個字元都會記錄成一次更改,因此使用 undo
就只會取消一個字元。當 delay
設置為 1000 時,undo
將會撤銷最後 1000 毫秒內發生的所有變更。
預設值:100
設定歷史操作紀錄堆疊的最大值。與 delay
選項合併的變更算是一次變更操作。
預設值:false
預設的情況下,無論 source
是 user
或是透過 api
的方式進行的所有變更。都視為同等的操作,並且變更可以從 history
redo
/undo
。如果 userOnly
設為 true
,則只會處理使用者的變更。
const quill = new Quill('#editor', { modules: { history: { delay: 2000, maxStack: 500, userOnly: true } }, theme: 'snow'});
清除 history
的所有堆疊紀錄
方法:
clear()
範例:
quill.history.clear();
通常短時間內連續進行的變更,我們可以透過 delay
設置來合併成為一次歷史紀錄,以便觸發更多的 undo
的變更。使用 cutoff
將重置合併窗口,以便呼叫 cutoff
之前和之後的更改不會被合併。
方法:
cutoff()
範例:
quill.history.cutoff();
取消最後一次的變更操作。
方法:
undo()
範例:
quill.history.undo();
如果上次的操作是 undo
,則還原 undo
。
方法:
redo()
範例:
quill.history.redo();
今天嘗試了初始化的時候加入 History module 的配置設定參數,另外也透過按鈕的方式來呼叫 history module 的 API,也能觀察到其 history stack 的變化,不過目前 @types/quill
的 history 版本似乎沒看到有 history module 的其他屬性,只有加上 API 的定義而已,感覺可以再提一個新 PR 了 XD
昨天運氣不錯,搭客運回宜蘭牽車沒遇到塞車,順利的開車到新竹接朋友出發到宜蘭,這聽起來有點瘋狂,我只是喜歡開車而已XD 不過到了住宿的停車場,因為是機械式的,沒注意到後面兩側還有塗上黃色的支撐桿,今天早上出發前才看到右後輪拱有擦到 Orz 前一晚停車時原本以為是機械車位的地板阻尼之類的作動聲,沒想到是磨擦聲,儘管是老車了,也多少有一些擦傷,但還是免不了會心痛 QQ,找時間再去買幾支板金補漆筆塗一下了,畢竟輪拱最邊緣的地方有一小部分都看到銀色的部分,應該是底漆也有刮掉了 (哭
文章同步發表於2023 iThome 鐵人賽
]]>context
剩下的參數以及設定相關的介紹。當使用者的游標從開始移動到 offset
指定的特定的位置則觸發 handler,例如:當 offset
為 3
時,只有使用者選擇的文字或是游標是在同一行的第三個字元位置開始,才會執行對應的 handler。另外執行的時機點是在使用者輸入內容的時候就已經判定的,因此使用者如果在第 3 個字元時按下按鍵輸入文字,則 handler 會在文字輸入之前就被執行。
quill.keyboard.addBinding( { key: 'o' }, { offset: 2 }, // 當游標在第3個字元前面時觸發 (range, context) => { // 插入特殊符號的代碼 quill.insertText(range.index, '★★★'); });
一個正則表達式(Regex)屬性,用於指定必須與使用者選擇的區域或游標開始位置之前的文字比對的模式。換句話說,當該正則表達式匹配到使用者選擇開始位置前方的文字時,相關的處理函數(handler)才會被觸發。例如,當使用者輸入一個 @
符號,然後按下 k
時,這個 handler 會被觸發。prefix: /@$/
確保了只有當游標(或選取範圍)前方是 @
符號時,這個 handler 才會執行:
quill.keyboard.addBinding({ key: 'k' }, { prefix: /@$/, // 前置文本必須是 @}, (range, context) => { // 這裡實現你的自定義邏輯,例如彈出一個用戶列表以供選擇 console.log("觸發了 @ 符號的自定義行為");});
context.prefix
在這個例子中會是 @
,因為它包含了選擇開始位置之前的整個文字區塊。如此一來,我們就可以在使用者輸入 @
符號後進行特定操作,像是顯示一個下拉清單讓使用者選擇名稱。
和 prefix
的概念相同,只是比對的位置是使用者選擇的內容或游標的位置的後面開始。
預設的情況下,Quill 內建幾個實用的按鍵綁定,例如使用 Tab 鍵進行縮排。我們可以在初始化時加入自訂的按鍵綁定。
有些綁定對於防止瀏覽器的危險預設行為(如 Enter 鍵和 Backspace 鍵)是必要的。不能移除這些綁定以恢復到瀏覽器的原生行為。然而,由於在配置中指定的綁定會在 Quill 的預設綁定之前運行,您可以處理特殊情況並將其傳播給 Quill。
使用 quill.keyboard.addBinding
加入綁定不會在 Quill 的預設綁定之前運行,因為到那時預設綁定已經被加入。
每個綁定配置必須包含鍵(key)和處理器(handler)選項,並且可以選擇性地包括任何 context
選項。
const bindings = { // 這將覆蓋名為 'tab' 的預設綁定 tab: { key: 9, handler: function() { // 處理 Tab 鍵 } }, // 沒有名為 'custom' 的預設綁定, // 因此這將會被新增,而不會覆蓋任何內容 custom: { key: 'B', shiftKey: true, handler: (range, context) => { // 處理 Shift + B } }, // 當按 Backspace 鍵並且格式為 list 時 list: { key: 'backspace', format: ['list'], handler: (range, context) => { if (context.offset === 0) { // 若在 list 的第一個字元上按 Backspace, // 則移除該列表 this.quill.format('list', false, Quill.sources.USER); } else { // 否則,傳給 Quill 做預設處理 return true; } } }};// 初始化 Quill,並指定 keyboard module 的綁定var quill = new Quill('#editor', { modules: { keyboard: { bindings: bindings } }});
和 DOM event 相同,Quill key binding 在每次比對時都會阻擋呼叫,因此為一個非常普通的按鍵綁定一個複雜的 handler 不是一個好的實現方式。在套用像是滑鼠移動或卷軸滾動的 DOM 事件時,盡可能的套用性能較好的實現以確保一定品質的使用者體驗。
這兩天探討了 Quill 的 Keyboard module,讓我們可以自定義鍵盤事件的處理。Quill 的 keyboard module 主要有兩種用途:
我們也了解如何使用不同的 context
參數來更精細的控制 handler 的觸發時機,包含游標的位置、目前使用中的格式、以及前後緊鄰的文本內容等。
此外,keyboard module 也提供了豐富的設定選項,讓我們可以在初始化時加入自訂的綁定,或是覆蓋Quill 的預設綁定。
今天午餐後整理一下文章,晚一點就要出發去載朋友來宜蘭玩了,希望不要塞車塞得太嚴中 XD。昨天看了同事的分享會 Feedback,看到很多有趣的回應。其中還有提到下班後學習這件事,我認為學習是屬於個人的事情,至於有沒有要求下班後學習這件事,最終決定權還是在自己手上。若真的有興趣的而且學到之後能讓自己在上班的過程更順暢也能克服一些挑戰,我想這個學習過程應該是相當精彩的,儘管最後發現也許是個坑,但這都是成長的一部分。
文章同步發表於2023 iThome 鐵人賽
]]>Keyboard module 支援特定 context 中鍵盤事件的自訂行為。Quill 使用 Keyboard module 來綁定格式化快捷鍵並防止一些瀏覽器副作用。
Keyboard handler 綁訂到特定的按鍵與修飾鍵。key
是 JavaScript event 的 key code,但也允許英文字母與數字鍵,以及常用的按鍵的字串縮寫設定。常見的修飾鍵例如:metaKey
,ctrl
,shift
,以及 alt
等。另外 shortKey
是指特定平台的修飾鍵,像是 MacOS 上的 metaKey
,以及 Linux 和 Windows 上的 ctrlKey
。
我們可以將指定的按鍵和修飾鍵綁定到一個 handler。當這個鍵被按下時,handler 就會被執行,並將使用者選擇的範圍傳入以及綁定到 keyboard module 當下的 instance:
quill.keyboard.addBinding({ key: 'B', shortKey: true}, function(range, context) { quill.formatText(range, 'bold', true);});// addBinding 也能只帶入一個參數,並加上 handlerquill.keyboard.addBinding({ key: 'B', shortKey: true, handler: function(range, context) { quill.formatText(range, 'bold', true); }});
這個範例是當使用者按下 B
鍵加上修飾鍵(Mac 上的 metaKey
或 Windows 和 Linux 上的 ctrlKey
)時,選取的文字會變粗體。
我們還可以設定更多的條件,讓 handler 只在特定的情境下被呼叫。例如,當使用者選擇的是一個空行或者是列表項目時,才會觸發相對應的 handler:
// 如果使用者在 list 或 blockquote 的開頭按了 ctrl + d,// 則刪除 list 或 blockquote 的格式quill.keyboard.addBinding( { key: 'd', shortKey: true }, { collapsed: false, format: ['blockquote', 'list'], offset: 0, }, function (range, context) { console.log('backspace pressed'); if (context.format.list) { quill.format('list', false); } else { quill.format('blockquote', false); } });
不過需要注意的地方是,當編輯器初始化之後才加入的 keyboard binding,需要確認內建的部分是否也有監聽,否則會因為按鍵事件發生時逐條比對條件的關係,就被前面的規則代為執行了。例如 backspace
的 keycode
是 8
:
如果為 true
則當使用者的游標停在編輯器上,在沒有選擇任何文字的情況下觸發 handler。collapsed
翻成中文是收折的意思,但實際上就是指游標停在編輯器上並沒有選取任何文字的狀態。
如果為 true
,當使用者的游標在一行空白的時候會觸發。設為 false
則代表非空行,另外當 empty
為 true
時,意思就是 collapsed
也要是 true
,且 offset
必須是 0
,這樣才是真正完全的一行空白。
例如當使用者換行的時候加上一個星星符號:
quill.keyboard.addBinding({ key: 'enter' }, { empty: true // 只在空行觸發}, function(range, context) { // 插入特殊符號的代碼 this.editor.insertText(range.index, '★');});
format
這個參數用來控制 handler 在哪些特定的格式條件下會被觸發。
format
是一個陣列時,如果當前活動(active)的格式中包含陣列裡面指定的任何一種格式,則會觸發 handler:quill.keyboard.addBinding({ key: Keyboard.keys.ENTER }, { format: ['bold', 'italic'] // 只要文字是粗體或斜體,處理函數就會觸發}, function(range, context) { // 插入特殊符號的代碼 this.editor.insertText(range.index, '★');});
format
是一個物件:所有指定的格式條件必須全部滿足,handler 才會觸發。quill.keyboard.addBinding({ key: Keyboard.keys.ENTER }, { format: { bold: true, italic: true } // 當文字是粗體且斜體,處理函數才會觸發}, function(range, context) { // 插入特殊符號的代碼 this.editor.insertText(range.index, '★');});
在任何情況下,context
參數的 format
屬性都會是一個物件,其中包含了所有當前活動的格式。這個物件的結構和 quill.getFormat()
回傳的結構是相同的。
今天研究了 Quill 的 keyboard module,並了解如何加入自訂的 keyboard binding,也看到 context
的內容有哪些可以讓我們運用,明天接著看 context
其他的參數介紹。
今天下班前,不小心把弄了一陣子的 Git stash 給 drop 掉了,然後又因為是在 Git Graph 上執行,所以也沒有留意到 hash 的部分,當下真的有 BBQ 的感覺,不死心的我花了一點時間研究,總算找到解法,第一次使用 git fsck
,搭配 sh 腳本執行,把碎片找回來從裡面去翻之前改過的程式片段,找到後來改的內容,趕快把 hash 記下來,接著 apply,逝去的青春終於又回來了(誤。這故事給了我一個教訓,以後還是乖乖建 commit 吧…Orz
文章同步發表於2023 iThome 鐵人賽
]]>工具列模組 (Toolbar module) 可以讓使用者輕鬆的將文本內容套用格式。Toolbar 除了初始化設置要開啟的功能後直接渲染,我們也可以自行定義 container 內容以及工具列功能的處理器 (handler)。
Toolbar 的設定方式可分為兩種,一種是指定 toolbar 的容器 (container),並視需求加上 HTML 控制項以及對應的處理器 (handler),另一種則是直接使用陣列來設置。
透過指定 container 的設置方式:
<p>toolbar-practice works!</p><div #myToolbar></div><div #quillContainer></div>
@ViewChild('quillContainer') quillContainer!: ElementRef;@ViewChild('myToolbar') myToolbar!: ElementRef;quill!: Quill;ngAfterViewInit(): void { this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: { container: this.myToolbar.nativeElement, handlers: { bold: (value: boolean) => { console.log('value', value); this.quill.format('bold', value); }, } } } });}
toolbar
也可直接給 container
的 id selector
,這裡我們直接用 template reference
:
const quill = new Quill(this.quillContainer.nativeElement, { modules: { // Equivalent to { toolbar: { container: '#toolbar' }} toolbar: this.myToolbar.nativeElement } });
可以看到直接指定 container
之後,就可以直接渲染,但因為沒有設定要放哪些功能按鈕在工具列上,所以現在看到是還沒有任何按鈕的:
工具列的控制項可以帶入控制項名稱的陣列或自定義 HTML 容器來指定。
從基本的陣列來設定 toolbar
開始:
const toolbarOptions = ['bold', 'italic', 'underline', 'strike']; this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: toolbarOptions } });
控制項也能放在巢狀陣列來表示設定分組,這樣可以將同一組的控制項放在 className
為 ql-formats
的 <span>
標籤下以提供佈景主題利用,例如在佈景主題 snow
) 時,就會在這些分組間加上間距,方便使用者進行操作:
const toolbarOptions = [['bold', 'italic'], ['link', 'image']];
另外可以使用一個物件來指定自定義值的按鈕,並將格式名稱作為 key:
const toolbarOptions = [{ 'header': '3' }];
上面這個範例會在工具列上加入一個按鈕,這個按鈕代表的是 header
格式,並且按鈕會套用 3
這個自定義的值。換句話說,當點擊這個按鈕時,選定的文字會變成第三級標題 <h3>
。
下拉選單也是透過物件來定義,但與其他元素不同的地方在於,這裡會用一個陣列來存入所有可能的選項值。下拉選單選項的視覺表現(例如文字標籤或顏色)是由 CSS 來控制的。
例如,設定字體大小 size
的選項:
// Note false, not 'normal', is the correct value // quill.format('size', false) removes the format, // allowing default styling to work const toolbarOptions = [ { size: [ 'small', false, 'large', 'huge' ]} ];
size
陣列中的 false
是用於移除格式,也就是把文字的大小回到預設的狀態。
某些佈景主題,例如 Snow,會為下拉選單(如顏色和背景格式)提供預設值。當設定空的陣列在 color
或 background
時, Snow 將預設提供 35 種顏色選項:
const toolbarOptions = [ ['bold', 'italic', 'underline', 'strike'], // toggled buttons ['blockquote', 'code-block'], [{ 'header': 1 }, { 'header': 2 }], // custom button values [{ 'list': 'ordered'}, { 'list': 'bullet' }], [{ 'script': 'sub'}, { 'script': 'super' }], // 升冪與降冪 [{ 'indent': '-1'}, { 'indent': '+1' }], // 縮排與減少縮排 [{ 'direction': 'rtl' }], // text direction [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown [{ 'header': [1, 2, 3, 4, 5, 6, false] }], [{ 'color': [] }, { 'background': [] }], // dropdown 從 theme 獲取預設值 [{ 'font': [] }], [{ 'align': [] }], ['clean'] // 移除格式 ]; this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: toolbarOptions }, theme: 'snow' });
如果需要對工具列更多的客製化,也能直接用 HTML 來手動創建工具列。只需要將 DOM 元素或選擇器傳遞給 Quill 即可。ql-toolbar
類會被添加到工具列容器中,而 Quill 會自動為具有 ql-${format}
格式名稱的 <button>
和 <select>
元素附加對應的內建 handler。
<!-- Create toolbar container --> <div #myToolbar><!-- Add font size dropdown --> <select class="ql-size"> <option value="small"></option> <!-- Note a missing, thus falsy value, is used to reset to default --> <option selected></option> <option value="large"></option> <option value="huge"></option> </select> <!-- Add a bold button --> <button class="ql-bold"></button> <!-- Add subscript and superscript buttons --> <button class="ql-script" value="sub"></button> <button class="ql-script" value="super"></button></div><div #quillContainer></div> <!-- Initialize editor with toolbar -->
this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: this.myToolbar.nativeElement } });
當我們提供自己的 HTML 元素作為 Quill 的工具列時,Quill 會尋找特定的輸入元素來綁定功能。然而,除了 Quill 會自動識別和處理的元素外,你仍然可以加入和設計與 Quill 無關的自定義輸入元素。這些自定義的輸入元素可以和 Quill 的元素共存,且不會產生衝突。
<div #myToolbar><!-- Add buttons as you would before --> <button class="ql-bold"></button> <button class="ql-italic"></button> <!-- But you can also add your own --> <button class="custom-button" (click)="doSomething()">do something</button></div><div id="editor"></div>
this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: this.myToolbar.nativeElement }});
工具列的控制項預設會套用或移除格式,但我們也可以用自定義的 handler 來取代行為,例如顯示外部的使用者介面。
Handler function 會綁定到工具列,並且傳入輸入元素的 value
屬性。如果相對應的格式是非啟動狀態,則會傳入 false
。加入自定義的 handler 會覆寫預設的工具列和主題行為。
const toolbarOptions = { handlers: { // handlers 物件會與預設的 handler 物件合併 link: (value: string) => { if (value) { const href = prompt('Enter the URL'); this.quill.format('link', href); } else { this.quill.format('link', false); } }, }}this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: toolbarOptions }});// Handler 也可以在初始化之後加入const toolbar = this.quill.getModule('toolbar');toolbar.addHandler('image', showImageUI);
在上面的範例,為 link
格式定義了一個 custom handler。當使用者點擊工具列的連結按鈕時,會彈出一個提示框可以輸入URL。當使用者輸入URL 則會把選取的文字變成一個連結。反之當使用者取消操作會移除文字的連結格式。另外,可以在 Quill 初始化之後,動態地加入更多的 handler,如 showImageUI
這個函數用來處理圖像插入的 UI。
Quill 在初始化的時候,就算沒有給任何的 toolbar 設定,也會提供預設的功能選項直接使用,設定 toolbar 主要有兩種方式:
container
ql-*
class 名稱的 HTML 帶入對應的內建功能透過這些方式,我們可以靈活設計和調整 Quill 編輯器的工具列來滿足各種需求。
今天對於北北基桃來說是個再正常不過的上班日了,但不知道為啥就沒有上班的氛圍,我想應該是新竹以南的夥伴們都在家防颱吧。沒有會議的一天可以完全專注的在開發工作上,雖然過程也遇到一些意外的挑戰,但還好都有初步解決了。明天還有一天班,之後就要好好充電休息一下,但還是要持續發文到 15 號了,希望扛的住…XD
文章同步發表於2023 iThome 鐵人賽
]]>Module 允許 Quill 的操作行為與功能實現客製化。有幾個官方支援的模組可供選擇,其中一些還有額外的配置選項和API。目前官網列出支援的模組有:Toolbar
,Keyboard
,History
,Clipboard
,以及Syntax Highlighter
。各章節也都會提到如何使用以及有哪些 API 可供操作。
要啟用模組只需要把要使用的模組加到 Quill 的配置中即可:
const quill = new Quill('#editor', { modules: { 'history': { // Enable with custom configurations 'delay': 2500, 'userOnly': true }, 'syntax': true // Enable with default configuration } });
Clipboard,Keyboard 和 History 模組是 Quill 所必需的,不需要明確設定就預設在裡面了,但也可以像其他模組一樣進行設定。
模組也可以繼承和重新註冊,替換掉原本的模組。甚至原本預設內建的必要模組也能重新註冊來做替換。例如繼承 clipboard 模組並自訂一些功能:
const Clipboard = Quill.import('modules/clipboard'); const Delta = Quill.import('delta'); class PlainClipboard extends Clipboard { convert(html = null) { if (typeof html === 'string') { this.container.innerHTML = html; } let text = this.container.innerText; this.container.innerHTML = ''; return new Delta().insert(text); } } Quill.register('modules/clipboard', PlainClipboard, true); // Will be created with instance of PlainClipboard const quill = new Quill('#editor');
上面這個範例只是為了解釋 module 提供的可能性。單純用既有模組提供的 API 或 config 通常會更容易些。在這個 clipboard
模組擴充的操作範例中,用現有的 addMatcher
其實就能夠滿足大部分的情境需求了。
今天介紹了 Quill 的 module,強調其客製化,以及可繼承並擴展。Quill 內建了許多豐富的 module,讓我們可以按照需求選擇和配置。繼承的部分則允許開發者擴充新功能並替換原有的模組,同時也提到單純使用既有的 API 或設定也許就能滿足大部分的需求。在這個章節,我們了解如何利用 module 來啟用 Quill 的功能,並依照實際需求進行繼承及擴充自訂功能,之後來介紹並研究一下第三方的開源套件要如何使用,以及他們是如何實現自訂功能的。應該能有不少收穫。
最近上下班的運氣都還不錯,儘管有下雨,但出門跟下班回家的這段時間都是無雨的,今天又去了整復保養一下,然後再去看中醫,弄得時間有點晚。這次的颱風感覺也是來者不善,放颱風假就乖乖待在家,看點書追個劇也好。我明天要繼續去上班了 QQ
文章同步發表於2023 iThome 鐵人賽
]]>提供除錯用的靜態方法,可以開啟指定層級的 log 訊息,例如:error
,warn
,log
,或 info
。
傳入 true
等同於傳入 log
,傳入 false
則是關閉所有 log 訊息。
方法:
Quill.debug(level: String | Boolean)
範例:
Quill.debug('info');
將指定的擴充功能或模組引入 Quill。
方法:
Quill.import(path): any
範例:
const Parchment = Quill.import('parchment');const Delta = Quill.import('delta');const Toolbar = Quill.import('modules/toolbar');const Link = Quill.import('formats/link');// 類似 ES6 的 import 語法: `import Link from 'quill/formats/link';`
用於註冊 module、theme 或 format。可以讓我們擴充和自定義 Quill 的功能。註冊執行之後可以使用 Quill.import
獲取。使用路徑前綴 ‘formats/‘、’modules/‘ 或 ‘themes/‘ 分別註冊 formats
、modules
或 themes
。對於 format
,可以直接帶入且路徑將自動生成。也會覆蓋掉具有相同路徑的定義。
方法:
Quill.register(format: Attributor | BlotDefinintion, supressWarning: Boolean = false)Quill.register(path: String, def: any, supressWarning: Boolean = false)Quill.register(defs: { [String]: any }, supressWarning: Boolean = false)
範例:
// 自訂一個空 moduleconst Module = Quill.import('core/module');class CustomModule extends Module {}Quill.register('modules/custom-module', CustomModule);
register
方法使 Quill 的功能更加彈性和可擴展,允許開發人員自定義格式、模組和主題,進而更滿足特定的應用需求。
註冊之後要留意一下初始化的
options
裡面是否也有加入 custom-module!
在 Quill container 內加入一個容器元素 (container element) 並回傳,作為編輯器本身的同層元素。通常 Quill 模組都會有以 ql- 當作前綴的 class name。選擇性的參數 refNode
,表示容器的插入位置應該在這個 refNode
之前。
方法:
addContainer(className: String, refNode?: Node): ElementaddContainer(domNode: Node, refNode?: Node): Element
範例:
// 使用 className 加入 container elementconst container = quill.addContainer('ql-custom');// 使用 element reference 取得的 DOMaddContainerWithNativeElement(quill: Quill, nativeElement: HTMLElement) { const toolEditor = document.querySelector('.ql-editor'); console.log('addContainerWithNativeElement'); quill.addContainer(nativeElement, toolEditor);}
因為是在 Angular 專案上,所以建議還是使用 @ViewChild
取得 element reference,如此一來在套用 CSS 樣式的時候,就不需要再加上像 ::ng-deep
的方式套用, 避免影響子元件樣式。
使用 element reference 加上指定位置後的效果:
取得已加入 Quill instance 的模組。
方法:
getModule(name: String): any
範例:
const toolbar = quill.getModule('custom-module');
Quill 在擴充功能的部分提供了幾個 API,包含了模組引入、除錯、註冊,也能加入自訂的 container element,並直接獲取 Quill instance 裡面指定的模組,稍微整理一下:
debug
:靜態方法用於開啟不同層級的 log 訊息,有助於開發和除錯。import
:用於回傳 Quill library、格式、模組或主題的靜態方法。使自定義和擴充變得非常靈活。register
:這個方法允許註冊和定義自己的模組、主題或格式,提高 Quill 的可擴展性。addContainer
:允許在 Quill 容器內新增容器元素,使得界面結構更加靈活。getModule
:取得已經加入到編輯器的模組,有助於模組的管理和操控。大多數情況下,靜態方法如 register
和 import
最好是在 new Quill()
之前使用,以確保在初始化 Quill 時能夠使用這些自定義 module 或定義。而 debug
則可以根據實際需要來決定使用的時機。
今天整理文章的時候,看到新聞上寫有颱風名字叫做小犬,於是心血來潮查了一下,別問我為什麼要查 XD
根據教育部的辭典網站釋義:
1)幼小的狗。清.孔尚任《桃花扇》第四○齣:「行到那舊院,何用輕敲,也不怕小犬哰哰。」
2)謙稱自己的兒子。《紅樓夢》第一三回:「待服滿後,親帶小犬到府叩謝。」也作「豚犬」、「豚兒」。
貌似第一次聽到這樣的命名,以前的名字都滿酷的,但最近的颱風名稱似乎有點微妙。聽說小犬一點都不小,大家要做好防颱措施阿…QQ
文章同步發表於2023 iThome 鐵人賽
]]>透過 Model API 找到的 Blot 物件是 LinkedList
的資料結構:
這是一個靜態方法,可以代入 DOM 節點並回傳 Quill 或 Blot Instance。在後者的情況下,對 bubble
參數傳入 true 會向上尋找目標 DOM 的祖先,直到找到相應的 Blot。
方法:
Quill.find(domNode: Node, bubble: boolean = false): Blot | Quill
範例:
find(quill: Quill, container: HTMLElement) { // 帶入 container 尋找並取得 quill instance const target = Quill.find(container); console.log('target is quill instance', target === quill); // 編輯器輸入連結文字並嘗試取得 link node quill.insertText(0, 'Hello, World!', 'link', 'https://google.com'); const linkNode = container.querySelector('a'); const findLinkNode = Quill.find(linkNode!); console.log('linkNode', findLinkNode);}
回傳從文件開頭到帶入的 blot 之間的距離長度。
方法:
getIndex(blot: Blot): Number
範例:
// 預先輸入文字並取得第 10 個字元的 blotquill.insertText(0, 'Hello, World!');const [line, offset] = quill.getLine(10);console.log('line', line);// 帶入 blot 取得 indexconst index = quill.getIndex(line); // index + offset should == 10console.log('index', index);console.log('offset', offset);
回傳文件中指定索引處的葉節點。leaf
通常指的是資料結構中的末端節點。
方法:
getLeaf(index: Number): Blot
範例:
quill.setText('Hello Good World!');quill.formatText(6, 4, 'bold', true);const [leaf, offset] = quill.getLeaf(7);// leaf 會是帶有值為 "Good" 的葉節點// offset 應為 1,因為回傳的葉節點在索引 6 開始console.log('leaf', leaf);console.log('offset', offset);
回傳帶入的索引值指定位置的行 blot 。
方法:
getLine(index: Number): [Blot, Number]
範例:
quill.setText('Hello\nWorld!');const [line, offset] = quill.getLine(7);// line 應為代表第二個 "World!" 行的 Block Blotconsole.log('line', line);// offset 為 1,因為 index 7 是在第二行 "World!" 的第二個字元console.log('offset', offset);
回傳指定位置的行中所包含的 blot。
方法:
getLines(index: Number = 0, length: Number = remaining): Blot[]getLines(range: Range): Blot[]
範例:
quill.setText('Hello\nGood\nWorld!');quill.formatLine(1, 1, 'list', 'bullet');const lines = quill.getLines(2, 5);// 帶有 ListItem 與 Block Blot 的陣列// 代表是前面的兩行console.log('lines', lines);// 帶入 range 物件const linesByRange = quill.getLines({ index: 8, length: 5 });console.log('linesByRange', linesByRange);
今天看了 Model 提供的 API,這些 API 主要是用於尋找 Blot 的相關應用,對於未來要自訂編輯器模組的功能實現時,可以利用這些 API 來找到正確的 Blot 並進一步處理文本內容,並且 Blot 提供的是 linkedList
的資料結構,因此對於節點的尋找來說,未來編輯內容量大的時候,可以研究看看linkedList
訪問節點的技巧來實現較有效率的搜尋處理。
Quill 的觀念基本上不難,較有挑戰的地方在於未來要滿足各種特殊需求時,要建立自訂的 Blot 必須要很清楚底層的生命週期與處理過程,這樣才能打造出高效且實用的自訂功能。找時間再繼續研究使用一些第三方套件,並嘗試了解這些套件是如何實現的,對於自訂功能的實現與優化應該會有所幫助。
再整理一下今天嘗試的 API:
find
:透過 DOM 節點找到 Quill 或 Blot 實例。getIndex
:回傳文件開頭到指定 blot 之間的距離。getLeaf
:回傳指定索引處的葉節點。getLine
:回傳指定索引位置的行 blot。getLines
:回傳指定位置內的所有行 blot。今天突然意識到,這星期上完班之後,又是一個連續假期,這次的假期是要回宜蘭帶朋友四處走走,雖然住在宜蘭很久了,但還是有不少地方沒去過,趁這個機會去走走看。不過 11 月就完全沒有連假了,週末期望能好好的學習,並嘗試一些新玩意兒,還有買了一些書,要好好的閱讀一番。
文章同步發表於2023 iThome 鐵人賽
]]>text-change
事件在編輯器的內容發生變化時觸發。變更的細節、變更前的內容,以及變更的來源都會提供出來。來源如果是使用者觸發的,則 source
就會是 user
。例如:
特例:
觸發內容變更的事件雖然也可能透過 API 呼叫,但如果觸發的原因是使用者操作導致的話,source
仍然要設為 user
。舉個例子:當使用者點擊工具欄的模組功能,該模組會呼叫變更的 API,但由於是使用者點擊所造成的變化,因此我們在模組呼叫 API 的時候,帶入的 source
仍必須是 user
。
Silent Source:
呼叫 API 處理的內容變更也可能以 source
為 silent
的方式觸發,在這樣的情況下 text-change
將不會被觸發。不建議這樣的操作,因為這樣可能會導致撤銷的堆疊紀錄異常,或是間接影響到需要完整內容變化紀錄的功能。
選取 (Selection) 發生變化
文字內容的變化可能導致 selection 變化(例如,打字使游標前進),但是在 text-change
handler 執行期間,selection 尚未更新,加上原生瀏覽器的行為可能導致 selection 狀態不一致的情況。因此要使用 selection-change
或 editor-change
來處理 selection 更新比較穩定。
Callback Signature:
handler(delta: Delta, oldContents: Delta, source: String)
範例:
quill.on('text-change', function(delta, oldDelta, source) { if (source == 'api') { console.log("An API call triggered this change."); } else if (source == 'user') { console.log("A user action triggered this change."); } });
當使用者或 API 造成 selection 變更時觸發,range
代表 selection 的邊界。當 range
為 null
時,表示 selection 的丟失(通常是由於編輯器失去焦點)。我們也可以在收到 range
是 null
的時候,用這個事件當作焦點變更的 event 確認。
API 造成的選取範圍變更也可能會以 source
為 silent
觸發,在這樣的情況下就不會觸發 selection-change
。如果 selection-change
是 side effect 的話就很有用。例如:輸入文字造成 selection 變更,但每個字元都觸發 selection-change
的話就可能會造成干擾。
Callback Signature:
handler(range: { index: Number, length: Number }, oldRange: { index: Number, length: Number }, source: String)
範例:
quill.on('selection-change', function(range, oldRange, source) { if (range) { if (range.length == 0) { console.log('User cursor is on', range.index); } else { const text = quill.getText(range.index, range.length); console.log('User has highlighted', text); } } else { console.log('Cursor not in the editor'); }});
當觸發 text-change
或 selection-change
事件時,也會跟著觸發 editor-change
,即使 source
是 silent
也是一樣。第一個參數是事件名稱,不是 text-change
就是 selection-change
,之後的通常是傳遞給這些相應的 handler 參數。
Callback Signature:
handler(name: String, ...args)
範例:
quill.on('editor-change', function(eventName, ...args) { if (eventName === 'text-change') { // args[0] will be delta } else if (eventName === 'selection-change') { // args[0] will be old range }});
監聽特定的事件並加入 event handler。
方法:
on(name: String, handler: Function): Quill
範例:
quill.on('text-change', function() { console.log('Text change!'); });
為事件的一次觸發加入 event handler。
方法:
once(name: String, handler: Function): Quill
範例:
quill.once('text-change', function() { console.log('First text change!');});
移除 event handler
方法:
off(name: String, handler: Function): Quill
範例:
function handler() { console.log('Hello!');}quill.on('text-change', handler);quill.off('text-change', handler);
Quill 提供了三種事件監聽類型分別是 text-change
,selection-change
,以及 editor-change
,整理一下今天練習的 event 方法:
text-change
與 selection-change
的變更。前幾天在 DefinitelyTyped 提的 Quill PR 終於合併了,目前只要重新 npm install 就能夠把 OP 類型錯誤的問題解決了,要確認一下 types 的版本是 2.0.12
。久違的 OpenSource contribution XD 希望對大家有所幫助 :D
文章同步發表於2023 iThome 鐵人賽
]]>移除編輯器的 focus
狀態,從使用這的角度來看就是輸入文字的游標離開編輯器。
方法:
blur()
範例:
quill.blur();
控制編輯器是否能讓使用者進行輸入。當編輯器在 disabled
狀態時,不影響 source
為 api
與 slient
的 API 呼叫。
方法:
enable(enabled: boolean = true)
範例:
quill.enable();quill.enable(false); // 禁用使用者輸入
將編輯器設為禁用編輯狀態,如同上面的範例所提到的,相當於 enable(false)
的意思。
將焦點回到編輯器上,游標會停留在上一次離開 (blur
) 的地方。
方法:
focus()
範例:
quill.focus();
確認焦點是否在編輯器的輸入範圍,這邊需要留意的是焦點在 toolbar
或是 tooltip
時,都不算在編輯器。
方法:
hasFocus(): Boolean
範例:
quill.hasFocus();
同步檢查編輯器的使用者更新,並在發生修改時觸發事件。對於有協作需求要解決衝突時,需要最新的狀態下相當實用。Source
的來源可以是 user
,api
, 以及 silent
。
由於這主要是用於線上共筆時可能造成編輯衝突時,可以透過 update
方法來同步編輯器的狀態,因此這之後如果有機會再來嘗試看看。
方法:
update(source: String = 'user')
範例:
quill.update();
稍微回顧一下今天研究 Editor 相關的 API:
enable(false)
,禁止使用者輸入。update
的操作沒辦法立即呈現之外,大部分的 API 都還滿淺顯易懂的。今天邊塞車邊寫文章,還好有弟弟幫忙開車,早上七點多有驚無險的避免了一場危險,前面的車子似乎快睡著了又沒有打開車道維持輔助,導致車子直接嚕到中央護欄,還好沒翻車,雖然沒看到左側的鈑金狀況,但應該是滿慘的。再次證明了保持車距的重要性。
文章同步發表於2023 iThome 鐵人賽
]]>這個方法非常實用,用於獲取指定索引或選取範圍的界限(bounds)。並返回一個包含界限的物件,裡面會有 left
,top
,width
以及 height
屬性,分別代表指定索引的左上角位置和尺寸。通常用來定位游標或選取的範圍在編輯器容器內的位置。例如可以利用這個方法決定在編輯器內容旁邊或是游標的位置顯示自訂的選單或者工具提示。
方法:
getBounds(index: Number, length: Number = 0): { left: Number, top: Number, height: Number, width: Number }
範例:
先在編輯器的元素下面新增一個 tooltip
標籤:
<div id="tooltip" style="display: none; position: absolute; background-color: lightgray;"> 我是一個小提示 </div>
實現監聽事件,在文本選取的時候判斷選取的位置來顯示 tooltip:
// 監聽文本選擇事件quill.on('selection-change', function(range) { if (range) { if (range.length > 0) { // 獲取選擇範圍的界限 const bounds = quill.getBounds(range.index, range.length); // 定位和顯示小提示 const tooltip = document.getElementById('tooltip'); tooltip.style.left = bounds.left + 'px'; tooltip.style.top = (bounds.top + bounds.height) + 'px'; tooltip.style.display = 'block'; } else { // 隱藏小提示 const tooltip = document.getElementById('tooltip'); tooltip.style.display = 'none'; } }});
效果如下:
獲取編輯器中當前的選取範圍。可帶入 optional 參數 focus
,如果為 true
,則獲取焦點之後返回選取的範圍 index
與 length
,如果為 false
,則返回 null
。
方法:
getSelection(focus = false): { index: Number, length: Number }
範例:
var range = quill.getSelection();if (range) { if (range.length == 0) { console.log('User cursor is at index', range.index); } else { var text = quill.getText(range.index, range.length); console.log('User has highlighted: ', text); }} else { console.log('User cursor is not in editor');}
設置編輯器中的選取範圍,這也會使編輯器是在 focus
的狀態。如果傳入的參數為 null
,則會離開焦點並觸發 blur
事件。
方法:
setSelection(index: Number, length: Number = 0, source: String = 'api')setSelection(range: { index: Number, length: Number }, source: String = 'api')
範例:
quill.setSelection(0, 5);
稍微整理一下:
getBounds
: 獲取指定索引的座標或界限資訊,常用於定位游標或選取範圍在編輯器內的位置。getSelection
: 獲取編輯器中當前的選取範圍,可用於判斷用戶選取的內容或游標位置。setSelection
: 設置編輯器中的選取範圍,使編輯器處於 Focus 狀態。今天詳細探討了編輯器的 Selection 功能,Quill 提供了選取範圍相關的控制方法,到目前為止,無論是文本樣式或是內容選取,可以看到大部分的操作都離不開 index
與 length
,而 range
是一個滿方便使用的參數,可以知道選取的起點以及選取的長度,以便我們在自訂功能的時候可以做為位置索引的參考。
已經好幾年沒有玩過手機遊戲了,最近上下班通勤時間都會玩一款新的手機遊戲叫 Monster Hunter Now,整體還滿有趣的,是 Niantic 也就是開發 Ingress 以及 Pokemon Go 的開發商,這遊戲也是要走出去戶外實際去看地圖上有哪些資源以及魔物,也能夠與其他玩家來進行遊戲。不過有個小缺點,就是當 HP 不夠的時候需要使用藥水,那個藥水除了每天提供五罐免費的之外,其他時間若喝完的話,就需要等時間回復或者直接打開線上商城買藥水道具,就是要課金的意思,但畢竟是休閒,沒血的話就乖乖的等待回滿再繼續被魔物虐了 XD
文章同步發表於2023 iThome 鐵人賽
]]>根據使用者當前選擇的字串套用文字格式,回傳的 Delta 代表變更的內容。當使用者選擇字串長度為 0
時,代表是游標的狀態,對應的文字樣式則會變成啟動狀態,使用者接下來輸入的內容則會套用啟動的文字樣式。Source
一樣可以設定 user
,api
或 silent
。當呼叫的時候如果編輯器為禁用(disabled) 狀態,則會直接略過 source
為 user
的呼叫。
source
預設是 api
方法:
format(name: String, value: any, source: String = 'api'): Delta
範例:
quill.format('color', 'red');quill.format('align', 'right');
將選到的行數套用樣式,回傳的 Delta 代表變更的內容。關於可使用的樣式有哪些,可以參考官網文件 formats描述。這個方法主要是處理區塊 (block) 樣式,當呼叫的時候如果帶入的樣式是屬於行內 (inline) 樣式,則會沒有效果。要移除格式的話直接在 value
的參數傳入 false
即可。另外套用區塊樣式的時候,可能會在套用後導致使用者當前的選擇被取消,並且游標移動到新的位置。
source
預設是 api
方法:
formatLine(index: Number, length: Number, source: String = 'api'): DeltaformatLine(index: Number, length: Number, format: String, value: any, source: String = 'api'): Delta formatLine(index: Number, length: Number, formats: { [String]: any }, source: String = 'api'): Delta
範例:
quill.setText('Hello\nWorld!\n');quill.formatLine(1, 2); // 沒有給樣式的話,預設套用的樣式是 boldquill.formatLine(1, 2, 'align', 'right'); // 第一行置右quill.formatLine(4, 4, 'align', 'center'); // 兩行都置中// 套用多個區塊樣式quill.formatLine(0, 5, { list: 'bullet', align: 'right',});
最主要的差別就是 format
用在更改選取範圍內的特定格式,例如字體大小、顏色、粗體等。而 formatLine
處理的是整行的樣式,例如列表、對齊方式等。
一樣是在編輯器針對選定的範圍套用文字的樣式,回傳的是內容變更的 Delta,如果要移除文字樣式,則直接在對應樣式的值帶入 false
即可移除。如果是操作 block 相關的樣式,使用者的選擇範圍可能不會保留。Source
的來源有 user
,api
,以及 silent
,當編輯器為 disabled
狀態則會直接無視 source
為 user
的呼叫。
source
預設是 api
方法:
formatText(index: Number, length: Number, source: String = 'api'): Delta formatText(index: Number, length: Number, format: String, value: any, source: String = 'api'): DeltaformatText(index: Number, length: Number, formats: { [String]: any }, source: String = 'api'): Delta
範例:
quill.setText('Hello\nWorld!\n');quill.formatText(0, 5, 'bold', true); // 將 Hello 設為粗體quill.formatText(0, 5, { // 將 Hello 解除粗體,並設為藍色 'bold': false, 'color': 'rgb(0, 0, 255)' });quill.formatText(5, 1, 'align', 'right'); // 將 Hello 的那一行置右
這個方法可以讓我們查詢特定範圍內文字的格式。如果範圍內的所有文字共用相同的格式,則會回傳該格式。如果有不同的真值 (truthy value),則會回傳所有的真值在陣列中。當不帶參數呼叫此方法,將針對當前使用者選取的範圍進行操作。
source
預設是 api
方法:
getFormat(range: Range = current): { [String]: any }getFormat(index: Number, length: Number = 0): { [String]: any }
範例:
// 假設設定一段文字 Hello World!,並設定樣式quill.setText('Hello World!');quill.formatText(0, 2, 'bold', true);quill.formatText(1, 2, 'italic', true);quill.getFormat(0, 2); // { bold: true }quill.getFormat(1, 1); // { bold: true, italic: true }quill.formatText(0, 2, 'color', 'red');quill.formatText(2, 1, 'color', 'blue');quill.getFormat(0, 3); // { color: ['red', 'blue'] }quill.setSelection(3);quill.getFormat(); // { italic: true, color: 'blue' }quill.format('strike', true);quill.getFormat(); // { italic: true, color: 'blue', strike: true }quill.formatLine(0, 1, 'align', 'right');quill.getFormat(); // { italic: true, color: 'blue', strike: true, // align: 'right' }
將選定的範圍內刪除所有的格式及嵌入內容,並回復到沒有格式的狀態。回傳的 Delta 代表變更的操作,如果範圍內包含到 block format,也會一併移除。因此使用者的選取狀態可能不會被保留。Source
可以是 user
,api
或 silent
。當編輯器為 disabled
狀態時,source
為 user
的呼叫將會被忽略。
source
預設是 api
方法:
removeFormat(index: Number, length: Number, source: String = 'api'): Delta
範例:
quill.setContents([ { insert: 'Hello', { bold: true } }, { insert: '\n', { align: 'center' } }, { insert: { formula: 'x^2' } }, { insert: '\n', { align: 'center' } }, { insert: 'World', { italic: true }}, { insert: '\n', { align: 'center' } }]);quill.removeFormat(3, 7);// 編輯器在執行之後內容會變成// [// { insert: 'Hel', { bold: true } },// { insert: 'lo\n\nWo' },// { insert: 'rld', { italic: true }},// { insert: '\n', { align: 'center' } }// ]
今天嘗試使用格式化相關的 API,基本上使用的方式都差不多,但我們還沒討論到 Format 還有哪些可以使用,剛才的介紹中也有提到這篇文件有列出所有支援的格式,要找時間來實驗看看並感受一下。
明天就開始一小段假期,不過按照往年的慣例,都會先回去宜蘭開車回台南拜拜,希望這次塞車不要塞的太久QQ,儘管早上四點半就起床,五點就出門了,到了七八點還是會開始塞。印象中過台中之前都滿大的機會遇到塞車的情況,還好可以跟弟弟輪流開,不至於累到不行 XD
祝中秋佳節愉快 :)
文章同步發表於2023 iThome 鐵人賽
]]>將嵌入式內容插入編輯器,return 為更改後的 Delta 物件。source
可以是 user
、api
或 silent
。當編輯器是 disabled
狀態時,當 source
設為 user
的呼叫則會被忽略。
index
可以選擇要插入的位置索引值方法:
insertEmbed(index: Number, type: String, value: any, source: String = 'api'): Delta
範例:
quill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png');
顧名思義將文字插入編輯器,可以選擇使用指定格式或多種格式。return 收到的是更新後的 Delta 物件。source
可以是 user
、api
或 silent
。當編輯器 disabled
時,source
為 user
的呼叫將直接略過。
方法共有三種,後兩者的差別在於 format 可以設一個或多個文字格式。
insertText(index: Number, text: String, source: String = 'api'): Delta insertText(index: Number, text: String, format: String, value: any, source: String = 'api'): Delta insertText(index: Number, text: String, formats: { [String]: any }, source: String = 'api'): Delta
範例:
quill.insertText(0, 'Hello'); quill.insertText(3, 'Hello', 'bold', true); quill.insertText(8, 'Quill', { 'color': '#ffff00', 'italic': true });
將參數的內容覆蓋編輯器。內容必須以換行符號 \n
結尾。return 收到的是更新後的 Delta。如果給定 Delta 沒有無效操作,這將與傳入的 Delta 相同。source
可以為 user
、api
或 silent
。當編輯器是 disabled
狀態時,當source
為 user
的呼叫則會被忽略。
方法:
setContents(delta: Delta, source: String = 'api'): Delta
範例:
// 使用 new Delta() 新增 Delta 物件const delta = new Delta() .insert('This is a title') .insert('\n', { header: 1 }) .insert('This is a subtitle \n', {header: 2, color: 'red' }) .insert('The description is Hello World', { bold: true, color: 'purple', });quill.setContents(delta);
上面這個範例可以觀察到套用 header
的變化,除了從 text-change
觀察到的套用方式,如果想要在一個 insert
就實現樣式與 header
格式套用,可以在文字內容的最後加上換行符號,這樣加上 header
在 attribute
上才會有效果。
將純文字內容覆蓋到編輯器,return 收到的是更新後的 Delta,文字內容必須以換行符號做結尾,沒有加上的話,編輯器會另外加上。與 setContents
不同的是,setText
只能將純文字覆蓋到編輯器,而 setContents
的文字內容可以包含不同的格式。source
可以為 user
、api
或 silent
,預設是 api
。當編輯器是 disabled
狀態時,當source
為 user
的呼叫則會被忽略。
方法:
setText(text: String, source: String = 'api'): Delta
範例:
quill.setText('Hello\n');
將 Delta 資料更新到編輯器,return 收到的是更新操作的 Delta。如果傳入的 Delta 沒有不合法的操作,return 收到的 Delta 則會是相同的內容。舉例來說,當編輯器沒有內容,但仍然執行 retain(6)
的話,實際上回傳的 Delta 中的 retain
會只有 1,因為空白的編輯器會預設一個換行符號,因此長度只有 1
可以 retain
。
另外,即使執行 delete(5)
,收到的 Delta 變化也不會有看到 ops 中有 delete
的操作,畢竟編輯器沒有內容可以讓我們刪除。
方法:
updateContents(delta: Delta, source: String = 'api'): Delta
範例:
// 假設編輯器當前的內容 [{ insert: 'Hello World!' }]quill.updateContents(new Delta() .retain(6) // Keep 'Hello ' .delete(5) // 'World' is deleted .insert('Quill') .retain(1, { bold: true }) // Apply bold to exclamation mark);// 編輯器現在會變成 [// { insert: 'Hello Quill' },// { insert: '!', attributes: { bold: true} }// ]
在實際看過每個方法及體驗過使用方式後,對於 Contents API 的運用有初步的認識,並在不同的情境下選擇適合的 API ,透過帶入不同參數的呼叫方式實現功能,我們也可以在特殊情況自訂 source
來決定保留或跳過編輯器的觸發機制,明天接著進入到 Formatting 的章節,也就是套用文字格式。
最近午餐跟著其他同事點外賣,不過也許是上班日的關係,在尖峰時段單點東西似乎特別容易漏掉,漏餐的話,幫忙開團的同事還要確認是否有其他同事也沒拿到,然後還要處理退款的申請,再次感謝願意開團的同事 XD。看來以後在尖峰時段還是盡量點套餐比較保險…也許吧XD
文章同步發表於2023 iThome 鐵人賽
]]>Source
在閱讀技術文件的時候,有部分的 function
會提供 Source
的參數名稱,稍微研究了一下。
大部分的情況是不需要自訂設定這些值,只有在較特殊的情況下,需要額外設置以更精確的控制編輯的行為。例如:
// 使用 'api' 作為 source,代表這個更改是由 API 控制的quill.format('bold', true, 'api'); // 使用 'user' 作為 source,代表這個更改是模擬使用者操作quill.format('italic', true, 'user');
透過這樣的分別使用,我們可以在事件監聽或其他處理邏輯中區分更改的來源,進而執行不同的操作或處理。例如我們只對使用者所做的更改進行特定的處理,並忽略由 API 控制的更改。透過 source
的設置就讓我們滿足這樣的需求。
刪除的來源可以是從 user
, api
或 silent
。當編輯器狀態為 disabled 時,會直接忽略掉從 user
來的呼叫
方法:
deleteText(index: Number, length: Number, source: String = 'api'): Delta
範例:
quill.deleteText(4, 6) // 從第 4 個位置,刪除長度 6 的內容
獲取編輯器的內容以及格式資料,收到的是 Delta 物件。可選參數有兩個:
index
:指定獲取內容的起始索引,預設是從 0
length
:指定要獲取內容的長度,預設 remaining
是指從起始索引後的剩餘內容方法:
getContents(index: Number = 0, length: Number = remaining): Delta
範例:
// 獲取完整內容的 Deltaconst delta = quill.getContents();// 獲取部分內容的 Deltaconst delta = quill.getContents(27, 5);
獲取編輯器內容的長度。需要注意的是,即使 Quill 為空,仍然有一個由 ‘\n’ 表示的空行,因此 getLength 將返回 1。
方法:
getLength(): Number
範例:
const length = quill.getLength();
獲取編輯器的字串內容,非字串的內容會直接省略,因此返回的字串長度可能會比呼叫 getLength
回傳的編輯器長度短些。這邊一樣要留意的是,即使編輯器是空的沒有內容,仍然會留一個空行,所以在這樣的情況將會返回 \n
。
index
:指定獲取內容的起始索引,預設是從 0
length
:指定要獲取內容的長度,預設 remaining
是指從起始索引後的剩餘內容方法:
getText(index: Number = 0, length: Number = remaining): String
範例:
// 獲取從 0 開始,長度為 10 的文本內容const text = quill.getText(0, 10);
今天開始仔細閱讀技術文件,會相對的比較乏味,但是能徹底的去看每個方法及參數要如何使用,知道自己有哪些武器可以用,對於特殊的需求也比較能找到合適的方法來實現。
由於中秋節快到了,開始收到各種月餅禮盒,周遭也開始出現柚子,雖然還沒開始烤肉,但希望中秋之後別長太多肥肉出來,剛轉換跑道一陣子,還在適應節奏的階段,需要找到合適的運動時間,目前看來只剩下早上了,下班後加上通勤時間到健身房,運動完回家洗完澡也差不多到睡覺的時間了,最近嘗試調整起床的時間,先從六點半開始觀察看看囉…XD
文章同步發表於2023 iThome 鐵人賽
]]>