-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
1943 lines (935 loc) · 572 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>Day 30:使用 Parchment 實現類似 Medium 的編輯器 - Videos & Tweets</title>
<link href="/2023/10/15/quill-day-30/"/>
<url>/2023/10/15/quill-day-30/</url>
<content type="html"><![CDATA[<p>今天來到了挑戰的最後一天,接著把剩下的 Videos 和 Tweets 等自訂 Blot 體驗一遍。</p><h2 id="Videos"><a href="#Videos" class="headerlink" title="Videos"></a>Videos</h2><p>我們將以和 images 的實現方式來實現 Videos。從第一個直覺或許可以使用 HTML 的 <code><video></code> 標籤,但我們無法用這種方式來播放 Youtube 的影片,考慮到 Youtube 影片是目前主流看影片的其中一種方式,我們就用 <code><iframe></code> 標籤來實現。如果希望多個 Blot 使用相同的標籤,除了 <code>tagName</code> 之外,我們還可以使用 <code>className</code>,下一個 Tweets 練習會示範這個部分。</p><p>另外,我們將支援對寬度與高度,作為未註冊的 Formats。特定於 Embeds 的 Formats 不需要單獨註冊,只要它們與已註冊的 Formats 沒有命名空間的衝突即可。這樣可以運作是因為 Blots 只是將未知的 Foramts 傳遞給其子元素,最終達到葉節點。這也允許不同的 Embeds 以不同的方式處理未註冊的 Formats。例如,我們之前的插入圖片可能會跟我們在這裡的 Videos 以不同的方式來識別和處理寬度格式。</p><pre><code class="typescript">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); } }}</code></pre><p>新增 VideoBlot 之後,和前面幾次練習一樣,註冊到 Quill,並且加上對應的 Click Event 到 Component:</p><pre><code class="html"> <button type="button" title="video" id="video-button" (click)="addVideo()"> <i class="fa fa-play"></i> </button></code></pre><pre><code class="typescript"> 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 ); }</code></pre><p>點擊按鈕嵌入 Youtube 影片之後,可以看到編輯器的內容加了一個 <code>iframe</code> 標籤:</p><p><img src="/2023/10/15/quill-day-30/20090749bmGrHQyGj4.png" alt="編輯器的內容加了一個 `iframe` 標籤"></p><p>如果打開 dev tool 使用 <code>getContents</code> 方法來查看編輯器內容,Quill 會回傳 Video 的 Delta 內容像這樣:</p><pre><code class="json">{ ops: [{ insert: { video: 'https://www.youtube.com/embed/QHH3iSeDBLo?showinfo=0' }, attributes: { height: '170', width: '400' } }]}</code></pre><h2 id="Tweets"><a href="#Tweets" class="headerlink" title="Tweets"></a>Tweets</h2><p>Medium 支援多種嵌入類型,但我們練習就只專注於 Tweets。Tweet Blot 的實現方式與 images 幾乎完全相同。我們利用 Embed Blots 不一定要對應到一個空(void)節點的行為。它可以是任何自定義的節點,Quill 會將其視為一個空節點,而不遍歷其子節點或後代節點。這使我們可以使用一個 <code><div></code>,並讓原生的 Twitter Javascript 函式庫在我們指定的 <code><div></code> 容器內運作。</p><p>由於我們的根 Scroll Blot 也使用了一個 <code><div></code>,所以我們還指定了一個 className 來消除歧義。需要注意的是,Inline Blots 預設使用 <code><span></code>,而 Block Blots 預設使用 <code><p></code>。因此,如果想讓自定義的 Blots 使用這些標籤,除了指定 <code>tagName</code> 之外,還要帶上一個 <code>className</code>。</p><p>我們使用 Tweet id 作為定義我們 Blot 的值。同樣的,在 Click Event handler 一樣帶入固定值來方便練習。</p><pre><code class="typescript">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']; }}</code></pre><p>上面這個範例程式中,如果直接加上 <code>twttr</code> 應該會出現 TypeScript 不認得的錯誤訊息,<code>twttr</code> 是 Twitter platform widgets.js 函式庫提供的,因此我們這邊就先使用 <code>declare</code> any 來定義它的型別:</p><pre><code class="typescript">declare var twttr: any;</code></pre><p>此外,我們還需要加上 Twitter widgets 的 JS script,這邊我們可以利用 Angular 提供的 <code>Renderer2</code> 來插入外部的 <code>script</code>,當然也可以直接在 <code>index.html</code> 的檔案上加入:</p><pre><code class="typescript">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); }</code></pre><p>建立好 TweetBlot 之後,我們一樣進行 Quill 註冊以及綁定 click event 對應的按鈕:</p><pre><code class="html"> <button type="button" title="tweet" id="tweet-button" (click)="addTweet()"> <i class="fa-brands fa-twitter"></i> </button></code></pre><p>使用 Quill Instance 提供的方法取得游標位置,並插入 TweetBlot 的區塊:</p><pre><code class="typescript"> 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 ); }</code></pre><p>點擊後的效果:</p><p><img src="/2023/10/15/quill-day-30/20090749OJWSQiyGaN.png" alt="點擊後的效果"></p><h2 id="最後的潤色"><a href="#最後的潤色" class="headerlink" title="最後的潤色"></a>最後的潤色</h2><p>我們從一堆按鈕和只能理解純文本的 Quill 核心開始。通過 Parchment,我們能夠添加粗體、斜體、連結、引用區塊、標題、分隔線、圖片、影片,甚至是 Tweets。所有這些都能在維持一個可預測且一致性的文件實現,這使我們能夠使用 Quill 的 API 來處理這些新的格式和內容。</p><p>讓我們為這個範例加上一些最後的潤色。雖然它不能與 Medium 的 UI 相比,但還是盡可能的去貼近它。</p><p>最後的效果,當選擇文本時會顯示工具列:</p><p><img src="/2023/10/15/quill-day-30/20090749aoYxsaa9cE.png" alt="顯示工具列"></p><p>游標換行之後停在最前面的時候顯示插入內容按鈕,點擊之後可以展開內容:</p><p><img src="/2023/10/15/quill-day-30/200907499NssOAmiEm.png" alt="點擊之後可以展開內容"></p><p>具體的程式碼變更可以參考對應的 commit 紀錄。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>終於來到了第 30 天,最後用了這個練習把 Quill 的東西初步都摸過了一遍。今天把剩下的練習像是 Video, Tweets 這些自訂的 Blot 插入,並把整個 UI 改成像是 Medium 的編輯器風格。從中學到不少東西,也因為很久沒碰 Angular,有一些對我來說可能是新的東西也派上用場,目前的實現並不是最好的實現方式,因為官方提供的範例是直接用 jQuery,那我想在 Angular 專案的話,應該要透過 Angular 的生態系統下來實現正確的 UI 操作方式,這個未來可以在持續的探討。同時有機會的話也可以改成 Signal 的版本 XD</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天下午繼續參加週末的 MNH 社群日,遇到很多低等的會想挑戰高階的魔物,想挑戰的心態值得嘉許,但更多的可能是想蹭並獲取材料,對於等級可能剛剛好可以應付魔物的玩家,如果遇到這樣的情況就會很尷尬,因為不一定能夠扛著住,所以現在加入一個組隊之後,都要先觀望一下隊友的等級,才知道這場會不會又整個翻車,畢竟藥水真的不便宜 QQ</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/87471dd20edfb511016b37facb434a65c25499f6">今日份的練習:加入 Videos, Tweets</a></li><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/4bc1769ecf17e035cd0a56d4555fb1f03e81ea6d">今日份的練習:改成 Medium like UI</a></li><li><a href="https://quilljs.com/guides/cloning-medium-with-parchment/#videos">Cloning Medium with Parchment - Quill (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10340029">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 29:使用 Parchment 實現類似 Medium 的編輯器 - Dividers & Images</title>
<link href="/2023/10/14/quill-day-29/"/>
<url>/2023/10/14/quill-day-29/</url>
<content type="html"><![CDATA[<p>昨天體驗了基本的行內格式 Blot 以及區塊格式 Blot,今天繼續實現類似 Medium 編輯器的最後四個部分,分別為分隔線、圖片、影片、以及推文的自訂功能實現。</p><h2 id="分隔線-Dividers"><a href="#分隔線-Dividers" class="headerlink" title="分隔線 (Dividers)"></a>分隔線 (Dividers)</h2><p>接下來的步驟中,我們將實作第一個所謂的「葉子 Blot (Leaf Blot) 」。不同於先前我們練習過的 Blot,這些主要是負責文本格式化—例如定義文字的外觀或調整排列,並實作<code>format()</code>方法。Leaf Blot 的主要職責則是提供特定的內容,並透過實作 <code>value()</code> 方法來達成。</p><p>Leaf Blot 可以是文本 (Text) 型態或嵌入 (Embed) 型態的 Blot。在本例中,我們會實作一個屬於嵌入型態的 Blot,即分隔線 (Divider)。值得注意的是,一旦 Embed Blot 建立,其內含的值將會是不可變的 (Immutable) 。因此,如果你需要變更這個 Blot 的內容,則必須先將其從文本中刪除,再重新插入新的內容。</p><p>首先我們新增一個 TS 檔當作 Leaf Blot 的練習,並加入 Divider 的 Blot:</p><pre><code class="typescript">import Quill from 'quill';const BlockEmbed = Quill.import('blots/block/embed');export class DividerBlot extends BlockEmbed { static blotName = 'myDivider'; static tagName = 'hr';}</code></pre><p>我們的 click handler 呼叫了 <code>insertEmbed()</code> 方法,這個方法不像 <code>format()</code> 那麼方便可以確定、保存和恢復使用者的選擇區域。因此我們需要自行做一些額外的工作來維護這個選擇區域。此外,當我們嘗試在一個 Block 的中間插入一個 Block Embed 時,Quill 會自動為我們將該 Block 分割開來。為了讓這個行為更為明確,我們會在插入分隔線之前明確地插入一個換行符,以自行分割該 Block。</p><p>建立 DividerBlot 之後,回到 Component 註冊 DividerBlot 並新增 <code>addDivider</code> 方法:</p><pre><code class="typescript">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 );}</code></pre><p>接著將對應的 button 加上事件綁定:</p><pre><code class="html"> <button type="button" title="divider" id="divider-button" (click)="addDivider()" > <i class="fa fa-minus"></i> </button></code></pre><p>輸入兩行 Hello World 之後,游標停留在第一行的 Hello 後面,並點擊加入分隔線,可以看到 HTML 被強制換行後加入分隔線:</p><p><img src="/2023/10/14/quill-day-29/20090749xs4eCKsO7y.png" alt="強制換行後加入分隔線"></p><h2 id="圖片"><a href="#圖片" class="headerlink" title="圖片"></a>圖片</h2><p>圖片的處理可以使用我們在建立 Link 和 Divider blots 時所學到的概念來新增。我們會使用一個物件作為圖片的值來展示如何被支援的。我們用於插入圖像的 click handler 直接帶入 hardcode 的內容來專注在插入圖片 Blot 的實現。</p><p>建立 ImageBlot,分別有 <code>create</code> 以及 <code>value</code> 兩個靜態方法:</p><pre><code class="typescript">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'), }; }}</code></pre><p>接著在 Component 加上插入圖片的 handler,並綁定到對應的 button:</p><pre><code class="html"><button type="button" title="image" id="image-button" (click)="addImage()"> <i class="fa fa-camera"></i></button></code></pre><pre><code class="typescript">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 ); }</code></pre><p>看一下加入圖片後的效果:</p><p><img src="/2023/10/14/quill-day-29/20090749KoNw5w1Re2.png" alt="加入圖片後的效果"></p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天主要就兩個 Embed Blot 的實現,我們透過繼承 Quill 底下的 Parchment Embed Blot 來建立自定義的 Blot,對於 Quill 的方法及應用有比較深入的理解。 整體的實現上都是與 DOM 去做對應在編輯器中加入內容,因此都會經過 <code>Create()</code> 方法來新增 DOM,如果是簡單的 HTML,沒有太多的加工處理,則直接帶上 <code>blotNmae</code> 和 <code>tagName</code> 即可,按照官網文件的說明,Quill 的確也讓編輯器的內容與結構盡可能的單純易懂。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>這個週末是魔物獵人 Now 的社群日,有期間限定的櫻火龍,貌似對拿弓箭的玩家來說是不錯的裝備材料收集,準備好今天的文章之後,等等就要出去晃晃,希望不會太快就把藥水喝完 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/02143d0c84a93c8c4a6e254fb52e083bc6a100e4">今日份的練習</a></li><li><a href="https://quilljs.com/guides/cloning-medium-with-parchment/#dividers">Cloning Medium with Parchment - Quill (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10339493">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 28:使用 Parchment 實現類似 Medium 的編輯器 - Basic Formatting & myBlot</title>
<link href="/2023/10/13/quill-day-28/"/>
<url>/2023/10/13/quill-day-28/</url>
<content type="html"><![CDATA[<p>昨天我們新增了一個元件並初始化 Quill 的核心,今天繼續實現 Medium 編輯器的練習。</p><h2 id="實作基礎格式"><a href="#實作基礎格式" class="headerlink" title="實作基礎格式"></a>實作基礎格式</h2><p>我們之前提到過,Inline 不貢獻任何格式。這是為 Inline 基礎類別所制定的例外,而不是規則。基本的 block Blot 和區塊級的元素 (Block level element) 的運作方式相同。<br>要實作粗體和斜體,我們只需要繼承 Inline,設定 blotName 和 tagName,並註冊到 Quill 中即可。有關繼承和靜態方法和變數的內容介紹可以參考 Parchment 的介紹。</p><pre><code class="typescript">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';}</code></pre><p>這裡跟著 Medium 的範例使用 <code>Strong</code> 以及 <code>em</code> 標籤,但我們也可以使用 <code>b</code> 和 <code>i</code> 標籤。Quill 將使用 blot 的名稱當作格式名稱,透過註冊我們的 Blot,我們現在可以在新格式上使用 Quill 的完整 API:</p><pre><code class="typescript">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);}</code></pre><p>接著將按鈕的 <code>click</code> 事件加上,這邊為了示範方便,我們直接寫死一個 <code>true</code> 在程式裡面,這樣就會一直是加上格式的操作。在 App 中,我們可以使用 <code>getFormat()</code> 來尋找指定範圍內的文本格式,來決定是否新增或刪除格式。Toolbar 模組因為 Quill 已經實現了,就不在這重新實作。</p><p>兩個按鈕都點擊之後的效果如下:<br><img src="/2023/10/13/quill-day-28/20090749XSOo5OU7wF.png" alt="都點擊之後的效果"></p><h2 id="實作連結-Link"><a href="#實作連結-Link" class="headerlink" title="實作連結 (Link)"></a>實作連結 (Link)</h2><p>與其他格式(如粗體或斜體)不同,Link 需要存入更多資訊,特別是 URL。這主要影響到「Link blot」的兩個方面:建立和格式檢索。</p><ol><li><strong>建立(Creation)</strong>: 當建立一個 Link 時,除了表示它是一個 Link 外,還需要加上 URL。這通常會以字串的形式來表示。</li><li><strong>格式檢索(Format Retrieval)</strong>: 當需要找出或修改一個已存在的 Link 格式時,除了知道它是一個Link 外,我們還需要取得或修改 Link 的 URL。</li></ol><p>雖然 URL 通常以字串的型別存入,但也可以用其他方式來表示,例如以一個包含 URL Key value 的物件。這樣做可以允許我們加入其他的 Key/Value 來定義一個連結,提供更多自定義的選項。</p><p>新增一個 Link Blot:</p><pre><code class="typescript">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'); }}</code></pre><p>Component 加入新的註冊和方法:</p><pre><code class="typescript">registerBasicFormatting() { Quill.register(BoldBlot); Quill.register(ItalicBlot); Quill.register(LinkBlot);}</code></pre><p>考慮到安全性,這裡我們可以在 <code>constructor</code> 注入 Angular 提供的 <code>DomSanitizer</code> “消毒” (sanitize)輸入的 URL 避免 XSS 問題發生:</p><pre><code class="typescript">constructor(private sanitizer: DomSanitizer) {}addLink() { const url = prompt('請輸入 URL'); const safeUrl = this.sanitizer.sanitize(SecurityContext.URL, url); this.quillInstance.format('myLink', safeUrl);}</code></pre><p>接著嘗試選取文本內容,並點擊加入連結的按鈕,輸入網址後可以看到效果:</p><p><img src="/2023/10/13/quill-day-28/20090749DdXqJnTjSl.png" alt="加入連結效果"></p><h2 id="區塊引用-Blockquote-與標題-Headers"><a href="#區塊引用-Blockquote-與標題-Headers" class="headerlink" title="區塊引用 (Blockquote) 與標題 (Headers)"></a>區塊引用 (Blockquote) 與標題 (Headers)</h2><p>Blockquotes 繼承自 Block,這是基本的 Block Blot(一種自定義的文本塊)。與 Inline blots 不同的是,Block Blots 不能被嵌套。如果對同一範圍的文字套用多個 Block blots,它們不會互相包裹,而是會相互替換。也就是說,新套用的 Block Blot 會取代原有的 Block Blot。</p><p>建立 BlockquoteBlot:</p><pre><code class="typescript">const Block = Quill.import('blots/block');export class BlockquoteBlot extends Block { static blotName = 'myBlockquote'; static tagName = 'blockquote';}</code></pre><p>註冊 Blot:</p><pre><code class="typescript">Quill.register(BlockquoteBlot);</code></pre><p>Header 的實作方式完全相同,只有一處不同:它可以由多個 DOM 元素表示。預設情況下,格式的值將成為 tagName,而不僅僅是 true。我們可以透過擴充 formats() 來自訂,類似於我們對連結所做的那樣:</p><pre><code class="typescript">export class HeaderBlot extends Block { static blotName = 'myHeader'; static tagName = ['H1', 'H2']; static formats(node: HTMLElement) { return HeaderBlot.tagName.indexOf(node.tagName) + 1; }}</code></pre><p>為了方便測試,加入 CSS 的部分:</p><pre><code class="scss">::ng-deep h1, ::ng-deep h2 { margin-top: 0.5em; color: purple;}::ng-deep blockquote { border-left: 4px solid #111; padding-left: 1em;}</code></pre><p>最後在 Component 加入 event function,再和 template 的 <code>click</code> 事件綁定:</p><pre><code class="typescript">addBlockquote() { this.quillInstance.format('myBlockquote', true);}addHeader1() { this.quillInstance.format('myHeader', 1);}addHeader2() { this.quillInstance.format('myHeader', 2);}</code></pre><p>輸入不同段落的內容後,點擊按鈕試試看套用格式效果,可以看到對應的 HTML 元素也被成功加入了,並且套用了設定好的 CSS Style:</p><p><img src="/2023/10/13/quill-day-28/200907499JOpQmtui3.png" alt="套用了設定好的 CSS Style"></p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天嘗試跟著實現自訂的 Inline Blot 和 Block Blot,實際操作過一遍會比較有感覺,官方文件提供的範例是 JavaScript,那我們就直接以 Angular 的專案當作練習,以 Angular 的方式來實現對應的功能。對於自訂的 Blot 內容有進一步的理解,明天再接著練習後面的其他功能。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>轉眼間就來到第 28 天了,時間真的過的很快,也平安度過試用期(?)。但對於很多細節和產業的觀念還是持續學習中,白天工作內容的轟炸與考古,晚上則持續學習及寫文章做紀錄,上週的連假則是邊出去旅遊,回到住宿的地方後,繼續準備文章內容,腦袋裝了滿滿的東西。生活的節奏也比以往要快了許多,從進辦公室開始工作,回過神來就快下班了,除了充實,還是充實 XD…</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/0a2256e1d4601c4e5c8a388572cc03ce5c77e7ec">今日份的練習</a></li><li><a href="https://quilljs.com/guides/cloning-medium-with-parchment/">Cloning Medium with Parchment - Quill (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10338783">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 27:使用 Parchment 實現類似 Medium 的編輯器 - 準備工作</title>
<link href="/2023/10/12/quill-day-27/"/>
<url>/2023/10/12/quill-day-27/</url>
<content type="html"><![CDATA[<p>昨天透過 Parchement 新增了三種不同的 Attributor,今天來看要如何在編輯器上運用。官方文件介紹嘗試實現一個類似 Medium 的功能編輯器,今天就來逐步練習看看。</p><h2 id="實現-Medium-編輯器"><a href="#實現-Medium-編輯器" class="headerlink" title="實現 Medium 編輯器"></a>實現 Medium 編輯器</h2><p>為了提供一致的編輯體驗,我們需要同時具有一致的資料及可預測的行為,然而這兩項是 DOM 都沒有的。現代編輯器的解決方案是維護自己的文件模型來表示其內容。對於 Quill 來說,Parchment 就是這樣的一個解決方案。它在自己的 library 中有組織的架構,並有屬於自己的 API。透過 Parchment,我們就可以自定義 Quill 能夠識別的內容與格式,或者加入全新的格式。</p><p>在官網這份指南中,我們將使用 Parchment 和 Quill 提供的基礎模組來複製 Medium 上的編輯器。我們將從 Quill 的最基本架構開始,不涉及任何 Theme,額外的模組或格式。在這個基礎上,Quill 只能理解純文本。但跟著這份指南做到最後,連結,影片甚至推文都能被 Quill 所辨別。</p><h2 id="準備工作"><a href="#準備工作" class="headerlink" title="準備工作"></a>準備工作</h2><p>剛開始我們不使用 Quill,而只需要 <code>textarea</code> 及按鈕。並且將按鈕加上 event listener。文件的介紹是使用 jQuery 來實現,但我們就直接在 Angular 專案下來做這個練習囉。另外還需要 <a href="https://fonts.google.com/">Google Fonts</a> 和 <a href="https://fontawesome.io/">Font Awesome</a> 為練習的專案加上一些基本樣式。這些都和 Quill 或 Parchment 沒有直接關係,這部分就快速帶過。首先新增一個練習用的 Component,之後分別將 HTML 以及 CSS 加到 Component。</p><p>HTML :</p><pre><code class="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></code></pre><p>CSS:</p><pre><code class="scss">* { 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;}</code></pre><p>Component 我們只加了一個 <code>formatBold</code> 方法來和 template 做事件綁定:</p><pre><code class="typescript">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!'); }}</code></pre><p>執行 <code>serve</code> 指令之後確認渲染的結果:</p><p><img src="/2023/10/12/quill-day-27/20090749CWYQwIo4Xl.png" alt="執行 `serve` 指令之後確認渲染的結果"></p><h2 id="加入-Quill-核心"><a href="#加入-Quill-核心" class="headerlink" title="加入 Quill 核心"></a>加入 Quill 核心</h2><p>接下來,我們將用 Quill 核心取代文字區域,去除主題、格式和無關模組。打開 Dev tool,在編輯器中輸入內容時檢查示範。可以看到 Parchment 文件的 base building block 正在執行中。</p><p>HTML 的部分,將剛才加入的 <code>textarea</code> 改成 <code>div</code> 並帶入範本參考變數 (Template Reference Variable) <code>editorContainer</code>, 例如:</p><pre><code class="html"><div class="editor-container" #editorContainer>Tell your story...</div></code></pre><p>由於換成 <code>div</code>,所以 <code>editor-container</code> class 也有做了小更動:</p><pre><code class="scss">.editor-container { border: 1px solid #ccc; font-family: 'Open Sans', Helvetica, sans-serif; font-size: 1.2em; height: 200px; margin: 0 auto; width: 450px;}</code></pre><p>存檔重新整理之後,嘗試在編輯區域打字,可以看到 Quill 核心正在執行中:</p><p><img src="/2023/10/12/quill-day-27/20090749800mBZwczC.png" alt="在編輯區域打字"></p><p>就像 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 下。</p><p>這時我們無法僅透過輸入文本來觀察 Inline Blot,因為它不會為文件提供有意義的結構或格式。有效的 Quill 文件必須規範 (canonical) 且緊湊 (compact)。只有一棵有效的 DOM 樹可以表示給定的文件,並且該 DOM 樹包含最少數量的節點。</p><p>由於 <code><p><span>Text</span></p></code> 和 <code><p>Text</p></code> 代表著相同的內容, 前者是無效的,Quill 的優化過程之一就是拆開 <code><span></code>. 同樣地,一旦我們加入格式,<code><p><em>Te</em><em>st</em></p></code> 和 <code><p><em><em>Test</em></em></p></code> 也是無效的,因為它們不是最緊湊的表示方式。</p><p>因為這些限制,<strong>Quill 無法支援任意 DOM 樹和 HTML 變更</strong>。但正如我們將看到的,這種結構提供的一致性和可預測性使我們能夠輕鬆建立豐富的編輯體驗。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天開始嘗試從無到有實現 Quill 的基本功能,官網文件介紹是使用 jQuery 當作範例,但因為我們主要是在 Angular 的專案上開發,所以範例的部分都融入了 Angular 元件的生命週期,使用起來更貼近實際的開發情況。明天繼續練習 Basic Formatting 以及自訂 blot 的部分。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天中午吃飯經過南港展覽館,看到人比平常還多就知道這週末有展期了,分別是世界貓咪博覽會,還有攝影器材暨影音創作設備展,台灣戶外用品展,共有三個展覽同步在今天開始,如果是貓奴、有在玩影音創作相關設備或是時常在露營的人,感覺進去錢包就會被榨乾 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/5994068535154158c7fb31418ff6058ffeaf661e">今日份練習</a></li><li><a href="https://quilljs.com/guides/cloning-medium-with-parchment/">Cloning Medium with Parchment - Quill (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10338673">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 26:關於 Pachment 的 Attributors</title>
<link href="/2023/10/11/quill-day-26/"/>
<url>/2023/10/11/quill-day-26/</url>
<content type="html"><![CDATA[<p>之前有探討過 Parchment 與 Blot,而 Parchment 還有一個屬性器 (Atrributors),今天就來看一下關於 Attributor 的介紹以及使用方式。</p><p>Attributor 是另一種更輕量的表示格式的方式。與其對應的就是 DOM attribute。就像 DOM 屬性與節點的關係一樣,屬性也屬於 Blot。在 Inline 或 Block blot 上呼叫 <code>formats()</code> 如果有對應的 DOM 節點以及 DOM 節點 <code>attribute</code> 則將回傳其表示的格式。</p><h2 id="Attributor-Class"><a href="#Attributor-Class" class="headerlink" title="Attributor Class"></a>Attributor Class</h2><p>首先我們來看一下 Attributor 的介面:</p><pre><code class="typescript">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);}</code></pre><p>需要留意的地方是,自訂的 attributor 是 instance,而不是像 blot 一樣的 class 定義。與 Blot 相似,我們不會想要從頭開始建立,而是希望使用既有的 attributors 實現,例如基礎屬性器 (base Attributor),類別屬性器 (Class Attributor) 或樣式屬性器 (Style Attributor)。另外我們也可以透過<a href="https://github.com/quilljs/parchment/tree/main/src/attributor">原始碼</a>來看 attributor 的實現,其實沒有很複雜。</p><h2 id="Attributor"><a href="#Attributor" class="headerlink" title="Attributor"></a>Attributor</h2><p>使用 Attributor 來表示格式:</p><pre><code class="typescript">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></code></pre><p>可以看到我們直接以 <code>new Attributor()</code> 的方法來新增一個實體化 <code>width</code> 屬性後,以 <code>Quill.register()</code> 註冊 attribute,並且呼叫 <code>add</code> 方法將屬性加到 <code>img</code> DOM 上。然後可以透過 <code>value()</code> 取得目標 DOM 的 <code>width</code>,最後使用 <code>remove()</code> 將 <code>width</code> 從 <code>imageNode</code> 刪除。</p><h2 id="Class-Attributor"><a href="#Class-Attributor" class="headerlink" title="Class Attributor"></a>Class Attributor</h2><p>使用 Class Attributor 的方式來表示格式:</p><pre><code class="typescript">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></code></pre><p>有別於上一個 <code>new Attributor()</code>,一樣是 <code>new</code> 但後面換成是 <code>ClassAttributor</code>,帶入指定的 DOM <code>attribute</code> 並自訂一個名稱 <code>blot-align</code>,一樣註冊後使用。也能呼叫 <code>add()</code> 將自訂的 class attributor 加到目標 DOM。</p><h2 id="Style-Attributor"><a href="#Style-Attributor" class="headerlink" title="Style Attributor"></a>Style Attributor</h2><p>使用 Style Attributor 的方式來表示格式:</p><pre><code class="typescript">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></code></pre><p>這次則是在實體化的時候以 <code>new StyleAttributor</code> 來新增 attributor,一樣是操作 <code>text-align</code>,但這次加上了 <code>whitelist</code> 來表示合法的參數選項。沒有帶入值則代表 <code>left</code> 置左。在註冊之後呼叫 <code>add()</code> 方法並帶入 DOM 以及 <code>align</code> 的參數選項來套用。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天探討了 Parchment 的另一塊拼圖,Attributor,提供一個文本格式套用的簡易方式。一開始嘗試練習發現奇怪怎麼會出現找不到的錯誤,看了一下 sourcecode 才發現原來實現的方式已經換了,但 Github 的 repositroy README 還是古早的實現方式。這時只能看原始碼才能知道要怎麼使用了。</p><p>我們可以透過 Base Atrributor,Class Atrributor,以及 Style Attributor 來實現不同方式的文本樣式套用,並且 Attributor 也提供了幾個方法例如 <code>add()</code>,<code>value()</code>,<code>remove()</code> 等方法取得與操作對應的 blot 來編輯文本樣式。之後再研究看看如何將 attributor 應用到編輯器中來套用文本樣式。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>連假後上班的第一天,果然精神不是很好,儘管前一晚已經盡量提早躺平,但起床後還是有沒充滿電的感覺,由於台北住處附近不好停車,所以果斷的把車開回宜蘭停放,所以今天早上是從宜蘭搭車到台北,想說國光客運到南港展覽館離上班地點最近,沒想到七點半到轉運站,要能上車得要等到八點整的班次,到辦公室就都九點了,看來如果是從宜蘭到台北的話還要再更早一點到才行了QQ</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/a719545a2167edbda7099c5ff37222bf4c25bc30">今日份的練習</a></li><li><a href="https://github.com/quilljs/parchment/">quilljs/parchment (github.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10338302">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 25:ngx-quill 介紹</title>
<link href="/2023/10/10/quill-day-25/"/>
<url>/2023/10/10/quill-day-25/</url>
<content type="html"><![CDATA[<p>前面 14 天都在看 Quill Editor 的官方文件,突然想起我的主題還是要跟 Angular 有一點關係,除了直接在 Angular 引入 Quill 之外,其實也有方便的第三方套件可以使用,也就是 ngx-quill。</p><p>在 Angular 專案中,有時候會需要用到第三方套件,為了要能順利的融入 Angular 的世界,我們會需要額外的處理與封裝,讓套件使用體驗可以更 Angular。而使用 ngx-quill 的好處如下:</p><h2 id="資料與事件綁定"><a href="#資料與事件綁定" class="headerlink" title="資料與事件綁定"></a>資料與事件綁定</h2><p>我們都知道 Angular 使用 data binding 以及 event binding 作為核心特性之一,使用 ngx-quill 可以方便的透過綁定的方式來維護資料的狀態以及編輯器的互動等功能。</p><h2 id="模組化及相依性注入"><a href="#模組化及相依性注入" class="headerlink" title="模組化及相依性注入"></a>模組化及相依性注入</h2><p>Angular 專案中,我們透過 module (目前更推薦使用 standalone component),以及相依性注入 ( Dependency Injection ) 作為管理各種服務和元件的方式。ngx-quill 也是按照 Angular 的模組化及相依性注入的設計模式來建立。使其更容易整合到既有的 Angular App 中。</p><h2 id="表單控制"><a href="#表單控制" class="headerlink" title="表單控制"></a>表單控制</h2><p>Angular 有很強大的表單 module,包括了:<code>template-driven forms</code> 以及 <code>reactive Forms</code>。ngx-quill 可以輕鬆的整合到 Angular 的表單系統中,讓我們能使用 Angular 的驗證、狀態追蹤等功能。</p><h2 id="安裝與使用"><a href="#安裝與使用" class="headerlink" title="安裝與使用"></a>安裝與使用</h2><p>首先我們一樣透過 <code>npm install</code> 來安裝 <code>ngx-quill</code>:</p><pre><code class="bash">npm install ngx-quill --savenpm install @types/[email protected]</code></pre><p>另外需要注意的是,如果之前的練習有安裝到 <code>@types/quill</code> 的話,版本會是 <code>2.0.11</code>,這邊我們需要降版到 <code>1.3.10</code> 才不會導致編譯時的類型錯誤。</p><p>如果是全新的 Angular 專案,需要將 quill editor 的佈景主題 (theme) CSS Style 加到專案,例如:<br>要選用 <code>snow</code> 的主題,可以 import CSS 到 <code>styles.scss</code>:</p><pre><code class="scss">@import '~quill/dist/quill.snow.css';</code></pre><p>也可以把 <code>node_modules/quill/dist/quill.snow.css</code> 加到 <code>angular.json</code> 或 Nx 的 <code>project.json</code> 的 <code>styles</code> 陣列中。</p><pre><code class="json">"styles": [ "node_modules/quill/dist/quill.snow.css", "src/styles.scss"],</code></pre><p>安裝完畢之後,接著我們要將 <code>ngx-quill</code> 的 module 導入:</p><pre><code class="typescript">import { QuillModule } from 'ngx-quill';@NgModule({ imports: [ QuillModule.forRoot() ],})export class AppModule { }</code></pre><p>Import 之後就可以直接在 template 使用這個元件:</p><pre><code class="html"><quill-editor></quill-editor></code></pre><p>這時直接 <code>ng serve</code> 就可以看到有基本款的 Quill Editor 了。</p><h2 id="配置選項"><a href="#配置選項" class="headerlink" title="配置選項"></a>配置選項</h2><p>配置選項目前我們可以放在兩個地方,一個是在 template 的 component 屬性中,另一個則是在 import <code>QuillConfigModule.forRoot()</code> 的括號中帶入配置選項。</p><p>在 template 的 component 屬性加上 quill eidtor 的配置:</p><pre><code class="html"><quill-editor [modules]="{ toolbar: [ ['bold', 'italic'], ['link', 'blockquote'] ] }" [theme]="'snow'"></quill-editor></code></pre><p>透過 import <code>QuillConfigModule</code> 帶入配置:</p><pre><code class="typescript">import { QuillConfigModule, QuillModule } from 'ngx-quill';@NgModule({ imports: [ QuillModule.forRoot(), QuillConfigModule.forRoot({ modules: { toolbar: [ ['bold', 'italic'], ['link', 'blockquote'], ], }, }), ],})export class AppModule { }</code></pre><h3 id="Standalone-元件"><a href="#Standalone-元件" class="headerlink" title="Standalone 元件"></a>Standalone 元件</h3><p><code>ngx-quill</code> 也支援 standalone 的功能,可以直接使用 <code>provideQuillConfig</code> 方法進行配置,例如在 <code>main.ts</code> 的 <code>bootstrapApplication</code> 呼叫時,將配置加入到 <code>providers</code>:</p><pre><code class="typescript">import { provideQuillConfig } from 'ngx-quill/config';bootstrapApplication(AppComponent, { providers: [ provideQuillConfig({ modules: { syntax: true, toolbar: [ ['bold', 'italic'], ['link', 'blockquote'], ], } }) ]});</code></pre><p>此時的 <code>AppComponent</code> 對應的 standalone 設定如下:</p><pre><code class="typescript">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 {// ...}</code></pre><h2 id="與-Angular-Form-整合"><a href="#與-Angular-Form-整合" class="headerlink" title="與 Angular Form 整合"></a>與 Angular Form 整合</h2><p>有時候我們要確認編輯器的狀態,以根據需求進行像是表單驗證,或是否修改過等相關的操作,這時候可以搭配 Angular Form module 加到 <code>ngx-quill</code> 就能快速的實現表單操作與驗證的需求。例如以下的範例,搭配 <code>import</code> 對應的 <code>FormsModule</code> 或 <code>ReactiveFormsModule</code> 就能使用了:</p><pre><code class="html"><!-- Reactive Forms --><form [formGroup]="myForm"> <quill-editor formControlName="editorContent"></quill-editor></form><!-- Template-driven Forms --><quill-editor [(ngModel)]="editorContent" name="editorContent"></quill-editor></code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p><code>ngx-quill</code> 作為 Quill 的 Angular wrapper,為 Angular 開發者提供了一個更方便、更“Angular化”的方式來使用編輯器。從簡單的安裝配置到與 Angular Forms 的整合,可以省略掉前期的設定流程,直接無痛加入並使用。之後再繼續看 ngx-quill 的其他介紹內容。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>連假的最後一天,運氣還不錯,儘管前一天晚上豪大雨,但今天的天氣就陰陰的沒有下雨,是涼爽舒服的,去了傳統藝術中心,這次也待了比較多的時間在裡面度過,跟著導覽員去看各種不同的傳統文化,也看了很帥的霹靂布袋戲人偶,不論什麼時候看,精細的程度都不輸專業的模型,但真的很大尊,家裡空間不夠的收一尊就很極限了 XD 期待下次再來逛逛,會有不同的展覽內容。</p><h3 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h3><p><a href="https://github.com/KillerCodeMonkey/ngx-quill">KillerCodeMonkey/ngx-quill: Angular (>=2) components for the Quill Rich Text Editor (github.com)</a><br><a href="https://snyk.io/advisor/npm-package/@types/quill">@types/quill - npm Package Health Analysis | Snyk</a></p><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10337742">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 24:讀 Quill Editor API 技術文件 - Clipboard 與 Syntax Highlighter Module</title>
<link href="/2023/10/09/quill-day-24/"/>
<url>/2023/10/09/quill-day-24/</url>
<content type="html"><![CDATA[<p>連假第三天,終於來到最後的兩個章節,Clipboard & Syntax Highlighter Module。因為篇幅比較沒有那麼多,所以就放在一起看了。</p><p>剪貼簿 ( Clipboard Module ),負責處理 Quill Editor 與外部 App 之間的複製、剪下與貼上的操作。Clipboard module 提供了一組預設的判斷邏輯處理貼上的內容,並且我們能進一步通過加入自定義的匹配器(matcher)來調整或擴充這些預設行為。例如:我們可以讓特定的 HTML 標籤或文字段落在貼上時有特殊的格式或行為。</p><p>剪貼簿會通過後序遍歷(post-order)相應的 DOM 樹來處理貼上的 HTML,從而建構所有子樹的 Delta 表示形式。在每個子節點,matcher 函數會被呼叫,並傳入 DOM 節點和到目前為止的 Delta 處理,這樣可以讓 matcher 回傳一個修改過的 Delta。要能操作好 matcher,就需要熟悉和理解 Delta。</p><h2 id="可用的-API"><a href="#可用的-API" class="headerlink" title="可用的 API"></a>可用的 API</h2><h4 id="addMatcher"><a href="#addMatcher" class="headerlink" title="addMatcher"></a>addMatcher</h4><p>將自定義 matcher 新增到 clipboard module。使用 <code>nodeType</code> 的 matcher 會先被呼叫,按照它們被加入的順序,另一個是使用 CSS selector 的 matcher,也是按照被加入的順序。<code>nodeType</code> 可能是 <code>Node.ELEMENT_NODE</code> 或 <code>Node.TEXT_NODE</code>。</p><p>方法:</p><pre><code class="typescript">addMatcher(selector: String, (node: Node, delta: Delta) => Delta)addMatcher(nodeType: Number, (node: Node, delta: Delta) => Delta)</code></pre><p>範例:</p><pre><code class="typescript">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 }));});</code></pre><h4 id="dangerouslyPasteHTML"><a href="#dangerouslyPasteHTML" class="headerlink" title="dangerouslyPasteHTML"></a>dangerouslyPasteHTML</h4><p>在指定的索引位置將由 HTML 片段表示的內容插入到編輯器中。該片段會被剪貼簿的匹配器解釋,這可能不會產生完全相同的輸入 HTML。如果沒有提供插入索引,則會覆蓋整個編輯器的內容。來源可能是 “user”、”api” 或 “silent”。</p><p>不正確的處理 HTML 可能會導致跨站腳本攻擊(XSS),而未能正確清理 (sanitize) 則是引發網站漏洞的主要原因之一。明確的命名這個方法,以確保我們能注意到使用這個方法可能涉及的風險。這個命名方式也遵循了 React 框架的例子,React 也有類似的概念,如 <code>dangerouslySetInnerHTML</code> 用來提醒開發者必須謹慎操作。</p><p>方法:</p><pre><code class="typescript">dangerouslyPasteHTML(html: String, source: String = 'api')dangerouslyPasteHTML(index: Number, html: String, source: String = 'api')</code></pre><p>範例:</p><pre><code class="typescript">quill.setText('Hello!');quill.clipboard.dangerouslyPasteHTML(5, ' <b>World</b>');// 編輯器的 HTML 文本內容會是 '<p>Hello <strong>World</strong>!</p>';</code></pre><h2 id="Clipboard-配置設定"><a href="#Clipboard-配置設定" class="headerlink" title="Clipboard 配置設定"></a>Clipboard 配置設定</h2><h4 id="matchers"><a href="#matchers" class="headerlink" title="matchers"></a>matchers</h4><p>可以將 <code>matcher</code> 陣列傳遞到剪貼簿的配置選項中。這些將附加在 Quill 內建的 <code>matcher</code> 之後。</p><pre><code class="typescript">var quill = new Quill('#editor', { modules: { clipboard: { matchers: [ ['B', customMatcherA], [Node.TEXT_NODE, customMatcherB] ] } }});</code></pre><h3 id="Syntax-Highlighter-Module"><a href="#Syntax-Highlighter-Module" class="headerlink" title="Syntax Highlighter Module"></a>Syntax Highlighter Module</h3><p>語法高亮模組(Syntax Highlighter Module)在 Quill Editor 中用於增強程式碼區塊內容(Code Block)格式。它會自動檢測並套用語法高亮效果,且依賴於 highlight.js 函式庫來解析和標記程式碼區塊。</p><p>我們可以根據需求來配置 highlight.js。不過,Quill 要求 <code>useBR</code> 的選項必須設為 false。</p><p>範例:</p><pre><code class="typescript"><!-- 引入 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></code></pre><h3 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h3><p>剪貼簿(Clipboard)模組在 Quill Editor 中負責處理與外部應用間的複製、剪下和貼上操作。它提供了一套預設行為來解析貼上的內容,並允許開發者透過自定義 <code>matcher</code> 來進一步調整這些行為。這些 <code>matcher</code> 可以按照它們被加入的順序來進行呼叫,而在貼上 HTML 時,剪貼簿會後序遍歷對應的 DOM 樹來創建一個 Delta 表示形式。</p><p>基於安全性考慮,提供了一個 <code>dangerouslyPasteHTML</code> 的API,用在確認安全的操作情境下插入 HTML。剪貼簿模組不僅提供了彈性的自訂方式,也考慮到貼上內容的安全處理,使 Quill 更靈活實用。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>儘管昨日發現前一晚停車時刮到了輪拱板金,心情受到一些影響之外,整體的行程還不錯,在金車威士忌酒廠待了一整天,雖然之前也去過幾次,但都沒有導覽員,這次趁著人多時,有導覽員的服務,也學到不少威士忌的一些觀念,今天出發前就發了這篇文章,預計會去傳統藝術中心,今天天氣看起來很不錯,要多喝水避免被太陽曬昏頭了 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/715fbbeecb81d6092729b55db7edce578ed9fa18">今日份的練習</a></li><li><a href="https://quilljs.com/docs/modules/clipboard/">Clipboard Module - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10337353">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 23:讀 Quill Editor API 技術文件 - History Module</title>
<link href="/2023/10/08/quill-day-23/"/>
<url>/2023/10/08/quill-day-23/</url>
<content type="html"><![CDATA[<p>昨天泡了溫泉,休息一下,繼續看 History module。<br>History module 主要保存文本操作紀錄與處理與 Quill 的 undo 和 redo。<br>有以下的選項可以使用:</p><h2 id="配置設定參數"><a href="#配置設定參數" class="headerlink" title="配置設定參數"></a>配置設定參數</h2><h3 id="delay"><a href="#delay" class="headerlink" title="delay"></a>delay</h3><p>預設值:<code>1000</code> </p><p>設定在指定秒數內更改合併成一個更改紀錄。例如當 <code>delay</code> 設為 <code>0</code> 時,幾乎每個字元都會記錄成一次更改,因此使用 <code>undo</code> 就只會取消一個字元。當 <code>delay</code> 設置為 1000 時,<code>undo</code> 將會撤銷最後 1000 毫秒內發生的所有變更。</p><h3 id="maxStack"><a href="#maxStack" class="headerlink" title="maxStack"></a>maxStack</h3><p>預設值:<code>100</code></p><p>設定歷史操作紀錄堆疊的最大值。與 <code>delay</code> 選項合併的變更算是一次變更操作。</p><h3 id="userOnly"><a href="#userOnly" class="headerlink" title="userOnly"></a>userOnly</h3><p>預設值:<code>false</code></p><p>預設的情況下,無論 <code>source</code> 是 <code>user</code> 或是透過 <code>api</code> 的方式進行的所有變更。都視為同等的操作,並且變更可以從 <code>history</code> <code>redo</code>/<code>undo</code>。如果 <code>userOnly</code> 設為 <code>true</code>,則只會處理使用者的變更。</p><pre><code class="typescript">const quill = new Quill('#editor', { modules: { history: { delay: 2000, maxStack: 500, userOnly: true } }, theme: 'snow'});</code></pre><h2 id="API"><a href="#API" class="headerlink" title="API"></a>API</h2><h3 id="clear"><a href="#clear" class="headerlink" title="clear"></a>clear</h3><p>清除 <code>history</code> 的所有堆疊紀錄</p><p>方法:</p><pre><code class="typescript">clear()</code></pre><p>範例:</p><pre><code class="typescript">quill.history.clear();</code></pre><h3 id="cutoff-實驗性"><a href="#cutoff-實驗性" class="headerlink" title="cutoff (實驗性)"></a>cutoff (實驗性)</h3><p>通常短時間內連續進行的變更,我們可以透過 <code>delay</code> 設置來合併成為一次歷史紀錄,以便觸發更多的 <code>undo</code> 的變更。使用 <code>cutoff</code> 將重置合併窗口,以便呼叫 <code>cutoff</code> 之前和之後的更改不會被合併。</p><p>方法:</p><pre><code class="typescript">cutoff()</code></pre><p>範例:</p><pre><code class="typescript">quill.history.cutoff();</code></pre><h3 id="undo"><a href="#undo" class="headerlink" title="undo"></a>undo</h3><p>取消最後一次的變更操作。</p><p>方法:</p><pre><code class="typescript">undo()</code></pre><p>範例:</p><pre><code class="typescript">quill.history.undo();</code></pre><h3 id="redo"><a href="#redo" class="headerlink" title="redo"></a>redo</h3><p>如果上次的操作是 <code>undo</code>,則還原 <code>undo</code>。</p><p>方法:</p><pre><code class="typescript">redo()</code></pre><p>範例:</p><pre><code class="typescript">quill.history.redo();</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天嘗試了初始化的時候加入 History module 的配置設定參數,另外也透過按鈕的方式來呼叫 history module 的 API,也能觀察到其 history stack 的變化,不過目前 <code>@types/quill</code> 的 history 版本似乎沒看到有 history module 的其他屬性,只有加上 API 的定義而已,感覺可以再提一個新 PR 了 XD</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>昨天運氣不錯,搭客運回宜蘭牽車沒遇到塞車,順利的開車到新竹接朋友出發到宜蘭,這聽起來有點瘋狂,我只是喜歡開車而已XD 不過到了住宿的停車場,因為是機械式的,沒注意到後面兩側還有塗上黃色的支撐桿,今天早上出發前才看到右後輪拱有擦到 Orz 前一晚停車時原本以為是機械車位的地板阻尼之類的作動聲,沒想到是磨擦聲,儘管是老車了,也多少有一些擦傷,但還是免不了會心痛 QQ,找時間再去買幾支板金補漆筆塗一下了,畢竟輪拱最邊緣的地方有一小部分都看到銀色的部分,應該是底漆也有刮掉了 (哭</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/b399895e47cf5221bdaed7ef89fe4401b31b477c">今日份的練習</a></li><li><a href="https://quilljs.com/docs/modules/history/">History Module - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10336767">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 22:讀 Quill Editor API 技術文件 - Keyboard Module (下)</title>
<link href="/2023/10/07/quill-day-22/"/>
<url>/2023/10/07/quill-day-22/</url>
<content type="html"><![CDATA[<p>連假的第一天,今天繼續看 Keyboard module 的 <code>context</code> 剩下的參數以及設定相關的介紹。</p><h3 id="offset"><a href="#offset" class="headerlink" title="offset"></a>offset</h3><p>當使用者的游標從開始移動到 <code>offset</code> 指定的特定的位置則觸發 handler,例如:當 <code>offset</code> 為 <code>3</code> 時,只有使用者選擇的文字或是游標是在同一行的第三個字元位置開始,才會執行對應的 handler。另外執行的時機點是在使用者輸入內容的時候就已經判定的,因此使用者如果在第 3 個字元時按下按鍵輸入文字,則 handler 會在文字輸入之前就被執行。</p><pre><code class="typescript">quill.keyboard.addBinding( { key: 'o' }, { offset: 2 }, // 當游標在第3個字元前面時觸發 (range, context) => { // 插入特殊符號的代碼 quill.insertText(range.index, '★★★'); });</code></pre><h3 id="prefix"><a href="#prefix" class="headerlink" title="prefix"></a>prefix</h3><p>一個正則表達式(Regex)屬性,用於指定必須與使用者選擇的區域或游標開始位置之前的文字比對的模式。換句話說,當該正則表達式匹配到使用者選擇開始位置前方的文字時,相關的處理函數(handler)才會被觸發。例如,當使用者輸入一個 <code>@</code> 符號,然後按下 <code>k</code> 時,這個 handler 會被觸發。<code>prefix: /@$/</code> 確保了只有當游標(或選取範圍)前方是 <code>@</code> 符號時,這個 handler 才會執行:</p><pre><code class="typescript">quill.keyboard.addBinding({ key: 'k' }, { prefix: /@$/, // 前置文本必須是 @}, (range, context) => { // 這裡實現你的自定義邏輯,例如彈出一個用戶列表以供選擇 console.log("觸發了 @ 符號的自定義行為");});</code></pre><p><code>context.prefix</code> 在這個例子中會是 <code>@</code>,因為它包含了選擇開始位置之前的整個文字區塊。如此一來,我們就可以在使用者輸入 <code>@</code> 符號後進行特定操作,像是顯示一個下拉清單讓使用者選擇名稱。</p><h3 id="suffix"><a href="#suffix" class="headerlink" title="suffix"></a>suffix</h3><p>和 <code>prefix</code> 的概念相同,只是比對的位置是使用者選擇的內容或游標的位置的後面開始。</p><h2 id="配置"><a href="#配置" class="headerlink" title="配置"></a>配置</h2><p>預設的情況下,Quill 內建幾個實用的按鍵綁定,例如使用 Tab 鍵進行縮排。我們可以在初始化時加入自訂的按鍵綁定。</p><p>有些綁定對於防止瀏覽器的危險預設行為(如 Enter 鍵和 Backspace 鍵)是必要的。不能移除這些綁定以恢復到瀏覽器的原生行為。然而,由於在配置中指定的綁定會在 Quill 的預設綁定之前運行,您可以處理特殊情況並將其傳播給 Quill。</p><p>使用 <code>quill.keyboard.addBinding</code> 加入綁定不會在 Quill 的預設綁定之前運行,因為到那時預設綁定已經被加入。</p><p>每個綁定配置必須包含鍵(key)和處理器(handler)選項,並且可以選擇性地包括任何 <code>context</code> 選項。</p><pre><code class="typescript">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 } }});</code></pre><h2 id="性能考量"><a href="#性能考量" class="headerlink" title="性能考量"></a>性能考量</h2><p>和 DOM event 相同,Quill key binding 在每次比對時都會阻擋呼叫,因此為一個非常普通的按鍵綁定一個複雜的 handler 不是一個好的實現方式。在套用像是滑鼠移動或卷軸滾動的 DOM 事件時,盡可能的套用性能較好的實現以確保一定品質的使用者體驗。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>這兩天探討了 Quill 的 Keyboard module,讓我們可以自定義鍵盤事件的處理。Quill 的 keyboard module 主要有兩種用途:</p><ol><li>綁定格式化快捷鍵:比如 “Ctrl + B” 可以讓選中的文字變粗。</li><li>防止瀏覽器的一些預設行為:這樣可以確保應用程式的穩定性與使用者體驗。</li></ol><p>我們也了解如何使用不同的 <code>context</code> 參數來更精細的控制 handler 的觸發時機,包含游標的位置、目前使用中的格式、以及前後緊鄰的文本內容等。</p><p>此外,keyboard module 也提供了豐富的設定選項,讓我們可以在初始化時加入自訂的綁定,或是覆蓋Quill 的預設綁定。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天午餐後整理一下文章,晚一點就要出發去載朋友來宜蘭玩了,希望不要塞車塞得太嚴中 XD。昨天看了同事的分享會 Feedback,看到很多有趣的回應。其中還有提到下班後學習這件事,我認為學習是屬於個人的事情,至於有沒有要求下班後學習這件事,最終決定權還是在自己手上。若真的有興趣的而且學到之後能讓自己在上班的過程更順暢也能克服一些挑戰,我想這個學習過程應該是相當精彩的,儘管最後發現也許是個坑,但這都是成長的一部分。</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/be03de30380a0ae188fee3c97890ec9b6d5b0cbd">今日份的練習</a></li><li><a href="https://quilljs.com/docs/modules/keyboard/#offset">Keyboard Module - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10335867">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 21:讀 Quill Editor API 技術文件 - Keyboard Module (上)</title>
<link href="/2023/10/06/quill-day-21/"/>
<url>/2023/10/06/quill-day-21/</url>
<content type="html"><![CDATA[<p>今天來看 Keyboard module 的章節。</p><p>Keyboard module 支援特定 context 中鍵盤事件的自訂行為。Quill 使用 Keyboard module 來綁定格式化快捷鍵並防止一些瀏覽器副作用。</p><h2 id="Key-Bindings"><a href="#Key-Bindings" class="headerlink" title="Key Bindings"></a>Key Bindings</h2><p>Keyboard handler 綁訂到特定的按鍵與修飾鍵。<code>key</code> 是 JavaScript event 的 key code,但也允許英文字母與數字鍵,以及常用的按鍵的字串縮寫設定。常見的修飾鍵例如:<code>metaKey</code>,<code>ctrl</code>,<code>shift</code>,以及 <code>alt</code> 等。另外 <code>shortKey</code> 是指特定平台的修飾鍵,像是 MacOS 上的 <code>metaKey</code>,以及 Linux 和 Windows 上的 <code>ctrlKey</code>。</p><p>我們可以將指定的按鍵和修飾鍵綁定到一個 handler。當這個鍵被按下時,handler 就會被執行,並將使用者選擇的範圍傳入以及綁定到 keyboard module 當下的 instance:</p><pre><code class="typescript">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); }});</code></pre><p>這個範例是當使用者按下 <code>B</code> 鍵加上修飾鍵(Mac 上的 <code>metaKey</code> 或 Windows 和 Linux 上的 <code>ctrlKey</code>)時,選取的文字會變粗體。</p><h2 id="Context"><a href="#Context" class="headerlink" title="Context"></a>Context</h2><p>我們還可以設定更多的條件,讓 handler 只在特定的情境下被呼叫。例如,當使用者選擇的是一個空行或者是列表項目時,才會觸發相對應的 handler:</p><pre><code class="typescript">// 如果使用者在 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); } });</code></pre><p>不過需要注意的地方是,當編輯器初始化之後才加入的 keyboard binding,需要確認內建的部分是否也有監聽,否則會因為按鍵事件發生時逐條比對條件的關係,就被前面的規則代為執行了。例如 <code>backspace</code> 的 <code>keycode</code> 是 <code>8</code>:<br><img src="/2023/10/06/quill-day-21/20090749Cy3B9odY5Z.png" alt="被前面的規則代為執行"></p><h2 id="Context-的參數"><a href="#Context-的參數" class="headerlink" title="Context 的參數"></a>Context 的參數</h2><h3 id="collapsed"><a href="#collapsed" class="headerlink" title="collapsed"></a>collapsed</h3><p>如果為 <code>true</code> 則當使用者的游標停在編輯器上,在沒有選擇任何文字的情況下觸發 handler。<code>collapsed</code> 翻成中文是收折的意思,但實際上就是指游標停在編輯器上並沒有選取任何文字的狀態。</p><h3 id="empty"><a href="#empty" class="headerlink" title="empty"></a>empty</h3><p>如果為 <code>true</code>,當使用者的游標在一行空白的時候會觸發。設為 <code>false</code> 則代表非空行,另外當 <code>empty</code> 為 <code>true</code> 時,意思就是 <code>collapsed</code> 也要是 <code>true</code>,且 <code>offset</code> 必須是 <code>0</code>,這樣才是真正完全的一行空白。<br>例如當使用者換行的時候加上一個星星符號:</p><pre><code class="typescript">quill.keyboard.addBinding({ key: 'enter' }, { empty: true // 只在空行觸發}, function(range, context) { // 插入特殊符號的代碼 this.editor.insertText(range.index, '★');});</code></pre><h3 id="format"><a href="#format" class="headerlink" title="format"></a>format</h3><p><code>format</code> 這個參數用來控制 handler 在哪些特定的格式條件下會被觸發。</p><ul><li>當 <code>format</code> 是一個陣列時,如果當前活動(active)的格式中包含陣列裡面指定的任何一種格式,則會觸發 handler:</li></ul><pre><code class="typescript">quill.keyboard.addBinding({ key: Keyboard.keys.ENTER }, { format: ['bold', 'italic'] // 只要文字是粗體或斜體,處理函數就會觸發}, function(range, context) { // 插入特殊符號的代碼 this.editor.insertText(range.index, '★');});</code></pre><ul><li>當 <code>format</code> 是一個物件:所有指定的格式條件必須全部滿足,handler 才會觸發。</li></ul><pre><code class="typescript">quill.keyboard.addBinding({ key: Keyboard.keys.ENTER }, { format: { bold: true, italic: true } // 當文字是粗體且斜體,處理函數才會觸發}, function(range, context) { // 插入特殊符號的代碼 this.editor.insertText(range.index, '★');});</code></pre><p>在任何情況下,<code>context</code> 參數的 <code>format</code> 屬性都會是一個物件,其中包含了所有當前活動的格式。這個物件的結構和 <code>quill.getFormat()</code> 回傳的結構是相同的。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天研究了 Quill 的 keyboard module,並了解如何加入自訂的 keyboard binding,也看到 <code>context</code> 的內容有哪些可以讓我們運用,明天接著看 <code>context</code> 其他的參數介紹。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天下班前,不小心把弄了一陣子的 Git stash 給 drop 掉了,然後又因為是在 Git Graph 上執行,所以也沒有留意到 hash 的部分,當下真的有 BBQ 的感覺,不死心的我花了一點時間研究,總算找到解法,第一次使用 <code>git fsck</code>,搭配 sh 腳本執行,把碎片找回來從裡面去翻之前改過的程式片段,找到後來改的內容,趕快把 hash 記下來,接著 apply,逝去的青春終於又回來了(誤。這故事給了我一個教訓,以後還是乖乖建 commit 吧…Orz</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/37337ce44f6a8659a39be3107e739b032d3e9373">今日份的練習</a></li><li><a href="https://quilljs.com/docs/modules/keyboard/">Keyboard Module - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10335585">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 20:讀 Quill Editor API 技術文件 - Toolbar Module</title>
<link href="/2023/10/05/quill-day-20/"/>
<url>/2023/10/05/quill-day-20/</url>
<content type="html"><![CDATA[<p>今天開始第一個內建 module 的介紹,在第六天的時候我們就已經介紹如何自訂工具列,這篇就當作複習,跟著技術文件介紹來練習體驗。</p><p>工具列模組 (Toolbar module) 可以讓使用者輕鬆的將文本內容套用格式。Toolbar 除了初始化設置要開啟的功能後直接渲染,我們也可以自行定義 container 內容以及工具列功能的處理器 (handler)。</p><h2 id="Toolbar-module-設定"><a href="#Toolbar-module-設定" class="headerlink" title="Toolbar module 設定"></a>Toolbar module 設定</h2><p>Toolbar 的設定方式可分為兩種,一種是指定 toolbar 的容器 (container),並視需求加上 HTML 控制項以及對應的處理器 (handler),另一種則是直接使用陣列來設置。</p><p>透過指定 container 的設置方式:</p><pre><code class="html"><p>toolbar-practice works!</p><div #myToolbar></div><div #quillContainer></div></code></pre><pre><code class="typescript">@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); }, } } } });}</code></pre><p><code>toolbar</code> 也可直接給 <code>container</code> 的 id <code>selector</code>,這裡我們直接用 <code>template reference</code>:</p><pre><code class="typescript">const quill = new Quill(this.quillContainer.nativeElement, { modules: { // Equivalent to { toolbar: { container: '#toolbar' }} toolbar: this.myToolbar.nativeElement } });</code></pre><p>可以看到直接指定 <code>container</code> 之後,就可以直接渲染,但因為沒有設定要放哪些功能按鈕在工具列上,所以現在看到是還沒有任何按鈕的:</p><p><img src="/2023/10/05/quill-day-20/200907490cqE545H5z.png" alt="現在看到是還沒有任何按鈕"></p><h2 id="Container"><a href="#Container" class="headerlink" title="Container"></a>Container</h2><p>工具列的控制項可以帶入控制項名稱的陣列或自定義 HTML 容器來指定。<br>從基本的陣列來設定 <code>toolbar</code> 開始:</p><pre><code class="typescript">const toolbarOptions = ['bold', 'italic', 'underline', 'strike']; this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: toolbarOptions } });</code></pre><h3 id="控制項分組與自定義"><a href="#控制項分組與自定義" class="headerlink" title="控制項分組與自定義"></a>控制項分組與自定義</h3><p>控制項也能放在巢狀陣列來表示設定分組,這樣可以將同一組的控制項放在 <code>className</code> 為 <code>ql-formats</code> 的 <code><span></code> 標籤下以提供佈景主題利用,例如在佈景主題 <a href="https://quilljs.com/docs/themes/#snow/"><code>snow</code></a>) 時,就會在這些分組間加上間距,方便使用者進行操作:</p><pre><code class="typescript">const toolbarOptions = [['bold', 'italic'], ['link', 'image']];</code></pre><p>另外可以使用一個物件來指定自定義值的按鈕,並將格式名稱作為 key:</p><pre><code class="typescript">const toolbarOptions = [{ 'header': '3' }];</code></pre><p>上面這個範例會在工具列上加入一個按鈕,這個按鈕代表的是 <code>header</code> 格式,並且按鈕會套用 <code>3</code> 這個自定義的值。換句話說,當點擊這個按鈕時,選定的文字會變成第三級標題 <code><h3></code>。</p><h2 id="下拉選單"><a href="#下拉選單" class="headerlink" title="下拉選單"></a>下拉選單</h2><p>下拉選單也是透過物件來定義,但與其他元素不同的地方在於,這裡會用一個陣列來存入所有可能的選項值。下拉選單選項的視覺表現(例如文字標籤或顏色)是由 CSS 來控制的。</p><p>例如,設定字體大小 <code>size</code> 的選項:</p><pre><code class="typescript">// 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' ]} ];</code></pre><p><code>size</code> 陣列中的 <code>false</code> 是用於移除格式,也就是把文字的大小回到預設的狀態。</p><h2 id="佈景主題和預設值"><a href="#佈景主題和預設值" class="headerlink" title="佈景主題和預設值"></a>佈景主題和預設值</h2><p>某些佈景主題,例如 Snow,會為下拉選單(如顏色和背景格式)提供預設值。當設定空的陣列在 <code>color</code> 或 <code>background</code> 時, <a href="https://quilljs.com/docs/themes/#snow/">Snow</a> 將預設提供 35 種顏色選項:</p><pre><code class="typescript">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' });</code></pre><h2 id="進階客製化(Advanced-Customization)"><a href="#進階客製化(Advanced-Customization)" class="headerlink" title="進階客製化(Advanced Customization)"></a>進階客製化(Advanced Customization)</h2><p>如果需要對工具列更多的客製化,也能直接用 HTML 來手動創建工具列。只需要將 DOM 元素或選擇器傳遞給 Quill 即可。<code>ql-toolbar</code> 類會被添加到工具列容器中,而 Quill 會自動為具有 <code>ql-${format}</code> 格式名稱的 <code><button></code> 和 <code><select></code> 元素附加對應的內建 handler。</p><pre><code class="html"><!-- 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 --> </code></pre><pre><code class="typescript">this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: this.myToolbar.nativeElement } }); </code></pre><h2 id="自訂按鈕(Custom-Buttons)"><a href="#自訂按鈕(Custom-Buttons)" class="headerlink" title="自訂按鈕(Custom Buttons)"></a>自訂按鈕(Custom Buttons)</h2><p>當我們提供自己的 HTML 元素作為 Quill 的工具列時,Quill 會尋找特定的輸入元素來綁定功能。然而,除了 Quill 會自動識別和處理的元素外,你仍然可以加入和設計與 Quill 無關的自定義輸入元素。這些自定義的輸入元素可以和 Quill 的元素共存,且不會產生衝突。</p><pre><code class="html"><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></code></pre><pre><code class="typescript">this.quill = new Quill(this.quillContainer.nativeElement, { modules: { toolbar: this.myToolbar.nativeElement }}); </code></pre><h2 id="處理器-Handler"><a href="#處理器-Handler" class="headerlink" title="處理器 (Handler)"></a>處理器 (Handler)</h2><p>工具列的控制項預設會套用或移除格式,但我們也可以用自定義的 handler 來取代行為,例如顯示外部的使用者介面。<br>Handler function 會綁定到工具列,並且傳入輸入元素的 <code>value</code> 屬性。如果相對應的格式是非啟動狀態,則會傳入 <code>false</code>。加入自定義的 handler 會覆寫預設的工具列和主題行為。</p><pre><code class="typescript">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);</code></pre><p>在上面的範例,為 <code>link</code> 格式定義了一個 custom handler。當使用者點擊工具列的連結按鈕時,會彈出一個提示框可以輸入URL。當使用者輸入URL 則會把選取的文字變成一個連結。反之當使用者取消操作會移除文字的連結格式。另外,可以在 Quill 初始化之後,動態地加入更多的 handler,如 <code>showImageUI</code> 這個函數用來處理圖像插入的 UI。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>Quill 在初始化的時候,就算沒有給任何的 toolbar 設定,也會提供預設的功能選項直接使用,設定 toolbar 主要有兩種方式:</p><ol><li>使用 HTML 設置功能按鈕並指定 toolbar 的 <code>container</code><ul><li>Quill 會依照 <code>ql-*</code> class 名稱的 HTML 帶入對應的內建功能</li></ul></li><li>使用 toolbar options 陣列設置需要的功能<ul><li>不需要額外給定 HTML 或 container 即可初始化後渲染到 Quill 容器上</li></ul></li></ol><p>透過這些方式,我們可以靈活設計和調整 Quill 編輯器的工具列來滿足各種需求。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天對於北北基桃來說是個再正常不過的上班日了,但不知道為啥就沒有上班的氛圍,我想應該是新竹以南的夥伴們都在家防颱吧。沒有會議的一天可以完全專注的在開發工作上,雖然過程也遇到一些意外的挑戰,但還好都有初步解決了。明天還有一天班,之後就要好好充電休息一下,但還是要持續發文到 15 號了,希望扛的住…XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/cc354e601455a78eb7e63d82e711b31c67733d03">今日份的練習</a></li><li><a href="https://quilljs.com/docs/modules/toolbar/">Toolbar Module - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10334843">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 19:讀 Quill Editor API 技術文件 - Modules</title>
<link href="/2023/10/04/quill-day-19/"/>
<url>/2023/10/04/quill-day-19/</url>
<content type="html"><![CDATA[<p>明天北北基桃沒有放颱風假,大家上班注意安全。<br>今天開始進到 Module 的章節。</p><p>Module 允許 Quill 的操作行為與功能實現客製化。有幾個官方支援的模組可供選擇,其中一些還有額外的配置選項和API。目前官網列出支援的模組有:<code>Toolbar</code>,<code>Keyboard</code>,<code>History</code>,<code>Clipboard</code>,以及<code>Syntax Highlighter</code>。各章節也都會提到如何使用以及有哪些 API 可供操作。</p><p>要啟用模組只需要把要使用的模組加到 Quill 的配置中即可:</p><pre><code class="typescript">const quill = new Quill('#editor', { modules: { 'history': { // Enable with custom configurations 'delay': 2500, 'userOnly': true }, 'syntax': true // Enable with default configuration } });</code></pre><p>Clipboard,Keyboard 和 History 模組是 Quill 所必需的,不需要明確設定就預設在裡面了,但也可以像其他模組一樣進行設定。</p><h2 id="繼承-Extending"><a href="#繼承-Extending" class="headerlink" title="繼承 (Extending)"></a>繼承 (Extending)</h2><p>模組也可以繼承和重新註冊,替換掉原本的模組。甚至原本預設內建的必要模組也能重新註冊來做替換。例如繼承 clipboard 模組並自訂一些功能:</p><pre><code class="typescript">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');</code></pre><p>上面這個範例只是為了解釋 module 提供的可能性。單純用既有模組提供的 API 或 config 通常會更容易些。在這個 <code>clipboard</code> 模組擴充的操作範例中,用現有的 <code>addMatcher</code> 其實就能夠滿足大部分的情境需求了。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天介紹了 Quill 的 module,強調其客製化,以及可繼承並擴展。Quill 內建了許多豐富的 module,讓我們可以按照需求選擇和配置。繼承的部分則允許開發者擴充新功能並替換原有的模組,同時也提到單純使用既有的 API 或設定也許就能滿足大部分的需求。在這個章節,我們了解如何利用 module 來啟用 Quill 的功能,並依照實際需求進行繼承及擴充自訂功能,之後來介紹並研究一下第三方的開源套件要如何使用,以及他們是如何實現自訂功能的。應該能有不少收穫。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>最近上下班的運氣都還不錯,儘管有下雨,但出門跟下班回家的這段時間都是無雨的,今天又去了整復保養一下,然後再去看中醫,弄得時間有點晚。這次的颱風感覺也是來者不善,放颱風假就乖乖待在家,看點書追個劇也好。我明天要繼續去上班了 QQ</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://quilljs.com/docs/modules/">Modules - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10334295">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 18:讀 Quill Editor API 技術文件 - Extension</title>
<link href="/2023/10/03/quill-day-18/"/>
<url>/2023/10/03/quill-day-18/</url>
<content type="html"><![CDATA[<p>今天接著看 Extension 的部分,顧名思義就是和擴充功能相關的 API 使用。</p><h2 id="debug"><a href="#debug" class="headerlink" title="debug"></a>debug</h2><p>提供除錯用的靜態方法,可以開啟指定層級的 log 訊息,例如:<code>error</code>,<code>warn</code>,<code>log</code>,或 <code>info</code>。<br>傳入 <code>true</code> 等同於傳入 <code>log</code>,傳入 <code>false</code> 則是關閉所有 log 訊息。</p><p>方法:</p><pre><code class="typescript">Quill.debug(level: String | Boolean)</code></pre><p>範例:</p><pre><code class="typescript">Quill.debug('info');</code></pre><h2 id="import"><a href="#import" class="headerlink" title="import"></a>import</h2><p>將指定的擴充功能或模組引入 Quill。</p><p>方法:</p><pre><code class="typescript">Quill.import(path): any</code></pre><p>範例:</p><pre><code class="typescript">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';`</code></pre><h2 id="register"><a href="#register" class="headerlink" title="register"></a>register</h2><p>用於註冊 module、theme 或 format。可以讓我們擴充和自定義 Quill 的功能。註冊執行之後可以使用 <code>Quill.import</code> 獲取。使用路徑前綴 ‘formats/‘、’modules/‘ 或 ‘themes/‘ 分別註冊 <code>formats</code>、<code>modules</code> 或 <code>themes</code>。對於 <code>format</code>,可以直接帶入且路徑將自動生成。也會覆蓋掉具有相同路徑的定義。</p><p>方法:</p><pre><code class="typescript">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)</code></pre><p>範例:</p><pre><code class="typescript">// 自訂一個空 moduleconst Module = Quill.import('core/module');class CustomModule extends Module {}Quill.register('modules/custom-module', CustomModule);</code></pre><p><code>register</code> 方法使 Quill 的功能更加彈性和可擴展,允許開發人員自定義格式、模組和主題,進而更滿足特定的應用需求。</p><blockquote><p>註冊之後要留意一下初始化的 <code>options</code> 裡面是否也有加入 custom-module!</p></blockquote><h2 id="addContainer"><a href="#addContainer" class="headerlink" title="addContainer"></a>addContainer</h2><p>在 Quill container 內加入一個容器元素 (container element) 並回傳,作為編輯器本身的同層元素。通常 Quill 模組都會有以 ql- 當作前綴的 class name。選擇性的參數 <code>refNode</code>,表示容器的插入位置應該在這個 <code>refNode</code> 之前。</p><p>方法:</p><pre><code class="typescript">addContainer(className: String, refNode?: Node): ElementaddContainer(domNode: Node, refNode?: Node): Element</code></pre><p>範例:</p><pre><code class="typescript">// 使用 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);}</code></pre><p>因為是在 Angular 專案上,所以建議還是使用 <code>@ViewChild</code> 取得 element reference,如此一來在套用 CSS 樣式的時候,就不需要再加上像 <code>::ng-deep</code> 的方式套用, 避免影響子元件樣式。 </p><p>使用 element reference 加上指定位置後的效果:<br><img src="/2023/10/03/quill-day-18/20090749iPB48sCf4t.png" alt="加上指定位置後的效果"></p><h2 id="getModule"><a href="#getModule" class="headerlink" title="getModule"></a>getModule</h2><p>取得已加入 Quill instance 的模組。</p><p>方法:</p><pre><code class="typescript">getModule(name: String): any</code></pre><p>範例:</p><pre><code class="typescript">const toolbar = quill.getModule('custom-module');</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>Quill 在擴充功能的部分提供了幾個 API,包含了模組引入、除錯、註冊,也能加入自訂的 container element,並直接獲取 Quill instance 裡面指定的模組,稍微整理一下:</p><ul><li><code>debug</code>:靜態方法用於開啟不同層級的 log 訊息,有助於開發和除錯。</li><li><code>import</code>:用於回傳 Quill library、格式、模組或主題的靜態方法。使自定義和擴充變得非常靈活。</li><li><code>register</code>:這個方法允許註冊和定義自己的模組、主題或格式,提高 Quill 的可擴展性。</li><li><code>addContainer</code>:允許在 Quill 容器內新增容器元素,使得界面結構更加靈活。</li><li><code>getModule</code>:取得已經加入到編輯器的模組,有助於模組的管理和操控。</li></ul><p>大多數情況下,靜態方法如 <code>register</code> 和 <code>import</code> 最好是在 <code>new Quill()</code> 之前使用,以確保在初始化 Quill 時能夠使用這些自定義 module 或定義。而 <code>debug</code> 則可以根據實際需要來決定使用的時機。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天整理文章的時候,看到新聞上寫有颱風名字叫做小犬,於是心血來潮查了一下,別問我為什麼要查 XD<br>根據教育部的辭典網站釋義:<br>1)幼小的狗。清.孔尚任《桃花扇》第四○齣:「行到那舊院,何用輕敲,也不怕小犬哰哰。」<br>2)謙稱自己的兒子。《紅樓夢》第一三回:「待服滿後,親帶小犬到府叩謝。」也作「豚犬」、「豚兒」。</p><p>貌似第一次聽到這樣的命名,以前的名字都滿酷的,但最近的颱風名稱似乎有點微妙。聽說小犬一點都不小,大家要做好防颱措施阿…QQ</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/1c036fa131e6c06204b206da7b891c07ddf8fe27">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#extension">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10333603">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 17:讀 Quill Editor API 技術文件 - Model</title>
<link href="/2023/10/02/quill-day-17/"/>
<url>/2023/10/02/quill-day-17/</url>
<content type="html"><![CDATA[<p>今天看 Quill Editor 的 Model 技術文件介紹,根據文件的描述,語意版本控制(Semantic Versioning)不適用於實驗性 API,意思是 Model 的 API 目前仍然處於實驗性階段,代表未來可能會出現一些重大的改動而影響到 API 的穩定性,但可以先看過一遍並嘗試玩看看,未來有機會正式發布後,再考慮應用到正式的專案上。</p><p>透過 Model API 找到的 Blot 物件是 <code>LinkedList</code> 的資料結構:<br><img src="/2023/10/02/quill-day-17/20090749KPxBk00GWC.png" alt="LinkedList 的資料結構"></p><h2 id="find"><a href="#find" class="headerlink" title="find"></a>find</h2><p>這是一個靜態方法,可以代入 DOM 節點並回傳 Quill 或 Blot Instance。在後者的情況下,對 <code>bubble</code> 參數傳入 true 會向上尋找目標 DOM 的祖先,直到找到相應的 Blot。</p><p>方法:</p><pre><code class="typescript">Quill.find(domNode: Node, bubble: boolean = false): Blot | Quill</code></pre><p>範例:</p><pre><code class="typescript">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);}</code></pre><h2 id="getIndex"><a href="#getIndex" class="headerlink" title="getIndex"></a>getIndex</h2><p>回傳從文件開頭到帶入的 blot 之間的距離長度。</p><p>方法:</p><pre><code class="typescript">getIndex(blot: Blot): Number</code></pre><p>範例:</p><pre><code class="typescript">// 預先輸入文字並取得第 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);</code></pre><h2 id="getLeaf"><a href="#getLeaf" class="headerlink" title="getLeaf"></a>getLeaf</h2><p>回傳文件中指定索引處的葉節點。<code>leaf</code> 通常指的是資料結構中的末端節點。</p><p>方法:</p><pre><code class="typescript">getLeaf(index: Number): Blot</code></pre><p>範例:</p><pre><code class="typescript">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);</code></pre><h2 id="getLine"><a href="#getLine" class="headerlink" title="getLine"></a>getLine</h2><p>回傳帶入的索引值指定位置的行 blot 。</p><p>方法:</p><pre><code class="typescript">getLine(index: Number): [Blot, Number]</code></pre><p>範例:</p><pre><code class="typescript">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);</code></pre><h2 id="getLines"><a href="#getLines" class="headerlink" title="getLines"></a>getLines</h2><p>回傳指定位置的行中所包含的 blot。</p><p>方法:</p><pre><code class="typescript">getLines(index: Number = 0, length: Number = remaining): Blot[]getLines(range: Range): Blot[]</code></pre><p>範例:</p><pre><code class="typescript">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);</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天看了 Model 提供的 API,這些 API 主要是用於尋找 Blot 的相關應用,對於未來要自訂編輯器模組的功能實現時,可以利用這些 API 來找到正確的 Blot 並進一步處理文本內容,並且 Blot 提供的是 <code>linkedList</code> 的資料結構,因此對於節點的尋找來說,未來編輯內容量大的時候,可以研究看看<code>linkedList</code> 訪問節點的技巧來實現較有效率的搜尋處理。</p><p>Quill 的觀念基本上不難,較有挑戰的地方在於未來要滿足各種特殊需求時,要建立自訂的 Blot 必須要很清楚底層的生命週期與處理過程,這樣才能打造出高效且實用的自訂功能。找時間再繼續研究使用一些第三方套件,並嘗試了解這些套件是如何實現的,對於自訂功能的實現與優化應該會有所幫助。</p><p>再整理一下今天嘗試的 API:</p><ul><li><code>find</code>:透過 DOM 節點找到 Quill 或 Blot 實例。</li><li><code>getIndex</code>:回傳文件開頭到指定 blot 之間的距離。</li><li><code>getLeaf</code>:回傳指定索引處的葉節點。</li><li><code>getLine</code>:回傳指定索引位置的行 blot。</li><li><code>getLines</code>:回傳指定位置內的所有行 blot。</li></ul><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天突然意識到,這星期上完班之後,又是一個連續假期,這次的假期是要回宜蘭帶朋友四處走走,雖然住在宜蘭很久了,但還是有不少地方沒去過,趁這個機會去走走看。不過 11 月就完全沒有連假了,週末期望能好好的學習,並嘗試一些新玩意兒,還有買了一些書,要好好的閱讀一番。</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/f888866bcc3263bcdec796f564cb2ba38521c13e">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#model">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10332980">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 16:讀 Quill Editor API 技術文件 - Events</title>
<link href="/2023/10/01/quill-day-16/"/>
<url>/2023/10/01/quill-day-16/</url>
<content type="html"><![CDATA[<p>今天來看 Events 的部分,Event 的使用方式就和 JavaScript 的事件監聽一樣,透過指定的事件名稱來監聽,並執行指定的方法。</p><h3 id="text-change"><a href="#text-change" class="headerlink" title="text-change"></a>text-change</h3><p><code>text-change</code> 事件在編輯器的內容發生變化時觸發。變更的細節、變更前的內容,以及變更的來源都會提供出來。來源如果是使用者觸發的,則 <code>source</code> 就會是 <code>user</code>。例如:</p><ul><li>使用者在編輯器中打字</li><li>使用者使用工具列格式化文字</li><li>使用者使用快捷鍵回上一步</li><li>使用者使用作業系統拼寫校正</li></ul><p>特例:<br>觸發內容變更的事件雖然也可能透過 API 呼叫,但如果觸發的原因是使用者操作導致的話,<code>source</code> 仍然要設為 <code>user</code>。舉個例子:當使用者點擊工具欄的模組功能,該模組會呼叫變更的 API,但由於是使用者點擊所造成的變化,因此我們在模組呼叫 API 的時候,帶入的 <code>source</code> 仍必須是 <code>user</code>。 </p><p>Silent Source:<br>呼叫 API 處理的內容變更也可能以 <code>source</code> 為 <code>silent</code> 的方式觸發,在這樣的情況下 <code>text-change</code> 將不會被觸發。不建議這樣的操作,因為這樣可能會導致撤銷的堆疊紀錄異常,或是間接影響到需要完整內容變化紀錄的功能。</p><p>選取 (Selection) 發生變化<br>文字內容的變化可能導致 selection 變化(例如,打字使游標前進),但是在 <code>text-change</code> handler 執行期間,selection 尚未更新,加上原生瀏覽器的行為可能導致 selection 狀態不一致的情況。因此要使用 <code>selection-change</code> 或 <code>editor-change</code> 來處理 selection 更新比較穩定。</p><p>Callback Signature:</p><pre><code class="typescript">handler(delta: Delta, oldContents: Delta, source: String)</code></pre><p>範例:</p><pre><code class="typescript">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."); } });</code></pre><h3 id="selection-change"><a href="#selection-change" class="headerlink" title="selection-change"></a>selection-change</h3><p>當使用者或 API 造成 selection 變更時觸發,<code>range</code> 代表 selection 的邊界。當 <code>range</code> 為 <code>null</code> 時,表示 selection 的丟失(通常是由於編輯器失去焦點)。我們也可以在收到 <code>range</code> 是 <code>null</code> 的時候,用這個事件當作焦點變更的 event 確認。</p><p>API 造成的選取範圍變更也可能會以 <code>source</code> 為 <code>silent</code> 觸發,在這樣的情況下就不會觸發 <code>selection-change</code>。如果 <code>selection-change</code> 是 side effect 的話就很有用。例如:輸入文字造成 selection 變更,但每個字元都觸發 <code>selection-change</code> 的話就可能會造成干擾。</p><p>Callback Signature:</p><pre><code class="typescript">handler(range: { index: Number, length: Number }, oldRange: { index: Number, length: Number }, source: String)</code></pre><p>範例:</p><pre><code class="typescript">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'); }});</code></pre><h3 id="editor-change"><a href="#editor-change" class="headerlink" title="editor-change"></a>editor-change</h3><p>當觸發 <code>text-change</code> 或 <code>selection-change</code> 事件時,也會跟著觸發 <code>editor-change</code>,即使 <code>source</code> 是 <code>silent</code> 也是一樣。第一個參數是事件名稱,不是 <code>text-change</code> 就是 <code>selection-change</code>,之後的通常是傳遞給這些相應的 handler 參數。</p><p>Callback Signature:</p><pre><code class="typescript">handler(name: String, ...args)</code></pre><p>範例:</p><pre><code class="typescript">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 }});</code></pre><h2 id="on"><a href="#on" class="headerlink" title="on"></a>on</h2><p>監聽特定的事件並加入 event handler。</p><p>方法:</p><pre><code class="typescript">on(name: String, handler: Function): Quill</code></pre><p>範例:</p><pre><code class="typescript">quill.on('text-change', function() { console.log('Text change!'); });</code></pre><h2 id="once"><a href="#once" class="headerlink" title="once"></a>once</h2><p>為事件的一次觸發加入 event handler。</p><p>方法:</p><pre><code class="typescript">once(name: String, handler: Function): Quill</code></pre><p>範例:</p><pre><code class="typescript">quill.once('text-change', function() { console.log('First text change!');});</code></pre><h2 id="off"><a href="#off" class="headerlink" title="off"></a>off</h2><p>移除 event handler</p><p>方法:</p><pre><code class="typescript">off(name: String, handler: Function): Quill</code></pre><p>範例:</p><pre><code class="typescript">function handler() { console.log('Hello!');}quill.on('text-change', handler);quill.off('text-change', handler);</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>Quill 提供了三種事件監聽類型分別是 <code>text-change</code>,<code>selection-change</code>,以及 <code>editor-change</code>,整理一下今天練習的 event 方法:</p><ul><li><strong>text-change</strong>:內容變化時觸發,包括使用者操作或API呼叫等。</li><li><strong>selection-change</strong>:選取範圍變更時觸發,提供選取的邊界,也能作為焦點變更的事件。</li><li><strong>editor-change</strong>:結合觸發 <code>text-change</code> 與 <code>selection-change</code> 的變更。</li><li><strong>on</strong>:根據監聽類型加入對應的事件處理器。</li><li><strong>once</strong>:根據監聽類型加入只執行一次的事件處理器。</li><li><strong>off</strong>:移除事件處理器。</li></ul><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>前幾天在 DefinitelyTyped 提的 Quill PR 終於合併了,目前只要重新 npm install 就能夠把 OP 類型錯誤的問題解決了,要確認一下 types 的版本是 <code>2.0.12</code>。久違的 OpenSource contribution XD 希望對大家有所幫助 :D</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/789afdecfc34b102d486f85722dca578eecf4bfc">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#events">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10332125">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 15:讀 Quill Editor API 技術文件 - Editor</title>
<link href="/2023/09/30/quill-day-15/"/>
<url>/2023/09/30/quill-day-15/</url>
<content type="html"><![CDATA[<p>今天繼續配著烤肉和月餅來看 Editor 的部分。Editor API 比前面的操作簡單一些,主要用在判斷使用者的游標或焦點狀態,並提供啟用與禁用編輯功能。</p><h2 id="blur"><a href="#blur" class="headerlink" title="blur"></a>blur</h2><p>移除編輯器的 <code>focus</code> 狀態,從使用這的角度來看就是輸入文字的游標離開編輯器。</p><p>方法:</p><pre><code class="typescript">blur()</code></pre><p>範例:</p><pre><code class="typescript">quill.blur();</code></pre><h2 id="enable"><a href="#enable" class="headerlink" title="enable"></a>enable</h2><p>控制編輯器是否能讓使用者進行輸入。當編輯器在 <code>disabled</code> 狀態時,不影響 <code>source</code> 為 <code>api</code> 與 <code>slient</code> 的 API 呼叫。</p><p>方法:</p><pre><code class="typescript">enable(enabled: boolean = true)</code></pre><p>範例:</p><pre><code class="typescript">quill.enable();quill.enable(false); // 禁用使用者輸入</code></pre><h2 id="disable"><a href="#disable" class="headerlink" title="disable"></a>disable</h2><p>將編輯器設為禁用編輯狀態,如同上面的範例所提到的,相當於 <code>enable(false)</code> 的意思。</p><h2 id="focus"><a href="#focus" class="headerlink" title="focus"></a>focus</h2><p>將焦點回到編輯器上,游標會停留在上一次離開 (<code>blur</code>) 的地方。</p><p>方法:</p><pre><code class="typescript">focus()</code></pre><p>範例:</p><pre><code class="typescript">quill.focus();</code></pre><h2 id="hasFocus"><a href="#hasFocus" class="headerlink" title="hasFocus"></a>hasFocus</h2><p>確認焦點是否在編輯器的輸入範圍,這邊需要留意的是焦點在 <code>toolbar</code> 或是 <code>tooltip</code> 時,都不算在編輯器。</p><p>方法:</p><pre><code class="typescript">hasFocus(): Boolean</code></pre><p>範例:</p><pre><code class="typescript">quill.hasFocus();</code></pre><h2 id="update"><a href="#update" class="headerlink" title="update"></a>update</h2><p>同步檢查編輯器的使用者更新,並在發生修改時觸發事件。對於有協作需求要解決衝突時,需要最新的狀態下相當實用。<code>Source</code> 的來源可以是 <code>user</code>,<code>api</code>, 以及 <code>silent</code>。</p><p>由於這主要是用於線上共筆時可能造成編輯衝突時,可以透過 <code>update</code> 方法來同步編輯器的狀態,因此這之後如果有機會再來嘗試看看。</p><p>方法:</p><pre><code class="typescript">update(source: String = 'user')</code></pre><p>範例:</p><pre><code class="typescript">quill.update();</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>稍微回顧一下今天研究 Editor 相關的 API:</p><ul><li><strong>blur</strong>:移除編輯器的焦點狀態。</li><li><strong>enable</strong>:控制編輯器是否能讓使用者進行輸入,包括禁用使用者輸入。</li><li><strong>disable</strong>:相當於 <code>enable(false)</code>,禁止使用者輸入。</li><li><strong>focus</strong>:將焦點回到編輯器上,游標停留在上次離開的地方。</li><li><strong>hasFocus</strong>:確認焦點是否在編輯器上。</li><li><strong>update</strong>:同步檢查編輯器的使用者更新並在修改時觸發事件。<br>除了 <code>update</code> 的操作沒辦法立即呈現之外,大部分的 API 都還滿淺顯易懂的。</li></ul><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天邊塞車邊寫文章,還好有弟弟幫忙開車,早上七點多有驚無險的避免了一場危險,前面的車子似乎快睡著了又沒有打開車道維持輔助,導致車子直接嚕到中央護欄,還好沒翻車,雖然沒看到左側的鈑金狀況,但應該是滿慘的。再次證明了保持車距的重要性。</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/6f5bc5d83ebb73e78952a4567ea22f006343dd64">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#editor">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10331555">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 14:讀 Quill Editor API 技術文件 - Selection</title>
<link href="/2023/09/29/quill-day-14/"/>
<url>/2023/09/29/quill-day-14/</url>
<content type="html"><![CDATA[<p>昨天研究並練習關於套用文本的行內與區塊樣式,初步把每個 API 的使用方式都看過一遍,今天輪到編輯器選取功能相關的 API,繼續給他看下去 XD</p><h2 id="getBounds"><a href="#getBounds" class="headerlink" title="getBounds"></a>getBounds</h2><p>這個方法非常實用,用於獲取指定索引或選取範圍的界限(bounds)。並返回一個包含界限的物件,裡面會有 <code>left</code>,<code>top</code>,<code>width</code> 以及 <code>height</code> 屬性,分別代表指定索引的左上角位置和尺寸。通常用來定位游標或選取的範圍在編輯器容器內的位置。例如可以利用這個方法決定在編輯器內容旁邊或是游標的位置顯示自訂的選單或者工具提示。</p><p>方法:</p><pre><code class="typescript">getBounds(index: Number, length: Number = 0): { left: Number, top: Number, height: Number, width: Number }</code></pre><p>範例:<br>先在編輯器的元素下面新增一個 <code>tooltip</code> 標籤:</p><pre><code class="html"><div id="tooltip" style="display: none; position: absolute; background-color: lightgray;"> 我是一個小提示 </div></code></pre><p>實現監聽事件,在文本選取的時候判斷選取的位置來顯示 tooltip:</p><pre><code class="typescript">// 監聽文本選擇事件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'; } }});</code></pre><p>效果如下:<br><img src="/2023/09/29/quill-day-14/20090749cNFuDeHiXF.png" alt="Tooltip 效果如下"></p><h2 id="getSelection"><a href="#getSelection" class="headerlink" title="getSelection"></a>getSelection</h2><p>獲取編輯器中當前的選取範圍。可帶入 optional 參數 <code>focus</code>,如果為 <code>true</code>,則獲取焦點之後返回選取的範圍 <code>index</code> 與 <code>length</code>,如果為 <code>false</code>,則返回 <code>null</code>。</p><p>方法:</p><pre><code class="typescript">getSelection(focus = false): { index: Number, length: Number }</code></pre><p>範例:</p><pre><code class="typescript">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');} </code></pre><h2 id="setSelection"><a href="#setSelection" class="headerlink" title="setSelection"></a>setSelection</h2><p>設置編輯器中的選取範圍,這也會使編輯器是在 <code>focus</code> 的狀態。如果傳入的參數為 <code>null</code>,則會離開焦點並觸發 <code>blur</code> 事件。</p><p>方法:</p><pre><code class="typescript">setSelection(index: Number, length: Number = 0, source: String = 'api')setSelection(range: { index: Number, length: Number }, source: String = 'api')</code></pre><p>範例:</p><pre><code class="typescript">quill.setSelection(0, 5);</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>稍微整理一下:</p><ul><li><code>getBounds</code>: 獲取指定索引的座標或界限資訊,常用於定位游標或選取範圍在編輯器內的位置。</li><li><code>getSelection</code>: 獲取編輯器中當前的選取範圍,可用於判斷用戶選取的內容或游標位置。</li><li><code>setSelection</code>: 設置編輯器中的選取範圍,使編輯器處於 Focus 狀態。</li></ul><p>今天詳細探討了編輯器的 Selection 功能,Quill 提供了選取範圍相關的控制方法,到目前為止,無論是文本樣式或是內容選取,可以看到大部分的操作都離不開 <code>index</code> 與 <code>length</code>,而 <code>range</code> 是一個滿方便使用的參數,可以知道選取的起點以及選取的長度,以便我們在自訂功能的時候可以做為位置索引的參考。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>已經好幾年沒有玩過手機遊戲了,最近上下班通勤時間都會玩一款新的手機遊戲叫 Monster Hunter Now,整體還滿有趣的,是 Niantic 也就是開發 Ingress 以及 Pokemon Go 的開發商,這遊戲也是要走出去戶外實際去看地圖上有哪些資源以及魔物,也能夠與其他玩家來進行遊戲。不過有個小缺點,就是當 HP 不夠的時候需要使用藥水,那個藥水除了每天提供五罐免費的之外,其他時間若喝完的話,就需要等時間回復或者直接打開線上商城買藥水道具,就是要課金的意思,但畢竟是休閒,沒血的話就乖乖的等待回滿再繼續被魔物虐了 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/5a1302ac5145a19ebd6df9db2f9d6f22c4a66141">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#selection">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10330722">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 13:讀 Quill Editor API 技術文件 - Formatting</title>
<link href="/2023/09/28/quill-day-13/"/>
<url>/2023/09/28/quill-day-13/</url>
<content type="html"><![CDATA[<p>繼昨天的 Contents 相關的 API,今天來看看 Formatting 的部分。</p><h2 id="format"><a href="#format" class="headerlink" title="format"></a>format</h2><p>根據使用者當前選擇的字串套用文字格式,回傳的 Delta 代表變更的內容。當使用者選擇字串長度為 <code>0</code> 時,代表是游標的狀態,對應的文字樣式則會變成啟動狀態,使用者接下來輸入的內容則會套用啟動的文字樣式。<code>Source</code> 一樣可以設定 <code>user</code>,<code>api</code> 或 <code>silent</code>。當呼叫的時候如果編輯器為禁用(disabled) 狀態,則會直接略過 <code>source</code> 為 <code>user</code> 的呼叫。</p><ul><li><code>source</code> 預設是 <code>api</code></li></ul><p>方法:</p><pre><code class="typescript">format(name: String, value: any, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">quill.format('color', 'red');quill.format('align', 'right');</code></pre><h2 id="formatLine"><a href="#formatLine" class="headerlink" title="formatLine"></a>formatLine</h2><p>將選到的行數套用樣式,回傳的 Delta 代表變更的內容。關於可使用的樣式有哪些,可以參考官網文件 <a href="https://quilljs.com/docs/formats/">formats</a>描述。這個方法主要是處理區塊 (block) 樣式,當呼叫的時候如果帶入的樣式是屬於行內 (inline) 樣式,則會沒有效果。要移除格式的話直接在 <code>value</code> 的參數傳入 <code>false</code> 即可。另外套用區塊樣式的時候,可能會在套用後導致使用者當前的選擇被取消,並且游標移動到新的位置。</p><ul><li><code>source</code> 預設是 <code>api</code></li></ul><p>方法:</p><pre><code class="typescript">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</code></pre><p>範例:</p><pre><code class="typescript">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',});</code></pre><h2 id="format-VS-formatLine"><a href="#format-VS-formatLine" class="headerlink" title="format VS formatLine"></a>format VS formatLine</h2><p>最主要的差別就是 <code>format</code> 用在更改選取範圍內的特定格式,例如字體大小、顏色、粗體等。而 <code>formatLine</code> 處理的是整行的樣式,例如列表、對齊方式等。</p><h2 id="formatText"><a href="#formatText" class="headerlink" title="formatText"></a>formatText</h2><p>一樣是在編輯器針對選定的範圍套用文字的樣式,回傳的是內容變更的 Delta,如果要移除文字樣式,則直接在對應樣式的值帶入 <code>false</code> 即可移除。如果是操作 block 相關的樣式,使用者的選擇範圍可能不會保留。<code>Source</code> 的來源有 <code>user</code>,<code>api</code>,以及 <code>silent</code>,當編輯器為 <code>disabled</code> 狀態則會直接無視 <code>source</code> 為 <code>user</code> 的呼叫。</p><ul><li><code>source</code> 預設是 <code>api</code></li></ul><p>方法:</p><pre><code class="typescript">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</code></pre><p>範例:</p><pre><code class="typescript">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 的那一行置右</code></pre><h2 id="getFormat"><a href="#getFormat" class="headerlink" title="getFormat"></a>getFormat</h2><p>這個方法可以讓我們查詢特定範圍內文字的格式。如果範圍內的所有文字共用相同的格式,則會回傳該格式。如果有不同的真值 (truthy value),則會回傳所有的真值在陣列中。當不帶參數呼叫此方法,將針對當前使用者選取的範圍進行操作。</p><ul><li><code>source</code> 預設是 <code>api</code></li></ul><p>方法:</p><pre><code class="typescript">getFormat(range: Range = current): { [String]: any }getFormat(index: Number, length: Number = 0): { [String]: any }</code></pre><p>範例:</p><pre><code class="typescript">// 假設設定一段文字 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' }</code></pre><h2 id="removeFormat"><a href="#removeFormat" class="headerlink" title="removeFormat"></a>removeFormat</h2><p>將選定的範圍內刪除所有的格式及嵌入內容,並回復到沒有格式的狀態。回傳的 Delta 代表變更的操作,如果範圍內包含到 block format,也會一併移除。因此使用者的選取狀態可能不會被保留。<code>Source</code> 可以是 <code>user</code>,<code>api</code> 或 <code>silent</code>。當編輯器為 <code>disabled</code> 狀態時,<code>source</code> 為 <code>user</code> 的呼叫將會被忽略。</p><ul><li><code>source</code> 預設是 <code>api</code></li></ul><p>方法:</p><pre><code class="typescript">removeFormat(index: Number, length: Number, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">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' } }// ]</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天嘗試使用格式化相關的 API,基本上使用的方式都差不多,但我們還沒討論到 Format 還有哪些可以使用,剛才的介紹中也有提到<a href="https://quilljs.com/docs/formats/">這篇文件</a>有列出所有支援的格式,要找時間來實驗看看並感受一下。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>明天就開始一小段假期,不過按照往年的慣例,都會先回去宜蘭開車回台南拜拜,希望這次塞車不要塞的太久QQ,儘管早上四點半就起床,五點就出門了,到了七八點還是會開始塞。印象中過台中之前都滿大的機會遇到塞車的情況,還好可以跟弟弟輪流開,不至於累到不行 XD </p><p>祝中秋佳節愉快 :)</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/275e5d30a3206460dfbca1cc292dda0f6d5f15f5">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#format">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10330316">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 12:讀 Quill Editor API 技術文件 - Content (下)</title>
<link href="/2023/09/27/quill-day-12/"/>
<url>/2023/09/27/quill-day-12/</url>
<content type="html"><![CDATA[<p>今天就繼續來看 Content 相關的 API 後半段。</p><h2 id="insertEmbed"><a href="#insertEmbed" class="headerlink" title="insertEmbed"></a>insertEmbed</h2><p>將嵌入式內容插入編輯器,return 為更改後的 Delta 物件。<code>source</code> 可以是 <code>user</code>、<code>api</code> 或 <code>silent</code>。當編輯器是 <code>disabled</code> 狀態時,當 <code>source</code> 設為 <code>user</code> 的呼叫則會被忽略。</p><ul><li><code>index</code> 可以選擇要插入的位置索引值</li></ul><p>方法:</p><pre><code class="typescript">insertEmbed(index: Number, type: String, value: any, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">quill.insertEmbed(10, 'image', 'https://quilljs.com/images/cloud.png');</code></pre><h2 id="insertText"><a href="#insertText" class="headerlink" title="insertText"></a>insertText</h2><p>顧名思義將文字插入編輯器,可以選擇使用指定格式或多種格式。return 收到的是更新後的 Delta 物件。<code>source</code> 可以是 <code>user</code>、<code>api</code> 或 <code>silent</code>。當編輯器 <code>disabled</code> 時,<code>source</code> 為 <code>user</code> 的呼叫將直接略過。</p><p>方法共有三種,後兩者的差別在於 format 可以設一個或多個文字格式。</p><pre><code class="typescript">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</code></pre><p>範例:</p><pre><code class="typescript">quill.insertText(0, 'Hello'); quill.insertText(3, 'Hello', 'bold', true); quill.insertText(8, 'Quill', { 'color': '#ffff00', 'italic': true });</code></pre><h2 id="setContents"><a href="#setContents" class="headerlink" title="setContents"></a>setContents</h2><p>將參數的內容覆蓋編輯器。內容必須以換行符號 <code>\n</code> 結尾。return 收到的是更新後的 Delta。如果給定 Delta 沒有無效操作,這將與傳入的 Delta 相同。<code>source</code> 可以為 <code>user</code>、<code>api</code> 或 <code>silent</code>。當編輯器是 <code>disabled</code> 狀態時,當<code>source</code> 為 <code>user</code> 的呼叫則會被忽略。</p><p>方法:</p><pre><code class="typescript">setContents(delta: Delta, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">// 使用 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);</code></pre><p>上面這個範例可以觀察到套用 <code>header</code> 的變化,除了從 <code>text-change</code> 觀察到的套用方式,如果想要在一個 <code>insert</code> 就實現樣式與 <code>header</code> 格式套用,可以在文字內容的最後加上換行符號,這樣加上 <code>header</code> 在 <code>attribute</code> 上才會有效果。</p><h2 id="setText"><a href="#setText" class="headerlink" title="setText"></a>setText</h2><p>將純文字內容覆蓋到編輯器,return 收到的是更新後的 Delta,文字內容必須以換行符號做結尾,沒有加上的話,編輯器會另外加上。與 <code>setContents</code> 不同的是,<code>setText</code> 只能將純文字覆蓋到編輯器,而 <code>setContents</code> 的文字內容可以包含不同的格式。<code>source</code> 可以為 <code>user</code>、<code>api</code> 或 <code>silent</code>,預設是 <code>api</code>。當編輯器是 <code>disabled</code> 狀態時,當<code>source</code> 為 <code>user</code> 的呼叫則會被忽略。</p><p>方法:</p><pre><code class="typescript">setText(text: String, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">quill.setText('Hello\n');</code></pre><h2 id="updateContents"><a href="#updateContents" class="headerlink" title="updateContents"></a>updateContents</h2><p>將 Delta 資料更新到編輯器,return 收到的是更新操作的 Delta。如果傳入的 Delta 沒有不合法的操作,return 收到的 Delta 則會是相同的內容。舉例來說,當編輯器沒有內容,但仍然執行 <code>retain(6)</code> 的話,實際上回傳的 Delta 中的 <code>retain</code> 會只有 1,因為空白的編輯器會預設一個換行符號,因此長度只有 <code>1</code> 可以 <code>retain</code>。<br>另外,即使執行 <code>delete(5)</code>,收到的 Delta 變化也不會有看到 ops 中有 <code>delete</code> 的操作,畢竟編輯器沒有內容可以讓我們刪除。</p><p>方法:</p><pre><code class="typescript">updateContents(delta: Delta, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">// 假設編輯器當前的內容 [{ 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} }// ]</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>在實際看過每個方法及體驗過使用方式後,對於 Contents API 的運用有初步的認識,並在不同的情境下選擇適合的 API ,透過帶入不同參數的呼叫方式實現功能,我們也可以在特殊情況自訂 <code>source</code> 來決定保留或跳過編輯器的觸發機制,明天接著進入到 Formatting 的章節,也就是套用文字格式。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>最近午餐跟著其他同事點外賣,不過也許是上班日的關係,在尖峰時段單點東西似乎特別容易漏掉,漏餐的話,幫忙開團的同事還要確認是否有其他同事也沒拿到,然後還要處理退款的申請,再次感謝願意開團的同事 XD。看來以後在尖峰時段還是盡量點套餐比較保險…也許吧XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/ee5d69d6180cba551f71523af00152bf827b6f01">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#content">Quill API - Conetent</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10329481">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 11:讀 Quill Editor API 技術文件 - Content (上)</title>
<link href="/2023/09/26/quill-day-11/"/>
<url>/2023/09/26/quill-day-11/</url>
<content type="html"><![CDATA[<p>今天開始來細看 Quill Editor 的技術文件,看看 Quill 有哪些方法可以使用。同時也準備了範例練習,實際呼叫並觀察也許會比較有感覺。這會是一段細節探索的旅程。</p><h2 id="關於-Source"><a href="#關於-Source" class="headerlink" title="關於 Source"></a>關於 <code>Source</code></h2><p>在閱讀技術文件的時候,有部分的 <code>function</code> 會提供 <code>Source</code> 的參數名稱,稍微研究了一下。</p><ul><li>API:當更改的事件來自 API 的呼叫,例如直接用 JavaScript 呼叫 Instance 的 function</li><li>User:當變更來自使用者的操作時,例如:使用者在編輯器打字輸入內容、貼上圖片或是修改文字樣式。</li><li>Silent:此選項允許我們在不觸發任何 Quill 事件的情況下進行更改。這在某些情況下很實用,例如,我們想要在不通知其他程式的情況下更新編輯器的內容。</li></ul><p>大部分的情況是不需要自訂設定這些值,只有在較特殊的情況下,需要額外設置以更精確的控制編輯的行為。例如:</p><pre><code class="typescript">// 使用 'api' 作為 source,代表這個更改是由 API 控制的quill.format('bold', true, 'api'); // 使用 'user' 作為 source,代表這個更改是模擬使用者操作quill.format('italic', true, 'user'); </code></pre><p>透過這樣的分別使用,我們可以在事件監聽或其他處理邏輯中區分更改的來源,進而執行不同的操作或處理。例如我們只對使用者所做的更改進行特定的處理,並忽略由 API 控制的更改。透過 <code>source</code> 的設置就讓我們滿足這樣的需求。</p><h2 id="deleteText:在指定位置刪除文字"><a href="#deleteText:在指定位置刪除文字" class="headerlink" title="deleteText:在指定位置刪除文字"></a>deleteText:在指定位置刪除文字</h2><p>刪除的來源可以是從 <code>user</code>, <code>api</code> 或 <code>silent</code>。當編輯器狀態為 disabled 時,會直接忽略掉從 <code>user</code> 來的呼叫</p><p>方法:</p><pre><code class="typescript">deleteText(index: Number, length: Number, source: String = 'api'): Delta</code></pre><p>範例:</p><pre><code class="typescript">quill.deleteText(4, 6) // 從第 4 個位置,刪除長度 6 的內容</code></pre><h2 id="getContents:獲取編輯器指定位置與長度的內容"><a href="#getContents:獲取編輯器指定位置與長度的內容" class="headerlink" title="getContents:獲取編輯器指定位置與長度的內容"></a>getContents:獲取編輯器指定位置與長度的內容</h2><p>獲取編輯器的內容以及格式資料,收到的是 Delta 物件。可選參數有兩個:</p><ul><li><code>index</code>:指定獲取內容的起始索引,預設是從 <code>0</code></li><li><code>length</code>:指定要獲取內容的長度,預設 <code>remaining</code> 是指從起始索引後的剩餘內容</li></ul><p>方法:</p><pre><code class="typescript">getContents(index: Number = 0, length: Number = remaining): Delta</code></pre><p>範例:</p><pre><code class="typescript">// 獲取完整內容的 Deltaconst delta = quill.getContents();// 獲取部分內容的 Deltaconst delta = quill.getContents(27, 5);</code></pre><h2 id="getLength:獲取編輯器內容的長度"><a href="#getLength:獲取編輯器內容的長度" class="headerlink" title="getLength:獲取編輯器內容的長度"></a>getLength:獲取編輯器內容的長度</h2><p>獲取編輯器內容的長度。<strong>需要注意的是,即使 Quill 為空,仍然有一個由 ‘\n’ 表示的空行,因此 getLength 將返回 1</strong>。</p><p>方法:</p><pre><code class="typescript">getLength(): Number</code></pre><p>範例:</p><pre><code class="typescript">const length = quill.getLength();</code></pre><h2 id="getText:獲取指定位置與長度的文本內容"><a href="#getText:獲取指定位置與長度的文本內容" class="headerlink" title="getText:獲取指定位置與長度的文本內容"></a>getText:獲取指定位置與長度的文本內容</h2><p>獲取編輯器的字串內容,非字串的內容會直接省略,因此返回的字串長度可能會比呼叫 <code>getLength</code> 回傳的編輯器長度短些。這邊一樣要留意的是,即使編輯器是空的沒有內容,仍然會留一個空行,所以在這樣的情況將會返回 <code>\n</code>。</p><ul><li><code>index</code>:指定獲取內容的起始索引,預設是從 <code>0</code></li><li><code>length</code>:指定要獲取內容的長度,預設 <code>remaining</code> 是指從起始索引後的剩餘內容</li></ul><p>方法:</p><pre><code class="typescript">getText(index: Number = 0, length: Number = remaining): String</code></pre><p>範例:</p><pre><code class="typescript">// 獲取從 0 開始,長度為 10 的文本內容const text = quill.getText(0, 10);</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天開始仔細閱讀技術文件,會相對的比較乏味,但是能徹底的去看每個方法及參數要如何使用,知道自己有哪些武器可以用,對於特殊的需求也比較能找到合適的方法來實現。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>由於中秋節快到了,開始收到各種月餅禮盒,周遭也開始出現柚子,雖然還沒開始烤肉,但希望中秋之後別長太多肥肉出來,剛轉換跑道一陣子,還在適應節奏的階段,需要找到合適的運動時間,目前看來只剩下早上了,下班後加上通勤時間到健身房,運動完回家洗完澡也差不多到睡覺的時間了,最近嘗試調整起床的時間,先從六點半開始觀察看看囉…XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/1f147960445f8f26c42cc653221d10e7443cabdc">今日份的練習</a></li><li><a href="https://quilljs.com/docs/api/#content">API - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10328811">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 10:編輯內容的呈現 - Parchment 與 Blot</title>
<link href="/2023/09/25/quill-day-10/"/>
<url>/2023/09/25/quill-day-10/</url>
<content type="html"><![CDATA[<p>今天我們將一同探討 Quill 背後的兩個關鍵概念:Blot 和 Parchment。雖然這些名詞可能聽起來有些陌生,但它們在 Quill 的運作中扮演了重要的角色。讓我們來了解一下,究竟 Blot 和 Parchment 是什麼,以及它們如何影響著 Quill 的編輯內容。</p><h3 id="什麼是-Parchment?"><a href="#什麼是-Parchment?" class="headerlink" title="什麼是 Parchment?"></a>什麼是 Parchment?</h3><p>Parchment 是 Quill 的文件模型,它是與 DOM (Document Object Model) 並行的樹狀結構,並提供了對內容編輯(例如 Quill)的功能。一個 Parchment 樹是由多個 Blot 組成的,這些 Blot 會反射對應的 DOM 節點。Blot 如同剛才提到的,它可以提供結構、格式與內容。換句話說,它們扮演著為文件元素提供實質性、樣式和功能的角色。除了 Blot 之外,Attributors 也可以提供輕量的格式化資訊,可定義如何將某些簡單的格式套用到文本或其他元素。</p><h3 id="什麼是-Blot?"><a href="#什麼是-Blot?" class="headerlink" title="什麼是 Blot?"></a>什麼是 Blot?</h3><p>Blot 是構成 Parchment 文件的基本區塊,有幾個基本的實現例如:<code>Block</code>, <code>Inline</code>, 以及 <code>Embed</code>。大部分的情況下,我們不會從頭開始實現 Blot,而是從其中一個基本實現來建立自訂功能。一個最基本的 Blot 必須使用一個靜態的 <code>blotName</code> 來命名,並且有一個相關聯的 <code>tagName</code> 或 <code>className</code>。如果 Blot 是透過標籤和 Class 定義的,Class 會是第一個優先,標籤則會作為備用。Blot 也需要有一個範圍,作為確認是行內 (inline) 還是區塊 (block)。</p><pre><code class="typescript">class Blot { static blotName: string; static className: string; static tagName: string | string[]; static scope: Scope; domNode: Node; prev: Blot | null; next: Blot | null; parent: Blot; // 建立對應的 DOM 節點 static create(value?: any): Node; constructor(domNode: Node, value?: any); // 對於子集來說,是 Blot 的長度 // 對於父節點來說,是所有子節點的總和 length(): Number; // 如果適用,則按照給定的 index 和 length 進行處理 // 經常會把響應轉移到合適的子節點上 deleteAt(index: number, length: number); formatAt(index: number, length: number, format: string, value: any); insertAt(index: number, text: string); insertAt(index: number, embed: string, value: any); // 回傳當下 Blot 與父節點之間的偏移量 offset(ancestor: Blot = this.parent): number; // 在更新的生命週期結束後被呼叫 // 不能修改文件的值和長度,並且任何 DOM 的操作必須降低 DOM 樹的複雜度 // 共用的 context 物件會被傳到所有的 Blot optimize(context: {[key: string]: any}): void; // 當 blot 發生變化時呼叫,並帶著其變化的紀錄。 // blot 值得內部紀錄可以被更新,並允許修改 Blot 本身。 // 可以透過使用者操作或 API 呼叫觸發 // 共用 context 物件並傳給所有的 Blot update(mutations: MutationRecord[], context: {[key: string]: any}); /** Leaf Blots only **/ // 如果是 Blot 的類型,則回傳由 domNode 表示的值 // 本身沒有對 domNode 的類型校驗,需要應用程式在呼叫前進行外部校驗 static value(domNode): any; // 給定一個 node 和 DOM 選擇範圍內的偏移量,回傳一個該位置的 index index(node: Node, offset: number): number; // 給定一個 Blot 的座標位置,回傳目前節點在 DOM 可以選範圍的偏移量 position(index: number, inclusive: boolean): [Node, number]; // 回傳目前 Blot 代表的值 // 除了來自 API 或透過 update 可檢測的使用者變更,否則不應該被改變 value(): any; /** Parent blots only **/ // Blots 的白名單陣列,可以是直接的子節點 static allowedChildren: Blot[]; // 預設節點,當節點為空時會被插入 static defaultChild: string; children: LinkedList<Blot>; // 在建構時呼叫,應該填入其子節點的 LinkedList build(); // 對後代有用的搜尋功能,不應修改 descendant(type: BlotClass, index: number, inclusive): Blot descendents(type: BlotClass, index: number, length: number): Blot[]; /** Formattable blots only **/ // 如果是 Blot 的類型,則回傳 domNode 格式化後的值 // 不需要檢查 domNode 是否為 blot 類型 static formats(domNode: Node); // 套用格式到 blot,不應該傳到子節點或其他的 Blot format(format: name, value: any); // 回傳代表 Blot 的格式,包括來自 Attributors formats(): Object; }</code></pre><h3 id="自訂-Blot"><a href="#自訂-Blot" class="headerlink" title="自訂 Blot"></a>自訂 Blot</h3><p>我們可以透過自訂 Blot 的屬性和行為,來實現各種自訂的功能和外觀。這讓我們能夠按照專案的需求來建立符合特定用途的編輯器元素。例如我們可以建立一個紅色的 <code>span</code> 元素:</p><pre><code class="typescript">import Quill from 'quill';// 自訂一個紅色的 span 元素export class RedTextBlot extends Quill.import('blots/inline') { static blotName = 'red-text'; static tagName = 'span'; static create(value: string) { const node: HTMLElement = super.create() as HTMLElement; if (value) { node.style.color = 'red'; } return node; }}// 初始化 Quillconst quillEditor = new Quill('#editor', { theme: 'snow', modules: { toolbar: true, },});// 插入自訂的紅色文字quillEditor.insertText(0, '這是自訂的', 'red-text', true);</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>剛開始研究的時候一頭霧水,需要多看幾次搭配練習才逐漸有感覺。Blot、Parchment 和 Delta 在 Quill Editor 中是密切相關的三個核心概念,它們共同形成了 Quill Editor 的基礎架構和功能。</p><ol><li><p><strong>Delta:</strong> Delta 是 Quill 使用的資料結構,專門用來描述編輯器內容或其變更。Delta 包含一個稱為 <code>ops</code> 的物件陣列,其中每個物件都是一個操作,這些操作可以是插入文字、刪除內容或套用格式等。</p></li><li><p><strong>Blot:</strong> Blot 是 Quill 文件模型 Parchment 中的基本建構單位,代表編輯器中的各種元素,例如文字、圖片和樣式。每個 Blot 都具有特定的屬性和行為,能夠包含或被其他 Blots 包含,形成一個層次化的樹狀結構。</p></li><li><p><strong>Parchment:</strong> Parchment 是 Quill 的文件模型,它存在於 DOM 樹結構的平行層面。Parchment 提供了一系列對內容編輯有用的功能和接口。一個 Parchment 主要由多個 Blot 組成,這些 Blot 對應到 DOM 樹中的特定節點。</p></li></ol><p>今天初步了解了 Quill 其中的核心概念:Blot 和 Parchment。透過理解這些概念,讓我們能對於 Quill 的功能與機制有所掌握,並在在實際專案開發中較能得心應手。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>昨天參加了教師節聚餐,意外發現有兩個學妹在同公司,但我現在要叫學姊 XD 難得有這樣的機會可以跟學弟妹們交流。吃完飯之後還到了咖啡廳邊喝咖啡邊聊天,聊了很多,無論是技術或是職涯規劃,都有很棒的收穫。老師還加碼續攤請我們吃晚餐,體驗到某家美墨餐廳美味餐點的強大。我覺得每年盡可能的來參加這樣的活動,除了敘舊之外,更多的是互相同步一下近況,也能看到很多厲害的學弟妹們卓越的成就。期待下一次的聚餐!</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference:"></a>Reference:</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/875db66a72dcdb6e5ef27a98609ef9854f982dea">今日份的練習</a></li><li><a href="https://github.com/quilljs/parchment">quilljs/parchment (github.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10327573">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 9:Quill Editor 的資料物件 - Delta (下)</title>
<link href="/2023/09/24/quill-day-9/"/>
<url>/2023/09/24/quill-day-9/</url>
<content type="html"><![CDATA[<p>昨天探討了 Delta,初步有了一些概念,今天就來嘗試練習看看,為了方便查看,先新增一個 <code>quill-editor.service</code>,並將 Delta 相關的練習內容都放到這裡面。</p><h2 id="使用-new-Delta-操作"><a href="#使用-new-Delta-操作" class="headerlink" title="使用 new Delta() 操作"></a>使用 new Delta() 操作</h2><p>在上一篇文章中,官方不建議手動建立 Delta 物件,應該要透過可連結 Deltas 物件的方法像是:<code>insert()</code>、<code>delete()</code>,和 <code>keep()</code> 等方法來建立新的 Delta。</p><p>因此使用的操作方式會是每次都直接 new 一個 <code>Delta</code>,並把要進行的文本操作透過鏈式呼叫(Method Chaining) 的方式處理,最後再呼叫 <code>quill.updateContents</code> 方法並帶入新增的 <code>helloWorldDelta</code>:</p><pre><code class="typescript">const helloWorldDelta = new Delta().insert('Hello World!');quill.updateContents(helloWorldDelta as any); // types 問題,暫時 as any</code></pre><p>從上面看到我在 <code>helloWorldDelta</code> 後面加了 as any,如果沒加的話會導致型別的錯誤,因為 <code>@types/quill</code> 並不是官方的類型定義庫,因此在後來的版本有修改了 <code>Op</code> 這個 interface,導致在編譯的時候會發生型別錯誤,目前暫解就是 <code>as any</code>,我也提個一個 PR,主要是變更 <code>quill-delta</code> 的版本號,還不確定能不能過 XD 先等看看 reviewer 有沒有什麼回應了。</p><h3 id="鏈式呼叫-Method-Chaining"><a href="#鏈式呼叫-Method-Chaining" class="headerlink" title="鏈式呼叫 (Method Chaining)"></a>鏈式呼叫 (Method Chaining)</h3><p>如果直接執行上方的範例,應該會看到編輯器出現了 Hello World!,但當你先輸入一些內容之後再執行這個方法,就會看到 Hello World! 並不是從游標後面接著進去的,原因是 delta 加入內容的操作都是從頭開始塞進去的,所以這裡我們需要使用鏈式呼叫的方式,在插入新內容之前計算一下目前的游標位置,並在這個位置後面加上內容:</p><pre><code class="typescript">// 使用 `getSelection()` 取得選取狀態const currentIndex = quill.getSelection()?.index;if (typeof currentIndex === 'number') {// 將內容插入 const insertContent = 'Hello World!'; const helloWorldDelta = new Delta() .retain(currentIndex) .insert(insertContent); quill.updateContents(helloWorldDelta);}</code></pre><p>這時我們就能跟著游標位置插入內容,但又注意到另一個問題,插入內容之後游標卻還是在原地,印象中好的操作體驗應該是插入內容後,游標也應該跟著移動到新增的內容後面才對。這時我們還需要呼叫一個方法來更新游標的位置。</p><h3 id="更新游標位置"><a href="#更新游標位置" class="headerlink" title="更新游標位置"></a>更新游標位置</h3><p>使用 <code>setSelection</code> 更新編輯器游標位置,新的 <code>index</code> 可以用 <code>currentIndex</code> + <code>insertContent.length</code> 來獲得:</p><pre><code class="typescript">// 使用 `getSelection()` 取得選取狀態const currentIndex = quill.getSelection()?.index;if (typeof currentIndex === 'number') {// 將內容插入 const insertContent = 'Hello World!'; const helloWorldDelta = new Delta() .retain(currentIndex) // 保留到游標前的內容 .insert(insertContent); // 插入內容 quill.updateContents(helloWorldDelta); // 帶入 Delta 更新內容 quill.setSelection(currentIndex + insertContent.length, 0); // 更新游標位置}</code></pre><h3 id="Line-Formatting"><a href="#Line-Formatting" class="headerlink" title="Line Formatting"></a>Line Formatting</h3><p>除了內容的輸入,有時候需要加入整行的內容並加上文字格式,例如我們可以插入一個 H1 級別的 Header 內容,為了能夠套用到整行,必須再加上一個換行的 <code>Delta</code> 並加上 Header 的 attribute:</p><pre><code class="typescript">const currentLength = quill.getLength();const currentIndex = quill.getSelection()?.index;if (typeof currentIndex === 'number') { const headerContent = 'This is Header'; const headerDelta = new Delta() .retain(currentLength) .insert(headerContent) .insert('\n', { header: 1 }); quill.updateContents(headerDelta); quill.setSelection(currentIndex + headerContent.length + 1, 0);}</code></pre><p>上面這個範例可以看到除了 <code>insert(headerContent)</code> 將 header 的內容加上之外,後面還 <code>insert('\n', { header:1 })</code> 表示 header 的樣式是加在換行符號上的。即使最後一行沒有套用格式,所有 Quill 文件都必須以換行符號結尾,這樣我們就始終能有一個字元位置來套用行格式。</p><h2 id="從內容變化時看-Delta-內容"><a href="#從內容變化時看-Delta-內容" class="headerlink" title="從內容變化時看 Delta 內容"></a>從內容變化時看 Delta 內容</h2><p>我們可以註冊 Quill 的 <code>text-change</code> 事件,這個事件會提供 Delta 作為文本內容發生變化時的描述。先在 <code>quill-editor.service.ts</code> 新增一個 <code>updateQuillChanges</code> 方法來處理 Quill 事件註冊:</p><pre><code class="typescript">@Injectable({ providedIn: 'root',})export class QuillEditorService { quillUpdateSubject$ = new Subject<Delta>(); // ... updateQuillChanges(quill: Quill) { quill.on('text-change', (delta) => this.quillUpdateSubject$.next(delta)); }}</code></pre><p>在 <code>updateQuillChanges</code> 底下註冊 <code>text-change</code> 事件,並把獲得帶有內容變更的 delta 使用 <code>subejct</code> 方式打出去。這個方法可以在 Quill 初始化後,接著加入事件監聽並訂閱。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>Delta 的操作其實還滿單純的,我們可以透過監聽事件觀察 Delta 如何描述內容變更,同時我們也可以使用 <code>quill.getContent()</code> 取得完整描述文本的 Delta 狀態,透過這樣的觀察可以回推當有特定需求的時候,我們可以如何實現較正確的方式來變更編輯器的內容。搭配 Angular 實現 Quill 相關的機制在專案管理及維護上都能有所幫助。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天室友發了一個很酷的 30 天鐵人賽,是關於桃園青埔的建案描述,雖然跟 IT 沒有太大的關聯,但也是滿有趣的挑…戰? XD 這讓我想到有一個說法是要培養一個習慣需要花 30 天來練習。希望寫文章的習慣可以在這次挑戰之後,對於寫文章就比較不會太卡,往往都是面對一頁空白的時候不知道從何下筆,但都用電腦設備打文章了,先寫一點東西就對了 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://quilljs.com/docs/delta/">Delta - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10326421">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 8:Quill Editor 的資料物件 - Delta (上)</title>
<link href="/2023/09/23/quill-day-8/"/>
<url>/2023/09/23/quill-day-8/</url>
<content type="html"><![CDATA[<p>今天初步探討 Quill Editor 資料物件之一,Delta,就一起來了解一下吧。</p><h3 id="什麼是-Delta-物件?"><a href="#什麼是-Delta-物件?" class="headerlink" title="什麼是 Delta 物件?"></a>什麼是 Delta 物件?</h3><p>不要被 Delta 這酷酷的名字給嚇到,它其實是很單純的東西。</p><p>首先我們將官網文件的描述翻譯來看:</p><blockquote><p>Delta 是一種簡單但富有表現力的格式,可用於<strong>描述 Quill 的內容和變化</strong>。這個格式是 JSON 的嚴格子集,可讀性高,並且機器也能容易解析。Delta 可以描述任何 Quill 文件,包含所有文字和格式資訊,並去除 HTML 的歧義與複雜性。</p></blockquote><p>Delta 是 Quill Editor 使用的資料物件,它同時也作為<a href="https://github.com/quilljs/delta/">獨立的儲存庫</a>,使 Delta 可以在 Quill 外的情境中被使用。主要用途在處理<a href="https://en.wikipedia.org/wiki/Operational_transformation">操作轉換(Operational Transform)</a>,並且可以套用到即時協同編輯的應用,像 Google Docs,之後我們再來深入了解 Delta 的格式設計概念。</p><p>因此,Delta 主要用來儲存編輯器的內容,它由一系列的操作物件組成,每個操作物件代表一個對內容的變更,例如插入文字、刪除內容、新增樣式等。Delta 的結構主要是以操作物件(OPS Object)的陣列方式呈現。</p><p>另外需要注意的一點是:不建議手動建立 Delta 物件,應該要透過可連結 Deltas 物件的方法像是:<code>insert()</code>、<code>delete()</code>,和 <code>keep()</code> 等方法來建立新的 Delta。</p><h2 id="當-Delta-為文件內容的描述"><a href="#當-Delta-為文件內容的描述" class="headerlink" title="當 Delta 為文件內容的描述"></a>當 Delta 為文件內容的描述</h2><p>每個 Delta 物件都包含一個 <code>ops</code> 屬性,這個屬性是一個操作陣列。每個操作都是一個物件,包含 <code>insert</code>、<code>delete</code>、<code>retain</code> 等屬性,分別代表插入文字、刪除文字、保留文字等操作。</p><p>以下是一個 Delta 物件的範例:</p><pre><code class="typescript">{ ops: [ { insert: 'Hello, ' }, { insert: 'world', attributes: { bold: true } }, { insert: '!' }, ]}</code></pre><p>在這個範例中,Delta 物件表示一個包含「Hello, 」和「world」(加粗樣式)兩個片段的內容, 是不是非常容易閱讀及理解呢?上面有提到,Delta 是用來<strong>描述文件內容及變更的內容</strong>,而當 Deltas 用於描述內容時,當套用到空白的文件時,可以將其視為要建立的內容。</p><h3 id="Delta-ops-的屬性:"><a href="#Delta-ops-的屬性:" class="headerlink" title="Delta ops 的屬性:"></a>Delta ops 的屬性:</h3><ul><li><code>insert</code>:插入文字,可以指定要插入的文字內容。</li><li><code>delete</code>:刪除文字,可以指定要刪除的文字數量。</li><li><code>retain</code>:保留文字,可以指定要保留的文字數量。</li><li><code>attributes</code>:操作的屬性,例如文字樣式、顏色等。</li></ul><h3 id="操作的順序和組合:"><a href="#操作的順序和組合:" class="headerlink" title="操作的順序和組合:"></a>操作的順序和組合:</h3><p>Delta 的操作按照順序應用於文本,從左到右。它們可以組合在一起,以建立完整的編輯器內容。例如 Delta 物件表示在游標的位置插入「Hello, world!」這段文字:</p><pre><code class="typescript">{ ops: [ { insert: 'Hello, world!' }, ]}</code></pre><h3 id="操作的應用:"><a href="#操作的應用:" class="headerlink" title="操作的應用:"></a>操作的應用:</h3><p>我們可以使用 Quill Editor 提供的 <code>setContents</code> 方法,將 Delta 物件設置到編輯器中。這樣就能夠在編輯器中動態修改內容。</p><pre><code class="typescript"> setEditorContent() { const delta = { ops: [{ insert: 'Hello, world!' }], }; this.quillEditor.setContents(delta as any); }</code></pre><h2 id="Embeds-嵌入式內容:"><a href="#Embeds-嵌入式內容:" class="headerlink" title="Embeds 嵌入式內容:"></a>Embeds 嵌入式內容:</h2><p>除了文字內容與樣式的資料,Delta 也可以描述嵌入式 (Embed) 的內容,例如:圖片、超連結或數學公式。這類型的內容在 Delta 物件中必須要有一個描述該類型的 key,而這個 key 同時也是 <code>bolt</code> 的名稱,如果有用 <code>parchment</code> 來建立自訂內容,嵌入式內容也可以像文字一樣擁有 <code>attributes</code> 的 key ,讓嵌入的內容也能有格式的設定。另外所有嵌入式內容的在編輯器中的內容長度 <code>length</code> 均為 <strong>1</strong>。</p><pre><code class="typescript">{ ops: [{ // 圖片連結 insert: { image: 'https://quilljs.com/assets/images/icon.png' }, attributes: { link: 'https://quilljs.com' } }] }</code></pre><p>關於 <code>BoltName</code> 與 <code>Parchment</code> 的介紹,我們之後再來仔細探討。(<del>又挖坑…</del>) XD</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天初步了解 Quill Editor 使用的 Delta 物件,在編輯器的處理情境中,我們有時候會需要利用 Delta 處理編輯器的內容,透過 Delta 可以解析文本內容,並依照專案需求進一步的處理資料,最後利用內建的方法將處理後的內容更新回編輯器,因此要能對 Quill 得心應手,對於 Delta 物件的理解也是很重要的一環。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>本來還煩惱著要怎麼安排週末的行程,因為週日要到台中參加教師節聚餐,同時還要記著上來日更文章,但週六是要補班的,然後昨天才知道原來今天不需要補班,還好是昨天就知道,如果今天匆匆到公司之後才發現,大概會很錯愕吧,雖然接著回老家非常方便,站點就在展覽館旁邊而已 XD </p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://quilljs.com/docs/delta/">Delta - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10325791">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 7:Quill Editor 擴充自訂功能</title>
<link href="/2023/09/22/quill-day-7/"/>
<url>/2023/09/22/quill-day-7/</url>
<content type="html"><![CDATA[<p>Quill 作為編輯器的核心優勢在於它提供了豐富的 API 與容易實現客製化的功能。當我們基於 Quill 的 API 實作功能時,可以包成一個 module 較方便使用。今天一起探討如何擴充 Quill 編輯器的功能,讓我們可以依照特定的需求實現並加入,使編輯器的功能更豐富以貼近專案的特殊需求。</p><h2 id="註冊-Quill-自訂模組"><a href="#註冊-Quill-自訂模組" class="headerlink" title="註冊 Quill 自訂模組"></a>註冊 Quill 自訂模組</h2><p>假設我們需要新增一個能顯示編輯器當前字數的計數器,首先需要建立一個名為 <code>counter</code> 的 module: </p><pre><code class="typescript">// 建立自訂 ModulecreateCustomModule() { Quill.register( 'modules/counter', // 自訂 module 名稱 function (quill: Quill, options: QuillOptions) { const container: HTMLDivElement = document.querySelector('#counter')!; // 透過 text-change 事件監聽處理 quill.on('text-change', function () { // 獲取 quill 文本內容 const text = quill.getText(); // 根據 module options 來決定計算單位 if (options.unit === 'word') { container.innerText = text.split(/\s+/).length + ' words'; } else { container.innerText = text.length + ' characters'; } }); } );}// 初始化的操作const quill = new Quill('#editor', { modules: { counter: { container: '#counter', // 設定 counter HTML id unit: 'word' // 設定文本計算單位 } }});</code></pre><p>上面的範例中,我們宣告了一個 <code>createCustomModule</code> function,並透過 <code>Quill.register</code> 註冊一個名為 <code>counter</code> 的 module,第一個參數是 module name, 第二個參數則是帶入一個 function,這個 function 可以取得 <code>Quill</code> 及 <code>options</code> 參數。</p><p>function 的實現是利用監聽 Quill 的 <code>text-change</code> 事件,當文本內容改變的時候觸發,並執行對應的操作。這裡根據 <code>unit</code> 參數來決定計算單位。Quill 初始化的時候,我們可以在 <code>modules</code> 底下設定 <code>counter</code> 也就是 custom module 名稱,並帶入像是 <code>container</code>, <code>unit</code> 的參數設定計數器的 HTML 以及計算單位。</p><h2 id="Keyboard-module-監聽事件"><a href="#Keyboard-module-監聽事件" class="headerlink" title="Keyboard module 監聽事件"></a>Keyboard module 監聽事件</h2><p>我們可以透過監聽事件來執行額外的操作,並根據事件觸發類型相應的功能。Quill 提供了幾種不同的監聽事件,除了像上面的範例用到了 <code>text-change</code> 之外,在 <code>keyboard</code> module 也提供了按鍵事件綁定,讓我們可以設定,當某個按鍵觸發的時候,執行自訂的功能。</p><p>處理 Quill Editor 的按鍵事件,可以透過 <code>keyboard</code> module 設定監聽按鍵的事件,例如按下某個按鍵或是組合鍵就進行對應的處理:</p><pre><code class="typescript">const quillConfig = { modules: { // 其他模組... keyboard: { bindings: { enter: { key: 'Enter', handler: function(range, context) { // 在這裡處理按下 Enter 鍵後的邏輯 console.log('Enter 鍵被按下'); } } } } }, theme: 'snow',}; </code></pre><p>上面的範例是當 <code>Enter</code> 鍵按下的時候,就執行 <code>handler</code> 帶入的 function,這裡我們簡單用個 <code>console.log</code> 實驗一下就好,至於參數 <code>range</code> 及 <code>context</code> 是什麼,我們之後再來細看。</p><h2 id="實用的擴充套件"><a href="#實用的擴充套件" class="headerlink" title="實用的擴充套件"></a>實用的擴充套件</h2><p>當需要更特殊或複雜的功能時,我們可以透過自訂的擴充套件來實現。只要依照 Quill 的擴充自訂功能的方式,就能夠為編輯器新增各種功能。官方文件也整理了<a href="https://github.com/quilljs/awesome-quill">一份清單</a>,收錄了好用的擴充套件供大家參考並能安裝使用。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天我們初步體驗了如何自訂並註冊自訂的 module,並且利用 Quill 提供的監聽事件來處理額外的事情,也嘗試加入按鍵綁定事件,並在指定的按鍵觸發的時候呼叫自訂的方法。只是簡單的利用 Quill 提供的 API 就能讓我們實現額外的功能,由此可知 Quill 的可擴充性是非常高的。在 Github 上我們也能看到多樣的自訂功能模組可以直接安裝使用。但當遇到非常特殊的需求時,我們也可以按照需求來實現。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天參加了 Study4 的活動小聚,然後不知道怎麼聊的,聊到同事A需要在上班時間偶爾回訊息給老婆,同事B就問說上班時間為什麼要回訊息,同事A說,因為有時候要安撫一下老婆,回應老婆大人的抱怨什麼之類的,於是我就比喻,上班時間偶爾還是會被 ping 一下,我們還是要回個 <code>200</code>,這時另一個社群朋友就問,那如果 <code>404</code> 或 <code>500</code> 怎麼辦?我想,回去就完蛋吧 ˊ_>ˋ 室友說:也許可以用 <code>301</code>,我:???</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://quilljs.com/guides/building-a-custom-module/">Building a Custom Module - Quill Rich Text Editor (quilljs.com)</a></li><li><a href="https://github.com/jeffwu85182/quill-editor-todo/commit/1184ea0cf270d4172b8be18b3a346e612b1e8502">今日份的練習</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10325338">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 6:Quill Editor 自訂工具列</title>
<link href="/2023/09/21/quill-day-6/"/>
<url>/2023/09/21/quill-day-6/</url>
<content type="html"><![CDATA[<p>昨天我們知道如何設定編輯器的 CSS 樣式,並且修改既有的功能按鈕。今天接著研究要如何自訂工具列的內容以及加入自訂功能的按鈕。</p><h2 id="自訂工具列:Toolbar-Options"><a href="#自訂工具列:Toolbar-Options" class="headerlink" title="自訂工具列:Toolbar Options"></a>自訂工具列:Toolbar Options</h2><p>Quill Editor 的工具列有兩種設置方式,一種是定義 <code>toolbarOption</code> 物件,由於 Quill 內建相當豐富實用的功能,因此我們可以直接以陣列的方式設定想要加入的控制按鈕,陣列底下的每個元素也是陣列,用來表示一個群組,除了可以調整工具列的功能按鈕順序之外,我們也能以分組的方式將同類型的功能擺在一起。</p><pre><code class="typescript">const toolbarOptions = { container: [ ['bold', 'italic', 'underline', 'strike'], // 預設的工具按鈕 [{ header: 1 }, { header: 2 }], // 預設的工具按鈕 [{ list: 'ordered' }, { list: 'bullet' }], // 預設的工具按鈕 [{ script: 'sub' }, { script: 'super' }], // 預設的工具按鈕 [{ indent: '-1' }, { indent: '+1' }], // 預設的工具按鈕 [{ direction: 'rtl' }], // 預設的工具按鈕 [{ size: ['small', false, 'large', 'huge'] }], // false 是 normal [{ header: [1, 2, 3, 4, 5, 6, false] }], // 預設的工具按鈕 [{ color: [] }, { background: [] }], // 預設的工具按鈕 [{ font: [] }], // 預設的工具按鈕 ['image', 'customButton'], // 自定義與內建工具按鈕 ['clean'], // 預設的工具按鈕 ],}</code></pre><p>另外需要留意的部分像是 <code>size</code> 以及 <code>header</code>,分別是文字的大小以及標題層級,兩者都與字體大小有關,陣列中有設定值是 <code>false</code> ,在畫面渲染之後,可以看到下拉清單會表示 <code>Normal</code>,意思是移除該樣式,僅顯示原本的大小。</p><p><img src="/2023/09/21/quill-day-6/200907493oFpjVVyUC.png" alt="toolbar options"></p><p>例如原本選擇了一段文字並設定 <code>header</code> 為 <code>1</code>,意思就是把選取的範圍文字套上 <code><h1></code> 標籤,這時的字體是呈現大標題的樣式,接著我們再把一樣的選取範圍切換到 <code>Normal</code>,原本的 <code>H1</code> 標題樣式就會被移除,回到原始的文字狀態。</p><h2 id="加入自定義按鈕"><a href="#加入自定義按鈕" class="headerlink" title="加入自定義按鈕"></a>加入自定義按鈕</h2><p>接著我們嘗試新增一個 <code>customButton</code> 的自訂按鈕,並透過 <code>toolbar</code> 物件下的 <code>handlers</code> 屬性註冊按鈕的自訂函式,這個函式主要處理按下按鈕後的操作。我們可以在 <code>handlers</code> 中實作我們需要的功能,例如插入特定的內容、修改編輯器文本內容…等等。另外 <code>toolbar</code> 也提供了群組按鈕的設定,例如要將相同類型的按鈕放一起,直接放在同一個陣列即可。</p><h2 id="使用自訂按鈕功能"><a href="#使用自訂按鈕功能" class="headerlink" title="使用自訂按鈕功能"></a>使用自訂按鈕功能</h2><p>在 <code>quillConfig</code> 的 <code>toolbar</code> 中加入自訂功能按鈕的名稱及對應的 <code>handler</code>,並且設定按鈕的 icon:</p><pre><code class="typescript">const quillConfig = { modules: { toolbar: { container: [ // ...其他按鈕 ['image', 'customButton'], // 自定義的工具按鈕群組 ], handlers: { "customButton": () => console.log('handle custom button') } }, theme: 'snow', }};// 使用 font awesome 自訂按鈕 iconconst icons = Quill.import('ui/icons');icons['customButton'] = '<i class="fa-regular fa-star"></i>';</code></pre><h2 id="自訂工具列:HTML"><a href="#自訂工具列:HTML" class="headerlink" title="自訂工具列:HTML"></a>自訂工具列:HTML</h2><p>我們也可以自訂工具列的 HTML,在初始化的時候,Quill 會根據 <code>toolbar</code> 屬性給的工具列容器選擇器進行內建功能的事件綁定,我們也可以自訂額外的按鈕或元件在上面。例如:</p><pre><code class="html"><div #toolbar> <span class="ql-formats"> <button class="ql-bold">Bold</button> <button class="ql-italic">Italic</button> <button class="ql-link">Link</button> <button class="ql-list" value="bullet">Ul</button> </span> <!-- customButton --> <span class="ql-formats"> <button class="ql-customButton" [title]="'Custom Button'"> Custom Button </button> </span></div></code></pre><p>在 Component 初始化的時候帶入指定的容器,這邊我們一樣透過 <code>@ViewChild</code> 來獲取 HTML,同時也將對應的 <code>handlers</code> 加上來模擬觸發按鈕的時候進行的操作:</p><pre><code class="typescript">@ViewChild('editor') editor!: ElementRef;@ViewChild('toolbar') toolbar!: ElementRef;// ...const quill = new Quill(this.editor.nativeElement, { modules: { // toolbar: toolbarOptions, toolbar: { container: this.toolbar.nativeElement, handlers: { customButton: () => console.log('handle custom button'), }, }, } });</code></pre><p>重新整理之後可以看到工具列變成自訂的 HTML 內容:<br><img src="/2023/09/21/quill-day-6/20090749mykJprNm2j.png" alt="自訂的 HTML 內容"></p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天我們了解到工具列的設定,Quill 提供了這兩種設定方式,在大部分的情況下,我們可以選擇定義 <code>toolbarOption</code> 物件的方式,將想要使用的內建功能分門別類組成不同的群組後編排順序,當遇到較特殊的情況,只設定顯示以及順序無法滿足需求的時候,我們可以直接以 HTML 的方式完全自訂一個工具列,並按照需求加入額外的介面或其他功能。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>昨天下班的時候路過南港展覽館,發現今天突然好多人,而且感覺就不是剛下班的樣子,也看到許多代客寄物的小攤,看到告示牌才知道,原來今天展覽館有舉辦演唱會,外頭都是排隊的人潮。可以看到很多元的打扮風格,不過人真的是多到差點進不去捷運站。到了隔天上班的時候發現展覽館外圍地上一堆垃圾,有沒喝完的飲料,更多的是抽完菸的菸蒂,對於厭惡抽菸的我來說,很久沒看到這麼精彩的景象了,還是希望民眾參加演唱會之後,也別忘了把垃圾帶走,抽完菸的菸蒂熄滅之後也順手丟到垃圾桶吧。真心覺得素質還有待提升QQ</p><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10324342">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 5:自訂 Quill Editor 外觀樣式</title>
<link href="/2023/09/20/quill-day-5/"/>
<url>/2023/09/20/quill-day-5/</url>
<content type="html"><![CDATA[<p>今天我們將著重於自訂 Quill Editor 的外觀與樣式,讓編輯器更符合專案的設計風格需求。Quill Editor 提供了幾種方法來調整外觀樣式,讓我們一探究竟!</p><h2 id="修改工具列樣式"><a href="#修改工具列樣式" class="headerlink" title="修改工具列樣式"></a>修改工具列樣式</h2><p>在初始化的時候我們透過 <code>new Quill()</code> 並帶入編輯器容器的 HTML 元素以及編輯器配置的物件共兩個參數進行初始化。初始化後可以看到編輯器上方有一排工具列,工具列提供了編輯器預設的功能按鈕,我們可以用 CSS 對工具列及按鈕調整 UI 樣式,例如在 <code>styles.scss</code> 或對應的專案目錄下的 CSS 檔案加入新的 Style:</p><pre><code class="scss">.ql-toolbar { background-color: aqua; /* 修改編輯器工具列的背景顏色 */}.ql-editor { background-color: #f0f0f0; /* 使用你想要的背景顏色 */ border: 1px solid #ccc; /* 使用你想要的邊框樣式 */ }</code></pre><p>但這時候如果直接儲存重整頁面你會發現工具列的樣式完全沒有變化,這是為什麼呢?我們利用 <code>ngAfterViewInit</code> 觀察 <code>QuillEditorComponent</code> 在 Quill 初始化渲染後的 <code>nativeElement</code>:</p><pre><code class="typescript">@Component({ selector: 'app-quill-editor', standalone: true, imports: [CommonModule], templateUrl: './quill-editor.component.html', styleUrls: ['./quill-editor.component.scss'],})export class QuillEditorComponent implements AfterViewInit { @ViewChild('editor') editorElementRef!: ElementRef; ngAfterViewInit() { this.initQuillEditor(); // 監聽編輯器內容變化事件,並將變化同步到 Angular 的資料模型 this.quillEditor.on('text-change', () => { this.content = this.quillEditor.root.innerHTML; }); console.log(this.editorElementRef.nativeElement); }</code></pre><p>可以看到 <code>console.log</code> 出來的 <code>nativeElement</code> 並沒有包含 <code>ql-toolbar</code>,而是在 <code>#editor-container</code> 的上面:<br><img src="/2023/09/20/quill-day-5/20090749LZoCDHBBgj.png" alt="console.log('nativeElement')"></p><p>這時候我們就需要新增一個 Component 的屬性 <code>encapsulation</code> 並且值設為 <code>ViewEncapsulation.None</code> ,透過這個設定可以取消 Angular 的樣式封裝機制,將元件樣式直接套用到全局樣式中,不過要小心可能會導致樣式污染和衝突。另一種方式是直接自訂 Toolbar 的 HTML 內容,這個我們明天接著研究。</p><p>加上之後就可以看到編輯器的工具列顏色有了變化:<br><img src="/2023/09/20/quill-day-5/200907493mmVEK5DQj.png" alt="顏色有了變化"></p><h2 id="自訂工具列圖示"><a href="#自訂工具列圖示" class="headerlink" title="自訂工具列圖示"></a>自訂工具列圖示</h2><p>如果想要自訂工具列按鈕的圖示,可以使用 <code>Quill.import('ui/icons')</code> 並選擇要更換 icon 的功能按鈕指定自訂的 icon 元素,例如把粗體的 icon 換成 Font-Awesome 的 icon:</p><ol><li>先安裝 Font Awesome node module</li></ol><pre><code class="bash">npm install @fortawesome/fontawesome-free --save</code></pre><ol start="2"><li>將 <code>fontawesome-free</code> CSS & JS 加入 angular.json:</li></ol><pre><code class="json">{ // ... "architect": { "build": { "options": { "styles": [ "node_modules/@fortawesome/fontawesome-free/css/all.min.css", "node_modules/quill/dist/quill.snow.css", "src/styles.css" ], "scripts": [ "node_modules/@fortawesome/fontawesome-free/js/all.min.js" ] // ... }, // ... }, // ... }}</code></pre><p>完成 FontAwesome 設置之後,我們可以直接在初始化 Quill Editor 之前先調整我們要的 icon:</p><pre><code class="typescript"> const icons = Quill.import('ui/icons'); icons['bold'] = '<i class="fa-solid fa-hippo"></i>'; this.quillEditor = new Quill(this.editorElementRef.nativeElement, { // ... });</code></pre><p>重整頁面看看粗體的按鈕是不是變成河馬的樣式了 XD<br><img src="/2023/09/20/quill-day-5/200907499yUTtqmFJ4.png" alt="變成河馬的樣式了"></p><h2 id="自訂-Quill-Editor-佈景主題:"><a href="#自訂-Quill-Editor-佈景主題:" class="headerlink" title="自訂 Quill Editor 佈景主題:"></a>自訂 Quill Editor 佈景主題:</h2><p>如果想要更進一步自訂佈景主題,可以在 CSS 檔案中像上述的修改方式一樣來撰寫 CSS 樣式。Quill Editor 使用一些特定的 CSS class 來控制不同的元素,我們可以透過這些 class 來實現想要的效果。當然在全球最大交友平台 Github 上也有其他精美的佈景主題可以選擇,就看專案需求囉。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>透過自訂 Quill Editor 外觀和樣式,我們可以依專案需求進行調整。自訂外觀可以讓編輯器與專案整體的設計風格是一致的,從 UI/UX 的角度提供更好的使用者體驗。下一篇文章,我們將介紹 Quill Editor 的擴充套件和自定義功能。</p><h4 id="雜記"><a href="#雜記" class="headerlink" title="雜記"></a>雜記</h4><p>今天寫文章之前,先去整復保養一下,不知不覺已經到了需要為身體繳保養費的人生階段,現在要熬夜都是一件要命的事情。想當初第一次去整復的時候,既緊張又怕受傷害。先是對肩頸做紅外線熱敷十五分鐘,之後就先從正面開始,然後是背部壓到都能聽到筋骨啪啪啪鬆開的聲音,最後壓軸則是用毛巾繞住脖子連同腰椎一起拉開,那一下直接腦袋空白了好幾秒,接著出去之後看到的光景似乎變明亮了,視覺的色彩飽和度也變的很清晰。真的是很神奇的體驗。後來只要每次做完,姿勢都會端正一陣子,深怕拉開的筋骨又太快被打回原形 XD</p><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10323588">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 4:Quill Editor 的基本操作與編輯功能</title>
<link href="/2023/09/19/quill-day-4/"/>
<url>/2023/09/19/quill-day-4/</url>
<content type="html"><![CDATA[<p>在前一篇文章中,我們新增了 Angular 專案並安裝與配置了 Quill Editor,並建立了一個 “quill-editor” 元件。今天就讓我們繼續探索 Quill Editor 的基本功能,試著編輯操作和使用工具列選項體驗一下。</p><p><img src="/2023/09/19/quill-day-4/20090749uuZobTAYi2.png" alt="toolbar"></p><h2 id="編輯器工具列"><a href="#編輯器工具列" class="headerlink" title="編輯器工具列"></a>編輯器工具列</h2><p>Quill 預設提供了幾個實用的功能在工具列上,讓使用者可以輕鬆的進行文字編輯。包含了常用的文字格式化功能,例如粗體、斜體、底線,還有文字大小及顏色的調整。此外,還可以插入清單(有序清單、無序清單)的功能。透過這些功能,我們可以輕鬆編輯文本內容及文字格式。</p><h2 id="加入連結"><a href="#加入連結" class="headerlink" title="加入連結"></a>加入連結</h2><p>在 Quill 編輯器中加入超連結的方式和大部分常見的編輯器一樣,使用者只需要選擇想要加上連結的文字,然後點擊工具列中的連結按鈕並輸入連結的 URL,按下確認鈕,即可在你的內容中插入超連結。</p><h2 id="插入圖片"><a href="#插入圖片" class="headerlink" title="插入圖片"></a>插入圖片</h2><p>除了文字超連結的功能,Quill 也有插入圖片的功能。在理想的情況下,我們只需點擊工具列上的圖片按鈕,選擇要插入的圖片檔案,Quill Editor 就會自動將圖片嵌入到內容。</p><p>不過在第一次嘗試初始化 Quill Editor 的時候,可以看到上方的工具列示沒有插入圖片的按鈕可以點擊的,卻可以將螢幕截圖或是從網頁複製的圖片貼到編輯器中。</p><p>看了官方文件之後才知道,Quill 內建的功能在初始化的時候預設都是全開的。主要是 Toolbar 的預設值沒有把對應的功能按鈕打開,因此我們還要加上插入圖片的按鈕,這可以在初始化的設定裡修改 toolbar 的選項:</p><pre><code class="typescript">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' }], // superscript/subscript [{ indent: '-1' }, { indent: '+1' }], // outdent/indent [{ direction: 'rtl' }], // text direction [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown [{ header: [1, 2, 3, 4, 5, 6, false] }], ['link', 'image', 'video', 'formula'], // add's image support [{ color: [] }, { background: [] }], // dropdown with defaults from theme [{ font: [] }], [{ align: [] }], ['clean'], // remove formatting button ];</code></pre><p>透過上面的設定,就可以看到有更多內建功能按鈕加到工具列了。至於還有哪些內建的功能可以加在 Toolbar 上,可以參考<a href="https://quilljs.com/docs/formats/">技術文件</a> 。之後我們還可以加入自訂 module 來擴充編輯器,使編輯器支援更豐富的文本編輯功能,例如拖曳的方式插入圖片、或插入圖片後可縮放等。</p><p>加入 <code>toolbarOptions</code> 之後的效果:<br><img src="/2023/09/19/quill-day-4/20090749dPKiTby69O.png" alt="toolbar options"></p><h2 id="編輯的內容就是實際呈現"><a href="#編輯的內容就是實際呈現" class="headerlink" title="編輯的內容就是實際呈現"></a>編輯的內容就是實際呈現</h2><p>由於 Quill 提供了「所見即所得」的編輯介面,我們可以在編輯過程中即時預覽最終的呈現效果,使文字格式和插入的圖片等內容能立即展示出來,比起有些較早期的編輯器,Quill 消除了需要頻繁切換頁面以檢查預覽效果的麻煩。讓我們在編輯時所看到的畫面就能確保最終輸出與預期一致。</p><h2 id="鍵盤快速鍵"><a href="#鍵盤快速鍵" class="headerlink" title="鍵盤快速鍵"></a>鍵盤快速鍵</h2><p>我們不僅可以用滑鼠點擊工具列的功能圖示格式化文本,Quill Editor 也提供了一些常見且方便的快速鍵,讓我們能直覺又快速的編輯文本。例如,我們使用 Ctrl+B 把文字設為粗體,使用 <code>Ctrl + I</code> 設為斜體,還有 <code>Ctrl + U</code> 是斜體等常見的快速鍵。除了格式化的快捷鍵之外,我們也能透過 <code>Ctrl + Z</code> 以及 <code>Ctrl + Shift + Z</code> 回上一步或進到下一步的操作紀錄。這些快捷鍵可以提高編輯的效率,讓使用者更能專注於輸入與編輯內容。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天我們探討了 Quill Editor 的基本使用和編輯功能,學會如何使用工具列進行文字格式化、插入連結和圖片,還有預覽內容的最終效果。我們也瞭解了如何使用鍵盤快捷鍵來提高編輯效率。下一篇文章將探討如何自訂 Quill Editor 的外觀與樣式,讓我們能夠根據專案需求進行編輯器的樣式設定。</p><h4 id="雜記:"><a href="#雜記:" class="headerlink" title="雜記:"></a>雜記:</h4><p>雖然已經知道在網頁上編輯內容沒有定時存檔或 <code>beforeunload</code> 是一件很抖的事情,當實際體會到在編輯一半時沒有存檔的情況下,誤觸鍵盤快速鍵或書籤連結導致頁面跳掉,真的會讓人滿臉糾結 Orz。以後還是乖乖的在本地的編輯器把稿子打好之後再貼上來吧…QQ</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><p><a href="https://quilljs.com/docs/formats/">Formats - Quill Rich Text Editor (quilljs.com)</a></p><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10322628">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 3:Quill 與 Angular 起手式</title>
<link href="/2023/09/18/quill-day-3/"/>
<url>/2023/09/18/quill-day-3/</url>
<content type="html"><![CDATA[<p><img src="/images/quill-day-3/20090749dwlMNWNaj5.png" alt="Quill"><br>上一篇我們看了 Why Quill,今天就來複習環境設置,並嘗試新增一個 Angular 專案,不過如果你是被標題吸引進來的同學,多少都有接觸過 Angular,或者主要就是用 Angular 開發的。這篇的目標是給沒接觸過 Angular 的朋友按照步驟體驗看看用 Angular 開發專案的感受。以前當過一陣子的 Angular 傳教士,雖然後來因為工作的關係換寫 React,但個人私心還是比較喜歡 Angular 的開發體驗 XD</p><h2 id="什麼是-Quill?"><a href="#什麼是-Quill?" class="headerlink" title="什麼是 Quill?"></a>什麼是 Quill?</h2><p>Quill 是一個簡單易用且可擴展的 WYSIWYG 開源文本編輯器,它提供了豐富的文本編輯功能,包括格式化文字、插入圖片、表格和連結等。Quill 的設計簡單、易於使用,它是以 JavaScript 為基礎,並且提供了豐富的插件和擴充功能,讓使用者可以輕鬆地滿足各種文本編輯的需求。</p><blockquote><p>WYSIWYG (What You See Is What You Get) 縮寫。<br>初次看到的時候也一頭霧水,就是所見即所得。</p></blockquote><h2 id="什麼是-Angular?"><a href="#什麼是-Angular?" class="headerlink" title="什麼是 Angular?"></a>什麼是 Angular?</h2><p>Angular 是一個基於 <a href="https://www.typescriptlang.org/">TypeScript</a> 的開發平台。而身為一個平台,Angular 包含:</p><ul><li>一個元件化的框架,用來建構可延展的 Web 應用程式。</li><li>一整套經深思熟慮而整合出來的函式庫,包含各種不同的功能,包含路由機制、表單管理、Client/Server 通訊,以及更多。</li><li>一組完善的開發工具,幫助你開發、建置、測試、更新你的程式碼。</li></ul><h2 id="安裝和配置-Quill-Editor-在-Angular-專案中"><a href="#安裝和配置-Quill-Editor-在-Angular-專案中" class="headerlink" title="安裝和配置 Quill Editor 在 Angular 專案中"></a>安裝和配置 Quill Editor 在 Angular 專案中</h2><p>首先我們需要準備 Node.js 環境,然後安裝 Angular CLI 並使用指令建立新專案,最後到專案目錄下用 NPM 安裝 Quill,步驟如下:</p><ol><li>在 Angular 專案中安裝 Quill 函式庫:使用 npm 或者 yarn 安裝 Quill,確保在專案中引入所需的 dependency:</li></ol><pre><code class="bash"># 安裝 Angular CLInpm install @angular/cli# 建立一個新專案ng new quill-editor-todo# 建立的過程會有一些提問,按照需求選擇即可,例如:# 要不要分享資料給官方作為改善用途: Yes# 選擇 Style 的語言: SCSS# 是否建立 Routing: No# Install Quill Editorcd quill-editor-todonpm install [email protected] --save# Install Quill Editor Typesnpm install @types/quill@1 --save-dev</code></pre><ol start="2"><li>引入 Quill Style</li></ol><pre><code>{ // ... "architect": { "build": { "options": { "styles": [ "node_modules/quill/dist/quill.snow.css", "src/styles.css" ], // ... }, // ... }, // ... }}</code></pre><ol start="3"><li>建立 Quill Editor 元件,來試試看 standalone:</li></ol><pre><code class="bash"># 建立一個 standalone componentng generate component quill-editor --standalone</code></pre><p>在 Component 的 template 簡單的加上一個帶有 Id 的 <code>div</code> 標籤</p><pre><code class="html"><!-- quill-editor.component.html --> <div id="quill" #quillContainer></div></code></pre><ol start="4"><li>在 quill-editor component 初始化 Quill Editor:</li></ol><pre><code class="typescript">// quill-editor.component.tsimport Quill from 'quill';import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core';import { CommonModule } from '@angular/common';@Component({ selector: 'app-quill-editor', standalone: true, imports: [CommonModule], templateUrl: './quill-editor.component.html', styleUrls: ['./quill-editor.component.css'],})export class QuillEditorComponent implements AfterViewInit { @ViewChild('quillContainer') quillContainer!: ElementRef; private quillEditor: any; content: string = ''; // 初始化編輯器內容為空字串 ngAfterViewInit() { this.quillEditor = new Quill(this.quillContainer.nativeElement, { // this.quillEditor = new Quill("#quill", { // also works theme: 'snow', // 可以選擇不同的主題,例如 'bubble' 或 'core' }); // 監聽編輯器內容變化事件,並將變化同步到 Angular 的資料模型 this.quillEditor.on('text-change', () => { this.content = this.quillEditor.root.innerHTML; }); }}</code></pre><p>雖然透過 <code>AfterViewInit</code> 可以拿到對應的 <code>id</code> selector,但用 <code>@ViewChild</code> 可以更符合 Angular 的操作方式來獲取 DOM,因此我們還是使用 <code>this.quillContainer.nativeElement</code> 來初始化 Quill。</p><p>當執行 <code>ng serve</code> 時,會看到這樣的 warning:</p><pre><code>Warning: D:\Projects\quill-editor-todo\src\app\quill-editor\quill-editor.component.ts depends on 'quill'. CommonJS or AMD dependencies can cause optimization bailouts.</code></pre><p>這是由於 Quill 是屬於 CommonJS module,進而影響到未來在建置的時候打包優化的問題。目前如果不想看到這個訊息,可以直接在 <code>project.json</code> 或 <code>angular.json</code> 裡面的 <code>build</code> 底下新增:</p><pre><code class="json">"allowedCommonJsDependencies": ["quill"]</code></pre><p>再 <code>ng serve</code> 一次就不會看到 warning 了。</p><p>最後在 app component import <code>QuillEditorComponent</code>,並且在 <code>app.component.html</code> 加上 tag:</p><pre><code class="html"><app-quill-editor></app-quill-editor></code></pre><p><code>ng serve</code> 並重新整理,試著在編輯器中輸入文字、調整文字樣式,以及插入連結等功能,來體驗 Quill Editor 的基本編輯功能。</p><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>今天我們體驗了如何安裝 Angular CLI ,並透過指令建立新專案後加入 Quill,讓我們能夠在這個專案輕鬆擁有一個功能豐富的文字編輯器。明天我們將會依照內建的功能介紹關於 Quill Editor 的應用,之後會提供一些實例和程式碼來幫助讀者更好理解如何操作。</p><blockquote><p>備註:如果你的 VSCode 有安裝 Nx console,這時候 Extension 會跳出來詢問是否直接套用 Nx,這個步驟可用可不用,筆者提供的範例是有透過 Nx 自動初始化,所以會和原本的 Angular 初始專案不太一樣。</p></blockquote><h4 id="雜記:"><a href="#雜記:" class="headerlink" title="雜記:"></a>雜記:</h4><p>很久沒玩手機遊戲了,最近跟室友開始玩魔物獵人 Now,跟 Pokemon Go 一樣,需要走到戶外才能玩這遊戲,邊散步邊打魔物感覺還滿新鮮的。給自己另一個出去走走的理由 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://github.com/jeffwu85182/quill-editor-todo">練習範例 Repo</a></li><li><a href="https://developer.mozilla.org/zh-TW/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Angular_getting_started">Angular 新手入門 - 學習該如何開發 Web | MDN (mozilla.org)</a></li><li><a href="https://quilljs.com/docs/quickstart/">Quickstart - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10321755">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 2:Why Quill?</title>
<link href="/2023/09/17/quill-day-2/"/>
<url>/2023/09/17/quill-day-2/</url>
<content type="html"><![CDATA[<p><img src="/images/quill-day-2/20230917153540.png"></p><h2 id="建立內容"><a href="#建立內容" class="headerlink" title="建立內容"></a>建立內容</h2><p>從網路誕生以來,建立內容一直是網路的核心。<code><textarea></code> 為大部分的網頁應用提供了原生且重要的解決方案,但有時候我們會需要為輸入的文本加上文字格式。這時 Rich Ediotr 的出現就派上用場了。目前有很多解決方案可以選擇,但 Quill 提供了一些現代的想法可供參考。</p><h2 id="API-驅動設計-API-Driven-Design"><a href="#API-驅動設計-API-Driven-Design" class="headerlink" title="API 驅動設計 (API Driven Design)"></a>API 驅動設計 (API Driven Design)</h2><p>Rich editor 的目的是協助使用者撰寫文本。但大多數的 rich editor 都不知道使用者寫了什麼文本。什麼意思呢? 就是說編輯器與 Web 開發者一樣是透過 DOM (Document Object Model) 的視角來查看內容。這樣的方式會出現一些阻礙,因為 DOM 是以非平衡樹 (Unbalanced Tree) 的節點組合而成,而文本則是由行、單詞與字符所構成。</p><p>沒有任何一個 DON API 是以字元為測量單位。也因為這樣的限制,大部分的 rich editor 無法回答出類似這樣的簡單問題:「這個範圍的文本是什麼?」 或「目前游標是否停在粗體的文字上?」DOM 與文本兩者的基礎結構不一致,造成開發者在嘗試提供更豐富且直覺的文本編輯體驗時面臨到的困難和挑戰。</p><p>Quill 專為編輯和字符而設計,並在這些基於自然文本為中心單位上開發了 API。要確定某些內容是否為粗體,Quill 不需要遍歷 DOM 來查找 <code><b></code> 或 <code><strong></code> 節點或字體粗細樣式屬性 - 只需呼叫 getFormat(5, 1)。其中所有核心 API 呼叫都允許使用任意索引 (index) 和長度 (length) 進行訪問或修改。其事件 API(Event API) 也以直觀的 JSON 格式回報變更的內容。不需要解析 HTML 或比對 DOM Tree。</p><h2 id="自定義內容與格式化"><a href="#自定義內容與格式化" class="headerlink" title="自定義內容與格式化"></a>自定義內容與格式化</h2><p>在不久前,評估 rich editor 很簡單,只需比較它支援了多少種格式。雖然這是一個重要的評估指標,但隨著需求的增加,這個下限正在趨近於無限大。簡單來說就是坑越來越深了。</p><p>文本的撰寫不再只是為了列印出來,如今也能渲染在網頁上。這是一個比紙更豐富的平台。內容可以是即時的、有互動的,可協作的。甚至有些 rich editor 支援如圖片和影片等媒體來豐富文本內容,但幾乎沒有可以像嵌入推文 (tweet )或互動圖表的。然而,這正是網路正在發展的方向:更豐富且可互動的內容。因此,這些建立內容的工具就需要考慮到這些使用案例。</p><p>Quill 提供了自己的文件模型 (document model),這是比 DOM 還要強大的抽象方式,允許進行擴充和自訂。Quill 可以支援的格式和內容幾乎是沒有限制的。使用者已經使用它來加入嵌入式幻燈片、互動式待辦事項清單甚至是 3D 模型。</p><h2 id="跨平台"><a href="#跨平台" class="headerlink" title="跨平台"></a>跨平台</h2><p>對很多 JavaScript 函式庫來說,跨平台支援是很重要的。但這個具體的概念也常因為函式庫的不同而有所差異。對 Quill 來說,它的標準不僅僅是能正常運作這麼簡單,無論是功能實現上要考慮跨平台的支援,或是使用者與開發者的體驗也必須是一致的。</p><p>舉例來說:</p><ul><li>如果在 OSX 上的 Chrome 瀏覽器中某些內容會產生特定的標記,那麼在 IE 上也應該產生相同的標記。(OS:還好目前的工作只需要專注在 chrome…)</li><li>如果在 Windows 上的 Firefox 瀏覽器中按下 Enter 鍵能保留文字加粗的格式,那麼在 Mobile 的 Safari 上也應該能保留。</li></ul><h2 id="易於使用"><a href="#易於使用" class="headerlink" title="易於使用"></a>易於使用</h2><p>上述的所有好處就集中在這個容易使用的 package 中。Quill 附帶了合理的預設配置,只需要幾行 JavaScript 程式碼,你就可以立即享用:</p><pre><code class="typescript">const quill = new Quill('#editor', { modules: { toolbar: true }, theme: 'snow'});</code></pre><p>Quill 已經為你提供了一個豐富且一致的使用體驗,這個體驗是“開箱即用”的。如果你的需求完全符合 Quill 預設提供的功能,且不需要額外自訂功能,那麼你只需安裝和使用它,不必進行任何額外的設定或開發。</p><blockquote><p>當然我是因為有很多大量的客製需求,所以才開始跳入這個坑的,明天就來練習一下,如何建立一個 Angular 專案,並把 Quill 加進去。</p></blockquote><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>Quill 是一個現代化的富文本編輯器,提供了基於自然文本的 API,讓開發者更容易操作和獲取文本資訊。它支援高度自定義和擴展,以適應多變和複雜的需求。同時,Quill 也重視跨平台的一致性和使用者體驗。Quill 的初始設置非常簡單,可謂是”開箱即用”的解決方案。</p><h4 id="雜記:"><a href="#雜記:" class="headerlink" title="雜記:"></a>雜記:</h4><p>前天跟隊友一起去吃飯,正在等候叫位的時候,來了一對老夫婦,老太太說這兩個人中間有個小孩,應該是吧? 朝著他們看的方向看了一下,原來是廁所的 icon,內心想:這對老夫婦大概是剛才錯把電梯的 icon 當成廁所了。這麼說,以前好像也滿常在三創把廁所跟電梯的 icon 給搞混的。平常還好,但 O 在滾的時候可是很要命的,再次體會到 UI/UX 的重要性 XD</p><h4 id="Reference"><a href="#Reference" class="headerlink" title="Reference"></a>Reference</h4><ul><li><a href="https://quilljs.com/guides/why-quill/">Why Quill - Quill Rich Text Editor (quilljs.com)</a></li></ul><p>文章同步發表於<a href="https://ithelp.ithome.com.tw/articles/10320449">2023 iThome 鐵人賽</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>Day 1:就從前言開始吧</title>
<link href="/2023/09/16/quill-day-1/"/>
<url>/2023/09/16/quill-day-1/</url>
<content type="html"><![CDATA[<p><img src="/images/quill-day-1/20090749y1RK7VNloK.png" alt="Quill"></p><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>最近開始新的職業生涯,重回 Angular 專案的懷抱,但也看到許多未曾接觸過的東西,例如 Quill Editor 這個編輯器,這裡面的東西非常的豐富,而且也有相當多的運用。剛好碰到鐵人賽準備開始,以前都沒參加過,但也看過不少完賽的鐵人大神,我想我也來試試看吧!至於參加這個鐵人賽的心情,我需要給自己設一下 mindset,除了給自己有機會可以好好了解一下這個編輯器在 Angular 專案上的使用,同時也可以透過寫文章的方式將學到的知識內化,並在未來當作複習參考的筆記、參考的筆記,的筆記,筆記。</p><blockquote><p>這是筆記,至少要自己能看懂,但也歡迎有任何問題都可以留言討論 :)</p></blockquote><h2 id="關於我"><a href="#關於我" class="headerlink" title="關於我"></a>關於我</h2><p>我是 Jeff Wu,曾經是推廣 Angular 的傳教士,喜歡參與社群與不同的開發者交流。以前參加過 Coscup 開源人年會擔任一小節的講者、Angular Taiwan 小聚的講者,不定期參加線上讀書會,前面幾年跳到 React.js 的開發團隊,所以將近四年沒有接觸實務上的 Angular 專案開發。在換工作之前,由於疫情的關係已經整整遠距工作長達兩年,平常習慣下班後去運動,但換了工作之後還在適應恢復通勤人生的節奏,以及新工作上的各種燒腦的龐大資訊量。休閒興趣是健身、模型以及看動畫。偶爾喜歡跟朋友喝威士忌、梅酒、紅酒,因此家裡也堆了不少酒…XD,但還不至於發生酒精驅動開發這種神奇的狀況就是了。</p><h2 id="關於主題"><a href="#關於主題" class="headerlink" title="關於主題"></a>關於主題</h2><p>雖然說是 Angular + Quill.js,但更多的會去了解 Quill Editor 的世界觀,例如底下有哪些模組(module)、內建的方法(method)或屬性(property)有哪些、 以及有什麼 API 可以使用,如果有自訂需求的時候要如何去實現,等相關議題,所以絕大部分都會是去看官方的技術文件,之後做一些簡單的練習來驗證。</p><p>而 Angular 的部分則會碰到的時候稍微提一下,例如要引用的時候需要留意的地方,或者使用 <code>ngx-qull</code> 套件將 Quill Editor 的引入更容易些。</p><p>與 Quill 有關的詞彙例如:Parchment、Blot、Delta…etc. 會是接下來主要想研究的內容。但如果有 Angular 相關的疑問,也歡迎留言,或者直接到 <a href="https://www.facebook.com/groups/augularjs.tw">Angular Taiwan</a> 社團發文討論。</p><p>文章同步發表於:(2023 iThome 鐵人賽)[<a href="https://ithelp.ithome.com.tw/articles/10319333]">https://ithelp.ithome.com.tw/articles/10319333]</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Quill </category>
</categories>
<tags>
<tag> 15th 30 Days Challenge </tag>
<tag> Quill </tag>
</tags>
</entry>
<entry>
<title>把 Angular SSR 應用部屬到 Firebase</title>
<link href="/2023/05/13/angular-ssr-firebase/"/>
<url>/2023/05/13/angular-ssr-firebase/</url>
<content type="html"><![CDATA[<h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>繼上次玩了一下 SSR Hydration 之後,這次直接體驗用 Firebase 來發布 Angular SSR 的應用,並把過程記錄下來。這次的實驗是使用 Firebase 的 Functions 來部屬 SSR 應用,以及使用 Firebase Hosting 來部屬靜態檔案。概念上就是 Hosting 的首頁直接導向 Server 的 API 來取得 SSR 的 HTML 字串,然後 Client 端再進行 Hydration 以此初始化 Angular 應用。<br>先看 Lighthouse 測一下跑分的結果:<br><img src="/images/angular-ssr-firebase/lighthouse.webp" alt="Firebase SSR Lighthouse"></p><h2 id="Ng-build-之前的準備"><a href="#Ng-build-之前的準備" class="headerlink" title="Ng build 之前的準備"></a>Ng build 之前的準備</h2><p>我直接基於之前的 TodoMVC2023 專案來實驗,在執行指令之前,我們需要做一下檔案的調整。</p><h3 id="修改-angular-json-的-build-outputPath"><a href="#修改-angular-json-的-build-outputPath" class="headerlink" title="修改 angular.json 的 build outputPath"></a>修改 angular.json 的 build outputPath</h3><p>把 <code>angular.json</code> 中的 build 與 server 的 outputPath 調整一下,將打包出來的檔案放在 <code>dist/functions</code> 目錄下。</p><pre><code class="json"> "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", "options": { "outputPath": "dist/functions/browser", ... ... "server": { "builder": "@angular-devkit/build-angular:server", "options": { "outputPath": "dist/functions/server", ... },</code></pre><h3 id="修改-server-ts"><a href="#修改-server-ts" class="headerlink" title="修改 server.ts"></a>修改 server.ts</h3><p>由於 Angular CLI 自動產生的 server.ts 裡面也有 <code>distFolder</code> 的配置,所以也要跟著調整:</p><pre><code class="typescript">const isDev = isDevMode(); // Don't forget to import isDevMode from @angular/coreconst website = isDev ? 'dist/functions/browser' : 'browser';const distFolder = join(process.cwd(), website);</code></pre><p>執行 <code>build:ssr</code> 指令:</p><pre><code class="bash">npm run build:ssr</code></pre><p>Build 完之後可以看到 dist 目錄下有 <code>function/server</code> 以及 <code>function/browser</code> 的資料夾,稍微確認一下有沒有正常出現。</p><h2 id="安裝-Firebase-Tools-CLI"><a href="#安裝-Firebase-Tools-CLI" class="headerlink" title="安裝 Firebase Tools CLI"></a>安裝 Firebase Tools CLI</h2><p>首先要先安裝 Firebase Tools CLI,這個工具可以讓我們在本機端進行 Firebase 操作指令,安裝方式如下:</p><pre><code class="bash">npm install -g firebase-tools</code></pre><p>安裝完成後可以透過 <code>firebase --version</code> 來確認是否安裝成功。</p><h2 id="登入-Firebase-CLI"><a href="#登入-Firebase-CLI" class="headerlink" title="登入 Firebase CLI"></a>登入 Firebase CLI</h2><p>接著要進行登入,輸入 <code>firebase login</code> 指令,會跳出瀏覽器視窗,請登入 Google 帳號,登入完成後,就可以在 CLI 看到登入成功的訊息。另外它會問你是否要讓它收集資料以改善服務,這邊就看個人意願了。</p><h2 id="初始化-Firebase"><a href="#初始化-Firebase" class="headerlink" title="初始化 Firebase"></a>初始化 Firebase</h2><h3 id="Project-Setup"><a href="#Project-Setup" class="headerlink" title="Project Setup"></a>Project Setup</h3><p>登入之後,就可以進行初始化,輸入 <code>firebase init</code> 指令,會跳出選單,我是選擇 <code>Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys</code> 以及 <code>Functions: Configure a Cloud Functions directory and its files</code>,接著會問你要使用哪個專案,可以選擇建立新的專案或是選擇既有的專案。</p><pre><code class="bash">? Please select an option: Create a new projecti If you want to create a project in a Google Cloud organization or folder, please use "firebase projects:create" instead, and return to this command when you\'ve created the project.? Please specify a unique project id (warning: cannot be modified afterward) [6-30 characters]: todomvc-ssr-demo? What would you like to call your project? (defaults to your project ID)✔ Creating Google Cloud Platform project✔ Adding Firebase resources to Google Cloud Platform project=== Your Firebase project is ready! ===Project information: - Project ID: todomvc-ssr-demo - Project Name: todomvc-ssr-demo</code></pre><h3 id="Hosting-Setup"><a href="#Hosting-Setup" class="headerlink" title="Hosting Setup"></a>Hosting Setup</h3><p>接著是 Hosting 相關的設定,主要會詢問你要部屬到哪個資料夾,以及是否要使用 SPA 模式:</p><pre><code class="bash">? What do you want to use as your public directory? functions? Configure as a single-page app (rewrite all urls to /index.html)? Yes? Set up automatic builds and deploys with GitHub? No+ Wrote public/index.html</code></pre><h3 id="Function-Setup"><a href="#Function-Setup" class="headerlink" title="Function Setup"></a>Function Setup</h3><p>設置 Functions 的部分,會有幾個提問,我們選擇 <code>JavaScript</code>,然後會問你要不要使用 ESLint,這邊我選擇 <code>No</code>,最後會問你要不要安裝相依套件,這邊我選擇 <code>Yes</code>。</p><pre><code class="bash">? What language would you like to use to write Cloud Functions? JavaScript? Do you want to use ESLint to catch probable bugs and enforce style? No+ Wrote functions/package.json+ Wrote functions/.gitignore+ Wrote functions/index.js? Do you want to install dependencies with npm now? Yes</code></pre><h3 id="functions-x2F-index-js-加入-ngssr-API"><a href="#functions-x2F-index-js-加入-ngssr-API" class="headerlink" title="functions/index.js 加入 ngssr API"></a>functions/index.js 加入 ngssr API</h3><p>初始化完成之後,刪除 <code>functions/index.html</code>,並且在 <code>functions/index.js</code> 這個檔案新增 API: ngssr:</p><pre><code class="JavaScript">const functions = require("firebase-functions");const mainjsfile = require(__dirname + '/server/main' );exports.ngssr = functions.https.onRequest(mainjsfile.app());</code></pre><h3 id="調整-firebase-json"><a href="#調整-firebase-json" class="headerlink" title="調整 firebase.json"></a>調整 firebase.json</h3><p>新增 <code>ngssr</code> API 之後,要修改 <code>firebase.json</code> 裡面的 <code>rewrites</code>,讓它可以正確的導向到 <code>ngssr</code> API:</p><blockquote><p>注意: 預設的 <code>rewrites</code> 是 <code>destination</code>,這邊要改成 <code>function</code>。</p></blockquote><pre><code class="json">"rewrites": [ { "source": "**", "function": "ngssr" } ]</code></pre><h2 id="執行本地端測試"><a href="#執行本地端測試" class="headerlink" title="執行本地端測試"></a>執行本地端測試</h2><p>使用 firebase emulators:start 來啟動本地端的測試環境,這邊會需要一些時間,因為它會幫你安裝相依套件,並且啟動本地端的測試環境。</p><pre><code class="bash">firebase emulators:start</code></pre><p>都沒有報錯的話,就可以在瀏覽器輸入 <code>http://localhost:5000</code> 來看到 SSR 的畫面了。</p><h2 id="發布到-Firebase"><a href="#發布到-Firebase" class="headerlink" title="發布到 Firebase"></a>發布到 Firebase</h2><p>都確認沒問題之後,就發佈到 Firebase 吧,輸入 <code>firebase deploy</code> 指令,就可以看到部屬的結果了。</p><pre><code class="bash">firebase deploy</code></pre><blockquote><p>注意: 如果是新的 Firebase Project,預設是免費的 Plan: Spark,可能需要轉成付費的 Blaze Plan 才能部屬成功。</p></blockquote><p>部屬的網址應該是 <code>YOUR_PROJECT_NAME.web.app</code>,deploy 成功後也會出現提示:</p><pre><code class="bash">+ Deploy complete!Project Console: https://console.firebase.google.com/project/YOUR_PROJECT_NAME/overviewHosting URL: https://YOUR_PROJECT_NAME.web.app</code></pre><h2 id="驗證結果"><a href="#驗證結果" class="headerlink" title="驗證結果"></a>驗證結果</h2><p>首先看一下 Network 第一時間拿到的 HTML:<br><img src="/images/angular-ssr-firebase/network.webp" alt="Firebase SSR Network"></p><p>使用 Dev Tool 的 Performance 錄製:<br><img src="/images/angular-ssr-firebase/performance.webp" alt="Firebase SSR Performance"></p><h2 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h2><p>儘管 Angular 配置 SSR 已經相對簡單了許多,不過要搭配什麼樣的 Server 具體還是要根據每個專案的需求去個別調整。這次的實驗是使用 Firebase Functions 來部屬 SSR 應用。另外,雖然升級 Blaze 付費方案後 Firebase Functions 還是有提供免費額度,但超過的話就要付費了,所以如果是大量使用 SSR 的話,還是要考慮一下成本的問題。另外還要注意 .gitignore 的設定,因為這個實驗是直接把 Firebase 專案放到 <code>dist</code> 目錄下,是沒有進版控的。如果要進版控的話,要把 <code>dist</code> 目錄下的 <code>functions</code> 資料夾加入到 <code>.gitignore</code> 中,避免把 <code>node_modules</code> 也一起上版控。</p><blockquote><p><strong><span style="color:red">!!!注意: 如果你只是嘗試玩一下的話,保險起見還是要到 GCP 帳戶設定一下預算,避免超支。!!!</span></strong></p></blockquote><p><img src="/images/angular-ssr-firebase/budget.webp" alt="Firebase SSR Budget"></p><h2 id="參考資料與範例"><a href="#參考資料與範例" class="headerlink" title="參考資料與範例"></a>參考資料與範例</h2><p><a href="https://github.com/jeffwu85182/TodoMVC2023">TodoMVC2023</a><br><a href="https://medium.com/@d.gerbede/angular-ssr-with-universal-and-firebase-e68e3989b8ff">Angular SSR with Universal and firebase</a><br><a href="https://fireship.io/lessons/angular-universal-firebase/">Angular Universal SSR with Firebase</a></p>]]></content>
<categories>
<category> Angular </category>
<category> Firebase </category>
</categories>
<tags>
<tag> Note </tag>
<tag> Angular 16 </tag>
<tag> SSR </tag>
<tag> Hydration </tag>
<tag> Firebase Functions </tag>
<tag> Firebase Hosting </tag>
</tags>
</entry>
<entry>
<title>使用原生 HTML Dialog</title>
<link href="/2023/05/09/html-dialog/"/>
<url>/2023/05/09/html-dialog/</url>
<content type="html"><![CDATA[<p><img src="/images/html-dialog/cover.webp" alt="HTML Dialog"></p><h2 id="關於-Modal-與-Dialog"><a href="#關於-Modal-與-Dialog" class="headerlink" title="關於 Modal 與 Dialog"></a>關於 Modal 與 Dialog</h2><p>在今日的網站開發上,使用彈窗式的介面來呈現資訊只要是接觸過網站開發的人應該都不陌生,而在過去我們對於彈窗式介面的實現方式,通常是透過 JavaScript 控制 class 來達成,但這樣的方式在維護上會有一些問題,例如:對於 Modal 或 Dialog 的顯示位置我們都需要另外寫一份 CSS 去設計,需要使用 <code>.open</code> 之類的 CSS class 來控制開關,或者需要額外的元素實現 Modal 的背景,而這樣的實現成本比較高,而且也不易維護。而在 HTML5 中,我們可以透過 <code>dialog</code> 標籤來實現彈窗式介面,這樣的實現方式不僅簡單,而且也不需要透過 JS 來控制 CSS Class,接下來就讓我們來看看如何使用 dialog element。</p><span id="more"></span><h2 id="Modal-與-Dialog-的差異"><a href="#Modal-與-Dialog-的差異" class="headerlink" title="Modal 與 Dialog 的差異"></a>Modal 與 Dialog 的差異</h2><p>Modal 是一種介面的設計模式,而 Dialog 則是一種介面的元素,Modal 通常會使用 Dialog 來實現,但 Dialog 並不一定是 Modal,例如:我們可以透過 Dialog 來實現一個提示訊息的介面,而這個介面並不是 Modal,因為它並不會阻止使用者對其他元素進行操作,而 Modal 則是會阻止使用者對其他元素進行操作,直到 Modal 關閉為止。<br></p><blockquote><p>兩者最大的差異在於:Modal 會阻止使用者對其他元素進行操作,而 Dialog 則不會。對我來說,Modal 是一個完全阻隔的彈窗內容並且顯示在畫面上的中央,而 Dialog 比較像是一個提示訊息的介面。</p></blockquote><h2 id="使用-Dialog"><a href="#使用-Dialog" class="headerlink" title="使用 Dialog"></a>使用 Dialog</h2><p>要使用 Dialog 很簡單,只需要在 HTML 中加入 <code><dialog></code> 元素即可,例如:</p><pre><code class="html"><dialog open> <p>Greetings, one and all!</p> <form method="dialog"> <button>OK</button> <button (click)="closeDialog()">Cancel</button> </form></dialog></code></pre><p><code>open</code> attribute 會讓 Dialog 在載入時就顯示出來,而 form 中的 method 屬性則是用來指定 Dialog 的行為,預設值為 <code>dialog</code>,也就是當使用者點擊 form 中的 button 時,Dialog 會關閉,如果我們將 method 屬性設定為 <code>get</code>,則會將 form 中的資料以 GET 的方式送出,而如果設定為 <code>post</code>,則會以 POST 的方式送出,這樣的設定方式和一般的 form 是一樣的。</p><h2 id="Dialog-的-JS-操作方法"><a href="#Dialog-的-JS-操作方法" class="headerlink" title="Dialog 的 JS 操作方法"></a>Dialog 的 JS 操作方法</h2><p>要在 JavaScript 操作 Dialog 或 Modal,我們可以透過 <code>show()</code> 來顯示 Dialog,而 <code>showModal()</code> 則是以 Modal 方式呈現,兩者皆可使用 <code>close()</code> 方法來關閉 Modal 與 Dialog:</p><pre><code class="js">const dialog = document.querySelector('dialog');function openDialog() { dialog.showModal(); // for modal // dialog.show(); // for dialog}function closeDialog() { dialog.close();}</code></pre><h2 id="Dialog-的-CSS-操作方法"><a href="#Dialog-的-CSS-操作方法" class="headerlink" title="Dialog 的 CSS 操作方法"></a>Dialog 的 CSS 操作方法</h2><p>關於 Style 的部分,Dialog 有一些預設的樣式,例如:<code>showModal()</code> 會自動置中,並且有陰影效果,這些預設的樣式讓我們只需要專注在介面呈現的風格,我們也可以透過 CSS 來進行修改,假設要調整 Modal 的陰影效果我們可以使用 <code>::backdrop</code> 調整:</p><pre><code class="css">dialog::backdrop { background-color: rgba(0, 0, 0, 0.5);}</code></pre><h2 id="Dialog-的事件"><a href="#Dialog-的事件" class="headerlink" title="Dialog 的事件"></a>Dialog 的事件</h2><p>Dialog 也有一些事件可以使用,例如:<code>close</code> 事件,當 Dialog 關閉時觸發,而 <code>cancel</code> 事件則是當使用者點擊 Dialog 中的取消按鈕時觸發,而 <code>submit</code> 事件則是當使用者點擊 Dialog 中的確認按鈕時觸發:</p><pre><code class="js">const dialog = document.querySelector('dialog');dialog.addEventListener('close', () => { console.log('Dialog closed');});dialog.addEventListener('cancel', () => { console.log('Dialog cancelled');});dialog.addEventListener('submit', () => { console.log('Dialog submitted');});</code></pre><h2 id="兼容性"><a href="#兼容性" class="headerlink" title="兼容性"></a>兼容性</h2><p>如果舊版的瀏覽器不支援 Dialog,可以透過 <a href="https://github.com/GoogleChrome/dialog-polyfill">dialog-polyfill</a> 來實現,根據 README 裡面的描述可以支援到 IE9 以上的瀏覽器。使用之前也可以上 <a href="https://caniuse.com/dialog">Can I use</a> 來查看瀏覽器的支援度。</p><h2 id="結論"><a href="#結論" class="headerlink" title="結論"></a>結論</h2><p>使用 HTML 原生的 Dialog 可以讓我們更能專注於畫面的設計,同時也帶來有語意化的架構確保可用性及可訪問性。避免產生不必要的 CSS 樣式來管理 Dialog 的開關或位置,使用起來更直覺且易於維護。</p><h2 id="參考資料"><a href="#參考資料" class="headerlink" title="參考資料"></a>參考資料</h2><p><a href="https://web-platform-yvpddf.stackblitz.io/">實作範例</a><br><a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog">MDN - The Dialog element</a><br><a href="https://blog.webdevsimplified.com/2023-04/html-dialog/">Modals Will Never Be The Same - HTML dialog Element</a></p>]]></content>
<categories>
<category> HTML </category>
</categories>
<tags>
<tag> Note </tag>
<tag> Dialog </tag>
</tags>
</entry>
<entry>
<title>關於 Angular SSR 與 Hydration</title>
<link href="/2023/05/08/angular-hydration-intro/"/>
<url>/2023/05/08/angular-hydration-intro/</url>
<content type="html"><![CDATA[<p><img src="/images/angular-hydration-intro/cover.webp" alt="Angular Hydration"></p><h2 id="什麼是-SSR"><a href="#什麼是-SSR" class="headerlink" title="什麼是 SSR ?"></a>什麼是 SSR ?</h2><p>SSR 的全名是 Server-Side Rendering,是一種網頁開發技術。它指的是在 server 端將網頁應用程式的原始碼轉換成 HTML 字串,然後將這些 HTML 發送到 client 端。當 client 端收到預先渲染的 HTML 時,它可以立即在瀏覽器上顯示,而無需等待 JavaScript 加載和執行。這樣可以提高網站的首次加載速度,改善搜尋引擎的索引效果,以及提供更好的使用者體驗。</p><h2 id="什麼是-Hydration"><a href="#什麼是-Hydration" class="headerlink" title="什麼是 Hydration ?"></a>什麼是 Hydration ?</h2><p>Hydration 是指 client 端將 server 端回傳的 HTML 字串轉換成具有 Angular 功能的實時應用。在這個過程中,Angular 會將預先渲染的 HTML 中的靜態內容與 Angular 的動態功能結合起來,並綁定事件,讓使用者可以直接操作 app,就像與 client 端渲染的應用進行交互一樣。</p><h2 id="v16-的-Hydration-亮點是什麼"><a href="#v16-的-Hydration-亮點是什麼" class="headerlink" title="v16 的 Hydration 亮點是什麼?"></a>v16 的 Hydration 亮點是什麼?</h2><p>舊版的 Hydration 過程是透過 server 端渲染畫面之後送到 client 端,client 端雖然可以快速的看到應用程式的第一個畫面,但實際上還是要<strong>整個 App 在 Client 端重新渲染一次</strong>,因為 client 端無法訪問 Angular app 的狀態,這樣的過程會影響到使用體驗而且不夠有效率。<br><br>新的 Angular hydration 通過允許 client 端訪問 Angular app 狀態來解決這個問題。這意味著 client 端不必在 server 端呈現 HTML 後重新渲染整個應用。相反,client 端可以直接混合應用程式狀態,這是一個更快的過程。</p><h2 id="如何使用-SSR"><a href="#如何使用-SSR" class="headerlink" title="如何使用 SSR ?"></a>如何使用 SSR ?</h2><p>啟用 SSR 有幾個步驟:</p><ol><li>確認是否有升級到 Angular v16:</li></ol><pre><code class="bash">npx ng updatenpx ng update @angular/core @angular/cli</code></pre><ol start="2"><li>加入 <code>@nguniversal/express-engine</code>,會出現詢問是否執行自動執行,選擇 <code>Yes</code>:</li></ol><pre><code class="bash">ng add @nguniversal/express-engine</code></pre><p>完成之後會產生四個檔案,並更新 <code>package.json</code>:</p><pre><code class="bash">CREATE src/main.server.tsCREATE src/app/app.config.server.tsCREATE tsconfig.server.jsonCREATE server.ts</code></pre><ol start="3"><li>建立一個 <code>app.config.ts</code>,並將 <code>main.ts</code> 中的 <code>bootstrapApplication</code> 第二個參數(<code>options</code>) 搬過來:<br>由於 <code>app.config.server.ts</code> 會引用 <code>app.config.ts</code>,但目前沒有建立,所以我們要手動新增,例如:</li></ol><pre><code class="TypeScript">import { ApplicationConfig, importProvidersFrom, isDevMode } from '@angular/core';import { BrowserModule } from '@angular/platform-browser';import { ServiceWorkerModule } from '@angular/service-worker';export const appConfig: ApplicationConfig = { providers: [ importProvidersFrom( BrowserModule, ServiceWorkerModule.register('ngsw-worker.js', { enabled: !isDevMode(), // Register the ServiceWorker as soon as the application is stable // or after 30 seconds (whichever comes first). registrationStrategy: 'registerWhenStable:30000', }) ), ],};</code></pre><ol start="4"><li>並把 <code>main.ts</code> 的 <code>options</code> 改成 import 的方式:</li></ol><pre><code class="TypeScript">import { bootstrapApplication } from '@angular/platform-browser';import { AppComponent } from './app/app.component';import { appConfig } from './app/app.config';bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));</code></pre><ol start="5"><li>執行 <code>npm run dev:ssr</code>,以自己做的 TODOMVC 的跑分結果。<br>沒有 SSR 的分數:<br><img src="/images/angular-hydration-intro/withoutSSR.webp" alt="Result"><br>使用 SSR 的分數:<br><img src="/images/angular-hydration-intro/result.webp" alt="Result"></li></ol><h2 id="SSR-的優點"><a href="#SSR-的優點" class="headerlink" title="SSR 的優點"></a>SSR 的優點</h2><ul><li>更快的首頁渲染時間:由於伺服器已經渲染了初始 HTML,客戶端可以立即顯示內容,而無需等待 JavaScript 加載和執行。</li><li>搜索引擎優化 (SEO): SSR 使搜索引擎更容易抓取和索引 app 的內容,因為它們可以直接讀取伺服器發送的 HTML。</li><li>提高可訪問性:部分瀏覽器可能對於 JavaScript 支持有限, SSR 可以確保這些瀏覽器至少能看到基本的內容和功能。</li></ul><h2 id="SSR-可能的挑戰及問題"><a href="#SSR-可能的挑戰及問題" class="headerlink" title="SSR 可能的挑戰及問題"></a>SSR 可能的挑戰及問題</h2><p>在實現 Angular SSR 時,可能會遇到一些需要探討的問題:</p><ul><li>性能問題:在 server 上渲染 app 會增加 server 的負擔,對於高流量的網站,這可能會導致性能問題。</li><li>client 端和 server 之間的狀態同步:由於 SSR 需要在 client 端和 server 之間共享狀態,開發人員需要確保兩者之間的狀態同步,以防止出現錯誤或不一致的行為。</li><li>環境差異:由於 client 端和 server 環境的差異,開發人員需要確保應用的程式碼可以在這兩個環境中正常運行。這可能涉及到檢查和處理某些特定於環境的 API 和功能。</li><li>增加複雜性:引入 SSR 可能會增加應用的複雜性,因為開發人員需要依照情況維護和管理兩個不同環境的程式碼。這可能需要更多的開發和測試時間。</li></ul><h2 id="參考資料"><a href="#參考資料" class="headerlink" title="參考資料"></a>參考資料</h2><p><a href="https://angular.io/guide/universal">Server-side rendering with Angular Universal</a><br><a href="https://angular.io/guide/hydration">Angular Hydration</a><br><a href="https://blog.angular.io/angular-v16-is-here-4d7a28ec680d">Angular v16 is here!</a><br><a href="https://www.youtube.com/watch?v=b6MfRwiPhpo">Server Side Rendering (SSR) in Angular v16</a><br><a href="https://www.youtube.com/watch?v=25FgSUH4DCk">All About Server-side Rendering w/Angular v16</a></p>]]></content>
<categories>
<category> Angular </category>
</categories>
<tags>
<tag> Note </tag>
<tag> Angular 16 </tag>
<tag> SSR </tag>
<tag> Hydration </tag>
</tags>
</entry>
<entry>
<title>使用 Angular Standalone Component 簡化開發</title>
<link href="/2023/04/21/angular-standalone-component/"/>
<url>/2023/04/21/angular-standalone-component/</url>
<content type="html"><![CDATA[<p>在 Angular 14 新增了 <code>Standalone Component</code> 的功能,以往我們新增的 Component、Directive 以及 Pipe 可以不需要透過 <code>NgModule</code> 來管理元件,同時也能簡化使用 Angular 開發應用的體驗。現有應用可以選擇性逐步採用新的獨立樣式,而無需進行任何重大更改。<br><img src="/images/angular-standalone-component/standalone.webp" alt="Stand Alone"></p><h2 id="如何使用-Standalone-Component"><a href="#如何使用-Standalone-Component" class="headerlink" title="如何使用 Standalone Component"></a>如何使用 Standalone Component</h2><h3 id="建立-Standalone-Component"><a href="#建立-Standalone-Component" class="headerlink" title="建立 Standalone Component"></a>建立 Standalone Component</h3><p>語法相當簡單,只需要在 <code>ng generate</code> 指令後面加上 <code>--standalone</code> 即可:</p><pre><code class="bash">ng generate component <component-name> --standalone</code></pre><h3 id="Component-轉換成-Standalone-Component"><a href="#Component-轉換成-Standalone-Component" class="headerlink" title="Component 轉換成 Standalone Component"></a>Component 轉換成 Standalone Component</h3><p>如果要在既有的 Component 轉換成 Standalone Component,只需要在 <code>@Component</code> 裡面加上 <code>standalone: true</code>:</p><pre><code class="typescript">@Component({ standalone: true, selector: 'photo-gallery', imports: [ImageGridComponent], template: ` ... <image-grid [images]="imageList"></image-grid> `,})export class PhotoGalleryComponent { // component logic}</code></pre><p>加上 <code>standalone: true</code> 之後,我們就可以在 Component 使用 imports 來引入其他的 Dependency,像是 Directive、Pipe、Component 等等。此外 <code>imports</code> 也可以引入其他的 NgModule。</p><h3 id="既有的-NgModule-加入-Standalone-Component"><a href="#既有的-NgModule-加入-Standalone-Component" class="headerlink" title="既有的 NgModule 加入 Standalone Component"></a>既有的 NgModule 加入 Standalone Component</h3><p>Standalone Component 透過 <code>NgModule.imports</code> 就可以加到既有的 NgModule 中:</p><pre><code class="typescript">@NgModule({ declarations: [AlbumComponent], exports: [AlbumComponent], imports: [PhotoGalleryComponent],})export class AlbumModule {}</code></pre><h2 id="直接從-Standalone-Component-執行應用"><a href="#直接從-Standalone-Component-執行應用" class="headerlink" title="直接從 Standalone Component 執行應用"></a>直接從 Standalone Component 執行應用</h2><p>我們可以不用透過任何 <code>NgModule</code> 來啟動應用,Angular 提供了 <code>bootstrapApplication</code> API,可以直接從 Standalone Component 啟動:</p><pre><code class="typescript">// in the main.ts fileimport { bootstrapApplication } from '@angular/platform-browser';import { PhotoAppComponent } from './app/photo.app.component';bootstrapApplication(PhotoAppComponent);</code></pre><h2 id="新的-Routing-API-簡化-lazy-loading"><a href="#新的-Routing-API-簡化-lazy-loading" class="headerlink" title="新的 Routing API 簡化 lazy-loading"></a>新的 Routing API 簡化 lazy-loading</h2><p>Angular router API 也更新並簡化,以便利用 Standalone Component,在許多常見 lazy-loading 的情境不再需要 <code>NgModule</code>。例如要 lazy-loading 一個 Component,只需要在 <code>Route.loadComponent</code> 裡面使用 <code>import()</code> 來引入 Component 即可:</p><pre><code class="typescript">export const ROUTES: Route[] = [ { path: 'admin', loadComponent: () => import('./admin/panel.component').then((mod) => mod.AdminPanelComponent), }, // ...];</code></pre><h3 id="一次-lazy-loading-多個路由"><a href="#一次-lazy-loading-多個路由" class="headerlink" title="一次 lazy-loading 多個路由"></a>一次 lazy-loading 多個路由</h3><p><code>LoadChildren</code> 現在支援加載一組新的子路由,不需要寫一個 lazy-load 的 <code>NgModule</code>,利用 <code>RouterModule.forChild</code> 宣告路由:</p><pre><code class="typescript">// In the main application:export const ROUTES: Route[] = [ { path: 'admin', loadChildren: () => import('./admin/routes').then((mod) => mod.ADMIN_ROUTES), }, // ...];// In admin/routes.ts:export const ADMIN_ROUTES: Route[] = [ { path: 'home', component: AdminHomeComponent }, { path: 'users', component: AdminUsersComponent }, // ...];</code></pre><h3 id="Lazyloading-與-default-exports"><a href="#Lazyloading-與-default-exports" class="headerlink" title="Lazyloading 與 default exports"></a>Lazyloading 與 default exports</h3><p>上面的範例中 <code>ADMIN_ROUTES</code> 也可以改成 <code>export default</code>,如此一來在 <code>loadChildren</code> 或 <code>loadComponent</code> 都只需要 <code>import('./admin/routes')</code> :</p><pre><code class="typescript">// In the main application:export const ROUTES: Route[] = [ { path: 'admin', loadChildren: () => import('./admin/routes') }, // ...];// In admin/routes.ts:export default [ { path: 'home', component: AdminHomeComponent }, { path: 'users', component: AdminUsersComponent }, // ...] as Route[];</code></pre><h3 id="為部分路由提供服務的方法"><a href="#為部分路由提供服務的方法" class="headerlink" title="為部分路由提供服務的方法"></a>為部分路由提供服務的方法</h3><p>對於 NgModules 的 lazy-load API(即 <code>loadChildren</code>),在讀取路由的延遲載入子路由時,會創建一個新的模組注入器(Injector)。這個功能經常被用來為特定的路由提供 service。舉個例子,如果把所有 <code>/admin</code> 下的路由都用 <code>loadChildren</code> 來設定範圍,那麼只有這些路由才能獲得針對 Admin 的特定 service。要做到這一點,即使不需要延遲載入相關路由,也需要使用 <code>loadChildren</code> API。</p><p>如今,Router 允許在路由上明確指定額外的 <code>Providers</code>,這樣可以在不需要延遲載入或 <code>NgModule</code> 的情況下實現相同的範圍設定。舉例來說,<code>/admin</code> 路由結構內範圍限定的 service 將如下:</p><pre><code class="typescript">export const ROUTES: Route[] = [ { path: 'admin', providers: [AdminService, { provide: ADMIN_API_KEY, useValue: '12345' }], children: [ { path: 'users', component: AdminUsersComponent }, { path: 'teams', component: AdminTeamsComponent }, ], }, // ... other application routes that don't // have access to ADMIN_API_KEY or AdminService.];</code></pre><p>我們可以將 provider 與額外路由配置的 <code>loadChildren</code> 相結合,以實現延遲載入帶有額外路由和路由級 provider 的 NgModule 的相同效果。這個例子配置了與上述相同的 provider/子路由,但是在延遲載入的邊界之後:</p><pre><code class="typescript">// Main application:export const ROUTES: Route[] = { // Lazy-load the admin routes. {path: 'admin', loadChildren: () => import('./admin/routes').then(mod => mod.ADMIN_ROUTES)}, // ... rest of the routes}// In admin/routes.ts:export const ADMIN_ROUTES: Route[] = [{ path: '', pathMatch: 'prefix', providers: [ AdminService, {provide: ADMIN_API_KEY, useValue: 12345}, ], children: [ {path: 'users', component: AdminUsersCmp}, {path: 'teams', component: AdminTeamsCmp}, ],}];</code></pre><p>要留意一下空路徑的路由下的 providers 是在所有的子路由共享的。 另外,<code>importProvidersFrom</code> 這個方法可以 import 基於 <code>NgModule</code> 的 DI 注入到 Route 的 providers 中:</p><pre><code class="typescript">export const ROUTES: Route[] = [ { path: 'foo', providers: [importProvidersFrom(NgModuleOne, NgModuleTwo)], component: YourStandaloneComponent, },];</code></pre><h2 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h2><p>當專案規模越來越大的時候 NgModule 的管理可能會面臨挑戰,時常要思考是否要建立新的 NgModule,或是這個元件是否會被重覆使用,一不小心就進入了重構地獄。Standalone Coponent 算是把 NgModule 一部分的功能下放到元件的層級,這樣可以避免建立元件時可能的摩擦,並且簡化了學習歷程,同時也可以讓延遲載入變得更容易。未來需要什麼東西就直接 import 到元件中,不需要再去管 NgModule 的事情。</p><h2 id="參考資料"><a href="#參考資料" class="headerlink" title="參考資料"></a>參考資料</h2><p><a href="https://angular.io/guide/standalone-components">Angular Standalone Component</a><br><a href="https://www.youtube.com/watch?v=x5PZwb4XurU&ab_channel=Angular">Getting started with Angular Standalone Component</a></p>]]></content>
<categories>
<category> Angular </category>
</categories>
<tags>
<tag> Note </tag>
<tag> Angular Standalone Component </tag>
</tags>
</entry>
<entry>
<title>原子化 CSS 學習筆記</title>
<link href="/2023/04/19/atomic-css-intro/"/>