wbr 的這些那些

在 responsive design 成為主流之後,有個問題也隨之被突顯出來,就是文字的換行,尤其是標題文字的換行位置,現在的瀏覽器的換行方式簡單來說就是超出區塊範圍的東西都往下放到下一行,所以在某些情況下,就會有第一行很長第二行很短的狀況,視覺上非常不平衡,以下圖為例,網頁的副標題沒有作特殊處理,所以會有可能會變成兩行長度差距很大的樣子:

line breaking

這個問題有蠻多解決方法的,目前我知道的就有:

  1. 微調 responsvie 的樣式來避免出現不平衡的狀態
  2. 在特定地方插入 避免換行
  3. 用 flex layout 來控制換行位置

基本上就是只要你不擇手段,問題還是可以解決的,不過我一直以來都是會偏好用標準的方法來解決問題,所以整理了一下我所知道可以拿來用的東西:

  1. <wbr>HTML element
  2. white-spaceCSS property
  3. <nobr>HTML element
  4. text-wrapCSS property

首先的想法是<wbr>配上white-space: nowrap;或是<nobr>,不過意外的是大部分瀏覽器都不支援這個組合,也就是說,包在<nobr>內的<wbr>的地方在現在大部分主流瀏覽器內是不會換行的:

<nobr>ChatGPT: Optimizing<wbr>Language Models<wbr>for Dialogue</nobr>

這就激起了我的好奇心了,於是我開始仔細的找資料,看看<nobr><wbr>到底是怎樣運作的。首先,就來看看<wbr>吧,雖然他第一次出現在 W3C 的文件內就是在 HTML5,但是其實它已經出現了 20 年以上,最早是作為 Netscape 的 HTML 2 extension 的一員:

The WBR element stands for Word BReak. This is for the very rare case when you have a NOBR section and you know exactly where you want it to break. Also, any time you want to give the Netscape Navigator help by telling it where a word is allowed to be broken. The WBR element does not force a line break (BR does that) it simply lets the Netscape Navigator know where a line break is allowed to be inserted if needed.

在 Internet Archive 上找到的備份,最早的定義其實明確的說著<wbr>處應該是要可以優先於<nobr>的,我甚至還在 bugzilla 上找到一個 24 年前的 bug report 在講這件事,根據這張票最後的關掉前的討論,其實可以用</nobr><nobr>來達成一樣的效果,然後 Firefox 不打算支援<wbr>,看到這邊,我只能說這解法怎麼這麼天才(稱讚的意味)。

查到這邊,我還是很好奇為什麼現在的主流瀏覽器依然<wbr>優先度比<nobr>還低,所以繼續找資料,這次看的就是最新的文件了,首先是<wbr>定義,在 HTML5 中變成:

Thewbrelement represents a line break opportunity.

從 "word break" 變成 "line break opportunity" 了,<nobr>則和<wbr>一樣一開始是 Netscape extension,不過他倒是沒有進到 HTML5,事實上,雖然<nobr>不是 HTML5 的一員,但是文件中定義的 default style 還是有它

br { display-outside: newline; } /* this also has bidi implications */
nobr { white-space: nowrap; }
wbr { display-outside: break-opportunity; } /* this also has bidi implications */
nobr wbr { white-space: normal; }

其實就是等價於white-space: nowrap,而white-space屬性在 CSS2 時是用來定義空白的處理方式:

This property declares how white space inside the element is handled.

到 CSS Text Level 3 時定義說的更明確,是用來決定遇到 "line break opportunity" 時的處理方式和是不是要合併 space 字元的屬性:

其實在 Level 4 更是可以分開設定兩種屬性,white-space變成一個 shorthand,而看到這些定義的演進,其實也讓人發現最早命名時其實只有考慮到西方語言的特色。

看到這邊,會發現有一個新的名詞:"line break opportunity",有些地方是稱為 "wrap opportunity",這個名詞其實是出現在 CSS Module Text 文件中的,顧名思義,就是可以換行的位置,而這份文件也是定義換行邏輯的文件,不過這邊其實沒把換行演算法(line breaking algorithm)明確定義下來,而是闡明各種相關的 CSS 屬性和它們會怎樣影響換行的結果,例如換行有分強制(<br>)和非強制(<wbr>),然後不同 CSS 屬性會影響這些換行點的出現與否,至於文本之中,哪些地方可以換行,就是換行演算法的部分了,這部份在 W3C 文件沒有定義死,所以是允許瀏覽器自己決定的,不過有提供一些參考文件,像是 Unicode 的附件 14:"Unicode Line Breaking Algorithm",或是稱為 UAX14,這份文件要搭配 Unicode Database(UCD) 的 Line_Break Property 資料,文件中的第五章有仔細的說明各種不同的 line breaking class,然後資料庫則是定義了所有 Unicode 字元的 Line_Break property,接著的第六章就是最重要的換行演算法了,這邊列出了 31 條規則,基本上是反向列舉,說哪些地方不能換行,例如 WJ(word joiner)前後都不能換行,數字中間的符號前後也不能換行之類的,不過不確定哪些瀏覽器是實作 UAX14 的,Chrome 似乎有用到 UCD,Firefox 則是以 JIS X 4051 為基礎做的換行演算法,其實 JIS X 4051 是我所知道,二戰後世界,最早的正式的文字編排的標準,查到的紀錄是 1989 有一版,而 UAX14 第一個非草稿的版本則是要到 1999 年才出來。

回到現在主流瀏覽器不支援<wbr>放在white-space: nowrap裡面無法換行的問題,其實要回到這個 HTML 標籤在繪製時,是不是有 magic 的,什麼是 magic 呢?簡單說就是,你能不能用 CSS 來定義該標籤的樣子,以及你能不能用 CSS 來改掉這些預設的樣式,而<br><wbr>正好是有 magic 的標籤,可以想想看,要怎樣用 CSS 讓文字內產生一個換行點,可能有人會說剛剛上面才看到的,不過,其實那幾個display-outside的值只存在於以前的草稿中,CSS-WG 決議不為了這個問題新增多的 display 狀態,所以問題就回到 HTML-WG 這邊了,CSS-WG 的 fantasai 其實有給了一組 default style:

br { all: unset !important; display: contents !important; content: "\a" !important; white-space: pre !important; }
wbr { all: unset !important; display: contents !important; content: "\200B" !important; }

我把!important拿掉整理一下:

br {
  all: unset;
  display: contents;
  content: "\a";
  white-space: pre;
}
wbr {
  all: unset;
  display: contents;
  content: "\200B";
}

其中,\a就是換行字元\200B則是 ZWSP,zero width space,因為是 zero width,所以看不到,然後又因為是 space,所以可以用來把字切開,也就表示可以在該處斷行。不過這個版本有些問題,因為有很多瀏覽器還不支援在::before, ::after以外的物件上套用content屬性,所以 fantasai 又提供一版用::before的版本:

br, wbr { all: unset !important; display: contents !important; white-space: pre !important; }
br::before { all: unset !important; content: "\a" !important; }
wbr::before { all: unset !important; content: "\200B" !important; }

但是,實際上直接拿這組定義來用,還是一樣有問題,就是有些瀏覽器已經讓<wbr>有 magic 了,結果wbr::before是沒有用的,目前 HTML 標準的修改也就還卡在這邊(issue 則是另外一個),HTML-WG 的 Domenic 開了這個 PR 要讓<br><wbr>就用 magic 來實現效果,不過這討論已經停很久了,所以最後會是怎樣的方案還不知道。

總之目前的結論就是,現在如果想要讓<wbr>照其定義的一樣,可以在<nobr>或是white-space: nowrap內產生換行,是辦不到的,不過可以用其他的標籤來辦到,像是:

.wbr::before {
  display: inline;
  content: "\00200B";
  white-space: normal;
}

配上

<nobr>ChatGPT: Optimizing<span class="wbr">Language Models<span class="wbr">for Dialogue</nobr>

這個寫法也有出現在一份由 Leif Halvard Silli 在提交 bug 給 WebKit 所做的 test 內

到這邊,大概已經把自己控制換行位置的部分講的差不多了,不過其實,還有一個方法可以處理一開始所提到的換行結果不理想的問題,就是在 CSS Text Module Level 4 中有一個新的屬性叫做text-wrap,其中有一個屬性值是balance,合起來就是

text-wrap: balance;

這樣設定,預期的結果就是會換行換在每一行的寬度最接近的位置,不過當然還沒有瀏覽器支援,連 caniuse 都還查不到text-wrap屬性,只是還是有 JS 的解決方案:

最後的最後補上一些參考資料,一篇是古老的 IE 時代的東西,一篇則是現在的相關 CSS 屬性,一篇則是 balance wrap 的介紹:


HTML 文件圖片預設寬高比

Intrinsic aspect ratio of incomplete image

我在 2018 年有篇文章 Intrinsic Size 媒體寬高比,介紹一個標準的草案intrinsicsize,為什麼會需要這東西在那篇文章也有講,主要就是要搭配像是:

img {
  max-width: 100%;
  height: auto;
}

這種寫法非常通用,但是在圖片讀取完成前,<img />標籤的佔位會無法先知道,這會造成頁面在圖片讀取完成後瀏覽器會需要比較大的重新繪製的工作。

這幾天想起來去查了一下近況,發現竟然早早就停止發展了,caniuse 那邊 也顯示瀏覽器都把一些實驗中的支援都拿掉了,細看下去,發現 Firefox 的人當時說要開始試驗另一個比較優雅的方法,也有和 CSS WG 的人開始討論,他們當時的想法就是直接用widthheight屬性來計算寬高比(aspect ratio),不過之後就都沒有標準文件相關的發展細節或連結了。

搜尋一番後,發現 Firefox 在 71 開始就已經發佈這個修改了,當時的 release note 其實有寫,而且 MDN 上還有一篇文章:Mapping the width and height attributes of media container elements to their aspect-ratio,也有介紹這個修改要解決的問題以及技術上是怎樣處理的,簡單一點形容就是:

img { 
  aspect-ratio: attr(width) / attr(height); 
}

不過實際上不是真的使用這條 User Agent style 來實作就是了,因為這個透過widthheight計算出來的寬高比只有在圖片還沒讀取的時候會有效,圖片讀取完成後就會改成用圖片實際的尺寸來計算怎麼顯示了,所以要說它是預設的寬高比也不太正確。

在 CSS Image 這個 Module 的第四章:Sizing Images and Objects in CSS 中,有詳細的定義要怎樣決定圖片在繪製在網頁上時要怎樣處理,還定義了一些專有名詞:

  • Intrinsic dimensions 簡單一點形容就是圖片的原始尺寸,尺寸包含了寬(intrinsic width)、高和寬高比,不一定會全部都有,像是向量圖就只有寬高比,另外 intrinsic 中文翻譯是固有、或是根本的,所以 intrinsic dimentions 也不是真的就是圖片原始尺寸,它的文字敘述是:a preferred or natural size of the object itself。
  • Specified size 使用 CSS 設定的物件大小。
  • Concrete object size 根據上面兩個資訊所決定的實際上物件要繪製的大小,也就是我們眼睛所會看到的圖片呈現的大小。

所以主要就是在寫怎樣計算 concrete object size 了,大概計算過程就是和各位腦袋中想的不會差距太大。不過在這個地方,有一個細節是在 CSS 文件中故意沒有講出來的,就是怎樣取得 intrinsic dimensions,CSS 文件中沒有明確的說 intrinsic width 是來自圖片標籤的width屬性,或是圖片的實際寬度(早在 CSS 2.1 就有寫出 CSS 文件不定義怎樣取得該數值了)。其實 HTML 圖片的 intrinsic dimensions 要怎麼取得是放在 HTML 文件的 15.4.3,15 章都在講 rendering,15.4 則是 replaced elements,也就是圖片、影片之類的內容會整個替換掉的元素,15.4.3 最後一段的第一點是說圖片如果有抓下來的話就直接用圖片的 intrinsic aspect ratio,而第二點是這樣寫的:「If img'swidthandheightattribute values, when parsed using the rules for parsing dimension values), are both not an error, not a percentage, and non-zero, then use the ratio resulting from dividing thewidthattribute value by theheightattribute value.」這邊就明確的寫到用兩個屬性來計算 intrinsic aspect ratio 了,不過針對這個圖片讀取完成前的 intrinsic aspect ratio,並沒有定義一個專有名詞,所以可以看到 cnaiuse 用一個複雜的文字來敘述這項修改,要是我的話應該會把這個值命名為 intrinsic aspect ratio of incomplete image 之類的吧。

最後整理一下:

  • 圖片讀取完成前,如果有設定正確的數值的 width 和 hieght 的話,瀏覽器會先它們來計算 intrinsic aspect ratio
  • 圖片讀取完成後,瀏覽器會用圖片原始的寬高比來作為 intrinsic aspect ratio
  • 本來是希望能用在所有的 replaced elements,不過會造成既有網站壞掉所以目前限制在<img>標籤
  • 以上的修改是大約是今年前半年才進入瀏覽器的

Scroll Margin/Padding

這篇文章要介紹一組算是蠻新的 CSS 屬性,分別是 scroll-marginscroll-padding,這兩個屬性是在 Scroll Snap Module Level 1 裡面定義的,目前主要的瀏覽器都有支援,IE 完全不支援,Safari 則是還在使用舊的非標準的屬性名稱,為什麼要介紹這兩個屬性呢?因為它除了原來 scroll-snap 微調的需求之外,還順便解決了一個存在已久的問題,也就是如果網頁的設計有固定浮動在頁面頂端的 header 時(Sticky/Fixed Header),使用 link 的 fragment 直接定位到頁面特定位置的時候,最上面一部份想顯示給訪客的內容會被 header 覆蓋到。

這個問題大概是從 Sticky Header 出現時就存在了,Stack Overflow 上就可以找到十年前的發問,以前的主要的解決方法就是讓連結的目標有看不到的高度,像是用padding-top,比較乾淨一點是用 pseudo-element 的:before來把內容往下推,不過這幾個方法雖然有用,但是對我來說卻一直不是個最佳解,所以一直以來我都期待能有更漂亮的解決方案,有事沒事就會搜尋一下,然後,終於我發現了這組 CSS 屬性可以用來讓瀏覽器自己捲動到目標時有指定的偏移。

scroll-margin是用在你想要捲動到的目標元件上,也就是有 id 的那個,像我的 blog 就是<article>標籤,給它加個scroll-margin-top: 56px;就可以了:

article {
  scroll-margin-top: 56px;
}

scroll-padding則是要用在 scroll container 上的,而不是目標的父母層,舉例來說,在我這邊,沒有特別設定任何overflow的情形下,scroll container 會落在<html>元件上,而不是<article>外面的<main>,所以設定就要改成:

html {
  scroll-padding-top: 56px;
}

這組 CSS 屬性我是今年才發現的,不過其實 CSS Trick 在去年就有文章介紹了:Fixed Headers, On-Page Links, and Overlapping Content, Oh My! ,真是太後知後覺了(其實也是因為我現在沒用 RSS reader 的關係)。


ENTER or SPACE, KEYDOWN or KEYUP

前一篇文章作動行為 Activation Behavior 發佈之後,卡西又做了一些測試,發現到 ENTERSPACE 的觸發時機其實不一樣:

然後我仔細測試過發現真的是這樣,而且 SPACEkeypress的狀態,就像是滑鼠按鍵按下去但是還沒放開時的樣子,然後這又讓我有點好奇起來了,仔細搜尋一番,發現 web 標準都沒有提到這個細節的定義,唯一有一點關係的是卡西也有找到的 WAI-ARIA Authoring Practices Issue 610,於是我就覺得這應該和 Web 標準定義沒關係,應該是更古老的預設行為,於是改變方向改找 Windows 預設行為相關的文件,搜尋一陣子其實也找不太到東西,大概是因為 GUI 和 Windows 剛出的時候其實 www 還不知道在哪裡吧,不過後來還是找到兩篇 stackoverflow 的問答看起來是相關的:

總和這兩篇的內容,大概整理一下:

這個行為應該是 Windows 一開始的時候就如此設計的了(看起來是很難找到相關設計的文件),然後實際上和 ENTER 相關(相對)的操作其實是 ESC 鍵,ENTER 鍵代表的是直接點 default button(例如 form 的 submit、dialog 的 ok 之類的),或是可以說是執行元件預設的行為,至於 ESC 鍵則是取消,不過取消在網頁的控制元件中幾乎是不存在的,過去有的大概只有<select>展開下拉選單後又決定不選時可以取消,到 HTML5 則又多了<dialog>有取消的行為(關閉 dialog),大概也是因為這個原因讓人忽略了 ENTERESC 的關係,變成注意到 ENTERSPACE 都可以操作元件;至於 SPACE 鍵其實就像是滑鼠點擊,keyDown如同mouseDownkeyUp如同mouseUp,要到keyUp才算一個點擊的動作,也就是到這時候才會去觸發click事件。

搞清楚這現象的原理之後,其實也就更容易理解 WAI-ARIA Authoring Practices 的範例那些 ENTERESCSPACE 幾個按鍵行為為什麼是那樣了,當然,以後需要客製 widget 時也不用再對這幾個按鍵的行為該怎樣定義苦惱了。


作動行為 Activation Behavior

前幾天全知全能的米奧大人在 Twitter 上徵求中階的 JavaScript 課程:

然後 Jedi 提供了一個題目:

後來米奧大人真的交作業了,也有提出一些問題,然後卡西有回應:

其中,「keyup 該觸發 button 上的 onclick」這句引起了我的興趣。

為了要顧及到網頁親和力,所有的控制元件的操作都應該要可以用鍵盤執行,所以像是 button 的動作也應該要可以用鍵盤控制,但是其實我以前一直搞不清楚,這之間正確的關係應該是怎樣,就三種可能性:

  • key 事件觸發 click 事件,click 事件有 default handler
  • click 事件觸發 key 事件,key 事件有 default handler
  • click 事件和 key 事件都有同一個 default handler

當我看到卡西那段文字的時候,我覺得他應該說的是有憑據的,不過我也覺得有些不正確,像是就我的認知,button 的 key 事件預設是不會觸發 click 事件的,於是我就花了點時間研究一下網路標準,這次終於找到規範和正確的關係了。

我先從 button 標籤開始查起,然後注意到一段,在說明 button 的 activation behavior 行為應該如何的文字,行為分成 submit button、reset button 和 button 三種,其中前兩個就像是在說 submit button 和 reset button 的行為一樣,所以我就了解到,activation behavior 就是我要找的關鍵字了,目前將它翻譯為「作動行為」。

然後在 HTML 6.3 Activation 找到:

Certain elements in HTML have an activation behavior, which means that the user can activate them. This is always caused by aclickevent.

The user agent should allow the user to manually trigger elements that have an activation behavior, for instance using keyboard or voice input, or through mouse clicks. When the user triggers an element with a defined activation behavior in a manner other than clicking it, the default action of the interaction event must be to fire aclickevent at the element.

第一段就是說作動行為(activation behavior)都是click事件觸發,第二段則是說瀏覽器要讓其它方法(像是鍵盤、語音操作等)可以觸發作動行為的話,實做的方法應該是在該事件的處理器(event handler)內觸發click事件來觸發該 HTML 元素的作動行為。這段文字就可以證明卡西說的基本上沒錯,另外就是我有疑惑的,應該是keydown還是keyup事件呢?根據我自己的實驗結果應該是要用keydown,不過總還是想找一下標準定義的出處,雖然沒有找到很明確的文字說明,不過 UI Events 3.5. Activation triggers and behavior 裡面的 EXAMPLE 4 內確實是寫 keydown event,當然keydown的時間點也比較符合期待,目前在不同標準文件內看到的範例也都是用 keydown。

查到這邊大概就可以確定,正確的關係應該是「key 事件觸發 click 事件,click 事件有 default handler」,不過卡西說的小錯誤是應該要用 keydown 事件,然後我在 twitter 有回說普通 button 不應該 keydown 觸發 click 則是我當時的錯誤認知(請見 ENTER or SPACE, KEYDOWN or KEYUP)。

再來,其實我還很好奇,哪裡有定義不同的元素分別用哪些按鍵 active 呢?因為表單送出是用 ENTER 鍵,但是像是 checkbox 的狀態切換卻是用 SPACE 鍵;上面提供的幾份文件也都沒講到這部分的定義,有種刻意避開的感覺,後來又找了許久才終於找到,其實是放在 WAI-ARIA Authoring Practices 這份 Working Group Note 內,拿 checkbox 為例,在它的 Keyboard Interaction 段落內就明白寫了:

When the checkbox has focus, pressing the Space key changes the state of the checkbox.

當然也有 button 的規範,就是同時有定義spaceenter;由於這份文件是 Working Group Note,規範的硬性比較低,這應該也是故意為之的。

最後來整理一下,首先是 HTML 文件有定義,預設的作動行為都是透過click事件觸發,但是同時也要保留其它操作介面觸發作動行為的可能性,像是常見的鍵盤行為,而其它操作方式都要透過觸發click事件的方式來觸發作動行為;再來就是不同 HTML 元素的作動行為要做哪些事情也是在 HTML 文件內;至於不同 HTML 元素要支援哪些按鍵呢,這部分就要交叉參考 ARIA in HTMLWAI-ARIA Authoring Practices 兩份文件了,前者用來查詢 HTML 元素對應的 ARIA role,後者可以根據 role 來判斷要支援哪些鍵盤按鍵。

以後要做自訂的控制元件的時候,就可以正大光明的把主要的動作寫在 onclick 事件下了(然後根據情況去加上 key event)。


W3C and WHATWG and HTML

w3c-whatwg-logos

「天下大勢,分久必合,合久必分」,沒想到 W3C 和 WHATWG 之間的複雜關係就這麼突然的踏上新的里程碑了,今天 W3C blog 發表了一篇文章-W3C and the WHATWG signed an agreement to collaborate on a single version of HTML and DOM,說兩個組織已經簽好合作的協議了,未來算是要共同維護同一份 HTML 和 DOM 的 spec,HTML WG 的章程也因此要重新制訂,現在有草稿可看,詳細一點的摘要可以看 W3C CEO Jeff Jaffe 的文章 - W3C and WHATWG to work together to advance the open Web platform

  • W3C and WHATWG work together on HTML and DOM, in the WHATWG repositories, to produce a Living Standard and Recommendation/Review Draft-snapshots
  • WHATWG maintains the HTML and DOM Living Standards
  • W3C facilitates community work directly in the WHATWG repositories (bridging communities, developing use cases, filing issues, writing tests, mediating issue resolution)
  • W3C stops independent publishing of a designated list of specifications related to HTML and DOM and instead will work to take WHATWG Review Drafts to W3C Recommendations

基本上就是 W3C 相關的 WG 以後都改成貢獻到 WHATWG 那邊(在 GitHub 上),然後 W3C 那邊會拿 WHATWG 標準文件的 snapshot 來作為 CR、PR、REC,或許也可以稱為 Living Standard 的勝利。

如果要看比較細節關於兩個組織間簽的合作內容也有公開在網路上-Memorandum of Understanding Between W3C and WHATWG,裡面還有列出所有相關的 W3C 的標準文件,以前我就一直很好奇到底全部是有哪些,剛好趁這機會一次收集齊全,其實還不少我沒看過的,甚至也有 404 的(?),其中推薦標準(REC)的部分:

  1. https://www.w3.org/TR/html5/ including other URLs under this directory
  2. https://www.w3.org/TR/html50/ including other URLs under this directory
  3. https://www.w3.org/TR/html51/ including other URLs under this directory
  4. https://www.w3.org/TR/html52/ including other URLs under this directory
  5. https://www.w3.org/TR/html/ including other URLs under this directory
  6. https://www.w3.org/TR/webstorage/
  7. https://www.w3.org/TR/webmessaging/
  8. https://www.w3.org/TR/eventsource/
  9. https://www.w3.org/TR/2dcontext/
  10. https://www.w3.org/TR/dom/

非推薦標準:

  1. https://w3c.github.io/html/ including other URLs under this directory
  2. https://www.w3.org/html/wg/drafts/html/master/ including other URLs under this directory
  3. https://www.w3.org/TR/websockets/
  4. https://www.w3.org/TR/2dcontext2/
  5. https://www.w3.org/TR/microdata/
  6. https://www.w3.org/TR/staticrange/
  7. https://www.w3.org/TR/workers/
  8. https://dvcs.w3.org/hg/webperf/raw-file/default/specs/RequestAnimationFrame/Overview.html
  9. https://w3c.github.io/dom/
  10. https://www.w3.org/TR/dom41/
  11. https://www.w3.org/TR/DOM-Parsing/
  12. https://www.w3.org/TR/html53/

這些東西在 WHATWG 那邊基本上都寫在 HTMLDOM 裡面,就是單一份標準文件內塞了比較多東西這樣。

最後就是我還發現一點有趣的,WHATWG 投票那邊,四位出來投票的分別是 Apple、Microsoft、Mozilla 和 Google 四間公司的人。然後我只對 Mozilla 的 dbaron 有印象而已。


Lab Gradient

Gradient

一早起來就看到這篇文章 視覺上的完美漸層 Chromatic,介紹了一個 Sketch plugin 可以用不同色彩系統的漸層來產生更好的視覺效果,之前其實也有注意到這個問題好幾次,就是覺得 CSS gradient 的效果不好,也有注意到一些其他的漸層方法,不過一直沒記錄下來,所以趁這時候把一些資源記錄一下,該篇文章的作者 Samuel 是推薦 Lab 色彩系統的漸層。

目前因為 CSS 就是只有 RGB 漸層,所以要用其他系統的漸層就只能用模擬的,SASS 的話有 chromatic-sass,PostCSS 則是 postcss-easing-gradients,這套背後用的則是首篇文章也有介紹的 chroma.js 來轉換的,不過它其實主要是在做 easing gradient 的,然後還有一些線上的模擬工具可以讓人直接看看效果,第一個其實是 easing gradient 的工具,其實就是在 easing gradient 標準提案時有人做來讓人體驗的,另一個介面比較不 fancy,但是我覺得比較實用的 Lch and Lab colour and gradient picker


CSS 屬性排序

CSS Box

上週在弄 Stylelint 的設定,然後理所當然的處理到屬性排序的問題,以前我是用 CSScomb 的 zen 這組設定, 這組排序規則是從 Zen Coding 那邊來的,它的規則其實應該就是 Concentric CSS 的規則,concentric 是同心的的意思,同心圓的那個同心,這組規則的基本原則就是從外到內,從外部的定位、排版方式、到邊距、外框、內距、寬高等 box model 的屬性,然後才到內文的屬性,這組也是我第一次知道的有系統的 CSS 屬性排序規則,而且我覺得這個規則很直覺,而且也是很有邏輯的設計,所以就一直用到現在了。

最近這次要弄 Stylelint 設定時順便又研究了一下現在主流的排序規則,發現現在 lint tool 如果有內建排序規則的話,一定會有的其實是照字母順序排序,我自己是覺得在 CSS 領域照字母排序根本是 anti-pattern 啦,不過一直都有聽說有些大型企業的專案會用這種排序規則。事實上在 CSS-Tricks 的 Poll Results: How do you order your CSS properties? 的這篇文章,裡面就有 14% 的人是使用字母排序,當年還是 2012,2017 年 SitePoint 的調查結果 其實沒有很大改變,變成 13%,所以雖然我覺得 anti-pattern 但其實還是有一定比例的人真的這樣用,其實要說的話也還是有好處啦,因為其它的屬性排序規則都是有個基本原則,實際上要仔細排列所有屬性的時候都還是有可能會有些地方有灰色地帶。

最高比例的排序方法則是照屬性類型分類,照屬性分類其實算是比較籠統的說法,Concentric CSS 的排序規則也算是照屬性分類的,同樣符合這樣條件的另外還有 Nicolas 的 Idomatic CSS 和 Jonathan Snook 的 SMACSS 的排序規則,其實這兩種方法的大方向也和 Concentric 的很接近,雖然是分成幾個大類別,但是大類別的排序基本上一樣是從外到內的方向,然後根據 npm 的安裝數字,目前使用度最高的應該是 SMACSS 的排序建議了吧,細部的完整排序可以在 Stylelint plugin package 的 repo 那邊看到。而除了 Stylelint 之外,PostCSS 也有 plugin 叫 css-declaration-sorter 來幫你排,也內建了 Concentric CSSSMACSS,而且該 plugin 還號稱:

  • Up-to-date CSS properties fetched from the MDN Web Platform.
  • Thought-out sorting orders out of the box, approved by their authors.

感覺很不錯的樣子,它們的完整排序清單也可以在 repo 內找到:SMACSSConcentric CSS,不過排序這東西應該還是要在 lint 的時候做啊,PostCSS 的產出物通常是 production 環境用的 code 了,只是 stylelint 那邊用的排序清單和這邊的又不同,不過其實 StylelintVSCode 都可以拿 PostCSS 的來用的樣子,還沒測試過就是~

最後提兩個 Concentric CSS 灰色地帶的問題:

  1. Grid、 column、 flex、 float 這幾個屬性你會怎麼排呢~?
  2. 如果box-sizing: border-box;設下去後,width/height 要放在 padding 前面還後面呢?

SVG

SVG

最近網站的 icon 都盡量改用 SVG 向量檔了,網路上也有不少資源,像是 Material Icons,累積了一些心得可以記錄一下,其實早在 GitHub 開使用 icon font 之前,大部分的瀏覽器就都有支援 SVG 了,只不過當時的支援還不夠完備,會各自有一些問題,這應該也是 Github 當年不先用 SVG,而是用 icon font 先檔了幾年的主因,總之現在比較不用擔心這些問題了,所以 GitHub 又開槍轉用 SVG icon 了。

GitHub 的用法是 inline SVG 為主,我自己則是用<picture>比較多:

<picture>
  <source srcset="/path/to/icon.svg" type="image/svg+xml">
  <img src="/path/to/icon.png" alt="" width="32" height="32" />
</picture>

CSS Trick 有一篇文章 A Complete Guide to SVG Fallbacks 則介紹了各種在網頁內放入 SVG 圖檔的古老方法,用<picture>是比較新的,沒列在其中,而我是為了向下相容選擇用<picture>,因為支援<picture>標籤的瀏覽器都很新了,對於 SVG 的支援度很夠,剩下少數(大概都是 IE)就讓他直接吃 png 之類的(PS: pngquant 處理過的 PNG 很多可以直接在 IE6 上顯示透明色),這樣用起來就像是一般<img>一樣,SVG 檔可以先用 svgo 工具處理過,除此之外,我通常會先用 Adobe Illustrator 把不必要的圖層階層刪除,圖層在 SVG 原始碼裡面通常是會<g>標籤,另外也記得要把圖層名稱(id)改成 ASCII only 的名稱,甚至直接編輯原始碼刪除 id,因為 svgo 不會處理這部分。

用外部 SVG 檔案好處是可以善用 browser cache 減少傳輸量,尤其是大量重複使用的圖片,不過也會犧牲一些 SVG 的特性,例如會因此不能直接用 CSS 來調整樣式,做 transition,做動畫等等,所以有時候也是會用 inline SVG,就可以搭配 CSS、JS 弄出很多不錯的效果(Safari 偶爾還有遇到無解 bug),不過要讓 inline SVG fallback 回一般圖片就比較麻煩些了,在做這件事之前可以先看一下是不是有需要支援,以前是 iOS 不支援,現在還有機會碰到的大概還是 IE(6-8)吧,總之,如果有需要的話,就是參考 CSS Trick 的 SVG Fallbacks 這篇文章,使用

<svg width="96" height="96">
  <image xlink:href="svg.svg" src="svg.png" width="96" height="96" />
</svg>

這是個很有趣的技巧,首先<image>標籤是合法的 SVG 元素,所以放在 SVG 不成問題,但是不支援 SVG 的瀏覽器,照理說也不應該認得<image>標籤啊,其實,<image>在很久以前就一直是<img>的別名了,甚至在 WHATWG 的 parse HTML 文件的流程裡都還有提到處理的方式,而經過測試也都證實了以前的瀏覽器確實是這樣的行為,文裡也有各瀏覽器的行為和支援狀況,不支援 inline SVG 的就會顯示替代的 png 或 jpg 。

文章裡面還有提到背景圖使用 SVG 的處理方式,不過因為我沒這樣使用,所以沒什麼實做到,另外最近也有人用外部 SVG 加上 CSS filter 來改變 SVG icon 顏色,不過這太技巧太新了,考慮到支援度我也是沒有用過。

親和力問題的部分,一樣 CSS Tricks 有篇文章 Accessible SVG 在講相關的作法,像是用<title>加替代文字,加上role="img"等;如果要自己編寫 SVG,W3C 還有份 Authering Guide 可以參考,除了親和力相關的資訊外還有不少技巧可以參考。

最後整理一下,拿到 svg 檔案後我的處理過程:

  1. 用 Adobe Illustrator 先開來整理一下,修改圖層名稱、刪去合併不必要的圖層
  2. 輸出成 SVG 和 1x 解析度的 PNG
  3. SVG 檔用svgo最佳化、PNG 會用pngquantzopflipng處理過

使用方式則還是當成一般外部圖片為主,需要動畫效果才會用 inline SVG。


表單 Practice

Form Validation

這邊是我最近對於表單的一些作法,因為內化還不夠,每次都會漏掉一些,所以花了些時間整理整理,適合的情境不是 single page application 就是了,比較偏傳統形式網頁的表單,然後可能也包括不少大家早就知道(?)的細節就是了。

首先,我現在偏好不用 JavaScript 做表單檢查,而是先做好最基本的 server side 檢查,然後加上 HTML5 的表單檢查,會這樣決定的主因是:

  1. JavaScript 的表單檢查 library 用起來都不太順手,而且不想花時間處理串接,且能少點 library 總是好的;
  2. 幾個常用的 type,像是 email, url 比較不需要擔心檢查的 pattern 有不周全的地方,我想很多人都有上網搜尋過這些欄位的 regular expression pettern 的經驗;
  3. 支援度已經不是大問題了,事實上我的工作上還需要支援 IE 10, 9 之類的,其實這些非 modern browser 的量都已經非常少了,所以就靠個 server side 檢查對付他們就好,使用體驗稍微差一點也還可以接受,這也是種 graceful degradation(優雅降級);
  4. HTML5 的表單檢查可以說是 web developer 當年對抗網路標準發展遲緩一大勝利指標,當然應該要好好用一下。

而用 HTML5 表單檢查還有個意外的好處是基本的錯誤訊息自動有翻譯(看使用者瀏覽器的語言),另外如果有自製的輸入元件,也有 API 可以串接,當然訊息就要自己提供就是了。

用 HTML5 表單檢查當然也不是完全沒有問題,例如目前 email 欄位還沒有瀏覽器支援 IDN domain 的信箱;另外就是上傳檔案的 file input 的值不能從 server 端直接給,這限制是因為會有安全性問題,而這限制所衍生的問題是:表單送到 server side 檢查後發現有錯誤時(例如 captcha 錯),使用者就一定要重新選取上傳的檔案,對於使用者體驗算是個扣分(而且上傳檔案大的話很花時間,然後另外還有個上傳檔案大小限制、就又是另外一個議題了),要解決這問題一般來說就是靠 JavaScript 做些加強,例如針對 captcha 可以先用 ajax call 檢查 captcha ,正確的話就換個 session token 之類的回來,不過即使這樣,還是逃不了完整的 server side 表單檢查,所以也還要處理 ajax submit 後的表單錯誤訊息顯示。

不管是 server side 檢查後產生的錯誤訊息,還是 ajax call 之前檢查產生的錯誤訊息,理所當然都會放在欄位附近,不過還要讓訊息和欄位之間建立關聯,才好進一步做一些處理,例如使用者有更新欄位值之後會把錯誤訊息隱藏之類的,或許很多人會用父層 DOM 節點加上特殊的 class 包起來找,不過我比較偏好用 aria-describedby,大概會看起像是:

<input id="mail" name="mail" type="email" aria-describedby="mail-field-info" />
<span id="mail-field-info">Required field!</span>

這樣只要找的到#mail欄位,就可以透過他的aria-describedby屬性找到該欄位的相關訊息的 DOM 節點,另外值得注意的是,aria-describedby 值的格式是 IDRefList,不是單一個 ID,而是一個用空白切分的 ID 指標們,所以如果有這種情形,還可以在錯誤訊息的那個 DOM 節點加上 role="alert" 給它用來辨識,其實就算只有一個 ID 也還是可以加上 role 屬性啦。如果真的需要用透過父層 DOM 節點來找的話,之前研究的結論是可以在預期的父層標籤用role="section"來方便定位,用 jQuery 大概會像是:

$fieldSection = $field.closest('[role="section"]');

這個標籤下應該會包括欄位的標籤(label)、欄位的 input element 以及相關的資訊(說明、錯誤訊息)等。

另外還有一點,就是要用 ajax 上傳檔案的話,需要有支援 FormData 的瀏覽器,並且如果用 jQuery 送 FormData 的話記得要加點設定:

contentType: false,
processData: false

還有就是 ajax 送表單的目標 URL,我目前比較喜歡的作法是讀<form>action屬性,也就是和瀏覽器自己送的 URL 一樣,然後透過 HTTP content negotiation 機制來決定回傳的格式,比較正確的作法是看Accept,以 jQuery 來說,如果要 server 回 JSON 格式的話,可以加上:

dataType: 'json'

這樣送出的 request 就會帶上正確的Acceptheader,向 server 端要求application/json,不過Accept的值解析起來比較麻煩些,其實是可以送出說 client 端可以接受多種格式,然後還加上個優先度的,也因此也有很多人是看X-Requested-With,一般 library 如果是透過 XHR 發的 request 都會有這個 header;還有就是送出的資料格式(Content-Type),即使是 ajax call,我目前也都不用 JSON 了,還是用application/x-www-form-urlencoded為主,另外要上傳檔案的話當然一定要用multipart/form-data,主要是因為:

  1. 送 JSON 的話就不會是 simple request 了,有些時候會比較麻煩,例如 Cross Origin 時會需要發 preflight,然後就可能遇到 AWS 以前不支援 preflight request 的 bug;
  2. 用這幾個老的 Content-Type 支援度還是比較高,對於 server 端實做和 client 端實做其實都相對友善一點,例如 jQuery 預設就依然是 form-urlencoded,没特别需求還是用標準一點的格式,特殊需求是例如 GraphQL,不過一般表單發送應該不會走 GraphQL 吧。

其實 JSON 雖然已經有 RFC 規範了,不過在 Web 標準的世界還沒相當深入內化,不知道以後有沒有機會更加的內化整合進去。

前面有提到 ajax call 送出的目標 URL 我會偏好從,<form>裡面讀,不過或許有的情境會讓 ajax call 必須要自己用不一樣的 API URL,這時候我建議還是把 API URL 寫在<form>的屬性裡面,這樣可以讓 JavaScript 的邏輯比較乾淨,也不用作什麼 mapping 或是常數來儲存 API 的 URL,維護修改時也不用兩邊檢查,屬性名稱可以用例如:data-action之類的屬性,data-*屬性正好適合來做這些事情,不但有 DOM API 支援,jQuery 也可以用.data()method 來讀取,命名上,如果覺得有個標準參考比較好,可以看看 jQuery-ujs 的設計,雖然比較長一點,它用的是:data-ujs:submit-button-formaction,我是覺得有些不正確啦,畢竟要送出表單不一定是點擊 submit button。

其實假設送出表單的動作都是滑鼠點擊 submit button 這是個親和力問題,如果只把 ajax call 送的函式 bind 在 submit button 的 click 事件上,這其實是不太好的,因為其實瀏覽器預設的行為是可以在很多地方用鍵盤送出表單,例如在 text input 上按下 Enter 鍵,或是在 submit button 上按下空白鍵之類的,所以針對表單還是要去 bind form submit 事件才是正解,至於 jQuery-ujs,其實也是這樣做的,它是用 delegate event 的形式去監聽傳遞到 document 上的 submit 事件,然後才去做後續的處理,只是命名上讓人覺得不太正確。

最後一項,前面說不用 JavaScript 做表單檢查(不看自訂輸入元件的話),其實有一個例外,就是上傳檔案的大小檢查,因為沒做對使用體驗的影響比較大,然後就是要還要記得針對 ajax call 送表單加上 HTTP 413 Status Code 的錯誤訊息處理。


此類別所有文章