应用程序须要数据。对大多数Web应用程序来讲,数据在服务器端组织和管理,客户端经过网络请求获取。随着浏览器变得愈来愈有能力,所以可选择在浏览器存储和操纵应用程序数据。javascript
本文向你介绍名为IndexedDB的浏览器端文档数据库。使用lndexedDB,你能够经过惯于在服务器端数据库几乎相同的方式建立、读取、更新和删除大量的记录。请使用本文中可工做的代码版本去体验,完整的源代码能够经过GitHub库找到。css
读到本教程的结尾时,你将熟悉IndexedDB的基本概念以及如何实现一个使用IndexedDB执行完整的CRUD操做的模块化JavaScript应用程序。让咱们稍微亲近IndexedDB并开始吧。html
什么是IndexedDBjava
图1:开发者工具查看一个object storejquery
所有的IndexedDB API请参考完整文档git
IndexedDB的架构很像在一些流行的服务器端NOSQL数据库实现中的设计典范类型。面向对象数据经过object stores(对象仓库)进行持久化,全部操做基于请求同时在事务范围内执行。事件生命周期使你可以控制数据库的配置,错误经过错误冒泡来使用API管理。github
object store是IndexedDB数据库的基础。若是你使用过关系数据库,一般能够将object store等价于一个数据库表。Object stores包括一个或多个索引,在store中按照一对键/值操做,这提供一种快速定位数据的方法。web
当你配置一个object store,你必须为store选择一个键。键在store中能够以“in-line”或“out-of-line”的方式存在。in-line键经过在数据对象上引用path来保障它在object store的惟一性。为了说明这一点,想一想一个包括电子邮件地址属性Person对象。您能够配置你的store使用in-line键emailAddress,它能保证store(持久化对象中的数据)的惟一性。另外,out-of-line键经过独立于数据的值识别惟一性。在这种状况下,你能够把out-of-line键比做一个整数值,它(整数值)在关系数据库中充当记录的主键。ajax
图1显示了任务数据保存在任务的object store,它使用in-line键。在这个案例中,键对应于对象的ID值。数据库
不一样于一些传统的关系数据库的实现,每个对数据库操做是在一个事务的上下文中执行的。事务范围一次影响一个或多个object stores,你经过传入一个object store名字的数组到建立事务范围的函数来定义。
建立事务的第二个参数是事务模式。当请求一个事务时,必须决定是按照只读仍是读写模式请求访问。事务是资源密集型的,因此若是你不须要更改data store中的数据,你只须要以只读模式对object stores集合进行请求访问。
清单2演示了如何使用适当的模式建立一个事务,并在这片文章的 Implementing Database-Specific Code 部分进行了详细讨论。
直到这里,有一个反复出现的主题,您可能已经注意到。对数据库的每次操做,描述为经过一个请求打开数据库,访问一个object store,再继续。IndexedDB API天生是基于请求的,这也是API异步本性指示。对于你在数据库执行的每次操做,你必须首先为这个操做建立一个请求。当请求完成,你能够响应由请求结果产生的事件和错误。
本文实现的代码,演示了如何使用请求打开数据库,建立一个事务,读取object store的内容,写入object store,清空object store。
IndexedDB使用事件生命周期管理数据库的打开和配置操做。图2演示了一个打开的请求在必定的环境下产生upgrade need事件。
图2:IndexedDB打开请求的生命周期
全部与数据库的交互开始于一个打开的请求。试图打开数据库时,您必须传递一个被请求数据库的版本号的整数值。在打开请求时,浏览器对比你传入的用于打开请求的版本号与实际数据库的版本号。若是所请求的版本号高于浏览器中当前的版本号(或者如今没有存在的数据库),upgrade needed事件触发。在uprade need事件期间,你有机会经过添加或移除stores,键和索引来操纵object stores。
若是所请求的数据库版本号和浏览器的当前版本号一致,或者升级过程完成,一个打开的数据库将返回给调用者。
固然,有时候,请求可能不会按预期完成。IndexedDB API经过错误冒泡功能来帮助跟踪和管理错误。若是一个特定的请求遇到错误,你能够尝试在请求对象上处理错误,或者你能够容许错误经过调用栈冒泡向上传递。这个冒泡天性,使得你不须要为每一个请求实现特定错误处理操做,而是能够选择只在一个更高级别上添加错误处理,它给你一个机会,保持你的错误处理代码简洁。本文中实现的例子,是在一个高级别处理错误,以便更细粒度操做产生的任何错误冒泡到通用的错误处理逻辑。
也许在开发Web应用程序最重要的问题是:“浏览器是否支持我想要作的?“尽管浏览器对IndexedDB的支持在继续增加,采用率并非咱们所但愿的那样广泛。图3显示了caniuse.com网站的报告,支持IndexedDB的为66%多一点点。最新版本的火狐,Chrome,Opera,Safar,iOS Safari,和Android彻底支持IndexedDB,Internet Explorer和黑莓部分支持。虽然这个列表的支持者是使人鼓舞的,但它没有告诉整个故事。
图3:浏览器对IndexedDB的支持,来自caniuse.com
只有很是新版本的Safari和iOS Safari 支持IndexedDB。据caniuse.com显示,这只占大约0.01%的全球浏览器使用。IndexedDB不是一个你认为可以理所固然获得支持的现代Web API,可是你将很快会这样认为。
浏览器支持本地数据库并非从IndexedDB才开始实现,它是在WebSQL实现以后的一种新方法。相似IndexedDB,WebSQL是一个客户端数据库,但它做为一个关系数据库的实现,使用结构化查询语言(SQL)与数据库通讯。WebSQL的历史充满了曲折,但底线是没有主流的浏览器厂商对WebSQL继续支持。
若是WebSQL其实是一个废弃的技术,为何还要提它呢?有趣的是,WebSQL在浏览器里获得稳固的支持。Chrome, Safari, iOS Safari, and Android 浏览器都支持。另外,并非这些浏览器的最新版本才提供支持,许多这些最新最好的浏览器以前的版本也能够支持。有趣的是,若是你为WebSQL添加支持来支持IndexedDB,你忽然发现,许多浏览器厂商和版本成为支持浏览器内置数据库的某种化身。
所以,若是您的应用程序真正须要一个客户端数据库,你想要达到的最高级别的采用可能,当IndexedDB不可用时,也许您的应用程序可能看起来须要选择使用WebSQL来支持客户端数据架构。虽然文档数据库和关系数据库管理数据有鲜明的差异,但只要你有正确的抽象,就可使用本地数据库构建一个应用程序。
如今最关键的问题:“IndexedDB是否适合个人应用程序?“像往常同样,答案是确定的:“视状况而定。“首先当你试图在客户端保存数据时,你会考虑HTML5本地存储。本地存储获得普遍浏览器的支持,有很是易于使用的API。简单有其优点,但其劣势是没法支持复杂的搜索策略,存储大量的数据,并提供事务支持。
IndexedDB是一个数据库。因此,当你想为客户端作出决定,考虑你如何在服务端选择一个持久化介质的数据库。你可能会问本身一些问题来帮助决定客户端数据库是否适合您的应用程序,包括:
若是你对其中的任何问题回答了“是的”,颇有可能,IndexedDB是你的应用程序的一个很好的候选。
如今,你已经有机会熟悉了一些的总体概念,下一步是开始实现基于IndexedDB的应用程序。第一个步骤须要统一IndexedDB在不一样浏览器的实现。您能够很容易地添加各类厂商特性的选项的检查,同时在window对象上把它们设置为官方对象相同的名称。下面的清单展现了window.indexedDB,window.IDBTransaction,window.IDBKeyRange的最终结果是如何都被更新,它们被设置为相应的浏览器的特定实现。
window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
如今,每一个数据库相关的全局对象持有正确的版本,应用程序能够准备使用IndexedDB开始工做。
在本教程中,您将学习如何建立一个使用IndexedDB存储数据的模块化JavaScript应用程序。为了了解应用程序是如何工做的,参考图4,它描述了任务应用程序处于空白状态。从这里您能够为列表添加新任务。图5显示了录入了几个任务到系统的画面。图6显示如何删除一个任务,图7显示了正在编辑任务时的应用程序。
图4:空白的任务应用程序
这个例子从实现这样一个模块开始,它负责从数据库读取数据,插入新的对象,更新现有对象,删除单个对象和提供在一个object store删除全部对象的选项。这个例子实现的代码是通用的数据访问代码,您能够在任何object store上使用。
这个模块是经过一个当即执行函数表达式(IIFE)实现,它使用对象字面量来提供结构。下面的代码是模块的摘要,说明了它的基本结构。
(function (window) { 'use strict'; var db = { /* implementation here */ }; window.app = window.app || {}; window.app.db = db; }(window));
用这样的结构,可使这个应用程序的全部逻辑封装在一个名为app的单对象上。此外,数据库相关的代码在一个叫作db的app子对象上。
这个模块的代码使用IIFE,经过传递window对象来确保模块的适当范围。使用use strict确保这个函数的代码函数是按照(javascript严格模式)严格编译规则。db对象做为与数据库交互的全部函数的主要容器。最后,window对象检查app的实例是否存在,若是存在,模块使用当前实例,若是不存在,则建立一个新对象。一旦app对象成功返回或建立,db对象附加到app对象。
本文的其他部分将代码添加到db对象内(在implementation here会评论),为应用程序提供特定于数据库的逻辑。所以,如你所见本文后面的部分中定义的函数,想一想父db对象移动,但全部其余功能都是db对象的成员。完整的数据库模块列表见清单2。
对数据库的每一个操做关联着一个先决条件,即有一个打开的数据库。当数据库正在被打开时,经过检查数据库版原本判断数据库是否须要任何更改。下面的代码显示了模块如何跟踪当前版本,object store名、某成员(保存了一旦数据库打开请求完成后的数据库当前实例)。
version: 1, objectStoreName: 'tasks', instance: {},
在这里,数据库打开请求发生时,模块请求版本1数据库。若是数据库不存在,或者版本小于1,upgrade needed事件在打开请求完成前触发。这个模块被设置为只使用一个object store,因此名字直接定义在这里。最后,实例成员被建立,它用于保存一旦打开请求完成后的数据库当前实例。
接下来的操做是实现upgrade needed事件的事件处理程序。在这里,检查当前object store的名字来判断请求的object store名是否存在,若是不存在,建立object store。
upgrade: function (e) { var _db = e.target.result, names = _db.objectStoreNames, name = db.objectStoreName; if (!names.contains(name)) { _db.createObjectStore( name, { keyPath: 'id', autoIncrement: true }); } },
在这个事件处理程序里,经过事件参数e.target.result来访问数据库。当前的object store名称的列表在_db.objectStoreName的字符串数组上。如今,若是object store不存在,它是经过传递object store名称和store的键的定义(自增,关联到数据的ID成员)来建立。
模块的下一个功能是用来捕获错误,错误在模块不一样的请求建立时冒泡。
errorHandler: function (error) { window.alert('error: ' + error.target.code); debugger; },
在这里,errorHandler在一个警告框显示任何错误。这个函数是故意保持简单,对开发友好,当你学习使用IndexedDB,您能够很容易地看到任何错误(当他们发生时)。当你准备在生产环境使用这个模块,您须要在这个函数中实现一些错误处理代码来和你的应用程序的上下文打交道。
如今基础实现了,这一节的其他部分将演示如何实现对数据库执行特定操做。第一个须要检查的函数是open函数。
open: function (callback) { var request = window.indexedDB.open( db.objectStoreName, db.version); request.onerror = db.errorHandler; request.onupgradeneeded = db.upgrade; request.onsuccess = function (e) { db.instance = request.result; db.instance.onerror = db.errorHandler; callback(); }; },
open函数试图打开数据库,而后执行回调函数,告知数据库成功打开能够准备使用。经过访问window.indexedDB调用open函数来建立打开请求。这个函数接受你想打开的object store的名称和你想使用的数据库版本号。
一旦请求的实例可用,第一步要进行的工做是设置错误处理程序和升级函数。记住,当数据库被打开时,若是脚本请求比浏览器里更高版本的数据库(或者若是数据库不存在),升级函数运行。然而,若是请求的数据库版本匹配当前数据库版本同时没有错误,success事件触发。
若是一切成功,打开数据库的实例能够从请求实例的result属性得到,这个实例也缓存到模块的实例属性。而后,onerror事件设置到模块的errorHandler,做为未来任何请求的错误捕捉处理程序。最后,回调被执行来告知调用者,数据库已经打开而且正确地配置,可使用了。
下一个要实现的函数是helper函数,它返回所请求的object store。
getObjectStore: function (mode) { var txn, store; mode = mode || 'readonly'; txn = db.instance.transaction( [db.objectStoreName], mode); store = txn.objectStore( db.objectStoreName); return store; },
在这里,getObjectStore接受mode参数,容许您控制store是以只读仍是读写模式请求。对于这个函数,默认mode是只读的。
每一个针对object store的操做都是在一个事物的上下文中执行的。事务请求接受一个object store名字的数组。这个函数此次被配置为只使用一个object store,可是若是你须要在事务中操做多个object store,你须要传递多个object store的名字到数组中。事务函数的第二个参数是一个模式。
一旦事务请求可用,您就能够经过传递须要的object store名字来调用objectStore函数以得到object store实例的访问权。这个模块的其他函数使用getObjectStore来得到object store的访问权。
下一个实现的函数是save函数,执行插入或更新操做,它根据传入的数据是否有一个ID值。
save: function (data, callback) { db.open(function () { var store, request, mode = 'readwrite'; store = db.getObjectStore(mode), request = data.id ? store.put(data) : store.add(data); request.onsuccess = callback; }); },
save函数的两个参数分别是须要保存的数据对象实例和操做成功后须要执行的回调。读写模式用于将数据写入数据库,它被传入到getObjectStore来获取object store的一个可写实例。而后,检查数据对象的ID成员是否存在。若是存在ID值,数据必须更新,put函数被调用,它建立持久化请求。不然,若是ID不存在,这是新数据,add请求返回。最后,无论put或者add 请求是否执行了,success事件处理程序须要设置在回调函数上,来告诉调用脚本,一切进展顺利。
下一节的代码在清单1所示。getAll函数首先打开数据库和访问object store,它为store和cursor(游标)分别设置值。为数据库游标设置游标变量容许迭代object store中的数据。data变量设置为一个空数组,充当数据的容器,它返回给调用代码。
在store访问数据时,游标遍历数据库中的每条记录,会触发onsuccess事件处理程序。当每条记录访问时,store的数据能够经过e.target.result事件参数获得。虽然实际数据从target.result的value属性中获得,首先须要在试图访问value属性前确保result是一个有效的值。若是result存在,您能够添加result的值到数据数组,而后在result对象上调用continue函数来继续迭代object store。最后,若是没有reuslt了,对store数据的迭代结束,同时数据传递到回调,回调被执行。
如今模块可以从data store得到全部数据,下一个须要实现的函数是负责访问单个记录。
get: function (id, callback) { id = parseInt(id); db.open(function () { var store = db.getObjectStore(), request = store.get(id); request.onsuccess = function (e){ callback(e.target.result); }; }); },
get函数执行的第一步操做是将id参数的值转换为一个整数。取决于函数被调用时,字符串或整数均可能传递给函数。这个实现跳过了对若是所给的字符串不能转换成整数该怎么作的状况的处理。一旦一个id值准备好了,数据库打开了和object store能够访问了。获取访问get请求出现了。请求成功时,经过传入e.target.result来执行回调。它(e.target.result)是经过调用get函数获得的单条记录。
如今保存和选择操做已经出现了,该模块还须要从object store移除数据。
'delete': function (id, callback) { id = parseInt(id); db.open(function () { var mode = 'readwrite', store, request; store = db.getObjectStore(mode); request = store.delete(id); request.onsuccess = callback; }); },
delete函数的名称用单引号,由于delete是JavaScript的保留字。这能够由你来决定。您能够选择命名函数为del或其余名称,可是delete用在这个模块为了API尽量好的表达。
传递给delete函数的参数是对象的id和一个回调函数。为了保持这个实现简单,delete函数约定id的值为整数。您能够选择建立一个更健壮的实现来处理id值不能解析成整数的错误例子的回调,但为了指导缘由,代码示例是故意的。
一旦id值能确保转换成一个整数,数据库被打开,一个可写的object store得到,delete函数传入id值被调用。当请求成功时,将执行回调函数。
在某些状况下,您可能须要删除一个object store的全部的记录。在这种状况下,您访问store同时清除全部内容。
deleteAll: function (callback) { db.open(function () { var mode, store, request; mode = 'readwrite'; store = db.getObjectStore(mode); request = store.clear(); request.onsuccess = callback; }); }
这里deleteAll函数负责打开数据库和访问object store的一个可写实例。一旦store可用,一个新的请求经过调用clear函数来建立。一旦clear操做成功,回调函数被执行。
如今全部特定于数据库的代码被封装在app.db模块中,用户界面特定代码可使用此模块来与数据库交互。用户界面特定代码的完整清单(index.ui.js)能够在清单3中获得,完整的(index.html)页面的HTML源代码能够在清单4中获得。
随着应用程序的需求的增加,你会发如今客户端高效存储大量的数据的优点。IndexedDB是能够在浏览器中直接使用且支持异步事务的文档数据库实现。尽管浏览器的支持可能不能保障,但在合适的状况下,集成IndexedDB的Web应用程序具备强大的客户端数据的访问能力。
在大多数状况下,全部针对IndexedDB编写的代码是自然基于请求和异步的。官方规范有同步API,可是这种IndexedDB只适合web worker的上下文中使用。这篇文章发布时,尚未浏览器实现的同步格式的IndexedDB API。
必定要保证代码在任何函数域外对厂商特定的indexedDB, IDBTransaction, and IDBKeyRange实例进行了规范化且使用了严格模式。这容许您避免浏览器错误,当在strict mode下解析脚本时,它不会容许你对那些对象从新赋值。
你必须确保只传递正整数的版本号给数据库。传递到版本号的小数值会四舍五入。所以,若是您的数据库目前版本1,您试图访问1.2版本,upgrade-needed事件不会触发,由于版本号最终评估是相同的。
当即执行函数表达式(IIFE)有时叫作不一样的名字。有时能够看到这样的代码组织方式,它称为self-executing anonymous functions(自执行匿名函数)或self-invoked anonymous functions(自调用匿名函数)。为进一步解释这些名称相关的意图和含义,请阅读Ben Alman的文章Immediately Invoked Function Expression (IIFE) 。
Listing 1: Implementing the getAll function
getAll: function (callback) { db.open(function () { var store = db.getObjectStore(), cursor = store.openCursor(), data = []; cursor.onsuccess = function (e) { var result = e.target.result; if (result && result !== null) { data.push(result.value); result.continue(); } else { callback(data); } }; }); },
// index.db.js ; window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction; window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange; (function(window){ 'use strict'; var db = { version: 1, // important: only use whole numbers! objectStoreName: 'tasks', instance: {}, upgrade: function (e) { var _db = e.target.result, names = _db.objectStoreNames, name = db.objectStoreName; if (!names.contains(name)) { _db.createObjectStore( name, { keyPath: 'id', autoIncrement: true }); } }, errorHandler: function (error) { window.alert('error: ' + error.target.code); debugger; }, open: function (callback) { var request = window.indexedDB.open( db.objectStoreName, db.version); request.onerror = db.errorHandler; request.onupgradeneeded = db.upgrade; request.onsuccess = function (e) { db.instance = request.result; db.instance.onerror = db.errorHandler; callback(); }; }, getObjectStore: function (mode) { var txn, store; mode = mode || 'readonly'; txn = db.instance.transaction( [db.objectStoreName], mode); store = txn.objectStore( db.objectStoreName); return store; }, save: function (data, callback) { db.open(function () { var store, request, mode = 'readwrite'; store = db.getObjectStore(mode), request = data.id ? store.put(data) : store.add(data); request.onsuccess = callback; }); }, getAll: function (callback) { db.open(function () { var store = db.getObjectStore(), cursor = store.openCursor(), data = []; cursor.onsuccess = function (e) { var result = e.target.result; if (result && result !== null) { data.push(result.value); result.continue(); } else { callback(data); } }; }); }, get: function (id, callback) { id = parseInt(id); db.open(function () { var store = db.getObjectStore(), request = store.get(id); request.onsuccess = function (e){ callback(e.target.result); }; }); }, 'delete': function (id, callback) { id = parseInt(id); db.open(function () { var mode = 'readwrite', store, request; store = db.getObjectStore(mode); request = store.delete(id); request.onsuccess = callback; }); }, deleteAll: function (callback) { db.open(function () { var mode, store, request; mode = 'readwrite'; store = db.getObjectStore(mode); request = store.clear(); request.onsuccess = callback; }); } }; window.app = window.app || {}; window.app.db = db; }(window));
// index.ui.js ; (function ($, Modernizr, app) { 'use strict'; $(function(){ if(!Modernizr.indexeddb){ $('#unsupported-message').show(); $('#ui-container').hide(); return; } var $deleteAllBtn = $('#delete-all-btn'), $titleText = $('#title-text'), $notesText = $('#notes-text'), $idHidden = $('#id-hidden'), $clearButton = $('#clear-button'), $saveButton = $('#save-button'), $listContainer = $('#list-container'), $noteTemplate = $('#note-template'), $emptyNote = $('#empty-note'); var addNoTasksMessage = function(){ $listContainer.append( $emptyNote.html()); }; var bindData = function (data) { $listContainer.html(''); if(data.length === 0){ addNoTasksMessage(); return; } data.forEach(function (note) { var m = $noteTemplate.html(); m = m.replace(/{ID}/g, note.id); m = m.replace(/{TITLE}/g, note.title); $listContainer.append(m); }); }; var clearUI = function(){ $titleText.val('').focus(); $notesText.val(''); $idHidden.val(''); }; // select individual item $listContainer.on('click', 'a[data-id]', function (e) { var id, current; e.preventDefault(); current = e.currentTarget; id = $(current).attr('data-id'); app.db.get(id, function (note) { $titleText.val(note.title); $notesText.val(note.text); $idHidden.val(note.id); }); return false; }); // delete item $listContainer.on('click', 'i[data-id]', function (e) { var id, current; e.preventDefault(); current = e.currentTarget; id = $(current).attr('data-id'); app.db.delete(id, function(){ app.db.getAll(bindData); clearUI(); }); return false; }); $clearButton.click(function(e){ e.preventDefault(); clearUI(); return false; }); $saveButton.click(function (e) { var title = $titleText.val(); if (title.length === 0) { return; } var note = { title: title, text: $notesText.val() }; var id = $idHidden.val(); if(id !== ''){ note.id = parseInt(id); } app.db.save(note, function(){ app.db.getAll(bindData); clearUI(); }); }); $deleteAllBtn.click(function (e) { e.preventDefault(); app.db.deleteAll(function () { $listContainer.html(''); addNoTasksMessage(); clearUI(); }); return false; }); app.db.errorHandler = function (e) { window.alert('error: ' + e.target.code); debugger; }; app.db.getAll(bindData); }); }(jQuery, Modernizr, window.app));
<!doctype html> <html lang="en-US"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Introduction to IndexedDB</title> <meta name="description" content="Introduction to IndexedDB"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/css/font-awesome.min.css" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/FontAwesome.otf" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.eot" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.svg" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.ttf" > <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs /font-awesome/4.1.0/fonts/fontawesome-webfont.woff" > <style> h1 { text-align: center; color:#999; } ul li { font-size: 1.35em; margin-top: 1em; margin-bottom: 1em; } ul li.small { font-style: italic; } footer { margin-top: 25px; border-top: 1px solid #eee; padding-top: 25px; } i[data-id] { cursor: pointer; color: #eee; } i[data-id]:hover { color: #c75a6d; } .push-down { margin-top: 25px; } #save-button { margin-left: 10px; } </style> <script src="//cdnjs.cloudflare.com/ajax/libs/modernizr /2.8.2/modernizr.min.js" ></script> </head> <body class="container"> <h1>Tasks</h1> <div id="unsupported-message" class="alert alert-warning" style="display:none;"> <b>Aww snap!</b> Your browser does not support indexedDB. </div> <div id="ui-container" class="row"> <div class="col-sm-3"> <a href="#" id="delete-all-btn" class="btn-xs"> <i class="fa fa-trash-o"></i> Delete All</a> <hr/> <ul id="list-container" class="list-unstyled"></ul> </div> <div class="col-sm-8 push-down"> <input type="hidden" id="id-hidden" /> <input id="title-text" type="text" class="form-control" tabindex="1" placeholder="title" autofocus /><br /> <textarea id="notes-text" class="form-control" tabindex="2" placeholder="text"></textarea> <div class="pull-right push-down"> <a href="#" id="clear-button" tabindex="4">Clear</a> <button id="save-button" tabindex="3" class="btn btn-default btn-primary"> <i class="fa fa-save"></i> Save</button> </div> </div> </div> <footer class="small text-muted text-center">by <a href="http://craigshoemaker.net" target="_blank">Craig Shoemaker</a> <a href="http://twitter.com/craigshoemaker" target="_blank"> <i class="fa fa-twitter"></i></a> </footer> <script id="note-template" type="text/template"> <li> <i data-id="{ID}" class="fa fa-minus-circle"></i> <a href="#" data-id="{ID}">{TITLE}</a> </li> </script> <script id="empty-note" type="text/template"> <li class="text-muted small">No tasks</li> </script> <script src="//ajax.googleapis.com/ajax/libs /jquery/1.11.1/jquery.min.js"></script> <script src="index.db.js" type="text/javascript"></script> <script src="index.ui.js" type="text/javascript"></script> </body> </html>