2016年8月14日 星期日

使用FileReader讀取file資料 - javascript

FileReader是HTML5的新Javascript物件,可以用來讀取input type="file"的file資料(其實就是FileList對像),其實不只input type="file"的FileList對像,還可以讀取許多不同的資料來原,例如Blob對像、拖拉產生的DataTransfer對像等。

今天要來實作一個簡單的圖片預覽器,須求如下:

  1. 選擇檔案後,如果是可顯示的圖片檔,會在下面的"圖片預覽"顯示出圖片。
  2. 如果沒有選擇檔案(例如取消了檔案選取),則"圖片預覽"的圖片會被清掉。
  3. 為了簡單起見,這裡不檢查file來源是不是圖片檔。


程式實作如下:
說明:

  1. FileReader.readAsDataURL(source)可以輸入file source並得到一串當下可用的URL,
    型式為 dtat:..........,存在FileReader.result中
    還有其他的read函式可用,例如:
    readAsArrayBuffer()、readAsBinaryString()、readAsText()
  2. FileReader在讀取完後會觸發load事件,還有其他的事件可用,例如:
    onabort:當讀取操作被中止時調用.
    onerror:當讀取操作發生錯誤時調用.
    onload:當讀取操作成功完成時調用.
    onloadend:當讀取操作完成時調用,不管是成功還是失敗.該處理程序在onload或者onerror之后调用.
    onloadstart:當讀取操作將要開始之前調用.
    onprogress:在讀取數據過程中周期性調用.
  3. FileReader:
    FileReader.readyState代表資料的讀取狀態,總共有三個值:
    FileReader.EMPTY = 0:還沒有加載任何數據.
    FileReader.LOADING = 1:數據正在被加載.
    FileReader.DONE = 2:已完成全部的讀取請求.

源碼(怕JsFiddle有問題):
html:

圖片預覽:
javascript:
var fileUploader = document.getElementById("fileUploader");
var imageView = document.getElementById("imageView");
//用來讀取file資料的FileReader
var fileReader = new FileReader();

//監控#fileUploader的值變化
fileUploader.addEventListener("change", function(event) {
  if (this.files.length > 0) {
   //有選取file時,使用fileReader讀取file資料
    //readAsDataURL可以將讀取到的file資料轉成
  //data:......的URL型式,在讀取完後觸發load
  //事件,URL存在FileReader.result中
    fileReader.readAsDataURL(this.files[0]);
  }else{
   //沒有選取file時,例如選擇取消,
    //將的src設成""
   imageView.src = "";
  }
}, false);

//fileReader讀取完file資料後會觸發load事件
fileReader.addEventListener("load", function(event) {
 //讀取後設定的src
  imageView.src = this.result;
}, false);

--------------------------------------------------------------------------------------------------
另做了一個讀取 video 檔案長度 (秒數) 的範例,程式碼基本上差不多,只是改成把readAsDataURL() 得到的 src 設定在 <video> 上:
HTML :
<div>Choose a browser supported video file to load video duration.</div>
<input id="fileUploader" type="file" />
<div>Video length : <span id="videoLengthDisplayer"></span> (s)</div>

JS :
var fileUploader = document.getElementById("fileUploader");
var videoLengthDisplayer = document.getElementById("videoLengthDisplayer");
//用來讀取file資料的FileReader
var fileReader = new FileReader();

//監控#fileUploader的值變化
fileUploader.addEventListener("change", function(event) {
  if (this.files.length > 0) {
   //有選取file時,使用fileReader讀取file資料
    //readAsDataURL可以將讀取到的file資料轉成
  //data:......的URL型式
    fileReader.readAsDataURL(this.files[0]);
  }else{
   
  }
}, false);

fileReader.addEventListener("load", function(event) {
 var video = document.createElement('video');
  video.src = this.result;
  video.load();
  video.addEventListener("loadeddata", function(){
   videoLengthDisplayer.innerHTML = video.duration;
  });
  
}, false);

參考資料:
  1. FileReader

2016年8月13日 星期六

自製檢查file size的directive (input type="file") - AngularJs

AngularJS的表單、欄位驗證(Validation)非常好用,但有時會碰到想要自訂驗證方式、
或是某個欄位AngularJS並沒有實作Validation時(例如input type="file"),
就需要自訂有客製化驗證能力的directive

在這邊的需求如下:

  1. 製作一個directive,名稱為file-validator
  2. 配合input type="file"使用
  3. 可以設定file size(大小,單位Byte)和file type(副檔名), 用法範例:
  4. 如果fileSize沒指定或小於等於0,則不對fileSize做限制驗證。
  5. 如果fileType沒指定或為空字串,則不對fileType做限制驗證。
完成的程式碼成品如下:


說明:

  1. 當我們在ng-form裡面的input設置name及ng-model (及ngModelController)後,如果AngularJS有實作此種input type類型的話,Angular會在View中的值或對應model的
    值改變時,進行View及model的雙向挷定、同時變更。
    ngModelController裡面會存放model的值,即ngModel.$modelValue。
    也會存放view(通常為input type中顯示的值),即ngModel.$viewValue。
    並且也會管理此input的valid狀態。

    但因為AngularJS並沒有對input type="file"進行實作,也就是,不管User在input中選了什麼檔案,皆不會存值到ngModel.$modelValue及ngModel.$viewValue中,當然雙向挷定的model中也不會有值。

    所以為們要帶自制的directive中,指定
    required : ngModel
    來得到管理這個input的ngModel,並且手動的設定model的值。
    (!!不要手動用ngModel.$setViewValue()設定viewValue的值,會導致model value 會被蓋掉)
  2. 因為要設定model的值,所以我們要引入$parse,$parse(code)可以代入一串程式碼,有點
    像eval,$parse(code)會返回一個函式,並且此函式有一個函式,assign(scope, value),
    可以在scope中執行程式碼,並將value賦值給程式碼執行得到的變數。
    例如:
    $parse("myCtrl.fileUpload").assign(scope, file);
    代表在scope中執行 :  myCtrl.fileUpload = file;
  3. 在ngModel中,可以使用
    ngModel.$setValidity( validType, isValid)
    來設定此input的validType是valid還是invalid。
  4. 在解析如以下字串時,
    file-validator="fileType:'png'; fileSize:333;"
    我們使用了兩個正規表達式:
    fileSize\s*:\s*(\d+)\s;

    fileType\s*:\s*(["'])(\w+)\1\s*;
    其中第二個正規達式中的 \1 的意思是跟第一個Group相匹配,而第一個Group就是(["']),
    也就是fileType:"png"; 或 fileType: 'png';可以匹配,但
                fileType:'png";  或 fileType:"png'; 不可以匹配,
    雙引號及單引號要成對使用。

原碼紀錄(怕jsFiddle出問題):
使用angular.js
html:

請選擇size小於3MB的PNG檔案


file valid: {{myForm.fileUpload.$valid}}
file size (Byte): {{myCtrl.fileUpload.size}}
副檔名錯誤
檔案過大


javascript:
 var myApp = angular.module("myApp", []);

 myApp.controller("myController", [function() {
   //
 }]);
 //自製的file validator
 myApp.directive('fileValidator', ["$parse", function($parse) {
   return {
     restrict: 'A',
     scope: true,
     require: 'ngModel', //代表管理input type="file"這個input的ngModelController,例如此例
     //ngModel.name = "fileUpload",
     //可以連結ng-model裡指定的變數值(ngModel.modelValue) 和
     //input view欄位中顯示的值(ngModel.viewValue)                          
     link: function(scope, elm, attrs, ngModel) {
       var expression = attrs.fileValidator;

       var fileSizeReg = /fileSize:\s*(\d+)\s*;/;
       var fileTypeReg = /fileType\s*:\s*(["'])(\w+)\1\s*;/;

       //規定file的size,單位Byte
       var fileSizeLimit = fileSizeReg.exec(expression)[1] || 0;
       //規定副檔名
       var fileTypeLimit = fileTypeReg.exec(expression)[2] || "";

       elm.bind('change', function() { //發現input type file值改變時
         scope.$apply(function() {
           var file = elm[0].files[0]; //取得file資料
           var fileType = /.+\.(.+)/.exec(file.name)[1];

           //$parse("程式碼")可以返回一個function,之後可以用這個function的
           //assign(scope, value)來在scope中進行賦值(value)動作,
           //例如: 在scope中墸行: 程式碼 = value (有點像eval())
           $parse(attrs.ngModel).assign(scope, file);

           //檢查file副檔名及size
           //用ngModel設置fileSize的valid
           if (fileTypeLimit === "" || fileType === fileTypeLimit) {
             ngModel.$setValidity("fileType", true);
           } else {
             ngModel.$setValidity("fileType", false);
           }
           //用ngModel設置fileSize的invalid
           if (fileSizeLimit <= 0 || file.size < fileSizeLimit) {
             ngModel.$setValidity("fileSize", true);
           } else {
             ngModel.$setValidity("fileSize", false);
           }
         });
       });
     }
   };
 }]);

2016年7月30日 星期六

使用Google Play Developer API讀取Google Play Store上APP的資訊 - Java

Google Play Developer API是Google提供的API,基本上用各種url以http來溝通,
使用它可以讓客戶端存取Play Store上APP的各種設定,就像是在Google Play Store Console上一樣。 (當然了客戶端必須被授權)

在這邊我們使用Java來讀取Google Play Store上APP的版本資訊,
使用 Google Play Developer API samples裡的ListApks.java範例來示範。

主要分成兩個部份:

下面開始說明一步步所需要做的步驟:
  • APP管理者:
    • Google Developers Console
      1. APP管理者指的是有APP在Play Store上上架的人,首先要進到Google Developers Console裡開一個新專案project(或使用舊專案)。
      2. 到「資料庫」搜尋並啟用「Google Play Android Developer API」
      3. 到「憑證」建立一個「服務帳戶金錀」
      4. 在「服務帳戶」選擇「新增服務帳戶」建立一個服務帳戶(Service Account),打入「服務帳戶名稱」後在下面就會得到一個「服務帳戶 ID」,這個之後會用到。
        「金鑰類型」選擇「P12」。

        按下建立後,就會得到一個副檔名為p12的檔案,這個之後也會用到。
    • Google Play Developer Console
      1. 接下來在Google Play Developer Console,到設定頁面(一個齒輪的圖案),選「API存取權」,選擇一個專案進行連結(就是在Google Developer Console設定的那個專案project)。
      2. 接著到下面的「服務帳戶」,選擇剛剛在Google Developer Console得到的「服務帳戶 ID」,點「賦予存取權」進入設定權限畫面 。
      3. 在設定畫面,選擇「新增應用程式 」,選擇要存取資訊的APP。
  • 服務請求者:
    •  Google Play Developer API samples
      1. Client Libraries and Code Samples中,可以下載Java的「androidpublisher_v2_public.zip」,裡面有需要Import進Java Project的Client Library,並且可以參考Code Sample如何使用Client Library。
      2. 這裡以ListApks.java為例,AndroidPublisherHelper.java是用來幫助我們使用Google Android Play API的輔助類,進去源碼可以看到有定義一個
        String, SRC_RESOURCES_KEY_P12 = "src/resources/key.p12" , 這就是我們要放置剛剛我們得到的副檔名為p12的地方。
      3. 在resources資料夾放好p12檔後,我們要到ApplicationConfig.java中設置一些東西。在ApplicationConfig.java中,我們可以看到以下設置:
        • String APPLICATION_NAME  
          • (應用程式名字,不是很重要,照這個"MyCompany-Application/1.0"格式就行)
        • String PACKAGE_NAME
          • (APP套件名稱,忘記了可以在Google Play Developer Console選APK按任一個Version的APK詳細資訊查到)
        • SERVICE_ACCOUNT_EMAIL
          • (這就是我們剛剛看到的服務帳戶 ID)
        • APK_FILE_PATH
          • (可以用在例如Upload APK時,這邊沒用到先忽略)
      4. 最後,我們執行ListApks.java,就可以看到在主控台中輸出如以下的訊息了:
      5. 檔案結構就像是這樣:
參考資料:

  1. Client Libraries and Code Samples
  2. Google Play Developer API samples
  3. Google Play Developer API
  4. Google Play Developer API V2 Rev32 1.22.0 (有Marven、Gradle等方式)
  5. Google Play Android Developer API return 401
  6. Why getting error 'The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console.

2016年6月22日 星期三

D3.js + AngularJS - 製作長條圖及圓餅圖動畫

今天要來結合好用的圖表工具D3.js和AngularJS來製作有動畫的長條圖和圓餅圖

範例的程式如下,相關的解釋都在寫在程式的註解中:
這裡對做點說明:

  1. 使用AngularJS的客製化directive來製做長條圖及圓餅圖,送入directive的資料跟<input>綁定。
  2. 使用svg來進行長條圖及圓餅圖的繪製。
  3. D3.js可用圖表幫助function將data包成datum,datum會包含原來的data(為傳reference的方式),並且擁有一些幫助繪圖的屬性,例如圓餅圖會用到的startAngle、endAngle。
  4. D3.js的data是使用如傳reference的方式,所以此例當AngularJS改變data時各element的data會跟著改變;但datum不是傳reference的方式,各element有自己的datum,改變parent的datum時記得手動改變下層element的datum。
  5. 在D3.js裡,select()、append會將parent的data和datum帶到下一個element。
    但selectAll()不會,要手動對下一個element呼叫data()或datum()。
  6. select()只會為呼叫者group的每一個element返回搜尋到的第一個element;而selectAll()會每呼叫者group的每一個element返回一個陣列,包含所有找到的element,其中each(function(d,i){//......});中的i為陣列的index。
  7. data()可送入一個陣列,陣列中的每一個元素會分給呼叫data()的每一個element。
    也可送入一個function,例如data(function(d,i){//....;  return [d];},在這裡d為parent的data或datum,return值為送入data()的參數。
參考資料:

  1. SVG D3.js - transition ( tween、interpolate )
  2. SVG D3.js - 直條圖 ( Bar Chart )
  3. SVG D3.js - transition 基本篇
  4. D3 on AngularJS

2016年6月8日 星期三

AngularJS - 自製跟ng-repeat一樣能力的directive (my-repeat)

在這篇文中,我們要來製作一個跟AngularJS的ng-repeat有幾乎相同能力的自製directive(當然ng-repeat更完整,有更多的可用變數、接受表達示等)。

我們先定義出自製directive要擁有的能力,在這裡我們取名我們的directive叫作my-repeat:
  1. 使用型式如同ng-repeat,可用attribute的型式放在html element tag的attribute中使用。
  2. 其中,屬性裡面可以用 "XX in YYY" 這種型式的表達示來表示要對哪個array型式(YYY)的內部資料(XX)做迴圈。
  3. 在directive中,可以解析YYY中的XX來顯示資料,也可解析其他資料,例如controller中的變數。
  4. 能夠使用 "-start" 和 "-end" 的multiElement的用法。
  5. 此directive要可以嵌套到另一個directive中(其他的directive或自己)。

下面就是實現出的程式碼,可以在Result看到,我們的my-repeat擁有和ng-repeat相同的上述能力。(**有使用jQuery做$(element)和before()的動作)


在程式碼中已經寫了詳細的註解,不過我們還必須對執行完上述程式碼後,AngularJS產生出了什麼樣的Scope關係。
在程式碼裡,我們已經在有scope出現的地方進行了console.log()的輸出,可以由其中的屬性物件、$id、_proto_了解到scope之間的關係,以下已經用簡單圖示來表示從console.log()結果中看出的關係:

2016年5月6日 星期五

html5 的 setCustomValidity(message) 觀念釐清

在這篇文中主要是要釐清在使用setCustomValidity(message)的一些觀念,
setCustomValidity(message)可以跟據送入的message是否是空字串來蓋住input元素的validity狀態,但是要注意的是,並不會蓋掉input元素的validity狀態,input元素的真實validity狀態並不會因此而改變。

為了驗證這個現像,我們可以看一下下面這個例子:
當我們呼叫
setCustomValidity("自訂訊息");
時,input的真正valid訊息會被隱藏起來,而這時去看input的validity.valid的話就得到false的結果,就算我們在input中打字也是一樣,
但其實input在有打字內容時其直正的validity.valid應該是true。
我們可以呼叫setCustomValidity("")來讓input的真正validity.valid不要被蓋往,這時我們可以從isValid(after setCustomValidity("")):來得到之前被隱藏的正確結果。

========================================================================

這裡我再舉出一個比較常用的使用setCustomValidity("")的例子:

可以看到我們再input的oninvalid和oninput都呼叫check(this),這是因為當在input中打字時,就算打的內容不符合設定的pattern,也不會觸發oninvalid,只會觸發oninput。

我們在check(target)中先呼叫setCustomValidity("")得到input的真正valid狀態,如果真正valid為false就呼叫setCustomValidity("自訂訊息"),而因為此呼叫會讓之後valid的真正狀態被隱藏,所以我們之後會再呼叫check時呼叫setCustomValidity("")來讓input的真正valid狀態不要被隱藏起來。

這樣之後再用if()詢問input的狀態時,就不會得到錯的、與實際不符的valid狀態了。
重點就是,在檢查valid的狀態時必須要真正的(沒被隱藏的),再判斷要不要設定自訂訊息後。

========================================================================

接下來我再舉出一個例子,有時候我們會想要對一個Group,即一群name一樣的input元素去做判斷,例如對於input="radio"只會想要同一個Group裡的radio被選取,像下面這樣:


如果想在radio的oninvalid和onclick使用上述的check(target)方法的話,會造成錯誤。
如果有兩個設置rquired的同name的radio且都沒有被點擊時,按下submit後會對每一個radio呼叫check(target)並都被隱藏真正valid狀態。
當點擊第二個radio時,會只對第二個radio呼叫check(target),此時第二個radio的valid會是true,但對一個radio的valid還是false,所以此時按下submit會無法submit,接著又因為submit會讓每一個check(target)都被呼叫,所以此時每一個radio的valid狀態都才會正常,再按一次submit就可以成功submit了。

結果是submit要按兩下才會成功,這很顯然不是我們想要的,所以我們要做一點改良。

以下就是改良的方法,checkValid(target),在其中也對一個input用radio來設置不同的pattern check:

重點就是在checkValid(target)裡面必須對所有group裡面的input都去做檢查並設置正確的valid狀態,如此一來,不管是submit還是onclick就都能對所有的input設置正確的valid狀態了
(當然submit會輪詢一次所有input,而checkValid(target)又會輪詢一次,所以會輪詢兩次造成效能上的浪費,不過這裡就忽略此issue了)。



2016年5月3日 星期二

html5 自訂表單驗證的警示文字 - setCustomValidity(message)

html5 的表單驗證非常方便,能根據設置的type (例如:email, phone等)、requiered屬性的有無設置等在form中有input為invalid時,顯示提示文字,

不過我們通常會想要的不是預設的,而是自訂的提示文字,這時就可以用到setCustomValidity(message)這個javascript方法。

setCustomValidity(message)可以由 input type='xxx' 的元素呼叫,
它接受一個字串作為自訂的提示文字,要注意的是,當此方式呼叫該方法時,input元素的valid狀態會被invalid蓋住(不是蓋掉)。如果我們送入的字串為空字串,則此input元素的valid狀態會回復正常(沒被蓋往)。

所以我們可以在input驗證不通過時呼叫setCustomValidity('XXX'),並在它的內容值有改變或有被key in值時呼叫setCustomValidity('')來得到input的真正valid狀態(否則那個input就永遠是invalid)。

看一下下面的例子:

這裡有兩個 form,並且各自有一個設置required的input type='text',兩個form都設置了
oninvalid="setCustomValidity('XXX')"
所以當沒有填值按下form的submit按鈕時,就會跳出自訂的提示文字並且阻止form的submit動作。

因為第一個form的input設置了onchange="setCustomValidity('');",所以在input中的值改變時會對此input進行invalid error的清除,但此時我們會發現當在此input中打字時,提示文字不會消除且一直出現。
這是因為onchange在此input失去焦點(off focus)且值有改變(change)時才會觸發,當我們點擊Form1的第二個input再點回要reqired的input並打字時,提示文字就不會出現了。
當然,這通常不是我們想要的結果。

第二個Form的input設置了oninput="setCustomValidity('');",使用了oninput而不是onchange來呼叫重設input的invalid狀態,oninput會在使用者對input打字時被觸發,於是當提示文字出現時,只要再input中打字(不用使其焦點),就可以得到input的invalid狀態了,而這個結果也是我們所期望的。

參考資料:

  1. JavaScript Validation API
  2. oninput Event
  3. onchange Event
  4. HTML5應用:setCustomValidity(message)接口


源碼:
<form>
  Form 1
  <input type='text' oninvalid="setCustomValidity('Cusotm Alert 1');" onchange="setCustomValidity('');" placeholder='This input is required' required/>
  <input type='text' />
  <input type='submit' />
</form>
<form>
  Form 2
  <input type='text' oninvalid="setCustomValidity('Cusotm Alert 2');" oninput="setCustomValidity('');" placeholder='This input is required' required/>
  <input type='text' />
  <input type='submit' />
</form>