沒穿方服

索引

顯示╱隱藏內文
顯示具有 JavaScript 標籤的文章。 顯示所有文章
顯示具有 JavaScript 標籤的文章。 顯示所有文章

設計

類似 term.ptt.cc 滑鼠移到圖片連結時「直接顯示圖片」的功能。
當場讀取圖片,浮動顯示,位置隨滑鼠游標移動。

備料

在 React 專案,採用 Floating UIFloatingPortal 作為圖片容器,用一個 atom 記錄目前要預覽的 URL,然後在 <a> 的 mouse 事件操作它們。

根據所在連結的 URL 判斷是否為圖片,是的話就載入,onLoad 時設定維度,讓 Floating UI 自己判斷位置。

打磨

  • 問題:圖片在畫面邊緣會跳動

    圖片切到畫面邊緣時,Floating UI 會嘗試重新定位,造成跳動。

    更糟的是,因為 mouseover 會更新滑鼠位置,而且「載入中」的圖片尺寸、載入後的圖片尺寸調整都會變更最終維度,以致跳動可能循環,看起來就是不斷抖動。

    不好解決,最後用騙的,把容器的 pointer-events 關掉避免重複滑鼠事件,再用 CSS 動畫讓圖片先保持透明,100 ms 後才看得見,這時閃爍多半也結束了。

    參考 commit 817b816

  • 問題:手機上沒有 mouse 事件

    手機上無法用 mouseover/mouseout 事件來偵測到需要預覽,會變成 touch 的時候啟動預覽。

    雖然仍有基本可用性,但無法用 mouseout 關閉預覽,且再度 touch 時,到底會再度預覽,還是變成 touch 連結而開啟,行為差異很細微,使用者難以預期。

    解決方式是另外處理 touch 事件(onTouchStart, onTouchEnd, onTouchCancel),自己處理預覽行為,唯有當 touch 按住超過一定時間後,才交還瀏覽器內建的處理器(打開連結)。

    參考 commit dbf3fe43d56fce

成果

這個功能做在 Feeders 事實選集和地圖留言中,原始碼都在 GitHub

這篇不是什麼新知,只是我第一次處理,覺得比看起來容易,希望改善「看起來比較難」情況。

村里邊界,右下文字部分要自己處理 mouse 事件

整個處理大致都被 GeoJSON 包裝好了

原本沒碰過 GeoJSON,自己想像要處理區塊框線、互動,以為要折磨很多事;但其實在 Leaflet 裡可以看成載入一個 GeoJSON 資源,再用 addTo(map) 掛上去就結束了。

建立與顯示:

const layer = Leaflet.geoJSON(data as GeoJsonObject, options);
layer.addTo(map);

外觀、互動行為等,可以用 options 去調,以下是一個 options 物件:

{
  style: () => ({
    fill: false,
  }),
  onEachFeature: function (feat: Feature, layer: Path) {
    layer.on('mouseover', function () {
      layer.setStyle({ fill: true });
    });
    layer.on('mouseout', function () {
      layer.setStyle({ fill: false });
    });
  },
}

要關閉圖層,就 remove 掉:

layer.removeFrom(map);


取得地圖資料

另一個問題是圖資怎麼來,首選當然是內政部的開放資料:
縣市界:鄉鎮市區界線(TWD97經緯度)、村(里)界:村里界圖(TWD97經緯度)

但要怎麼轉成 GeoJSON 格式?尋找現成整理好的,似乎都失連,或看起來有點年代,幸好裡面原理沒變,參考 jason2506/Taiwan.TopoJSON,它是用 mapshaper 將 .shp 轉為 .geojson。

具體指令例,縣市:

mapshaper raw/COUNTY_MOI_1090820.shp -simplify interval=400 \
  -filter 'COUNTYNAME !== "金門縣" && COUNTYNAME !== "連江縣"' \
  -rename-fields name=COUNTYNAME -filter-fields name \
  -o format=geojson precision=0.0001 \
  counties.geojson

村里:

mapshaper raw/VILLAGE_NLSC_1130807.shp -simplify interval=30 \
  -rename-fields id=VILLCODE \
  -verbose \
  -filter 'COUNTYNAME !== "金門縣" && COUNTYNAME !== "連江縣"' \
  -each 'name=TOWNNAME + " " + VILLNAME' -filter-fields id,name \
  -o format=geojson precision=0.0001 \
  villages.geojson

其中 -simplify interval=precision 參數可以控制產出的精細度,也決定了檔案大小。 上面範例出來的結果是 61.35 KB 和 7.21 MB,後者檔案很大,但其實掛在 Cloudflare R2,在 HTTP/2 和壓縮加持下傳輸量只有 1.7 MB,使用上是沒什麼問題的。

從 react-masonry-css 示例

註 1:這篇談的是 2020 前的 CSS 解法 (flexbox),且使用 react-masonry-css 這個老套件,並非新的 JS 解或還未定案的 CSS 標準(參考),目前(2024)看來,舊方法還是有適用的地方。

註 2:能調整的前提是每個元素的高度已知。

以 CSS 為主的解法,看中的是單純性和效能,避免頻繁計算每個元素的實際尺寸,而缺點如下:

  • 排序問題,因為以多個 column「直行」為容器,所以元素其實是由上排到下,而不是由左到右。
    這點在 react-masonry-css 是用 JS 硬把元素分配給各個 column 解決。
  • 高度不均。因為根本沒有考慮元素的高度,當然避免不了有的 column 特別長或短的問題。
    具體可以看 issue #73 中的附圖:very different columns height - paulcollett/react-masonry-css

針對高度不均,造成底部有很大落差不齊的情況,折衷辦法是預先給予每個元素「高度」的參考,類似給每個 <img> 賦予已知的 height 值;然後再用 JS 計算累積高度,最後搬動特別突出的元素,直到無法再用搬動方式改善為止。

實作也很單純,主要價值還是知道多做這一小步,就能得到不少改善。

程式碼參考我的 fork: bootleq/react-masonry-css at forker,具體在 balanceColumnsmoveOddItem 兩個函數:
react-masonry-css/src/react-masonry-css.js at 4866d3596#L72

處理了一個以日文 IME(半型假名模式)輸入數字時,按下 Enter,值就變成 2 倍的狀況。

組字中,輸入 123 組字完成,值變成 123123

這個表單用了一個自訂的 angular $formatter,每當輸入值變動時,便即時把非數字的字元移掉(使用 $element.val(newValue))。

先不論在 $formatter 中變更元素的值是否恰當,這個問題最後被釐清為一個情境,就是在 compositionend(IME 組字完成)事件中,如果以 JavaScript 修改 <input> 的 value,會發生什麼事呢?

經測試,Firefox 在「IME 送出字串」和「JS 設定的值」相同時,input 可以接受該值;反之不相同時,input 的值會被取消(變成空的)。

而這次的 bug 只會在 Google Chrome 上發生,input 內容會是「JS 設定的值」,後面再接著「IME 送出的最後一組字串」,合起來就是前面圖中的 123123。

測試用 jsfiddle:

Workaround

藉由 setTimeout 讓設值在 compositionend 之後再執行。

寫個 script 在 shell 透過 rhino.jar 跑 fulljslint.js,在 Vim 也寫個 function 呼叫它,於是可以用 quickfix window 除錯。

就像有個超級不吝指教的真人陪你寫程式

  • 在 Windows 上沒用。
  • 不使用 jslint.vim。 雖然跨系統、放 jslintrc 設定檔等作法很棒,但是 1) 即時檢查和 highlight 標錯的顯示方式都不是我要的,甚至有點惱人。 2) 想試試直接用大師寫的 fulljslint.js,在 Vim 以外的地方可能也較好整合。
  • 不使用 javaScriptLint.vim,因為 JavaScript Lint 不是 JSLint 啊,跟這主題無關。
  • 使用 Rhino 當 JavaScript engine,因為最好裝。
    不過它的 JavaScript 版本只實作到 1.7(Firefox 2 的程度)所以像 JSON 就不能直接吃,要多 include 個 JSON2.js
  • 具體用到的檔案:
    • ~/bin/jslint (唯一實際被叫的指令,內容主要是 java -cp rhino.jar …)
    • ~/scripts/fulljslint.js (即 Douglas Crockford 的 fulljslint.js)
    • ~/scripts/jslint.js (由官網的 rhino.js(現已移除)修改而來,連接 fulljslint 和 rhino)
    • ~/scripts/json2.js
    • ~/scripts/rhino.jar
  • 修改錯誤訊息的格式,改成跟 closure compiler 相似,就不必再調 Vim 的 errorformat。
  • 目前 JSLint 用的預設 option 直接寫在呼叫 fulljslint.js 用的 script(jslint.js)裡。
  • 在 shell 中呼叫的指令是 jslint file.js '{"extraOptionKey":value}'。 extraOption 可以用來蓋掉預設選項。
  • 在 Vim 中呼叫的指令是 :JSLint

取得檔案

註:僅測試於 Cygwin console Vim + Windows 版的 java,其他環境只測過 Win32 gVim ——失敗。

通用的解決方案請參考 othree 寫的 Vim 儲存完 JavaScript 檔案後自動用 yuicompressor
本篇特徵為:

  • 只使用 google closure-compiler
  • 採用 :make 執行,故能以 Quick Fix 視窗除錯。

    warning_level 設 DEFAULT 的話,壓 MooTools 居然不過……

  • 壓縮前未存檔、實行壓縮前、……幾個場合能先詢問確認。

    不存檔是壓什麼意思的? 若不詢問,每次 :w 就自動跑也是很費時的

  • 壓縮結果可選擇存到改名的檔案(傳回檔名)或存到暫存檔(傳回壓縮後的文字)。
    自動命名規則為 foo.js → foo.min.js 或 foo-debug.js → foo.js。

使用方式

  1. 把本文原始碼加進 .vimrc。 gist: 545665 - [.vimrc] function to use closure-compiler
  2. 下載 closure-compiler 將 jar 放到程式找得到的地方(如程式第一行 let jar = '/scripts/google-compiler-20100616.jar'
  3. 幾種用法:
    • function:call JsCompress(save [, interact [, options ]])
      save: 若為 1 則自動命名存檔,0 則存到暫存檔。
      interact: 若為 1,執行 make 前會詢問確認。
      options: compiler 用的參數字串,例如 '--compilation_level=WHITESPACE_ONLY'。這裡留意若不指定的話, warning_level 用的並非預設值而是 QUITE,因為 warnning 就會有 quickfix 而中斷壓縮。
      :call JsCompress(1, 0, '--warning_level=DEFAULT')
    • command:JsCompress[!] [interact] [options]
      同上但 save 參數被 ! (bang) 取代。
      :JsCompress! 1 '--compilation_level=WHITESPACE_ONLY'
    • autocmd:在 .vimrc 加上 au FileWritePost,BufWritePost *-debug.js :JsCompress! 1
      以後編完 foo-debug.js 存檔,便會詢問是否壓縮。

已知問題

中文檔名在 make 出錯時,quickfix 和產生的新檔是亂碼。

懷疑是 Windows java 的問題所以先不處理

SyntaxHighlighter(目前版本 1.5.1)在幫 pre 標籤變色時,需要 name="code" 這樣的標記才能生效,很不幸 pre 的 name 屬性不在 XHTML 1.0 規範內;所以如果你很喜歡 valid 的貼紙,還是參考 kev.in » Using dp.SyntaxHighlighter with Valid XHTML 自行改造 shCore.js 吧,改三行程式即可。

先確認改完後的用法:假設以 dp.SyntaxHighlighter.HighlightAll('code'); 啟動 SyntaxHighlighter,則網頁的 HTML 可以是 <pre class="code html">,或如原文所述用 <code> 標籤的進階語法。至於舊的 <pre name="code" class="html"> 寫法不必全面更新沒關係,仍然可以相容(當然仍不符標準)。


詳細步驟原文寫得很清楚,這邊只提出最後 regex 的部分:

options = options.replace(new RegExp("\\s*"+ name +"\\s*"),'');

原文中 RegExp("^"+name+"\\s") 僅符合 class="code html" 其中 code 出現在開頭的情形;改為以上 pattern 的話則會符合 class="html code  ",應該有稍微通用一點才是。

註:我自己是用 <pre class="javascript sh-code"> 這種型式。

Valid XHTML 1.0 Strict

Lightbox 2 三月放了新版本 v2.04,算是令人振奮的消息,因為上個版本我看不懂他的原始碼(這算啥理由,況且現在也沒看懂),想不到這改版一等就是 10 個月。

升級過程中我也驚覺之前 以 JavaScript 啟動 Lightbox 2 文章問題挺大的……順便修理一下。

v2.04 Changelog

  • NEW - Prototype 版本由 v1.4 升級至 v1.6.0.2。
  • NEW - 新增 labelImagelabelOf 設定項目,用來改「Image 1 of 4」部分的文字。
  • UPDATE - 程式碼大幅改寫。考慮全域命名空間和原生的 javascript 物件。(反正有變聰明就對了)
  • FIXED - 展示有 caption(說明文字)的圖片後,再展示無 caption 的圖片,caption 會顯示 "null" 而非沿用之前的文字。
  • FIXED - 按下 close 按鈕後位置不會因 focus 的框線而跑掉。

再補幾個:

  • 檔案大小由 24 KB 減至 19 KB。
  • 現在不會建立 myLightbox 這個物件了,之前有靠這個亂改的也會出狀況。
  • 不只滑鼠左鍵,連中鍵、右鍵都會啟動 Lightbox。

使用上跟舊版相容,若之前就按官網的 How to Use 安裝,直接升級也不會有問題。


接下來負責的作者要修正之前寫的 以 JavaScript 啟動 Lightbox 2 (更新版) 系列文章。

以 JavaScript 啟動 Lightbox 2(又改版)

先貼上發表文章用的 HTML

<a href="http://原圖.png">  
  <img src="http://縮圖.png" alt="替代文字" />  
</a>

之前最大問題是可能在 a 裡面又 insert 一個 a 元素,現在把整段 script 改成:(使用 Lightbox 2.04Prototype 1.6.0.2

$$('.postcontent img').each( function(i) {
  var fullSrc = i.src;              // 原圖的位址(預設跟縮圖位址相同)
  if( i.up('a') && i.up('a').href ) // img 外層有 a 的話,其 href 值可能就是原圖位址
  {
    var srcExt = i.up('a').href.split('.').pop().toLowerCase();
    if( srcExt == 'png' || srcExt == 'jpg' || srcExt == 'gif' || srcExt == 'bmp' )
    { fullSrc = i.up('a').href; }
  }
  if( i.up('a') ) {  // 把 Lightbox 需要的屬性/值寫到 a 上
    i.up('a').writeAttribute( { rel: 'lightbox[post]', href: fullSrc, title: i.alt } );
  }
  else {             // 外層沒有 a 就做一個包上去
    i.wrap('a', { rel: 'lightbox[post]', href: fullSrc, title: i.alt });
  }
});
Lightbox.prototype.updateImageList();   // updateImageList 會再次解析圖片,然後把 Lightbox 的功能綁上去

然後我還動了 lightbox.js 幾根寒毛:

  1. 只讓左鍵啟動 Lightbox
    updateImageList: function() {   
      this.updateImageList = Prototype.emptyFunction;
    
      document.observe('click', (function(event){
        if( !Event.isLeftClick(event) ) {return;} // 插入這一行
        var target = event.findElement('a[rel^=lightbox]') || event.findElement('area[rel^=lightbox]');
        if (target) {
          event.stop();
          this.start(target);
          }
      }).bind(this));
    },
  2. 把 Lightbox 內 next, prev, close 鏈結的 href='#' 全拿掉

    因為覺得 url# 出現在狀態列很礙眼,所以在原始碼的 initialize 部分把 Builder.node('a',{id:'nextLink', '#'}) 這類鏈結的 , '#' 統統刪除。
    不過滑鼠移上去的游標變成箭頭了,所以又去 lightbox.css 加上一行 #lightbox a {cursor:pointer;}


測試一下連 Picasa 的圖效果如何

第一張是自己從 Picasa 複製圖片網址,第二張是在 blogger 上傳圖片時自動插入的網址。參考原始碼:

<img alt="複製圖片網址 於 Picasa" src="http://lh3.ggpht.com/bootleq/R2aEsSUz5LI/AAAAAAAAAKE/RPzxhtdHhxE/lightbox2_exh1.jpg" width="200" />
 
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYleJpEOzCj907jJ25pYEMODGOoMEO8Ex33Nrl03DvxXWKf3R_Ibfpo7uITtju2hRsk_JQpZENS2auJdiQL9V3D7-o1k-KfheOW5MqHhrd-S_eGpjNrJpLEYOK-r5YZRgIB_ZnpwIr_Cg/s1600-h/sansao.png">
  <img id="BLOGGER_PHOTO_ID_5221300718735749538" alt="山臊行樂圖 - 圖片網址由 blogger 產生" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiYleJpEOzCj907jJ25pYEMODGOoMEO8Ex33Nrl03DvxXWKf3R_Ibfpo7uITtju2hRsk_JQpZENS2auJdiQL9V3D7-o1k-KfheOW5MqHhrd-S_eGpjNrJpLEYOK-r5YZRgIB_ZnpwIr_Cg/s320/sansao.png" />
</a>

複製圖片網址 於 Picasa

山臊行樂圖 - 圖片網址由 blogger 產生

好樣的,blogger 插的還是不行。換網址重貼。

山臊行樂圖

簡單的音效功能,應該沒人想自己寫吧。
先看幾個現存可用的:

  • Audio Engine

    就是 Scriptaculous 的 sound.js,單純地產生 <embed> 來發聲,在 ie 使用 bgsound,其他瀏覽器則仰賴 QuickTime 支援。可做 track 控制及全域 enable/disable 的切換;再移植版 jQuery 的 jquery.sound.js 還可設定 timeout 將產生的 <embed> 移除。

    $.sound.play( '/path/to/some.wav' , {track: "track1";} );
  • JSSoundKit (Javascript Sound Kit)

    透過 ActionScript 的 ExternalInterface,寫出一個 proxyMethods 方法來操作 Flash Sound 物件。功能自然也跟 ActionScript 2.0 的 Sound 類別一樣。

    var mysound = new Sound();
    mysound.loadSound('/path/to/some.wav' , true);
  • SoundManager2

    同樣依靠 flash,但它是再設計封裝過的、更完整的 API,也比 JSSoundKit 穩定可靠。壓縮後約 18 kb 說龐大難養也不至於。

    soundManager.play('mySound','/path/to/some.mp3');


※以上僅介紹我試用過的,當然有其他選擇,例如 MooTools 用的 mooSound


最後我懶得探索,於是自己寫了 jQuery plugin:flashSound,測試一下還算堪用,不過它只有最基本的功能(包括停用音效),可以發出登登登的聲音,但是要做歌曲播放器的話,很勉強喔。

詳細使用方面,接受兩種風格的寫法,其之一:

$.flashSound( 'foo.mp3', {id: 'se1'} );     // 讀取聲音檔(還不會播放),並將物件命名為 se1
$.flashSound.play('se1');                   // 播放
$.flashSound.play('se1', true);             // 播放,但先停止前一個播放的聲音
$.flashSound.stop('se1');                   // 停止播放
$.flashSound.remove('se1');                 // 移除 flash 物件

或者

var se1 = $.flashSound( 'foo.mp3' );   // 讀取聲音檔
se1.play();                            // 播放
se1.play(true);                        // 播放,但先停止前一個播放的聲音
se1.stop();                            // 停止播放
se1.remove();                          // 移除 flash 物件
// 請留意若在 se1 建立前呼叫 play, stop,會發生「物件沒有該方法」的錯誤。

另外全域啟用╱停用音效的方法是:

$.flashSound.enable();        // 全面啟用 flashSound
$.flashSound.disable();       // 全面停用 flashSound


下載、測試請至 flashSound 首頁參觀。

犯了不易察覺的錯誤:

使用 innerHTML 動態產生 flash 物件(object / embed)然後插入頁面,影片裡用 ExternalInterface.addCallback(ActionScript 3.0)公開給 JavaScript 的函數,在 ie 6 上會找不到。

問題點在以下片段:

var obj = document.createElement('div');
obj.innerHTML = '<object (略…)><embed (略…) /></object>';
document.body.appendChild(obj);

想重現的話,我寫了簡單的原始檔放在這裡(DivShare),別忘了要用 ie 開才能得到最佳瀏覽效果。

解決辦法是把 innerHTML 那行寫在最後,變成:

var obj = document.createElement('div');
document.body.appendChild(obj);
obj.innerHTML = '<object (略…)><embed (略…) /></object>';

結論,在 DOM 元素實際被加進頁面之前,ExternalInterface 不是不會生效(前面沒提到,ExternalInterface.call 會在 appendChild 之前就 call 了……),但是 ExternalInterface.addCallback 會碰到未知的問題。