套用樣式與顏色 - Web APIs

套用樣式與顏色
=======

* « 前頁
* 次頁 »

在繪畫圖形章節中，我們只用了預設的線條與填滿樣式，而在本章，我們將進一步看看所有可用的樣式選項，畫出更吸引人的圖。

顏色
--

U 截至目前為止我們只有看到繪圖環境的方法(methods)，如果我們想要設定圖形的顏色，我們有兩個屬性能用: `fillStyle`與`storkeStyle.`

`fillStyle = color`

設定填滿圖形時用的顏色.

`strokeStyle = color`

設定勾勒圖形時用的顏色.

其中`color`可以是 CSS`<color>` (en-US)表示字串、漸層色物件(gradient color)或是模式物件(pattern object)，現在先看一下 CSS{<color>}表示字串，稍後再看另外兩個項目.

預設上勾勒和填滿色是黑色(CSS 顏色值為#000000).

**備註：** 一旦改變了 strokeStyle 的顏色值，那麼之後圖形勾勒顏色都會變成新顏色，同樣狀況一樣適用於 fillStyle.

合格的顏色值請參照 CSS3`<color>` (en-US)規範，下面範例所標示的顏色都指向同一個顏色.

```js
// these all set the fillStyle to 'orange'

ctx.fillStyle = "orange";
ctx.fillStyle = "#FFA500";
ctx.fillStyle = "rgb(255,165,0)";
ctx.fillStyle = "rgba(255,165,0,1)";
```

**備註：** 目前 Gecko 引擎並不支援 CSS3 全部的顏色值，例如 hsl(100%,25%,0)和 rgb(0,100%,0)就不被支援.

### `fillStyle` 範例

這裡我們利用兩個 for 迴圈來畫出一個矩形陣列，而且陣列中每一個矩形的顏色都不相同。下面程式碼透過改變 i 和 j 兩個變數來分別變換 RGB 中的紅色值和綠色值，然後為每一個矩形產生自己專屬的顏色值。透過改變 RGB 的各顏色值，我們可以產生各式各樣的調色盤，像是逐步調整顏色值，你也可以做出像 Photoshop 內建一樣的調色盤。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 6; i++) {
    for (var j = 0; j < 6; j++) {
      ctx.fillStyle =
        "rgb(" +
        Math.floor(255 - 42.5 \* i) +
        "," +
        Math.floor(255 - 42.5 \* j) +
        ",0)";
      ctx.fillRect(j \* 25, i \* 25, 25, 25);
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

結果如下:

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `strokeStyle` 範例

本例和前例相當類似，不同的是我們改用 arc()方法畫圓形而不是矩形、改設定 strokeStyle 變換圖形輪廓顏色。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 6; i++) {
    for (var j = 0; j < 6; j++) {
      ctx.strokeStyle =
        "rgb(0," +
        Math.floor(255 - 42.5 \* i) +
        "," +
        Math.floor(255 - 42.5 \* j) +
        ")";
      ctx.beginPath();
      ctx.arc(12.5 + j \* 25, 12.5 + i \* 25, 10, 0, Math.PI \* 2, true);
      ctx.stroke();
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

結果如下:

| Screenshot | Live sample |
| --- | --- |
|  |  |

透明度
---

透過設定 globalAlpha 屬性或是以半透明顏色值設定 strokeStyle 與 fillStyle 屬性，除了畫不透明的圖形，我們還可以畫半透明的圖形。

`globalAlpha = transparencyValue`

允許值介於 0.0(全透明)到 1.0(不透明)。一旦設定後，之後畫布上畫的所有圖形的不透明度都會套用此設定值。預設值為 1.0。

當我們想畫一系列相同不透明度的圖，設定 globalAlpha 值是一個方便的作法。

由 CSS3 顏色值能夠指定不透明度，我們也可以如下面一般，設定 strokeStyle 以及 fillStyle 來變更不透明度。

```js
// Assigning transparent colors to stroke and fill style

ctx.strokeStyle = "rgba(255,0,0,0.5)";
ctx.fillStyle = "rgba(255,0,0,0.5)";
```

rgba()函數比 rgb()函數多出一個不透明度參數，允許值介於 0.0(全透明)到 1.0(不透明).

### `globalAlpha` 範例

下面我們將在四個方格色塊背景上畫一系列半透明圓形。對於所有圓形，我們藉由設置 globalAlpha 屬性值為 0.2 使得圓形變成半透明，然後 for 迴圈裡我們逐一增加圓形繪圖半徑，最終結果看起來便像是輻射狀漸層圖案，而且圓形相互疊加在彼此之上後，又加深了重疊區域的不透明度，只要我們不斷增加圓形數量，最後圖片中央將被完全遮蓋，看不到背後的背景。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  // draw background
  ctx.fillStyle = "#FD0";
  ctx.fillRect(0, 0, 75, 75);
  ctx.fillStyle = "#6C0";
  ctx.fillRect(75, 0, 75, 75);
  ctx.fillStyle = "#09F";
  ctx.fillRect(0, 75, 75, 75);
  ctx.fillStyle = "#F30";
  ctx.fillRect(75, 75, 150, 150);
  ctx.fillStyle = "#FFF";

  // set transparency value
  ctx.globalAlpha = 0.2;

  // Draw semi transparent circles
  for (i = 0; i < 7; i++) {
    ctx.beginPath();
    ctx.arc(75, 75, 10 + 10 \* i, 0, Math.PI \* 2, true);
    ctx.fill();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `rgba()` 使用範例

這個範例類似於上面的範例，但不同的是我們改畫半透明的矩形。rgba()在使用上會多一點彈性，因為我們可以分別設置勾勒和填滿圖形的不透明度。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // Draw background
  ctx.fillStyle = "rgb(255,221,0)";
  ctx.fillRect(0, 0, 150, 37.5);
  ctx.fillStyle = "rgb(102,204,0)";
  ctx.fillRect(0, 37.5, 150, 37.5);
  ctx.fillStyle = "rgb(0,153,255)";
  ctx.fillRect(0, 75, 150, 37.5);
  ctx.fillStyle = "rgb(255,51,0)";
  ctx.fillRect(0, 112.5, 150, 37.5);

  // Draw semi transparent rectangles
  for (var i = 0; i < 10; i++) {
    ctx.fillStyle = "rgba(255,255,255," + (i + 1) / 10 + ")";
    for (var j = 0; j < 4; j++) {
      ctx.fillRect(5 + i \* 14, 5 + j \* 37.5, 14, 27.5);
    }
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

線條樣式
----

有數種屬性可以讓我們設定線條樣式.

`lineWidth = value`

設定線條寬度。

`lineCap = type`

設定線條結尾的樣式。

`lineJoin = type`

設定線條和線條間接合處的樣式。

`miterLimit = value`

限制當兩條線相交時交接處最大長度；所謂交接處長度(miter length)是指線條交接處內角頂點到外角頂點的長度。

底下我們將一一示範這些屬性的用途。

### `lineWidth` 範例

此屬性決定線條寬度，必須為正數，預設值為 1.0 單位。

線條寬度的起算點是從繪圖路徑中央開始往兩旁各延伸一半設定寬度，由於畫布座標不直接對應到像素(pixel)，所以要比較小心設定好取得清晰的直線。

由下方例子可以明顯看到，畫布上有 10 條直線，由左至右，從最小的 1.0 單位寬開始逐漸加寬，請注意奇數寬度直線會因為繪圖路徑位置關係而比較模糊。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  for (var i = 0; i < 10; i++) {
    ctx.lineWidth = 1 + i;
    ctx.beginPath();
    ctx.moveTo(5 + i \* 14, 5);
    ctx.lineTo(5 + i \* 14, 140);
    ctx.stroke();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

為了畫出清晰的直線，我們需要了解繪圖路徑是如何產生；如下方圖示，網格代表畫布座標軸，網格所框出的方格則代表螢幕上的像素，第一張圖片填滿了座標(2,1)到(5,5)的紅色區域，而這個紅色區域的邊際正好符合像素間的邊際，所以會產生出清晰的影像。

![](/zh-TW/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas-grid.png)

第二張圖片中，有一條寬 1.0 單位的直線從座標(3,1)到(3,5)被畫在畫布上，不過由於線條寬度的起算點是從繪圖路徑中央開始往兩旁各延伸一半設定寬度，所以當勾勒線條時，繪圖路徑兩旁的像素格只有一半會被填滿暗藍色，至於另外一半則會經由計算填入近似色(淡藍色)，結果就是整格像素並非全部填入相同的暗藍色，進而產生出邊緣較為模糊的線條，上面程式碼範例中的奇數寬度直線就是因此而產生不清晰的線條。

為了避免劃出邊緣模糊直線，我們必須精準設定繪圖路徑位置，就本範例而言，如果我們的直線繪圖路徑是從座標(3.5, 1)到(3.5, 5)的話(如第三張圖)，那麼 1.0 單位寬的直線將剛好填滿像素格，所以我們將可以畫出清晰的直線。

**備註：** 請注意本範例的 Y 軸座標都是整數點，若非如此，一樣會導致線條端點的像素格無法剛好被填滿的現象，而且同時最後產生的結果也會被 lineCap 給影響；倘若 lineCap 值為預設 butt 時，我們會需要為奇數寬度直線計算一下非整數的座標點，倘若 lineCap 樣式為 square，那麼線段端點的像素格將自動被完整填滿。還有一點需要注意，只要繪圖路徑被 closePath()函數閉合起來，這樣便沒有了線條端點，所有的線條端點都會依據 lineJoin 樣式全部前後互相連接起來，這會自動延伸端點邊緣到線段接合處，如果此時接合端點是水平或垂直的話，位於中央的像素格將會被完整填滿。後面的說明會介紹 lineCap 和 lineJoin 樣式。

至於本例中偶數寬度的直線，為了避免模糊，繪圖路徑最好是落在整數座標點上。

雖然處裡 2D 繪圖縮放有些麻煩，但只要仔細計算像素格和繪圖路徑位置，縱使進行圖像縮放或變形，圖像輸出還是可以保持正確。一條寬 1.0 單位的直線，只要位置計算正確，放大兩倍後會變成一條 2 個像素寬的清晰直線，而且還是會保持正確位置。

### `lineCap` 範例

這個屬性決定線條端點的樣式，總共有三種樣式可選:

![](/zh-TW/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas_linecap.png)

`butt`

線條端點樣式為方形

`round`

線條端點樣式為圓形

`square`

增加寬同線條寬度、高線條寬度一半的的方塊於線條端點

下面程式碼會畫出三條線，每條線的 lineCap 值皆不同。然後為了看清差異點，我們加上了兩條淡藍色的輔助線，線條的繪圖起始點和終點都剛好落在輔助線上。

最左邊的線條其 lineCap 為 butt，不難看出它完全介於輔助線之間；第二條線其 lineCap 為 round，端點樣式為半徑等於線條寬度一半的半圓；最右邊的線條其 lineCap 為 square，端點樣式為寬同線條寬度、高線條寬度一半的的方塊。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var lineCap = ["butt", "round", "square"];

  // Draw guides
  ctx.strokeStyle = "#09f";
  ctx.beginPath();
  ctx.moveTo(10, 10);
  ctx.lineTo(140, 10);
  ctx.moveTo(10, 140);
  ctx.lineTo(140, 140);
  ctx.stroke();

  // Draw lines
  ctx.strokeStyle = "black";
  for (var i = 0; i < lineCap.length; i++) {
    ctx.lineWidth = 15;
    ctx.lineCap = lineCap[i];
    ctx.beginPath();
    ctx.moveTo(25 + i \* 50, 10);
    ctx.lineTo(25 + i \* 50, 140);
    ctx.stroke();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `lineJoin` 範例

lineJoin 屬性決定兩個連接區端(如線條、弧形或曲線)如何連接(對於長度為零，亦即終點和控制點為同一點的圖形無效)。

lineJoin 屬性共有三個屬性值如下，其中 miter 為預設值，請注意一點若是兩個連接區段的繪圖方向一致，那代表不會有連接處，所以測定是無效的。

![](/zh-TW/docs/Web/API/Canvas_API/Tutorial/Applying_styles_and_colors/canvas_linejoin.png)

`round`

代表圓弧型連接樣式。

`bevel`

代表斜面型連接樣式。在連接區段的共同終點處填滿一個三角形區域，將原本的外接角處形成一個切面。

`miter`

代表斜交型連接樣式。向外延伸連結區段外緣直到相交於一點，然後形成菱形區域，而 miterLimit 屬性會影響 miter 屬性。

下方程式碼和圖形輸出展示了 lineJoin 在不同屬性值下呈現的不同結果

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");
  var lineJoin = ["round", "bevel", "miter"];
  ctx.lineWidth = 10;
  for (var i = 0; i < lineJoin.length; i++) {
    ctx.lineJoin = lineJoin[i];
    ctx.beginPath();
    ctx.moveTo(-5, 5 + i \* 40);
    ctx.lineTo(35, 45 + i \* 40);
    ctx.lineTo(75, 5 + i \* 40);
    ctx.lineTo(115, 45 + i \* 40);
    ctx.lineTo(155, 5 + i \* 40);
    ctx.stroke();
  }
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `miterLimit` 屬性

前面範例顯示出，當 lineJoin 值為 miter 時，兩條線的外緣會延伸相交，所以，當這兩條相交線的相交角度越小的話，他們的延伸交會點就會越遠離內緣連接點，而且隨著角度變小，距離呈指數型增長。

miterLimit 會限制延伸交會點最遠可以離內緣連接點到多遠，當延伸交會點的落點超出這個範圍，那麼便以斜面(bevel)作為交接樣式。請注意，最大 miter 長度為線寬乘於 miterLimit 值，所以 miterLimit 可以獨立於目前顯示縮放尺寸或其他變形設定。

miterLimit 預設值為 10.0。

更精確來說，miter 限制是指延伸長度(在 HTML 畫布上，這個長度是外緣相交角到連接區段的共同繪圖路經終點)相對於一半線寬的最大允許比率；也等同於，外緣距內緣相交點之距離相對於線寬的的最大允許比率；相當於，連接區最小內緣角的一半角度的餘割(cosecant)值, 小於此值則便以斜面(bevel)作為交接樣式:

* `miterLimit` = **max** `miterLength` / `lineWidth` = 1 / **sin** ( **min** *θ* / 2 )
* 10.0 的預設 miterLimit 值會移除任何角度小於 11 度的相接線段的 miter 交接。
* miter 限制值如果等於根號 2(約 1.4142136)會移除銳角的 miter 交接，只有直角或鈍角的不會被移除。
* miter 限制值如果等於 1.0 會移除所有的 miter 交接。
* 小於 1.0 不是合法的限制值。

下面是一個範例，其中藍線標示出各個線條繪圖路徑的起始點與終點。

倘若設定範例程式碼中的 miterLimit 低於 4.2，所有的 miter 交接都會被移除，取而代之的是出現在藍線附近的 bevel 交接；倘若設定 miterLimit 大於 10，那麼大部分的 miter 交接都會出現，而且你會發現，由左到右，miter 長度逐漸縮短，這是由於線條相交角度逐漸加大之故；倘若設定中間值，那麼左邊會出現 bevel 交接，右邊會出現 miter 交接。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // Clear canvas
  ctx.clearRect(0, 0, 150, 150);

  // Draw guides
  ctx.strokeStyle = "#09f";
  ctx.lineWidth = 2;
  ctx.strokeRect(-5, 50, 160, 50);

  // Set line styles
  ctx.strokeStyle = "#000";
  ctx.lineWidth = 10;

  // check input
  if (document.getElementById("miterLimit").value.match(/\d+(\.\d+)?/)) {
    ctx.miterLimit = parseFloat(document.getElementById("miterLimit").value);
  } else {
    alert("Value must be a positive number");
  }

  // Draw lines
  ctx.beginPath();
  ctx.moveTo(0, 100);
  for (i = 0; i < 24; i++) {
    var dy = i % 2 == 0 ? 25 : -25;
    ctx.lineTo(Math.pow(i, 1.5) \* 2, 75 + dy);
  }
  ctx.stroke();
  return false;
}
```

```
<table>
  <tr>
    <td><canvas id="canvas" width="150" height="150"></canvas></td>
    <td>
      Change the <code>miterLimit</code> by entering a new value below and
      clicking the redraw button.<br /><br />
      <form onsubmit="return draw();">
        <label>Miter limit</label>
        <input type="text" size="3" id="miterLimit" />
        <input type="submit" value="Redraw" />
      </form>
    </td>
  </tr>
</table>
```

```
document.getElementById("miterLimit").value = document
  .getElementById("canvas")
  .getContext("2d").miterLimit;
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

漸層
--

如同其他繪圖軟體可以畫出線性和放射狀的漸層圖案，透過設定 fillStyle 和 strokeStyle 屬性為 canvasGradient 漸層物件，我們也可以在 canvas 上做到一樣的效果。要創造漸層物件，可以使用下面的方法:

`createLinearGradient(x1, y1, x2, y2)`

產生一個線性漸層物件，其漸層起始點為(x1, y1)、終點為(x2, y2)。

`createRadialGradient(x1, y1, r1, x2, y2, r2)`

產生一個放射狀漸層物件，第一個圓之圓心落在(x1, y1)、半徑為 r1，第一個圓之圓心落在(x2, y2)、半徑為 r2。

例如:

```js
var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
var radialgradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 100);
```

一旦產生了 canvasGradient 漸層物件，我們用 addColorStop()方法可以添加顏色上去。

`gradient.addColorStop(position, color)`

於 gradient 漸層物件建立一個顏色點，其中 color 是 CSS`<color>` (en-US)的字串表示，而 position 介於 0.0 到 1.0 之間，定義了該顏色在漸層中的相對位置。呼叫這個方法會指定當進行到設定的位置時，漸層需要完全轉變成設定的顏色。

我們可以按照需要設定無數個顏色點，下面是一個簡單的由白到黑的簡單漸層範例程式碼。

```js
var lineargradient = ctx.createLinearGradient(0, 0, 150, 150);
lineargradient.addColorStop(0, "white");
lineargradient.addColorStop(1, "black");
```

### `createLinearGradient` 範例

本範例中，我們將建立兩種漸層，如範例所示，strokeStyle 和 fillSyle 屬性都可以接受 canvasGradient 物件作為屬性值。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // Create gradients
  var lingrad = ctx.createLinearGradient(0, 0, 0, 150);
  lingrad.addColorStop(0, "#00ABEB");
  lingrad.addColorStop(0.5, "#fff");
  lingrad.addColorStop(0.5, "#26C000");
  lingrad.addColorStop(1, "#fff");

  var lingrad2 = ctx.createLinearGradient(0, 50, 0, 95);
  lingrad2.addColorStop(0.5, "#000");
  lingrad2.addColorStop(1, "rgba(0,0,0,0)");

  // assign gradients to fill and stroke styles
  ctx.fillStyle = lingrad;
  ctx.strokeStyle = lingrad2;

  // draw shapes
  ctx.fillRect(10, 10, 130, 130);
  ctx.strokeRect(50, 50, 50, 50);
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

第一個漸層為背景漸層，範例中我們在一個位置上指定了兩種顏色(白色到綠色)，這樣做會產生非常突然的顏色轉換，一般來說，不管如何設定顏色點順序都沒關係，然而就這個例子而言，這種作法太過強烈了，但是如果這是你想要的顏色漸層順序，那其實也是可以。

第二個漸層起始位置(position 0.0)的顏色並沒有被指定，所以下一個漸層顏色會自動被設為起始位置顏色，因此即使我們沒有指定漸層起始位置顏色也沒有關係，就像本範例自動會設定起始位置的顏色等於位置 0.5 的黑色。

| Screenshot | Live sample |
| --- | --- |
|  |  |

### `createRadialGradient` 範例

這邊我們定義了四種放射狀漸層，相較於一般在 Photoshop 看到的「經典」放射狀漸層圖案(漸層從一個圖案中心點向外呈圓心狀延伸)，因為我們可以控制漸層起始和終止點，我們可以做到更好的效果。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // Create gradients
  var radgrad = ctx.createRadialGradient(45, 45, 10, 52, 50, 30);
  radgrad.addColorStop(0, "#A7D30C");
  radgrad.addColorStop(0.9, "#019F62");
  radgrad.addColorStop(1, "rgba(1,159,98,0)");

  var radgrad2 = ctx.createRadialGradient(105, 105, 20, 112, 120, 50);
  radgrad2.addColorStop(0, "#FF5F98");
  radgrad2.addColorStop(0.75, "#FF0188");
  radgrad2.addColorStop(1, "rgba(255,1,136,0)");

  var radgrad3 = ctx.createRadialGradient(95, 15, 15, 102, 20, 40);
  radgrad3.addColorStop(0, "#00C9FF");
  radgrad3.addColorStop(0.8, "#00B5E2");
  radgrad3.addColorStop(1, "rgba(0,201,255,0)");

  var radgrad4 = ctx.createRadialGradient(0, 150, 50, 0, 140, 90);
  radgrad4.addColorStop(0, "#F4F201");
  radgrad4.addColorStop(0.8, "#E4C700");
  radgrad4.addColorStop(1, "rgba(228,199,0,0)");

  // draw shapes
  ctx.fillStyle = radgrad4;
  ctx.fillRect(0, 0, 150, 150);
  ctx.fillStyle = radgrad3;
  ctx.fillRect(0, 0, 150, 150);
  ctx.fillStyle = radgrad2;
  ctx.fillRect(0, 0, 150, 150);
  ctx.fillStyle = radgrad;
  ctx.fillRect(0, 0, 150, 150);
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

程式碼範例中，為了營造出 3D 效果，我們讓起始點和終止點位於不同位置，請注意，最好不要讓內外圈相重疊，以避免難以預測的奇怪效果。

每一個漸層圖案最後一個漸層色都是全透明的，如果希望倒數第二個漸層色能夠平順地轉換到這個最後一個漸層色，那麼兩者應該設定一樣的顏色值，像是程式碼範例中的漸層色 #019F62 其實就等於 rgba(1,159,98,1)。

| Screenshot | Live sample |
| --- | --- |
|  |  |

樣式(Patterns)
------------

先前的範例中，我們都是藉由迴圈來重複產生影像樣式，不過其實有一條更簡單的方法，那就是呼叫 createPattern 方法。

`createPattern(image, type)`

 呼叫 createPattern()會產一個畫布樣式物件，然後回傳出來。
 其中 image 是CanvasImageSource類別物件(像是`HTMLImageElement` (en-US),、<canvas>元素、`<video>` (en-US) 元素等)

Type 是一串字串，定義了如何產生樣式，允許的值有:

`repeat`

沿垂直與水平方向重複排列影像

`repeat-x`

只沿水平方向重複排列影像

`repeat-y`

只沿垂直方向重複排列影像

`no-repeat`

不重複排列影像，只使用一次

**備註：** Firefox 現在只支援 repeat，所以其他值都是無效的

**備註：** 傳入尺寸為 0x0 像素的畫布會引起錯誤

利用 createPattern()的方法和前面利用漸層的方法十分類似，我們呼叫 createPattern()產生`CanvasPattern` (en-US)物件，然後將{CanvasPattern}物件設成 fillStyle 或 strokeStyle 的屬性值，例如:

```js
var img = new Image();
img.src = "someimage.png";
var ptrn = ctx.createPattern(img, "repeat");
```

**備註：** 不像 drawImage()方法，呼叫 createPattern()方法前影像必須要先載入完成，否則可能圖像的程生會有問題。

### `createPattern` 範例

這個範例中我們把 fillStyle 屬性值存為樣式物件，比較值得注意的是影像 onload 事件處理器，這是為了確保影像載入完成後再進行。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  // create new image object to use as pattern
  var img = new Image();
  img.src = "/files/222/Canvas\_createpattern.png";
  img.onload = function () {
    // create pattern
    var ptrn = ctx.createPattern(img, "repeat");
    ctx.fillStyle = ptrn;
    ctx.fillRect(0, 0, 150, 150);
  };
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
draw();
```

結果如下 :

| Screenshot | Live sample |
| --- | --- |
|  |  |

陰影
--

要產生陰影只需要四個屬性:

`shadowOffsetX = float`

代表陰影從物件延伸出來的水平距離，預設為 0，不受變形矩陣影響。

`shadowOffsetY = float`

代表陰影從物件延伸出來的垂直距離，預設為 0，不受變形矩陣影響。

`shadowBlur = float`

代表陰影模糊大小範圍，預設為 0，不受變形矩陣影響，不等同於像素值。

`shadowColor = `<color>` (en-US)`

CSS 顏色值，代表陰影顏色，預設為全透明。

`shadowOffsetX和shadowOffsetY會決定陰影延伸大小，若是為正值，則陰影會往右(沿X軸)和往下(沿Y軸)延伸，若是為負值，則會往正值相反方向延伸。`

**備註：** 基於 HTML5 提議規格變更，從 開始，陰影只會在 source-over 的構圖排列下產生

### 文字陰影範例

本程式碼範例會產生一串帶有陰影的文字。

```js
function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  ctx.shadowOffsetX = 2;
  ctx.shadowOffsetY = 2;
  ctx.shadowBlur = 2;
  ctx.shadowColor = "rgba(0, 0, 0, 0.5)";

  ctx.font = "20px Times New Roman";
  ctx.fillStyle = "Black";
  ctx.fillText("Sample String", 5, 30);
}
```

```
<canvas id="canvas" width="150" height="80"></canvas>
```

```
draw();
```

| Screenshot | Live sample |
| --- | --- |
|  |  |

* « 前頁
* 次頁 »


基礎動畫 - Web APIs

基礎動畫
====

* « 前頁
* 次頁 »

控制`<canvas>`元素來產生互動式動畫不是一件難事，當然，如果產生的動畫越複雜越需要多費一些力氣，未來如果有機會我們將說明這一塊。

由於圖形一但產生後便靜止不動，所以我們必須重新繪圖好移動圖案，產生動畫效果，所以如果繪圖越複雜，繪圖運算也需要消耗越多運算資源和時間，換句話說，電腦效能的好壞將大大影響動畫順暢度，或許這也是畫布動畫最大的限制。

動畫基本步驟
------

產生一個畫面基本上需要以下步驟：

1. **清除畫布**
 除了不變的背景畫面，所有先前畫的圖案都要先清除，這個步驟可以透過 `clearRect()` 方法達成。
2. **儲存畫布狀態**
 若是想要每一次重新繪圖時畫布起始狀態都是原始狀態，那麼就需要先行儲存畫布原始狀態。
3. **畫出畫面**
 畫出需要畫面。
4. **復原畫布狀態**
 復原畫布狀態以備下次繪圖使用。
控制動畫
----

一般來說當程式碼執行完畢後我們才會看到繪圖結果，所以說我們無法靠執行 for 迴圈來產生動畫，我們得靠每隔一段時間繪圖來產生動畫，下面將介紹兩種作法。

### 排程更新

第一種作法是利用 `window.setInterval()` (en-US) 與 `window.setTimeout()` (en-US) 方法。

**備註：** 針對新版瀏覽器建議採用 `window.requestAnimationFrame()` 方法。

`setInterval(function, delay)`

每隔 delay 毫秒，執行輸入 function(函數)

`setTimeout(function, delay)`

過 delay 毫秒後，執行輸入 function(函數)

requestAnimationFrame(callback)

告訴瀏覽器你希望執行動畫的時候，要求瀏覽器在重繪下一張畫面之前，呼叫 callback 函數來更新動畫

如果希望不要有任何的使用者互動影響，請使用 setInterval()，因為它會確實地每隔一段時間就執行程式碼。如果你想製作遊戲，我們能夠使用 keyboard 或是 mouse event 來控制動畫，並使用 setTimeout() 函數一起。藉由設定 EventListeners，我們能夠捕捉任何使用者的動作，並執行我們的動畫函數。

**備註：** 在下面的範例,我們將使用 **`window.requestAnimationFrame()`** 方法來控制動畫，**`window.requestAnimationFrame()`** 方法為動畫提供更順暢更有效率的方式來執行,當系統準備好繪製畫面時，藉由呼叫動畫 andmation frame() 的 callback 函數。callback 通常每秒鐘執行 60 次，當執行 background tab 時，執行次數會更低，想知道更多關於動畫迴圈(animation loop)的資訊，尤其是遊戲的應用，請查看我們在 Game development zone 的主題 Anatomy of a video game (en-US)。

### 從使用者輸入操作控制動畫

我們也可以從使用者輸入操作控制動畫，就像是電玩遊戲一般；像是在鍵盤上設置事件處理器 `EventListener` 捕捉使用者輸入並執行對應動畫。

你可以利用我們的次要版 (en-US)或主要版動畫框架。

```js
var myAnimation = new MiniDaemon(null, animateShape, 500, Infinity);
```

或

```js
var myAnimation = new Daemon(null, animateShape, 500, Infinity);
```

在後面的範例我們主要將使用 window.setInterval()方法控制動畫，然後於本頁底部是一些使用 widnow.setTimeout()的範例連結。

#### 太陽系動畫

本例會產生一個小型太陽系運行動畫。

```js
var sun = new Image();
var moon = new Image();
var earth = new Image();
function init() {
  sun.src = "canvas\_sun.png";
  moon.src = "canvas\_moon.png";
  earth.src = "canvas\_earth.png";
  setInterval(draw, 100);
}

function draw() {
  var ctx = document.getElementById("canvas").getContext("2d");

  ctx.globalCompositeOperation = "destination-over";
  ctx.clearRect(0, 0, 300, 300); // clear canvas

  ctx.fillStyle = "rgba(0,0,0,0.4)";
  ctx.strokeStyle = "rgba(0,153,255,0.4)";
  ctx.save();
  ctx.translate(150, 150);

  // Earth
  var time = new Date();
  ctx.rotate(
    ((2 \* Math.PI) / 60) \* time.getSeconds() +
      ((2 \* Math.PI) / 60000) \* time.getMilliseconds(),
  );
  ctx.translate(105, 0);
  ctx.fillRect(0, -12, 50, 24); // Shadow
  ctx.drawImage(earth, -12, -12);

  // Moon
  ctx.save();
  ctx.rotate(
    ((2 \* Math.PI) / 6) \* time.getSeconds() +
      ((2 \* Math.PI) / 6000) \* time.getMilliseconds(),
  );
  ctx.translate(0, 28.5);
  ctx.drawImage(moon, -3.5, -3.5);
  ctx.restore();

  ctx.restore();

  ctx.beginPath();
  ctx.arc(150, 150, 105, 0, Math.PI \* 2, false); // Earth orbit
  ctx.stroke();

  ctx.drawImage(sun, 0, 0, 300, 300);
}
```

```
<canvas id="canvas" width="300" height="300"></canvas>
```

```
init();
```

#### 時鐘動畫

本例會產生一個時鐘指向現在時間。

```js
function init() {
  clock();
  setInterval(clock, 1000);
}

function clock() {
  var now = new Date();
  var ctx = document.getElementById("canvas").getContext("2d");
  ctx.save();
  ctx.clearRect(0, 0, 150, 150);
  ctx.translate(75, 75);
  ctx.scale(0.4, 0.4);
  ctx.rotate(-Math.PI / 2);
  ctx.strokeStyle = "black";
  ctx.fillStyle = "white";
  ctx.lineWidth = 8;
  ctx.lineCap = "round";

  // Hour marks
  ctx.save();
  for (var i = 0; i < 12; i++) {
    ctx.beginPath();
    ctx.rotate(Math.PI / 6);
    ctx.moveTo(100, 0);
    ctx.lineTo(120, 0);
    ctx.stroke();
  }
  ctx.restore();

  // Minute marks
  ctx.save();
  ctx.lineWidth = 5;
  for (i = 0; i < 60; i++) {
    if (i % 5 != 0) {
      ctx.beginPath();
      ctx.moveTo(117, 0);
      ctx.lineTo(120, 0);
      ctx.stroke();
    }
    ctx.rotate(Math.PI / 30);
  }
  ctx.restore();

  var sec = now.getSeconds();
  var min = now.getMinutes();
  var hr = now.getHours();
  hr = hr >= 12 ? hr - 12 : hr;

  ctx.fillStyle = "black";

  // write Hours
  ctx.save();
  ctx.rotate(
    hr \* (Math.PI / 6) + (Math.PI / 360) \* min + (Math.PI / 21600) \* sec,
  );
  ctx.lineWidth = 14;
  ctx.beginPath();
  ctx.moveTo(-20, 0);
  ctx.lineTo(80, 0);
  ctx.stroke();
  ctx.restore();

  // write Minutes
  ctx.save();
  ctx.rotate((Math.PI / 30) \* min + (Math.PI / 1800) \* sec);
  ctx.lineWidth = 10;
  ctx.beginPath();
  ctx.moveTo(-28, 0);
  ctx.lineTo(112, 0);
  ctx.stroke();
  ctx.restore();

  // Write seconds
  ctx.save();
  ctx.rotate((sec \* Math.PI) / 30);
  ctx.strokeStyle = "#D40000";
  ctx.fillStyle = "#D40000";
  ctx.lineWidth = 6;
  ctx.beginPath();
  ctx.moveTo(-30, 0);
  ctx.lineTo(83, 0);
  ctx.stroke();
  ctx.beginPath();
  ctx.arc(0, 0, 10, 0, Math.PI \* 2, true);
  ctx.fill();
  ctx.beginPath();
  ctx.arc(95, 0, 10, 0, Math.PI \* 2, true);
  ctx.stroke();
  ctx.fillStyle = "rgba(0,0,0,0)";
  ctx.arc(0, 0, 3, 0, Math.PI \* 2, true);
  ctx.fill();
  ctx.restore();

  ctx.beginPath();
  ctx.lineWidth = 14;
  ctx.strokeStyle = "#325FA2";
  ctx.arc(0, 0, 142, 0, Math.PI \* 2, true);
  ctx.stroke();

  ctx.restore();
}
```

```
<canvas id="canvas" width="150" height="150"></canvas>
```

```
init();
```

#### 循環景色

本例會產一個由左到右循環捲動美國優勝美地國家公園景色，你也可以自行替換其他比畫布還大的圖片。

```js
var img = new Image();

// User Variables - customize these to change the image being scrolled, its
// direction, and the speed.

img.src = "/files/4553/Capitan\_Meadows,\_Yosemite\_National\_Park.jpg";
var CanvasXSize = 800;
var CanvasYSize = 200;
var speed = 30; //lower is faster
var scale = 1.05;
var y = -4.5; //vertical offset

// Main program

var dx = 0.75;
var imgW;
var imgH;
var x = 0;
var clearX;
var clearY;
var ctx;

img.onload = function () {
  imgW = img.width \* scale;
  imgH = img.height \* scale;
  if (imgW > CanvasXSize) {
    x = CanvasXSize - imgW;
  } // image larger than canvas
  if (imgW > CanvasXSize) {
    clearX = imgW;
  } // image larger than canvas
  else {
    clearX = CanvasXSize;
  }
  if (imgH > CanvasYSize) {
    clearY = imgH;
  } // image larger than canvas
  else {
    clearY = CanvasYSize;
  }
  //Get Canvas Element
  ctx = document.getElementById("canvas").getContext("2d");
  //Set Refresh Rate
  return setInterval(draw, speed);
};

function draw() {
  //Clear Canvas
  ctx.clearRect(0, 0, clearX, clearY);
  //If image is <= Canvas Size
  if (imgW <= CanvasXSize) {
    //reset, start from beginning
    if (x > CanvasXSize) {
      x = 0;
    }
    //draw aditional image
    if (x > CanvasXSize - imgW) {
      ctx.drawImage(img, x - CanvasXSize + 1, y, imgW, imgH);
    }
  }
  //If image is > Canvas Size
  else {
    //reset, start from beginning
    if (x > CanvasXSize) {
      x = CanvasXSize - imgW;
    }
    //draw aditional image
    if (x > CanvasXSize - imgW) {
      ctx.drawImage(img, x - imgW + 1, y, imgW, imgH);
    }
  }
  //draw image
  ctx.drawImage(img, x, y, imgW, imgH);
  //amount to move
  x += dx;
}
```

循環景色就是在下方的`<canvas>`中捲動，請注意其中的 width 和 height 和程式碼中的 CanvasXZSize 與 CanvasYSize 一樣。

```html
<canvas id="canvas" width="800" height="200"></canvas>
```

##### 結果

其他範例
----

Gartic

多人繪圖遊戲

Canvascape

第一人稱 3D 冒險遊戲

A basic ray-caster

透過鍵盤控制動畫範例

canvas adventure

另一個透過鍵盤控制動畫範例

An interactive Blob

和 Blob 遊戲

Flying through a starfield

飛越星河

iGrapher

股票市場圖

See also
--------

* JavaScript timers
* `setInterval` – A little framework (en-US)
* JavaScript Daemons Management
* HTMLCanvasElement (en-US)
* « 前頁
* 次頁 » (en-US)


控制畫面方向 - Web APIs

控制畫面方向
======

**實驗性質:** **這是一個實驗中的功能 (en-US)**
此功能在某些瀏覽器尚在開發中，請參考兼容表格以得到不同瀏覽器用的前輟。

摘要
--

畫面方向（Screen Orientation）與裝置方向（Device Orientation） (en-US)略有不同。有時甚至裝置本身不具備方向偵測功能，但裝置的螢幕仍搭載方向功能。如果裝置可測知本身的方向又能控制畫面方向，就能隨時配合 Web Apps 而達到最佳效果。

現有 2 種方法可處理畫面的方向，但均需搭配 CSS 與 JavaScript。第一種方法就是方向的 Media Query (en-US)。根據瀏覽器視窗為橫放（寬度大於高度）或直放（高度大於寬度）狀態，而透過 CSS 調整網頁內容的配置。

第二種方法就是 JavaScript Screen Orientation API，可取得畫面目前的方向並進一步鎖定。

根據方向而調整配置
---------

方向改變最常見的情形之一，就是根據裝置的方向而修正內容的配置方式。舉例來說，你可能想將按鈕列拉到與裝置螢幕等長。而透過 Media Query 即可輕鬆達到此效果。

來看看下列 HTML 程式碼範例：

```html
<ul id="toolbar">
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

<p>
  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis lacinia nisi nec
  sem viverra vitae fringilla nulla ultricies. In ac est dolor, quis tincidunt
  leo. Cras commodo quam non tortor consectetur eget rutrum dolor ultricies. Ut
  interdum tristique dapibus. Nullam quis malesuada est.
</p>
```

CSS 將根據方向的 Media Query，處理畫面方向的特殊樣式：

```css
/\* First let's define some common styles \*/

html,
body {
  width: 100%;
  height: 100%;
}

body {
  border: 1px solid black;

  -moz-box-sizing: border-box;
  box-sizing: border-box;
}

p {
  font: 1em sans-serif;
  margin: 0;
  padding: 0.5em;
}

ul {
  list-style: none;

  font: 1em monospace;
  margin: 0;
  padding: 0.5em;

  -moz-box-sizing: border-box;
  box-sizing: border-box;

  background: black;
}

li {
  display: inline-block;
  margin: 0;
  padding: 0.5em;
  background: white;
}
```

在設定某些通用的樣式之後，即可針對方向定義特殊條件：

```css
/\* For portrait, we want the tool bar on top \*/

@media screen and (orientation: portrait) {
  #toolbar {
    width: 100%;
  }
}

/\* For landscape, we want the tool bar stick on the left \*/

@media screen and (orientation: landscape) {
  #toolbar {
    position: fixed;
    width: 2.65em;
    height: 100%;
  }

  p {
    margin-left: 2em;
  }

  li + li {
    margin-top: 0.5em;
  }
}
```

結果如下所示（若無法顯示，可至本文右上角切換回英文原文觀看）：

| Portrait | Landscape |
| --- | --- |
|  |  |

**備註：** 方向 Media Query 其實是以瀏覽器視窗 (或 iframe) 的方向為準，而非裝置本身的方向。

鎖定畫面方向
------

**警告：** 此 API 仍屬實驗性質，目前仍具備 `moz` 前綴而僅能用於 Firefox OS 與 Firefox for Android，而 Windows 8.1 以上版本的 Internet Explorer 則使用 `ms` 前綴。

某些裝置（主要為行動裝置）可根據本身方向而動態改變畫面的方向，讓使用者隨時閱讀畫面上的資訊。這種動作對文字類的內容影響不大，但某些內容就無法順利套用此功能。舉例來說，若遊戲需要裝置方向的相關資訊，就可能因為方向變化而發生混亂情形。

而 Screen Orientation API 即可用以避免或處理這類變化。

### 監聽方向變化

只要裝置改變了畫面方向與本身方向，就會觸發 `orientationchange` 事件，再由 `Screen.orientation` 屬性讀取之。

```js
screen.addEventListener("orientationchange", function () {
  console.log("The orientation of the screen is: " + screen.orientation);
});
```

### 避免方向改變

任何 Web Apps 均可鎖定畫面以符合本身需求。`Screen.lockOrientation()` (en-US) 函式可鎖定畫面方向；`Screen.unlockOrientation()` (en-US) 函式可解鎖畫面方向。

`Screen.lockOrientation()` (en-US) 將接受一組字串或系列字串，以定義畫面鎖定的方向。有效字串為：「`portrait-primary`」、「`portrait-secondary`」、「`landscape-primary`」、「`landscape-secondary`」、「`portrait`」、「`landscape`」。另可參閱 `Screen.lockOrientation` (en-US) 進一步了解這些有效值。

```js
screen.lockOrientation("landscape");
```

**備註：** 畫面鎖定功能將依 Web Apps 而有所不同。如果 App A 鎖定為 `landscape`；App B 鎖定為 `portrait，則此兩款 Apps 均將維持自己的方向。所以不論如何切換` A 與 B，均不會觸發 `orientationchange` 事件。但若必須改變方向以滿足畫面鎖定的需求，則鎖定方向時就會觸發 `orientationchange` 事件。

Firefox OS and Android: Orientation lock using the manifest
-----------------------------------------------------------

For a Firefox OS and Firefox Android (soon to work on Firefox desktop too) specific way to lock orientation, you can set the orientation field in app's your manifest file, for example:

```json
"orientation": "portrait"
```

參見
--

* `Screen.orientation`
* `Screen.lockOrientation()` (en-US)
* `Screen.unlockOrientation()` (en-US)
* `Screen.onorientationchange` (en-US)
* 方向的 Media Query (en-US)
* Firefox 3.5 的 Media Queries 簡介


使用IndexedDB - Web APIs

使用IndexedDB
===========

IndexedDB 提供了在瀏覽器上儲存保留資料的功能，藉由它，不論是線上或線下我們的應用都可以進行資料存取。

關於本文
----

本文會帶領各位操作非同步 IndexedDB 的 API，如果不知道甚麼是 IndexedDB，請先看看"IndexedDB 基本礎念"。

基本操作步驟
------

操作 IndexedDB 的基本步驟建議如下:

1. 開啟資料庫和交易(transaction)。
2. 建立物件存檔(object store)
3. 發出資料庫操作請求，例如新增或取得資料。
4. 聆聽對應 DOM 事件等待操作完成。
5. 從 result 物件上取得結果進行其他工作。

好了，知道了上述概念後，我們可以來實際做些甚麼。

建立和結構資料庫
--------

由於 IndexedDB 的標準仍在演進，所以目前一些實作還需要加上瀏覽器前綴標示(如 Gecko 基礎瀏覽器的前綴標示為 moz，WebKit 基礎瀏覽器的前綴標示為 webkit)，瀏覽器的實作也可能會有所差異，不過一旦共識標準達成，無瀏覽器前綴標示實作將出現。其實，Internet Explorer 10, Firefox 16, Chrome 24 已經有了無瀏覽器前綴標示實作。

### 操作實驗性質的 IndexedDB

如果需要試驗瀏覽器的前綴標示，可以如下:

```js
// In the following line, you should include the prefixes of implementations you want to test.
window.indexedDB =
  window.indexedDB ||
  window.mozIndexedDB ||
  window.webkitIndexedDB ||
  window.msIndexedDB;
// DON'T use "var indexedDB = ..." if you're not in a function.
// Moreover, you may need references to some window.IDB\* objects:
window.IDBTransaction =
  window.IDBTransaction ||
  window.webkitIDBTransaction ||
  window.msIDBTransaction;
window.IDBKeyRange =
  window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
// (Mozilla has never prefixed these objects, so we don't need window.mozIDB\*)
```

請注意瀏覽器前綴標示的實作可能不完整、有些問題或仍然遵守舊版標準，因此不建議在正式版程式碼中使用。與其宣稱支援又有問題，倒不如直接不支援。

```js
if (!window.indexedDB) {
  window.alert(
    "Your browser doesn't support a stable version of IndexedDB. Such and such feature will not be available.",
  );
}
```

### 開啟資料庫

開頭如下:

```js
// Let us open our database
var request = window.indexedDB.open("MyTestDatabase", 3);
```

注意到了嗎，開啟資料庫必須要進行請求。

開啟請求並不會立刻開啟資料庫或交易，呼叫 open()方法會回傳一個`IDBOpenDBRequest` (en-US)物件，這個物件擁有兩個事件(success 和 error)。大部分 IndexedDB 的非同步功能都會回傳一個`IDBDatabase` (en-US)類物件，然後我們可以註冊成功和失敗事件處理器。

Open 方法第二個參數是資料庫版本，資料庫版本決定了資料庫結構，也就是資料庫物件存檔的結構。如果請求版本不存在(比如因為這是一個新資料庫或是資料庫版本已升級)，onupgradeneeded 事件會被觸發，然後我們可以在 onupgradeneeded 事件處理器中再建立新的版本，下面升級資料庫版本有更詳細的說明。

#### 產生事件處理器

幾乎所有第一件要對請求做的事都是產生 success 和 error 事件處理器:

```js
request.onerror = function (event) {
  // Do something with request.errorCode!
};
request.onsuccess = function (event) {
  // Do something with request.result!
};
```

如果一切正常，success 事件(也就是 DOM 事件的 type 屬性設為 success)會以 request 為目標觸發，然後 request 物件上的 onsuccess 函數接著被呼叫，其中 success 事件就是參數；否則 error 事件(也就是 DOM 事件的 type 屬性設為 error)會以 request 為目標觸發，然後 request 物件上的 onerror 函數接著被呼叫，其中 error 事件就是參數。

IndexedDB 的 API 設計上盡量減少錯誤處理，所以不太常看到錯誤事件，不過開啟資料庫的時候還是有一些狀況會產產生錯誤，最常見的狀況是使用者拒絕我們建立資料庫。

IndexedDB 設計目標之一為存放大量資料以供離線使用(請參考"儲存限制"了解更多細節)。但很明顯地，瀏覽器又不樂見一些廣告網站或惡意網站汙染電腦，所以當任一個網路應用第一次開啟 IndexedDB 資料庫，瀏覽器會徵詢使用者是否准許其作業；同時在私密瀏覽中開啟作業也會失敗，因為私密瀏覽不會留下任何瀏覽痕跡。

這裡呼叫 indexedDB.open()開啟 indexedDB 資料庫並回傳 request 物件，假設使用者允許我們建立 indexedDB 資料庫，我們也收到 suceess 事件觸發了 success 回呼函數(callback)，request 物件的 result 屬性會是一個 IDBDatabase 物件 ，接下來便是要儲存這個物件之後使用。下方是整個作業的示範程式碼:

```js
var db;
var request = indexedDB.open("MyTestDatabase");
request.onerror = function (event) {
  alert("Why didn't you allow my web app to use IndexedDB?!");
};
request.onsuccess = function (event) {
  db = request.result;
};
```

#### 錯誤處理

錯誤事件會向上傳遞；錯誤事件以產生錯誤之請求為目標觸發，然後一路傳遞到交易，最後到資料庫物件；如果不想要為每一個請求新增錯誤處理器，可以改為資料庫物件加入一個錯誤處理器。

```js
db.onerror = function (event) {
  // Generic error handler for all errors targeted at this database's
  // requests!
  alert("Database error: " + event.target.errorCode);
};
```

最常見的錯誤之一就是 VER\_ERR，該錯誤代表現在資料料庫版本大於嘗試開啟的資料庫版本，這項錯誤必須要有錯誤處理器處理。

### 建立或更新資料庫版本

新版本資料庫建立會觸發 onupgradeneeded 事件，在這個事件的處理器中要建立這個版本資料庫需要的物件存檔。

```js
// This event is only implemented in recent browsers
request.onupgradeneeded = function (event) {
  var db = event.target.result;

  // Create an objectStore for this database
  var objectStore = db.createObjectStore("name", { keyPath: "myKey" });
};
```

資料庫版本是 unsigned long long 的數字，所以能夠非常長。

**警告：** 請注意這也意味著版本不能為浮點數，否則小數點部分將會無條件捨去，而交易也可能不會開始，upgradeneeded 事件也不會觸發。不要像以下例子以 2.4 作版本:

```js
var request = indexedDB.open("MyTestDatabase", 2.4); // don't do this, as the version will be rounded to 2
```

升級版本資料庫建立會觸發 onupgradeneeded 事件，這個時候資料庫裡面已經含有前版本下的物件存檔，所以說不需要再次建立這些物件存檔了，剩下的是新增或刪除物件存檔。如果想要更動既存物件存檔(例如改變資料鍵路徑)，那麼會需要先刪除舊的再產生一個新的(請注意這也會刪除物件存檔裡的資料，所以如果資料需要保留的話，請在升級前先讀出資料備份。)

Webkit 支援最新標準不過只有 Chrome 23 才開始導入，而較舊不支援最新版標準的版本則不支援 indexedDB.open(name, version).onupgradeneeded。關於如何在舊版標準下升級資料庫版本請參考"IDBDatabase 參考文章"。

### 結構化資料庫

indexedDB 不用資料表而是物件存檔，物件存檔可以有很多。一筆物件存檔裡的資料值對應一筆資料鍵，依據使用{資料鍵路徑(key path)}或{資料鍵產生器(key generator)}。

下表列出資料建各類產生途徑:

| Key Path | Key Generator | 描述 |
| --- | --- | --- |
| No | No | 物件存檔資料值能為任何型別，即使像數字或字串。每當新增一筆資料，必須提供不同的資料鍵。 |
| Yes | No | 物件存檔資料值僅能為 Javacript 物件，而該資料物件必須含有和資料鍵路徑相同名稱之屬性成員。 |
| No | Yes | 物件存檔資料值能為任何型別，資料鍵自動產生，但如果想要指定資料鍵也可以另外提供資料鍵。 |
| Yes | Yes | 物件存檔資料值僅能為 Javascript 物件。通常被產生的新資料鍵的值會被存在物件屬性名稱和資料鍵路徑名稱相同的物件屬性下，如果這個屬性已經存在，這個已經存在之屬性之值將被用作為資料鍵，而非新產生的資料鍵。 |

我們可以替任何儲存資料為物件型態而非原始資料型態的物件存檔建立索引，索引讓我們能夠用物件存檔中資料物件內的某一個屬性值查找資料，而非僅僅物件的資料鍵。

除此之外，利用索引還可以施加簡單的儲存限制；建立索引時設定獨特旗標(flag)，這個索引保證在此索引資料鍵下不會存在兩個物件存檔擁有同樣資料值，比如說現在有一個存放許多使用者的物件存檔，而且我們希望保證不會存在有兩個使用者的電子郵件地址一樣，此使索引的獨特旗標便可以幫助我們達成目標。

以上聽起來可能會有些複雜，請看一下下面的實例:

```js
// This is what our customer data looks like.
const customerData = [
  { ssn: "444-44-4444", name: "Bill", age: 35, email: "bill@company.com" },
  { ssn: "555-55-5555", name: "Donna", age: 32, email: "donna@home.org" },
];
const dbName = "the\_name";

var request = indexedDB.open(dbName, 2);

request.onerror = function (event) {
  // Handle errors.
};
request.onupgradeneeded = function (event) {
  var db = event.target.result;

  // Create an objectStore to hold information about our customers. We're
  // going to use "ssn" as our key path because it's guaranteed to be
  // unique.
  var objectStore = db.createObjectStore("customers", { keyPath: "ssn" });

  // Create an index to search customers by name. We may have duplicates
  // so we can't use a unique index.
  objectStore.createIndex("name", "name", { unique: false });

  // Create an index to search customers by email. We want to ensure that
  // no two customers have the same email, so use a unique index.
  objectStore.createIndex("email", "email", { unique: true });

  // Store values in the newly created objectStore.
  for (var i in customerData) {
    objectStore.add(customerData[i]);
  }
};
```

請注意 onupgradeneeded 事件是唯一能夠變更資料庫結構之處，在此事件才能建立或刪除物件存檔以及建立和刪除索引。

呼叫IDBDatabase類別物件的 createObjectStore 方法會立刻創造一個物件存檔，這個方法接收兩個參數，一個是物件存檔名稱，一個是非必填的參數物件，雖然參數物件不為必填但是卻相當重要，因為它讓我們定義了一些重要屬性(請參考createObjectStore)。本例中我們要求建立了一個"customer"物件存檔，定義"ssn"屬性為資料件路徑，使用"ssn"作為資料鍵路徑是因為每個客戶的 ssn 碼一定是獨立的。一旦決定了"ssn"作為資料鍵路徑，那麼每一筆資料一定要含有"ssn"。

本例還創建一個稱為"name"的索引，"name"索引查找目標為資料的"name"屬性，且不設立其獨特旗標(unique 為 false)，同樣地，我們又呼叫createIndex方法創建了一個"email"索引，不過"email"索引具備獨特旗標(unique 為 true)。雖然存在"name"索引，但資料不一定要含有"name"屬性，只是當搜索"name"索引時資料不會出現。

接下來我們可以開始用 ssn 從物件存檔中取出資料，或是用索引找出資料(請參考使用索引)。

### 使用資料鍵產生器

當建立物件存檔時設定 autoIncrement 旗標為 ture 將啟動資料鍵產生器，預設上該旗標為 false。

有了資料鍵產生器，當新增資料到物件存檔中，資料鍵產生器會自動幫我們產生資料鍵。資料鍵產生器產生的資料鍵由整數 1 開始，而基本上新產生的資料鍵是由前一個資料鍵加 1 產生。資料鍵的產生不會因為資料刪除或清空所有資料而重新開始起算，所以資料鍵值是一直累加上去的，除非資料庫操作中斷，整個交易作業被取消。

我們可以建立一個有資料鍵產生器的物件存檔如下:

```js
// Open the indexedDB.
var request = indexedDB.open(dbName, 3);

request.onupgradeneeded = function (event) {
  var db = event.target.result;

  // Create another object store called "names" with the autoIncrement flag set as true.
  var objStore = db.createObjectStore("names", { autoIncrement: true });

  // Because the "names" object store has the key generator, the key for the name value is generated automatically.
  // The added records would be like:
  // key : 1 => value : "Bill"
  // key : 2 => value : "Donna"
  for (var i in customerData) {
    objStore.add(customerData[i].name);
  }
};
```

關於資料鍵產生器細節，請參考"W3C Key Generators"。

新增和刪除資料
-------

在操作資料庫之前必須要先進行交易，交易來自資料庫物件，在交易中要指定涵蓋物件存檔範圍，然後也要決定是要變更資料庫或純粹讀取資料。交易共有三種種類，分別是讀取(read-only)，讀寫(read/write), 以及版本變更(versionchange)，如果只需要讀資料最好只使用讀取(read-only)交易，因為讀取(read-only)交易可以多重同步進行。

### 新增資料到資料庫

創建資料庫後，如果要寫入資料請這麼做:

```js
var transaction = db.transaction(["customers"], "readwrite");
// Note: Older experimental implementations use the deprecated constant IDBTransaction.READ\_WRITE instead of "readwrite".
// In case you want to support such an implementation, you can just write:
// var transaction = db.transaction(["customers"], IDBTransaction.READ\_WRITE);
```

呼叫transaction() (en-US)方法會回傳一個交易物件。transaction()第一個接受參數代表交易涵蓋的物件存檔，雖然傳入空陣列會讓交易涵蓋所有物件存檔，但請不要這麼做，因為根據正式標準傳入空陣列應該要導致 InvalidAccessError 錯誤；第二個參數代表交易種類，不傳入的話預設為讀取交易，本例要寫入資料庫所以傳入讀寫("readwrite")。

交易的生命週期和事件循環關係密切。當我們發起交易又回到事件循環中後，如果忽略，那麼交易將轉成結束，唯一保持交易存活的方法是在交易上發出請求；當請求完成後我們會收到 DOM 事件，假設請求結果成功，我們可以在事件的回呼函數(callback 中)繼續進行交易，反之，如果我們沒有繼續進行交易，那麼交易將結束，也就是說只要尚有未完成請求的話，交易就會繼續存活，如果收到 TRANSACTION\_INACTIVE\_ERR 錯誤那便意謂著交易早已結束，我們錯過了繼續進行交易的機會。

交易能收到三種事件: 錯誤(error)、中斷(abort)以及完成(complete)，其中錯誤事件會向上傳遞，所以任何一個交易下轄的請求產生錯誤事件，該交易都會收到。如果交易收到錯誤事件時，瀏覽器預設行為會中斷交易，除非我們有在錯誤事件上呼叫 preventDefault()阻擋瀏覽器預設行動，否則整筆交易都將取消、復原，這樣的設計告訴我們必須要思考如何處裡錯誤，或者說如果對每一個錯誤進行處裡過於麻煩，那麼至少加入一個概括性的錯誤處理器也是可以。只要不處裡錯誤或呼叫 abort()，交易將取消、復原，然後中斷事件接著觸發，反之，當所有請求都完成後，我們會收到一個完成事件，所以說如果我們同時發起多項請求時，可以只追蹤單一交易而非個別請求，這樣會大大減輕我們的負擔。

有了交易之後便能夠從中取得物件存檔，有了物件存檔便能夠新增資料(請注意唯有在建立交易時指定的物件存檔能夠取得)。

```js
// Do something when all the data is added to the database.
transaction.oncomplete = function (event) {
  alert("All done!");
};

transaction.onerror = function (event) {
  // Don't forget to handle errors!
};

var objectStore = transaction.objectStore("customers");
for (var i in customerData) {
  var request = objectStore.add(customerData[i]);
  request.onsuccess = function (event) {
    // event.target.result == customerData[i].ssn;
  };
}
```

呼叫add (en-US)方法可以加入一筆新資料，呼叫後會回傳一個IDBRequest物件，即為上方範例中的 request，如果新增成功，request 的成功事件會被觸發，而成功事件物件中有一個 result 屬性，這個 result 值剛好就等於新資料的資料鍵，所以說上方範例中的 event.target.result 剛好就等於顧客的 ssn 值(因為我們用 ssn 屬性作為資料鍵路徑)。請注意 add 方法只在當物件存檔中沒有相同資料鍵資料存在時有用，如果想要更動或是直接覆蓋現存資料請呼叫put (en-US)方法。

移除資料
----

移除資料十分容易:

```js
var request = db
  .transaction(["customers"], "readwrite")
  .objectStore("customers")
  .delete("444-44-4444");
request.onsuccess = function (event) {
  // It's gone!
};
```

讀取資料
----

要圖取資料庫內的資料有數種途徑，第一個最簡單的途徑就是提供資料鍵，呼叫get (en-US)方法取得資料:

```js
var transaction = db.transaction(["customers"]);
var objectStore = transaction.objectStore("customers");
var request = objectStore.get("444-44-4444");
request.onerror = function (event) {
  // Handle errors!
};
request.onsuccess = function (event) {
  // Do something with the request.result!
  alert("Name for SSN 444-44-4444 is " + request.result.name);
};
```

假設我們把錯誤處理放在資料庫層級，我們可以再縮短上面的程式碼如下:

```js
db
  .transaction("customers")
  .objectStore("customers")
  .get("444-44-4444").onsuccess = function (event) {
  alert("Name for SSN 444-44-4444 is " + event.target.result.name);
};
```

呼叫 transcation 方法而不指定模式會開啟讀取(readonly)模式，接著取得我們的目標物件存檔，輸入目標資料的資料鍵，呼叫 get 方法取得請求物件，然後在請求物件上註冊成功事件處理器，當作業成功後，成功事件會觸發，成功事件的物件中含有請求物件(event.target 如上述範例)，請求物件中含有請求結果(event.target.result 如上述範例)。

使用指標(Cursor)
------------

使用 get 方法需要知道資料鍵，如果想要一一存取物件存檔中的資料，我們可以利用指標:

```js
var objectStore = db.transaction("customers").objectStore("customers");

objectStore.openCursor().onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    alert("Name for SSN " + cursor.key + " is " + cursor.value.name);
    cursor.continue();
  } else {
    alert("No more entries!");
  }
};
```

openCursor (en-US)方法第一個參數用來接受資料鍵範圍物件來限制存取資料範圍，第二個參數用來指定存取進行方向，像上面的範例程式便是以資料鍵由小到大之方向存取資料；呼叫 openCursor 方法後一樣會回傳一個請求物件，成功時成功事件會觸發，不過這裡有些地方要特別注意，當成功事件處裡函數被喚起時，指標物件(cursor)會存放在 result 屬性內(亦即上述 event.target.result)，cursor 物件下有兩個屬性，key 屬性是資料鍵，value 屬性是資料值，如果要取得下一份資料就呼叫 cursor 的 continue()方法，然後 cursor 物件就會指向下一份資料，當沒有資料時，cursor 會是 undefined，當請求一開始便找沒有資料，result 屬性也會是 undefined。

以下用 cursor 存取一遍資料後放在陣列中的作法相當常見:

```js
var customers = [];

objectStore.openCursor().onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    customers.push(cursor.value);
    cursor.continue();
  } else {
    alert("Got all customers: " + customers);
  }
};
```

**警告：** 以下範例不是 IndexedDB 標準!

Mozilla 瀏覽器自己做了一個 getAll()方法來方便一次取得所有 cursor 下的資料值，這個方法相當方便，不過請小心未來它有可能會消失。以下程式碼的效果和上面的一樣:

```js
objectStore.getAll().onsuccess = function (event) {
  alert("Got all customers: " + event.target.result);
};
```

一一檢查 cursor 的 value 屬性較不利性能表現，因為物件是被動一一建立，然而呼叫 getAll()，Gecko 一定要一次建立所有物件，所以如果想要一次取得所有物件的資料值陣列使用 getAll()比較好，如果想要一一檢查每筆資料則請利用 cursor 的方法。

### 使用索引

利用一定唯一的 ssn 碼作為資料鍵相當合乎邏輯(隱私權的問題先擱置一放，不在本文探討範圍)。不過當我們想要查詢使用者的名字的時候，如果沒有索引就需要一一檢查每一筆資料，十分沒有效率，所以我們可以建立 name 的索引。

```js
var index = objectStore.index("name");
index.get("Donna").onsuccess = function (event) {
  alert("Donna's SSN is " + event.target.result.ssn);
};
```

因為 name 不是唯一值，所以可能會有多筆資料符合"Donna"名字，此時呼叫 get()會取得資料鍵最小值的資料。

如果我們想要查看特定名字下所有的資料，可以利用 cursor。有兩種 cursor 能用在索引上，第一種一般 cursor 會比對索引值並回傳整份資料(物件存檔中的物件)，第二種資料鍵 cursor 則只會回傳資料鍵值，請參考下方範例比較兩者差異:

```js
index.openCursor().onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the whole object.
    alert(
      "Name: " +
        cursor.key +
        ", SSN: " +
        cursor.value.ssn +
        ", email: " +
        cursor.value.email,
    );
    cursor.continue();
  }
};

index.openKeyCursor().onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    // cursor.key is a name, like "Bill", and cursor.value is the SSN.
    // No way to directly get the rest of the stored object.
    alert("Name: " + cursor.key + ", SSN: " + cursor.value);
    cursor.continue();
  }
};
```

### 設定指標查詢範圍和方向

如果想要限定指標查詢範圍，那麼在乎叫 openCursor()或 openKeyCursor()時第一個參數要傳入IDBKeyRange物件以限制範圍。IDBKeyRange 物件能夠只聚焦在單一資料鍵上或者一段上下限區間；上下限區間可以是封閉(含界限)或開放(不含界限)，請看以下範例:

```js
// Only match "Donna"
var singleKeyRange = IDBKeyRange.only("Donna");

// Match anything past "Bill", including "Bill"
var lowerBoundKeyRange = IDBKeyRange.lowerBound("Bill");

// Match anything past "Bill", but don't include "Bill"
var lowerBoundOpenKeyRange = IDBKeyRange.lowerBound("Bill", true);

// Match anything up to, but not including, "Donna"
var upperBoundOpenKeyRange = IDBKeyRange.upperBound("Donna", true);

// Match anything between "Bill" and "Donna", but not including "Donna"
var boundKeyRange = IDBKeyRange.bound("Bill", "Donna", false, true);

index.openCursor(boundKeyRange).onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the matches.
    cursor.continue();
  }
};
```

有時候我們會想要由大到小查看資料而非預設由小到大方向，傳入第二個"prev"字串便能做到:

```js
objectStore.openCursor(null, "prev").onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};
```

由於"name"索引不具唯一性，所以一個名字下可能會出現多筆資料，此時如果想要避開這多筆資料，請傳入"nextunique"或"prevunique"做為第二個方向參數，當傳入之後，如一個名字下遇到多筆資料，則只有資料鍵最小的資料會被回傳。

```js
index.openKeyCursor(null, "nextunique").onsuccess = function (event) {
  var cursor = event.target.result;
  if (cursor) {
    // Do something with the entries.
    cursor.continue();
  }
};
```

關於可傳入的方向參數，請參考IDBCursor常數。

當網頁應用程式於瀏覽器另一個分頁開啟時變更版本
-----------------------

請思考以下狀況: 使用者在第一個瀏覽器分頁中使用著舊版本，然後使用者又打開第二個分頁載入網頁，此時我們在新分頁需要對資料庫進行升級，所以呼叫 open()以更新版本開啟資料庫，但是由於第一個瀏覽器分頁並沒有關閉資料庫，因此第二個分頁的資料庫升級作業會被擋住。所以我們需要注意多個分頁同時開啟的狀況，每個分頁都得注意當發生資料庫升級事件時，要記得關閉資料庫，讓資料庫升級作業得以進行。實際作法如下:

```js
var openReq = mozIndexedDB.open("MyTestDatabase", 2);

openReq.onblocked = function (event) {
  // If some other tab is loaded with the database, then it needs to be closed
  // before we can proceed.
  alert("Please close all other tabs with this site open!");
};

openReq.onupgradeneeded = function (event) {
  // All other databases have been closed. Set everything up.
  db.createObjectStore(/\* ... \*/);
  useDatabase(db);
};

openReq.onsuccess = function (event) {
  var db = event.target.result;
  useDatabase(db);
  return;
};

function useDatabase(db) {
  // Make sure to add a handler to be notified if another page requests a version
  // change. We must close the database. This allows the other page to upgrade the database.
  // If you don't do this then the upgrade won't happen until the user closes the tab.
  db.onversionchange = function (event) {
    db.close();
    alert("A new version of this page is ready. Please reload!");
  };

  // Do stuff with the database.
}
```

安全性
---

IndexedDB 遵守同源政策，所以它綁定創建它的來源網站，其他來源網站無法存取。就像對載入 `<frame>` (en-US) 和 `<iframe>` (en-US) 網頁的第三方 cookie 所設下的安全性和隱私權考量限制，IndexedDB 無法在載入 `<frame>` (en-US) 和 `<iframe>` (en-US) 網頁上運作，詳情請見 Firefox bug 595307。

瀏覽器關閉風險
-------

當瀏覽器關閉，例如使用者按下關閉鈕，任何未完成 IndexedDB 交易都將默默中止，這些交易不會完成，錯誤事件也不會觸發。既然瀏覽器可能隨時被關閉，我們無法完全指望任何特定交易一定會完成，或是依賴錯誤事件做出相應處理，針對這種狀況，我們需要注意:

第一、每一筆交易結束後都應該要保持資料庫完整性，例如說，有一串使用者編輯項目清單正要存入資料庫，我們如果先在一個交易中清除舊清單，然後在另一個交易中存入新清單，那就會面臨到清除完就清單後，新清單存入交易還來不及回存，瀏覽器就關閉的風險，而這個時候資料庫裡面的清單資料將消失。所以比較好的做法應該是在同一筆交易中完成清除舊清單和存入新清單。

第二、永遠不要在 unload 事件中執行資料庫交易，因為如果 unload 事件是觸發在瀏覽器關閉下，任何資料庫交易都不會發生，或許，瀏覽器(或分頁)打開時讀取資料庫，更新資料庫當使用者編輯資料，當瀏覽器(或分頁)關閉時儲存資料這樣的做法比較直覺，不過資料庫的操作是非同步進行地，所以瀏覽器關閉的執行會在資料庫操作前發生，進而中斷後續非同步的資料庫交易，所以在 unload 事件中執行資料庫交易是行不通地。

事實上不論瀏覽器是否正常關閉，都沒有任何方法保證 IndexedDB 交易能夠順利完成，請見 Firefox bug 870645。

完整 IndexedDB 範例
---------------

### HTML

```html
<script
 type="text/javascript"
 src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>

<h1>IndexedDB Demo: storing blobs, e-publication example</h1>
<div class="note">
  <p>Works and tested with:</p>
  <div id="compat"></div>
</div>

<div id="msg"></div>

<form id="register-form">
  <table>
    <tbody>
      <tr>
        <td>
          <label for="pub-title" class="required"> Title: </label>
        </td>
        <td>
          <input type="text" id="pub-title" name="pub-title" />
        </td>
      </tr>
      <tr>
        <td>
          <label for="pub-biblioid" class="required">
            Bibliographic ID:<br />
            <span class="note">(ISBN, ISSN, etc.)</span>
          </label>
        </td>
        <td>
          <input type="text" id="pub-biblioid" name="pub-biblioid" />
        </td>
      </tr>
      <tr>
        <td>
          <label for="pub-year"> Year: </label>
        </td>
        <td>
          <input type="number" id="pub-year" name="pub-year" />
        </td>
      </tr>
    </tbody>
    <tbody>
      <tr>
        <td>
          <label for="pub-file"> File image: </label>
        </td>
        <td>
          <input type="file" id="pub-file" />
        </td>
      </tr>
      <tr>
        <td>
          <label for="pub-file-url">
            Online-file image URL:<br />
            <span class="note">(same origin URL)</span>
          </label>
        </td>
        <td>
          <input type="text" id="pub-file-url" name="pub-file-url" />
        </td>
      </tr>
    </tbody>
  </table>

  <div class="button-pane">
    <input type="button" id="add-button" value="Add Publication" />
    <input type="reset" id="register-form-reset" />
  </div>
</form>

<form id="delete-form">
  <table>
    <tbody>
      <tr>
        <td>
          <label for="pub-biblioid-to-delete">
            Bibliographic ID:<br />
            <span class="note">(ISBN, ISSN, etc.)</span>
          </label>
        </td>
        <td>
          <input
 type="text"
 id="pub-biblioid-to-delete"
 name="pub-biblioid-to-delete" />
        </td>
      </tr>
      <tr>
        <td>
          <label for="key-to-delete">
            Key:<br />
            <span class="note">(for example 1, 2, 3, etc.)</span>
          </label>
        </td>
        <td>
          <input type="text" id="key-to-delete" name="key-to-delete" />
        </td>
      </tr>
    </tbody>
  </table>
  <div class="button-pane">
    <input type="button" id="delete-button" value="Delete Publication" />
    <input
 type="button"
 id="clear-store-button"
 value="Clear the whole store"
 class="destructive" />
  </div>
</form>

<form id="search-form">
  <div class="button-pane">
    <input
 type="button"
 id="search-list-button"
 value="List database content" />
  </div>
</form>

<div>
  <div id="pub-msg"></div>
  <div id="pub-viewer"></div>
  <ul id="pub-list"></ul>
</div>
```

### CSS

```css
body {
  font-size: 0.8em;
  font-family: Sans-Serif;
}

form {
  background-color: #cccccc;
  border-radius: 0.3em;
  display: inline-block;
  margin-bottom: 0.5em;
  padding: 1em;
}

table {
  border-collapse: collapse;
}

input {
  padding: 0.3em;
  border-color: #cccccc;
  border-radius: 0.3em;
}

.required:after {
  content: "\*";
  color: red;
}

.button-pane {
  margin-top: 1em;
}

#pub-viewer {
  float: right;
  width: 48%;
  height: 20em;
  border: solid #d092ff 0.1em;
}
#pub-viewer iframe {
  width: 100%;
  height: 100%;
}

#pub-list {
  width: 46%;
  background-color: #eeeeee;
  border-radius: 0.3em;
}
#pub-list li {
  padding-top: 0.5em;
  padding-bottom: 0.5em;
  padding-right: 0.5em;
}

#msg {
  margin-bottom: 1em;
}

.action-success {
  padding: 0.5em;
  color: #00d21e;
  background-color: #eeeeee;
  border-radius: 0.2em;
}

.action-failure {
  padding: 0.5em;
  color: #ff1408;
  background-color: #eeeeee;
  border-radius: 0.2em;
}

.note {
  font-size: smaller;
}

.destructive {
  background-color: orange;
}
.destructive:hover {
  background-color: #ff8000;
}
.destructive:active {
  background-color: red;
}
```

### JavaScript

```js
(function () {
  var COMPAT\_ENVS = [
    ["Firefox", ">= 16.0"],
    [
      "Google Chrome",
      ">= 24.0 (you may need to get Google Chrome Canary), NO Blob storage support",
    ],
  ];
  var compat = $("#compat");
  compat.empty();
  compat.append('<ul id="compat-list"></ul>');
  COMPAT\_ENVS.forEach(function (val, idx, array) {
    $("#compat-list").append("<li>" + val[0] + ": " + val[1] + "</li>");
  });

  const DB\_NAME = "mdn-demo-indexeddb-epublications";
  const DB\_VERSION = 1; // Use a long long for this value (don't use a float)
  const DB\_STORE\_NAME = "publications";

  var db;

  // Used to keep track of which view is displayed to avoid uselessly reloading it
  var current_view_pub_key;

  function openDb() {
    console.log("openDb ...");
    var req = indexedDB.open(DB\_NAME, DB\_VERSION);
    req.onsuccess = function (evt) {
      // Better use "this" than "req" to get the result to avoid problems with
      // garbage collection.
      // db = req.result;
      db = this.result;
      console.log("openDb DONE");
    };
    req.onerror = function (evt) {
      console.error("openDb:", evt.target.errorCode);
    };

    req.onupgradeneeded = function (evt) {
      console.log("openDb.onupgradeneeded");
      var store = evt.currentTarget.result.createObjectStore(DB\_STORE\_NAME, {
        keyPath: "id",
        autoIncrement: true,
      });

      store.createIndex("biblioid", "biblioid", { unique: true });
      store.createIndex("title", "title", { unique: false });
      store.createIndex("year", "year", { unique: false });
    };
  }

  /\*\*
 \* @param {string} store\_name
 \* @param {string} mode either "readonly" or "readwrite"
 \*/
  function getObjectStore(store\_name, mode) {
    var tx = db.transaction(store_name, mode);
    return tx.objectStore(store_name);
  }

  function clearObjectStore(store\_name) {
    var store = getObjectStore(DB\_STORE\_NAME, "readwrite");
    var req = store.clear();
    req.onsuccess = function (evt) {
      displayActionSuccess("Store cleared");
      displayPubList(store);
    };
    req.onerror = function (evt) {
      console.error("clearObjectStore:", evt.target.errorCode);
      displayActionFailure(this.error);
    };
  }

  function getBlob(key, store, success\_callback) {
    var req = store.get(key);
    req.onsuccess = function (evt) {
      var value = evt.target.result;
      if (value) success\_callback(value.blob);
    };
  }

  /\*\*
 \* @param {IDBObjectStore=} store
 \*/
  function displayPubList(store) {
    console.log("displayPubList");

    if (typeof store == "undefined")
      store = getObjectStore(DB\_STORE\_NAME, "readonly");

    var pub_msg = $("#pub-msg");
    pub_msg.empty();
    var pub_list = $("#pub-list");
    pub_list.empty();
    // Resetting the iframe so that it doesn't display previous content
    newViewerFrame();

    var req;
    req = store.count();
    // Requests are executed in the order in which they were made against the
    // transaction, and their results are returned in the same order.
    // Thus the count text below will be displayed before the actual pub list
    // (not that it is algorithmically important in this case).
    req.onsuccess = function (evt) {
      pub_msg.append(
        "<p>There are <strong>" +
          evt.target.result +
          "</strong> record(s) in the object store.</p>",
      );
    };
    req.onerror = function (evt) {
      console.error("add error", this.error);
      displayActionFailure(this.error);
    };

    var i = 0;
    req = store.openCursor();
    req.onsuccess = function (evt) {
      var cursor = evt.target.result;

      // If the cursor is pointing at something, ask for the data
      if (cursor) {
        console.log("displayPubList cursor:", cursor);
        req = store.get(cursor.key);
        req.onsuccess = function (evt) {
          var value = evt.target.result;
          var list_item = $(
            "<li>" +
              "[" +
              cursor.key +
              "] " +
              "(biblioid: " +
              value.biblioid +
              ") " +
              value.title +
              "</li>",
          );
          if (value.year != null) list_item.append(" - " + value.year);

          if (
            value.hasOwnProperty("blob") &&
            typeof value.blob != "undefined"
          ) {
            var link = $('<a href="' + cursor.key + '">File</a>');
            link.on("click", function () {
              return false;
            });
            link.on("mouseenter", function (evt) {
              setInViewer(evt.target.getAttribute("href"));
            });
            list_item.append(" / ");
            list_item.append(link);
          } else {
            list_item.append(" / No attached file");
          }
          pub_list.append(list_item);
        };

        // Move on to the next object in store
        cursor.continue();

        // This counter serves only to create distinct ids
        i++;
      } else {
        console.log("No more entries");
      }
    };
  }

  function newViewerFrame() {
    var viewer = $("#pub-viewer");
    viewer.empty();
    var iframe = $("<iframe />");
    viewer.append(iframe);
    return iframe;
  }

  function setInViewer(key) {
    console.log("setInViewer:", arguments);
    key = Number(key);
    if (key == current_view_pub_key) return;

    current_view_pub_key = key;

    var store = getObjectStore(DB\_STORE\_NAME, "readonly");
    getBlob(key, store, function (blob) {
      console.log("setInViewer blob:", blob);
      var iframe = newViewerFrame();

      // It is not possible to set a direct link to the
      // blob to provide a mean to directly download it.
      if (blob.type == "text/html") {
        var reader = new FileReader();
        reader.onload = function (evt) {
          var html = evt.target.result;
          iframe.load(function () {
            $(this).contents().find("html").html(html);
          });
        };
        reader.readAsText(blob);
      } else if (blob.type.indexOf("image/") == 0) {
        iframe.load(function () {
          var img_id = "image-" + key;
          var img = $('<img id="' + img_id + '"/>');
          $(this).contents().find("body").html(img);
          var obj_url = window.URL.createObjectURL(blob);
          $(this)
            .contents()
            .find("#" + img_id)
            .attr("src", obj_url);
          window.URL.revokeObjectURL(obj_url);
        });
      } else if (blob.type == "application/pdf") {
        $("\*").css("cursor", "wait");
        var obj_url = window.URL.createObjectURL(blob);
        iframe.load(function () {
          $("\*").css("cursor", "auto");
        });
        iframe.attr("src", obj_url);
        window.URL.revokeObjectURL(obj_url);
      } else {
        iframe.load(function () {
          $(this).contents().find("body").html("No view available");
        });
      }
    });
  }

  /\*\*
 \* @param {string} biblioid
 \* @param {string} title
 \* @param {number} year
 \* @param {string} url the URL of the image to download and store in the local
 \* IndexedDB database. The resource behind this URL is subjected to the
 \* "Same origin policy", thus for this method to work, the URL must come from
 \* the same origin as the web site/app this code is deployed on.
 \*/
  function addPublicationFromUrl(biblioid, title, year, url) {
    console.log("addPublicationFromUrl:", arguments);

    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, true);
    // Setting the wanted responseType to "blob"
    // http://www.w3.org/TR/XMLHttpRequest2/#the-response-attribute
    xhr.responseType = "blob";
    xhr.onload = function (evt) {
      if (xhr.status == 200) {
        console.log("Blob retrieved");
        var blob = xhr.response;
        console.log("Blob:", blob);
        addPublication(biblioid, title, year, blob);
      } else {
        console.error(
          "addPublicationFromUrl error:",
          xhr.responseText,
          xhr.status,
        );
      }
    };
    xhr.send();

    // We can't use jQuery here because as of jQuery 1.8.3 the new "blob"
    // responseType is not handled.
    // http://bugs.jquery.com/ticket/11461
    // http://bugs.jquery.com/ticket/7248
    // $.ajax({
    // url: url,
    // type: 'GET',
    // xhrFields: { responseType: 'blob' },
    // success: function(data, textStatus, jqXHR) {
    // console.log("Blob retrieved");
    // console.log("Blob:", data);
    // // addPublication(biblioid, title, year, data);
    // },
    // error: function(jqXHR, textStatus, errorThrown) {
    // console.error(errorThrown);
    // displayActionFailure("Error during blob retrieval");
    // }
    // });
  }

  /\*\*
 \* @param {string} biblioid
 \* @param {string} title
 \* @param {number} year
 \* @param {Blob=} blob
 \*/
  function addPublication(biblioid, title, year, blob) {
    console.log("addPublication arguments:", arguments);
    var obj = { biblioid: biblioid, title: title, year: year };
    if (typeof blob != "undefined") obj.blob = blob;

    var store = getObjectStore(DB\_STORE\_NAME, "readwrite");
    var req;
    try {
      req = store.add(obj);
    } catch (e) {
      if (e.name == "DataCloneError")
        displayActionFailure(
          "This engine doesn't know how to clone a Blob, " + "use Firefox",
        );
      throw e;
    }
    req.onsuccess = function (evt) {
      console.log("Insertion in DB successful");
      displayActionSuccess();
      displayPubList(store);
    };
    req.onerror = function () {
      console.error("addPublication error", this.error);
      displayActionFailure(this.error);
    };
  }

  /\*\*
 \* @param {string} biblioid
 \*/
  function deletePublicationFromBib(biblioid) {
    console.log("deletePublication:", arguments);
    var store = getObjectStore(DB\_STORE\_NAME, "readwrite");
    var req = store.index("biblioid");
    req.get(biblioid).onsuccess = function (evt) {
      if (typeof evt.target.result == "undefined") {
        displayActionFailure("No matching record found");
        return;
      }
      deletePublication(evt.target.result.id, store);
    };
    req.onerror = function (evt) {
      console.error("deletePublicationFromBib:", evt.target.errorCode);
    };
  }

  /\*\*
 \* @param {number} key
 \* @param {IDBObjectStore=} store
 \*/
  function deletePublication(key, store) {
    console.log("deletePublication:", arguments);

    if (typeof store == "undefined")
      store = getObjectStore(DB\_STORE\_NAME, "readwrite");

    // As per spec http://www.w3.org/TR/IndexedDB/#object-store-deletion-operation
    // the result of the Object Store Deletion Operation algorithm is
    // undefined, so it's not possible to know if some records were actually
    // deleted by looking at the request result.
    var req = store.get(key);
    req.onsuccess = function (evt) {
      var record = evt.target.result;
      console.log("record:", record);
      if (typeof record == "undefined") {
        displayActionFailure("No matching record found");
        return;
      }
      // Warning: The exact same key used for creation needs to be passed for
      // the deletion. If the key was a Number for creation, then it needs to
      // be a Number for deletion.
      req = store.delete(key);
      req.onsuccess = function (evt) {
        console.log("evt:", evt);
        console.log("evt.target:", evt.target);
        console.log("evt.target.result:", evt.target.result);
        console.log("delete successful");
        displayActionSuccess("Deletion successful");
        displayPubList(store);
      };
      req.onerror = function (evt) {
        console.error("deletePublication:", evt.target.errorCode);
      };
    };
    req.onerror = function (evt) {
      console.error("deletePublication:", evt.target.errorCode);
    };
  }

  function displayActionSuccess(msg) {
    msg = typeof msg != "undefined" ? "Success: " + msg : "Success";
    $("#msg").html('<span class="action-success">' + msg + "</span>");
  }
  function displayActionFailure(msg) {
    msg = typeof msg != "undefined" ? "Failure: " + msg : "Failure";
    $("#msg").html('<span class="action-failure">' + msg + "</span>");
  }
  function resetActionStatus() {
    console.log("resetActionStatus ...");
    $("#msg").empty();
    console.log("resetActionStatus DONE");
  }

  function addEventListeners() {
    console.log("addEventListeners");

    $("#register-form-reset").click(function (evt) {
      resetActionStatus();
    });

    $("#add-button").click(function (evt) {
      console.log("add ...");
      var title = $("#pub-title").val();
      var biblioid = $("#pub-biblioid").val();
      if (!title || !biblioid) {
        displayActionFailure("Required field(s) missing");
        return;
      }
      var year = $("#pub-year").val();
      if (year != "") {
        // Better use Number.isInteger if the engine has EcmaScript 6
        if (isNaN(year)) {
          displayActionFailure("Invalid year");
          return;
        }
        year = Number(year);
      } else {
        year = null;
      }

      var file_input = $("#pub-file");
      var selected_file = file_input.get(0).files[0];
      console.log("selected\_file:", selected_file);
      // Keeping a reference on how to reset the file input in the UI once we
      // have its value, but instead of doing that we rather use a "reset" type
      // input in the HTML form.
      //file\_input.val(null);
      var file_url = $("#pub-file-url").val();
      if (selected_file) {
        addPublication(biblioid, title, year, selected_file);
      } else if (file_url) {
        addPublicationFromUrl(biblioid, title, year, file_url);
      } else {
        addPublication(biblioid, title, year);
      }
    });

    $("#delete-button").click(function (evt) {
      console.log("delete ...");
      var biblioid = $("#pub-biblioid-to-delete").val();
      var key = $("#key-to-delete").val();

      if (biblioid != "") {
        deletePublicationFromBib(biblioid);
      } else if (key != "") {
        // Better use Number.isInteger if the engine has EcmaScript 6
        if (key == "" || isNaN(key)) {
          displayActionFailure("Invalid key");
          return;
        }
        key = Number(key);
        deletePublication(key);
      }
    });

    $("#clear-store-button").click(function (evt) {
      clearObjectStore();
    });

    var search_button = $("#search-list-button");
    search_button.click(function (evt) {
      displayPubList();
    });
  }

  openDb();
  addEventListeners();
})(); // Immediately-Invoked Function Expression (IIFE)
```

線上範例下一步
---

請參考IndexedDB 文件，看看有甚麼 IndexedDB API 可供使用，實際試玩一下吧。

延伸閱讀
----

參照

* IndexedDB API Reference
* Indexed Database API Specification
* Using IndexedDB in chrome

相關教學

* A simple TODO list using HTML5 IndexedDB.

**備註：** 請注意此教學範例用到的已經廢棄的`setVersion()`方法。
* Databinding UI Elements with IndexedDB

相關文章

* IndexedDB — The Store in Your Browser

Firefox

* Mozilla interface files


GainNode.gain - Web APIs

GainNode.gain
=============

`GainNode` 介面的 `gain` 屬性是 a-rate (en-US) `AudioParam` (en-US)，代表增益的數值。

語法
--

```js
var audioCtx = new AudioContext();
var gainNode = audioCtx.createGain();
gainNode.gain.value = 0.5;
```

### 傳回值

一個 `AudioParam` (en-US).

**備註：** 雖然傳回的 `AudioParam` 是唯讀的，但是它所代表的值可以更改。

範例
--

See `BaseAudioContext.createGain()` (en-US) for example code showing how to use an `AudioContext` to create a `GainNode`, which is then used to mute and unmute the audio by changing the gain property value.

規格
--

| Specification |
| --- |
| Web Audio API # dom-gainnode-gain |

瀏覽器相容度
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* Using the Web Audio API (en-US)


Position.timestamp - Web APIs

Position.timestamp
==================

**`Position.timestamp`** 是一個唯讀的 `DOMTimeStamp` (en-US) 物件, 此物件代表建立 `Position` 物件的日期和時間，精確度為毫秒。

語法
--
```

coord = position.timestamp
```

規格
--

| Specification |
| --- |
| Geolocation API # timestamp-attribute |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Position`。


Position.coords - Web APIs

Position.coords
===============

**`Position.coords`** 是一個 `Coordinates` 物件的唯讀屬性，表示地理的特性：回傳的物件中包括位置、地球經緯度、海拔高度和速度，同時也包含著這些值的精準度訊息。

語法
--
```

coord = position.coords
```

規格
--

| Specification |
| --- |
| Geolocation API # coords-attribute |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Position`。


Node.removeChild() - Web APIs

Node.removeChild()
==================

**`Node.removeChild()`** 方法從 DOM 移除一個子節點，並傳回移除的節點。

語法
--
```

var oldChild = node.removeChild(child);
或
node.removeChild(child);
```

* `child` 是 DOM 中想要移除的子節點。
* `node` 是 `child` 的父節點。
* `oldChild` 為將被移除的子節點參照，例如：`oldChild === child`.

被刪除的子節點仍然存於記憶體之中，只是不在 DOM 了。從上述的第一種語法形式中，我們知道，透過引用 `oldChild` 還是可以在程式中重新使用已經被移除的子節點。

而第二種語法形式，因為沒有保留 `oldChild` 引用，因此假設你並沒有在其他地方保留節點引用，則它會立即無法使用且不可挽回，而且通常會在短時間內從內存管理中被自動刪除。

如果 `child` 並非某 `element` 節點的子元素，則此方法會拋出異常。而如果調用此方法時，`child` 雖是某 `element` 的子元素，但在嘗試刪除它的過程中已被其他事件處理程序刪除，也會拋出異常（例如 `blur` (en-US)）。

### 會丟出的錯誤

兩種不同的方式拋出異常：

1. 如果 `child` 確實是 `element` 的子元素且確實存在於 DOM，但已被刪除，則會丟出以下異常：
 `Uncaught NotFoundError: Failed to execute 'removeChild' on 'Node': The node to be removed is not a child of this node`.
2. 如果 `child` 不存在於頁面的 DOM，則會拋出下列的異常：
 `Uncaught TypeError: Failed to execute 'removeChild' on 'Node': parameter 1 is not of type 'Node'.`
例子
--

### 簡單的例子

HTML:

```html
<div id="top">
  <div id="nested"></div>
</div>
```

在知道父節點的情況下，刪除特定元素：

```js
let d = document.getElementById("top");
let d_nested = document.getElementById("nested");
let throwawayNode = d.removeChild(d_nested);
```

沒有指定父節點的情況下，刪除特定元素：

```js
let node = document.getElementById("nested");
if (node.parentNode) {
  node.parentNode.removeChild(node);
}
```

從一個元素中移除所有子元素：

```js
let element = document.getElementById("top");
while (element.firstChild) {
  element.removeChild(element.firstChild);
}
```

### 造成一個 TypeError

```html
<!--Sample HTML code-->
<div id="top"></div>

<script type="text/javascript">
 let top = document.getElementById("top");
 let nested = document.getElementById("nested");

 // Throws Uncaught TypeError
 let garbage = top.removeChild(nested);
</script>
```

### 造成一個 NotFoundError

```html
<!--Sample HTML code-->
<div id="top">
  <div id="nested"></div>
</div>

<script type="text/javascript">
 let top = document.getElementById("top");
 let nested = document.getElementById("nested");

 // This first call correctly removes the node
 let garbage = top.removeChild(nested);

 // Throws Uncaught NotFoundError
 garbage = top.removeChild(nested);
</script>
```

規範
--

* DOM Level 1 Core: removeChild
* DOM Level 2 Core: removeChild
* DOM Level 3 Core: removeChild
瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

相關連結
----

* `Node.replaceChild` (en-US)
* `Node.parentNode` (en-US)
* `ChildNode.remove` (en-US)


Coordinates.longitude - Web APIs

Coordinates.longitude
=====================

**`Coordinates.longitude`** 是個唯讀十進位的複數用來代表裝置經度的位置。

語法
--
```

lon = coordinates.longitude
```

規格
--

| Specification |
| --- |
| Geolocation API # latitude-longitude-and-accuracy-attributes |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於`Coordinates` 介面。


Coordinates.speed - Web APIs

Coordinates.speed
=================

**`Coordinates.speed`** 回傳唯讀的正複數代表速度，單位為公尺/秒。 如果裝置無法測量這個值則回傳 null。

語法
--
```

speed = coordinates.speed
```

規格
--

| Specification |
| --- |
| Geolocation API # speed-attribute |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Coordinates` 介面。


Coordinates.latitude - Web APIs

Coordinates.latitude
====================

**`Coordinates.latitude`** 是個唯讀十進位的複數用來代表裝置緯度的位置。

語法
--
```

lat = coordinates.latitude
```

規格
--

| Specification |
| --- |
| Geolocation API # latitude-longitude-and-accuracy-attributes |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Coordinates` 介面。


Coordinates.altitudeAccuracy - Web APIs

Coordinates.altitudeAccuracy
============================

**`Coordinates.altitudeAccuracy`** 是個唯讀的正複數用來代表高度的精準度，單位為公尺，並有 95% 可靠度。如果裝置無法提供這個值則回傳 null。

語法
--
```

altAcc = coordinates.altitudeAccuracy
```

規格
--

| Specification |
| --- |
| Geolocation API # altitude-and-altitudeaccuracy-attributes |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Coordinates` 介面。


Coordinates.heading - Web APIs

Coordinates.heading
===================

**`Coordinates.heading`** 是個唯讀的正複數用來代表裝置前進的方向。這個數值代表你偏離北方多少度，0 度代表你向著正北方，照著順時針的方向遞增(90 度代表正東方，270 度代表正西方)。如果`Coordinates.speed` 為 0 度，則此值為 `NaN` (en-US)。如果這個裝置無法提供這個值則回傳 null。

語法
--
```

heading = coordinates.heading
```

規格
--

| Specification |
| --- |
| Geolocation API # heading-attribute |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Coordinates` 介面。


Coordinates.altitude - Web APIs

Coordinates.altitude
====================

**`Coordinates.altitude`** 是個唯讀的正複數用來代表距離海平面的高度，單位為公尺。如果無法提供這個值則回傳 null。

語法
--
```

alt = coordinates.altitude
```

規格
--

| Specification |
| --- |
| Geolocation API # altitude-and-altitudeaccuracy-attributes |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Coordinates` 介面。


Coordinates.accuracy - Web APIs

Coordinates.accuracy
====================

**`Coordinates.accuracy`** 是個唯讀的正複數用來代表 `Coordinates.latitude` 和 `Coordinates.longitude` 的準確度，單位為公尺，並有 95% 可靠度。

語法
--
```

acc = coordinates.accuracy
```

規格
--

| Specification |
| --- |
| Geolocation API # latitude-longitude-and-accuracy-attributes |

瀏覽器的相容性
-------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)
* 屬於 `Coordinates` 介面。


Blob.size - Web APIs

Blob.size
=========

**`Blob.size`** 屬性回傳以 byte 為單位的 `Blob` 或一個 `File` 的大小。

語法
--
```

var sizeInBytes = blob.size
```

值
-

一個數字。

範例
--

```js
// fileInput 是個 HTMLInputElement: <input type="file" multiple id="myfileinput">
var fileInput = document.getElementById("myfileinput");

// files 是個 FileList 物件 （類似 NodeList）
var files = fileInput.files;

for (var i = 0; i < files.length; i++) {
  console.log(files[i].name + " has a size of " + files[i].size + " Bytes");
}
```

規格
--

| Specification |
| --- |
| File API # dfn-size |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* `Blob`


Blob() - Web APIs

Blob()
======

**`Blob()`** 建構式會回傳一個新建立的 `Blob` 物件。新物件的內容是由 *array* 參數的成員值串連所構成。

語法
--
```

var aBlob = new Blob( array, options );
```

### 參數

* *array* 可以是一個由 `ArrayBuffer`、`ArrayBufferView` (en-US)、`Blob` 或 `DOMString` 組成的 `Array` 物件，或是上述多種型別物件的混合陣列。這個陣列將會被放進新建立的 `Blob` 物件當中。DOMStrings 的編碼為 UTF-8。
* *options* 是選擇性的 `BlobPropertyBag` 字典物件，有以下兩個指定的屬性：
    + `type` 屬性，預設值為空字串 `""`，表示將被放進 `Blob` 物件的陣列內容之 MIME 類型。
    + `endings` 屬性，表示包含 `\n` 換行字元的字串要如何輸出，預設值為字串 `"transparent"`。此屬性值可為：`"native"`，代表換行字元會被轉為目前作業系統的換行字元編碼。也可以是 `"transparent"`，代表保留 `Blob` 物件中資料的換行字元。
    非標準
範例
--

```js
var aFileParts = ['<a id="a"><b id="b">hey!</b></a>']; // an array consisting of a single DOMString
var oMyBlob = new Blob(aFileParts, { type: "text/html" }); // the blob
```

規範
--

| Specification |
| --- |
| File API # constructorBlob |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* 廢棄的 `BlobBuilder` (en-US) 和取代他的建構子。


Blob.type - Web APIs

Blob.type
=========

`Blob` 物件的 **`type`** 屬性提供檔案的 MIME 類別 (en-US)。若無法辨明型別則回傳空字串。

語法
--
```

var mimetype = instanceOfFile.type
```

值
-

一個字串。

範例
--

```js
var i, fileInput, files, allowedFileTypes;

// fileInput 是個 HTMLInputElement: <input type="file" multiple id="myfileinput">
fileInput = document.getElementById("myfileinput");

// files 是個 FileList 物件 （類似 NodeList）
files = fileInput.files;

// 這範例接受 \*.png, \*.jpeg 和 \*.gif 圖片。
allowedFileTypes = ["image/png", "image/jpeg", "image/gif"];

for (i = 0; i < files.length; i++) {
  // 測試 file.type 是否是允許的類別。
  if (allowedFileTypes.indexOf(files[i].type) > -1) {
    // 若符合則執行這裡的程式碼。
  }
});
```

規格
--

| Specification |
| --- |
| File API # dfn-type |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* `Blob`


GlobalEventHandlers.onclose - Web APIs

GlobalEventHandlers.onclose
===========================

Baseline 2022
-------------

Newly availableSince March 2022, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
送至視窗的 close event 的處理器（handler）。（不支援 Firefox 2 及 Safari）

語法
--
```

window.onclose = funcRef;
```

### 參數

* `funcRef` 為函式的參照。
範例
--

```js
window.onclose = resetThatServerThing;
```

規範
--

| Specification |
| --- |
| HTML Standard # event-close |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.


submit - Web APIs

submit
======

`submit` 事件會在表單送出時觸發。

要注意的是，`submit` 事件**只會**在 form element (en-US) 上觸發， button (en-US) 或是 submit input (en-US) 則不會觸發。（送出的是「表單」，而非「按鈕」）

基本資料
----

定義規範

HTML5

Interface
`Event`
Bubbles

 是
 雖說在規範上這是個不 bubble 的事件

Cancelable

是

Target

Element

默認行動

修改（傳送表單內容至伺服器）。

屬性
--

| Property | Type | Description |
| --- | --- | --- |
| `target` Read only  | `EventTarget` | The event target (the topmost target in the DOM tree). |
| `type` Read only  | `DOMString` | The type of event. |
| `bubbles` Read only  | `Boolean` | Whether the event normally bubbles or not. |
| `cancelable` Read only  | `Boolean` | Whether the event is cancellable or not. |

規範
--

| Specification |
| --- |
| HTML Standard # event-submit |
| HTML Standard # handler-onsubmit |


Navigator.language - Web APIs

Navigator.language
==================

**`Navigator.language`** 是一個唯讀的屬性，回傳使用者偏好的語言字串，通常是瀏覽器 UI 的文字

語法
--

```js
const lang = navigator.language
```

### 值

一個 `DOMString`. `lang` 儲存一個代表此語言的字串。定義在BCP 47。範例: 合法的語言代碼 "en", "en-US", "fr", "fr-FR", "es-ES", etc.

 在 iOS 小於 10.2 的 Safari 國碼是回傳小寫的喲！
 "en-us", "fr-fr" etc.

範例
--

```js
if (/^en\b/.test(navigator.language)) {
  doLangSelect(window.navigator.language);
}
```

規格
--

| Specification |
| --- |
| HTML Standard # dom-navigator-language-dev |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* `Navigator.languages` (en-US)
* `navigator` (en-US)


Navigator.geolocation - Web APIs

Navigator.geolocation
=====================

**`Navigator.geolocation`** 回傳一個唯讀的 `Geolocation` 物件，透過這個物件可以存取設備的位置訊息。同時也允許網站或應用程式根據使用者的位置提供客製化的結果。

**備註：** 因為隱私的因素，當網頁要求存取位置資訊時，用戶會被提示通知並且詢問授權與否。注意不同的瀏覽器在詢問授權時有各自不同的策略和方式。

語法
--
```

geo = navigator.geolocation
```

規格
--

| Specification |
| --- |
| Geolocation API # dom-navigator-geolocation |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

請參考
---

* Using geolocation (en-US)


Firefox 3 Web-based protocol handler - Web APIs

Firefox 3 Web-based protocol handler
====================================

### 摘要

window.navigator.registerProtocolHandler 讓網站可以將自己註冊為特定通訊協定的處理者。

### 語法
```

window.navigator.registerProtocolHandler(protocol, uri, title);
```

* protocol 是網站想要處理的 protocol 名稱，用字串表示。
* uri 是要給 handler 處理的 URI 字串。你可以在字串裡用 "%s" 來代表 escaped 過、需要處理的 URI。
* title 是 handler 的 title，會以字串的形式呈現給使用者。
### 例子
```

navigator.registerProtocolHandler("mailto",
                                 "https://mail.google.com/mail?view=cm&tf=0&to=%s",
                                 "Google Mail");
```

這會建立一個 handler，它允許 mailto 的鏈結將使用者帶到 Google Mail，將鏈結中指定的 email 位址插入到 URL。

### 參考資料

1. DOM:window.navigator.registerProtocolHandler 原始網頁
### 延伸閱讀

1. WHATWG's Web Applications 1.0 working draft


File.File() - Web APIs

File.File()
===========

**`File()`** 建構子建立一個新的 `File` 物件實例

語法
--
```

var myFile = new File(bits, name[, options]);
```

### 參數

*bits*

An `Array` of `ArrayBuffer`, `ArrayBufferView` (en-US), `Blob`, or `DOMString` objects — 或是由這些物件組成的集合。這是以 UTF-8 編碼的檔案內容。

*name*

檔案名稱或檔案的路徑（`USVString` (en-US)）。

*options* 選擇性

物件選項，包含物件非必要的屬性，以下為可得到的屬性：

* `type`: 物件的 MIME 類型（`DOMString` ）將被放進檔案中，預設為 ""（空值）。
* `lastModified`: 檔案最後修改時間，格式為毫秒，預設為 `Date.now()`.

範例
--

```js
var file = new File(["foo"], "foo.txt", {
  type: "text/plain",
});
```

規格
--

| Specification |
| --- |
| File API # file-constructor |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

看更多
---

* `FileReader`
* `Blob`


File.fileName - Web APIs

File.fileName
=============

**非標準:** This feature is non-standard and is not on a standards track. Do not use it on production sites facing the Web: it will not work for every user. There may also be large incompatibilities between implementations and the behavior may change in the future.

**gecko 7.0:** 不推薦使用此功能。雖可能有一些瀏覽器仍然支援它，但也許已自相關的網頁標準中移除、正準備移除、或僅為了維持相容性而保留。避免使用此功能，盡可能更新現有程式；請參考頁面底部的相容性表格來下決定。請注意：本功能可能隨時停止運作。

總覽
--

回傳檔案名稱，基於安全因素，檔案路徑不包含這個屬性。

**備註：** 這個檔案是廢棄的，使用 `File.name` 取代。

語法
--

```js
var name = instanceOfFile.fileName;
```

數值
--

字串

格式
--

尚無資料

看更多
---

* `File.name`


HTMLSelectElement.setCustomValidity() - Web APIs

HTMLSelectElement.setCustomValidity()
=====================================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
**`HTMLSelectElement.setCustomValidity()`** 方法可為指定的元素設定自定義檢核訊息。使用空字串表示元素*沒有*發生違反自定檢核條件的錯誤。

語法
--

```js
selectElt.setCustomValidity(string);
```

### 參數

* *string* 為 `DOMString`，代表錯誤訊息。
規範
--

| Specification |
| --- |
| HTML Standard # dom-cva-setcustomvalidity-dev |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* 表單驗證 (en-US)


HTMLSelectElement.checkValidity() - Web APIs

HTMLSelectElement.checkValidity()
=================================

Baseline 2023
-------------

Newly availableSince March 2023, this feature works across the latest devices and browser versions. This feature might not work in older devices or browsers.

* Learn more
* See full compatibility
* Report feedback
**`HTMLSelectElement.checkValidity()`** 方法會檢查元素是否有任何的檢核、驗證條件，並且檢查是否滿足這些條件。如果元素沒有通過這些檢核，瀏覽器會於該元素上觸發一個可取消的 `invalid` (en-US) 事件，並回傳 `false`。

語法
--

```js
var result = selectElt.checkValidity();
```

規範
--

| Specification |
| --- |
| HTML Standard # dom-cva-checkvalidity-dev |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

參見
--

* Form validation. (en-US)


Document.registerElement() - Web APIs

Document.registerElement()
==========================

Limited availability
--------------------

This feature is not Baseline because it does not work in some of the most widely-used browsers.

* Learn more
* See full compatibility
* Report feedback
**已棄用:** 不推薦使用此功能。雖可能有一些瀏覽器仍然支援它，但也許已自相關的網頁標準中移除、正準備移除、或僅為了維持相容性而保留。避免使用此功能，盡可能更新現有程式；請參考頁面底部的相容性表格來下決定。請注意：本功能可能隨時停止運作。

**警告：** document.registerElement() 已經被棄用，建議使用 customElements.define().

**`document.registerElement()`** 可以在瀏覽器中註冊一個新的自訂標籤(元素)and returns a constructor for the new element.

**備註：** This is an experimental technology. The browser you use it in must support Web Components. See Enabling Web Components in Firefox (en-US).

語法
--
```

var constructor = document.registerElement(tag-name, options);
```

### 參數

*標籤名稱*

自訂的標籤名稱需有一個 橫線 ( - ), 例如`my-tag`.

*options 選擇性*

An object with properties **prototype** to base the custom element on, and **extends**, an existing tag to extend. Both of these are optional.

例子
--

這是一個非常簡單的例子:

```js
var Mytag = document.registerElement("my-tag");
```

現在新的標籤已經在瀏覽器中註冊了. The `Mytag` variable holds a constructor that you can use to create a `my-tag` element in the document as follows:

```js
document.body.appendChild(new Mytag());
```

This inserts an empty `my-tag` element that will be visible if you use the browser's developer tools. It will not be visible if you use the browser's view source capability. And it won't be visible in the browser unless you add some content to the tag. Here is one way to add content to the new tag:

```js
var mytag = document.getElementsByTagName("my-tag")[0];
mytag.textContent = "I am a my-tag element.";
```

瀏覽器支援性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

也看一下
----

* Custom Elements (en-US)


使用 Web Notifications - Web APIs

使用 Web Notifications
====================

**實驗性質:** **這是一個實驗中的功能 (en-US)**
此功能在某些瀏覽器尚在開發中，請參考兼容表格以得到不同瀏覽器用的前輟。

摘要
--

Web Notifications API 可將通知傳送至頁面以外的系統層級並顯示通知。因此即使 Web Apps 處於閒置狀態，亦可傳送資訊予使用者。絕佳範例之一，就是在使用其他 Apps 時，Web Mail App 同樣可通知使用者已接收到新郵件。

要求權限
----

### 網頁內容

在 Apps 傳送通知之前，使用者必須先許可 Apps 的動作。只要 APIs 嘗試予網頁之外的東西互動，均必須先獲得使用者的授權。如此可避免濫發通知而影響使用經驗。

透過 `Notification.permission` (en-US) 唯讀屬性，要傳送通知的 Apps 將檢查目前的授權狀態。此屬性共有 3 組參數：

* `default`：使用者尚未給予任何權限 (因此不會顯示任何通知)
* `granted`：使用者允許接收到 Apps 的通知
* `denied`：使用者拒絕接收 Apps 的通知

**備註：** Chrome 與 Safari 尚未建構 `permission` 屬性。

若使用者尚未給予權限，則 Apps 必須透過 `Notification.requestPermission()` (en-US) 函式讓使用者選擇，接著由此函式接收 1 組回呼 (Callback) 函式作為參數；而該回呼函式則提供使用者是否授權的資訊。

以下為啟動 Apps 時要求權限的常用範例：

```js
window.addEventListener("load", function () {
  Notification.requestPermission(function (status) {
    // This allows to use Notification.permission with Chrome/Safari
    if (Notification.permission !== status) {
      Notification.permission = status;
    }
  });
});
```

**備註：** Chrome 不允許於載入事件中呼叫 `Notification.requestPermission()` (en-US) (參閱 issue 274284)。

### 已安裝的 Apps

在安裝 Apps 之後，若於 Apps 的 manifest 檔案中直接添加權限，即可省去再次向使用者要求權限的動作。

```json
permissions: {
  "desktop-notification": {
    "description: "Allows to display notifications on the user's desktop.
  }
}
```

建立通知
----

透過 `Notification` 建構子 (Constructor) 即可建立通知。此建構子包含 1 組標題，可於通知內顯示；另有如 `icon` (en-US) 或文字 `body` (en-US) 等數個選項，可強化通知的內容。

在建立實體 (Instantiated) 之後，就會儘快顯示通知。若要追蹤通知的目前狀態，必須在 `Notification` 的實體階層觸發 4 個事件：

* show：對使用者顯示通知之後，隨即觸發
* click (en-US)：使用者點擊通知之後，隨即觸發
* close：關閉通知之後，隨即觸發
* error (en-US)：通知發生任何錯誤 (大多數是因為某種情況而未顯示通知)，隨即觸發

而透過 `onshow` (en-US)、`onclick` (en-US)、`onclose` (en-US)，或 `onerror` (en-US) 等事件處理器 (Event handler)，即可追蹤這些事件。由於 `Notification` 是繼承 `EventTarget` 而來，因此亦可使用 `addEventListener()` (en-US) 函式。

**備註：** Firefox 與 Safari 並未遵守 close 事件的規格。此規格雖然規定「僅限使用者能關閉通知」，但 Firefox 與 Safari 卻可於數分鐘後自動關閉通知。因此不一定是由使用者關閉通知。此規格並明確規定「應透過 `Notification.close()` (en-US) 函式，於應用程式層級完成自動關閉通知」。範例程式碼如下：

```js
var n = new Notification("Hi!");
n.onshow = function () {
  setTimeout(n.close, 5000);
};
```

### 簡易範例

先假設下列基本 HTML：

```html
<button>Notify me!</button>
```

則能以這種方法處理通知：

```js
window.addEventListener("load", function () {
  // At first, let's check if we have permission for notification
  // If not, let's ask for it
  if (Notification && Notification.permission !== "granted") {
    Notification.requestPermission(function (status) {
      if (Notification.permission !== status) {
        Notification.permission = status;
      }
    });
  }
  var button = document.getElementsByTagName("button")[0];
  button.addEventListener("click", function () {
    // If the user agreed to get notified
    if (Notification && Notification.permission === "granted") {
      var n = new Notification("Hi!");
    }
    // If the user hasn't told if he wants to be notified or not
    // Note: because of Chrome, we are not sure the permission property
    // is set, therefore it's unsafe to check for the "default" value.
    else if (Notification && Notification.permission !== "denied") {
      Notification.requestPermission(function (status) {
        if (Notification.permission !== status) {
          Notification.permission = status;
        }
        // If the user said okay
        if (status === "granted") {
          var n = new Notification("Hi!");
        }
        // Otherwise, we can fallback to a regular modal alert
        else {
          alert("Hi!");
        }
      });
    }
    // If the user refuses to get notified
    else {
      // We can fallback to a regular modal alert
      alert("Hi!");
    }
  });
});
```

### 現場測試結果

若無法顯示，可至本文右上角「Language」切換回英文原文觀看。

處理多筆通知
------

某些情況下 (如某個即時訊息 App 持續通知每一筆進來的訊息)，使用者可能會接收大量的通知。為了避免太多非必要訊息擠爆使用者的桌面，則應該讓等待中的通知進入佇列。

將標籤添加至任何新的通知，即可達到佇列效果。若通知擁有相同的標籤且尚未顯示，則新通知就會取代先前的通知；反之，若已顯示了相同標籤的通知，就會關閉先前的通知而顯示新通知。

### 標籤範例

先假設下列基本 HTML：

```html
<button>Notify me!</button>
```

則能以下列方式處理多筆通知：

```js
window.addEventListener('load', function () {
  // At first, let's check if we have permission for notification
  // If not, let's ask for it
  if (Notification && Notification.permission !== "granted") {
    Notification.requestPermission(function (status) {
      if (Notification.permission !== status) {
        Notification.permission = status;
      }
    });
  }
  var button = document.getElementsByTagName('button')[0];
  button.addEventListener('click', function () {
    // If the user agreed to get notified
    // Let's try to send ten notifications
    if (Notification && Notification.permission === "granted") {
      for (var i = 0; i < 10; i++) {
        // Thanks to the tag, we should only see the "Hi! 10" notification
        var n = new Notification("Hi! " + i, {tag: 'soManyNotification'});
      }
    }
    // If the user hasn't told if he wants to be notified or not
    // Note: because of Chrome, we are not sure the permission property
    // is set, therefore it's unsafe to check for the "default" value.
    else if (Notification && Notification.permission !== "denied") {
      Notification.requestPermission(function (status) {
        if (Notification.permission !== status) {
          Notification.permission = status;
        }
        // If the user said okay
        if (status === "granted") {
          for (var i = 0; i < 10; i++) {
            // Thanks to the tag, we should only see the "Hi! 10" notification
            var n = new Notification("Hi! " + i, {tag: 'soManyNotification'});
          }
        }
        // Otherwise, we can fallback to a regular modal alert
        else {
          alert(Hi!");
        }
      });
    }
    // If the user refuses to get notified
    else {
      // We can fallback to a regular modal alert
      alert(Hi!");
    }
  });
});
```

#### 現場測試結果

若無法顯示，可至本文右上角「Language」切換回英文原文觀看。

規格
--

| Specification |
| --- |
| Notifications API Standard # api |

瀏覽器相容性
------

BCD tables only load in the browser with JavaScript enabled. Enable JavaScript to view data.

另可參閱
----

* `Notification`