From 9dceeff81223bdad00acb28253325f6fa9f9face Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Wed, 20 Nov 2013 10:24:55 -0800 Subject: [PATCH 001/640] Fixed an issue with BrowserRange.normalize. #288 If the focusNode of a browser selection had an offset of zero and its first node was a non-text node that was empty of text nodes, the code would hit a TypeError. This happened because `normalize` used an index of -1 (0 - 1) into `@endContainer.childNodes` to find the previous node. This change adds a check for the offset if `normalize` did not find a text node. If the offset is zero, `normalize` passes the endContainer's previous sibling to `Util.getLastTextNodeUpTo` instead of one of its `childNodes`. --- src/range.coffee | 8 ++++++-- test/fixtures/range.html | 11 +++++++++++ test/spec/range_spec.coffee | 4 +++- 3 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/range.coffee b/src/range.coffee index 2c4e8bcf4..e51214e18 100644 --- a/src/range.coffee +++ b/src/range.coffee @@ -159,8 +159,12 @@ class Range.BrowserRange r.endOffset = 0 unless r.end? - # We need to find a text node in the previous node. - node = @endContainer.childNodes[@endOffset - 1] + # We need to find a text node in the previous sibling of the node at the + # given offset, if one exists, or in the previous sibling of its container. + if @endOffset + node = @endContainer.childNodes[@endOffset - 1] + else + node = @endContainer.previousSibling r.end = Util.getLastTextNodeUpTo node r.endOffset = r.end.nodeValue.length diff --git a/test/fixtures/range.html b/test/fixtures/range.html index 12d15aba6..5acbcdf40 100644 --- a/test/fixtures/range.html +++ b/test/fixtures/range.html @@ -12,3 +12,14 @@

Header Level 2

Humani generis

Lorem sed do eiusmod tempor.


Mortalium animos
Humani generis


Lorem sed do eiusmod tempor.



Mortalium animos
+ +

Header Level 2

+ +

+ Mauris lacinia ipsum nulla, id iaculis quam egestas quis. +

+ +

+ Fusce fermentum sit amet augue sed rutrum. +

+ diff --git a/test/spec/range_spec.coffee b/test/spec/range_spec.coffee index afe7f3de5..45a9f15f7 100644 --- a/test/spec/range_spec.coffee +++ b/test/spec/range_spec.coffee @@ -23,7 +23,8 @@ testData = [ [ "/div[2]/text()[2]",0,"/div[2]",7,"Lorem sed do eiusmod tempor.", "Text between br tags, with



at end"] [ "/div[2]", 3,"/div[2]/text()[2]",28,"Lorem sed do eiusmod tempor.", "Text between br tags, elementNode ref at start"] [ "/div[2]", 2,"/div[2]/text()[2]",28,"Lorem sed do eiusmod tempor.", "Text between br tags, with


at the start"] - [ "/div[2]", 1,"/div[2]/text()[2]",28,"Lorem sed do eiusmod tempor.", "Text between br tags, with


at the start"] + [ "/div[2]", 1,"/div[2]/text()[2]",28,"Lorem sed do eiusmod tempor.", "Text between br tags, with


at the start"], + [ "/h2[2]", 0,"/p[4]", 0, "Header Level 2\n\n\n Mauris lacinia ipsum nulla, id iaculis quam egestas quis.\n\n\n", "No text node at the end and offset 0"] ] describe 'Range', -> @@ -145,6 +146,7 @@ describe 'Range', -> range = new Range.BrowserRange(sel.getRangeAt(0)) norm = range.normalize(fix()) + console.log(textInNormedRange(norm), sel.expectation) assert.equal(textInNormedRange(norm), sel.expectation) for i in [0...testData.length] From 490b6de5705e09c8d82ddb78c0d44b1a613f8010 Mon Sep 17 00:00:00 2001 From: Andrew Brookins Date: Wed, 20 Nov 2013 10:35:26 -0800 Subject: [PATCH 002/640] Remove logging --- test/spec/range_spec.coffee | 1 - 1 file changed, 1 deletion(-) diff --git a/test/spec/range_spec.coffee b/test/spec/range_spec.coffee index 45a9f15f7..6cd18077a 100644 --- a/test/spec/range_spec.coffee +++ b/test/spec/range_spec.coffee @@ -146,7 +146,6 @@ describe 'Range', -> range = new Range.BrowserRange(sel.getRangeAt(0)) norm = range.normalize(fix()) - console.log(textInNormedRange(norm), sel.expectation) assert.equal(textInNormedRange(norm), sel.expectation) for i in [0...testData.length] From 9c47772554174ea0ce252d9e079737ebda5254da Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Mon, 9 Dec 2013 15:01:43 -0800 Subject: [PATCH 003/640] Consistently use string indexing for registry prop This makes the use of the registry as a dictionary more clear. It could also prove important for certain code eliminations paths if someone ever tries to use the Closure Compiler on Annotator. --- src/annotations.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/annotations.coffee b/src/annotations.coffee index 07a45ce98..6a80079e1 100644 --- a/src/annotations.coffee +++ b/src/annotations.coffee @@ -83,7 +83,7 @@ class AnnotationProvider safeCopy = $.extend(true, {}, obj) delete safeCopy._local - @registry.store[storeFunc](safeCopy) + @registry['store'][storeFunc](safeCopy) .then (ret) => # Empty object without changing identity for own k, v of obj From 08a088683f47ec28eaf372412bb46aec2c5def9d Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Mon, 9 Dec 2013 15:07:13 -0800 Subject: [PATCH 004/640] Improve consistency of event timing The 1.2.x series fired `beforeAnnotationCreated` before setting up the annotation. We should do the same here, and be consistent in where in the action lifecycle we fire the events for all of them. --- src/annotator.coffee | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index cbb1801bc..535dc5e52 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -122,10 +122,10 @@ class Annotator extends Delegator .on("edit", this.onEditAnnotation) .on("delete", (annotation) => @viewer.hide() + this.publish('beforeAnnotationDeleted', [annotation]) # Delete highlight elements. this.cleanupAnnotation(annotation) # Delete annotation - this.publish('beforeAnnotationDeleted', [annotation]) this.annotations.delete(annotation) .done => this.publish('annotationDeleted', [annotation]) ) @@ -677,6 +677,9 @@ class Annotator extends Delegator $.when(annotation) + .done (annotation) => + this.publish('beforeAnnotationCreated', [annotation]) + # Set up the annotation .then (annotation) => this.setupAnnotation(annotation) @@ -688,18 +691,17 @@ class Annotator extends Delegator # Edit the annotation .then (annotation) => - this.publish('beforeAnnotationCreated', [annotation]) this.editAnnotation(annotation) .then (annotation) => this.annotations.create(annotation) - .then (annotation) => - this.publish('annotationCreated', [annotation]) - annotation # Clean up the highlights .done (annotation) => $(annotation._local.highlights).removeClass('annotator-hl-temporary') + .done (annotation) => + this.publish('annotationCreated', [annotation]) + # Handle errors .fail(this.cleanupAnnotation, handleError) @@ -715,6 +717,9 @@ class Annotator extends Delegator $.when(annotation) + .done (annotation) => + this.publish('beforeAnnotationUpdated', [annotation]) + # Replace the viewer with the editor .then (annotation) => @viewer.hide() @@ -723,13 +728,12 @@ class Annotator extends Delegator # Edit the annotation .then (annotation) => - this.publish('beforeAnnotationUpdated', [annotation]) this.editAnnotation(annotation) .then (annotation) => this.annotations.update(annotation) - .then (annotation) => + + .done (annotation) => this.publish('annotationUpdated', [annotation]) - annotation # Handle errors .fail(handleError) From e00387a6f4b3fbfaae512deeabe18ebf5cf11da3 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 10 Dec 2013 21:12:34 -0800 Subject: [PATCH 005/640] move editor hide/show into editAnnotation --- src/annotator.coffee | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index 535dc5e52..9d942bcf3 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -465,11 +465,12 @@ class Annotator extends Delegator # Public: Waits for the @editor to submit or hide, returning a promise that # is resolved or rejected depending on whether the annotation was saved or # cancelled. - editAnnotation: (annotation) => + editAnnotation: (annotation, position) => dfd = $.Deferred() resolve = dfd.resolve.bind(dfd, annotation) reject = dfd.reject.bind(dfd, annotation) + this.showEditor(annotation, position) this.subscribe('annotationEditorSubmit', resolve) this.once 'annotationEditorHidden', => this.unsubscribe('annotationEditorSubmit', resolve) @@ -687,11 +688,10 @@ class Annotator extends Delegator # Show a temporary highlight so the user can see what they selected .done (annotation) => $(annotation._local.highlights).addClass('annotator-hl-temporary') - this.showEditor(annotation, position) # Edit the annotation .then (annotation) => - this.editAnnotation(annotation) + this.editAnnotation(annotation, position) .then (annotation) => this.annotations.create(annotation) @@ -714,21 +714,15 @@ class Annotator extends Delegator # Returns nothing. onEditAnnotation: (annotation) => position = @viewer.element.position() + @viewer.hide() $.when(annotation) .done (annotation) => this.publish('beforeAnnotationUpdated', [annotation]) - # Replace the viewer with the editor - .then (annotation) => - @viewer.hide() - this.showEditor(annotation, position) - annotation - - # Edit the annotation .then (annotation) => - this.editAnnotation(annotation) + this.editAnnotation(annotation, position) .then (annotation) => this.annotations.update(annotation) From 94c584c67fb072d68f4f11fb1260d49c631ba82a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 10 Dec 2013 22:01:54 -0800 Subject: [PATCH 006/640] bracket syntax for registry used as dict --- src/annotations.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/annotations.coffee b/src/annotations.coffee index 6a80079e1..503b00411 100644 --- a/src/annotations.coffee +++ b/src/annotations.coffee @@ -66,7 +66,7 @@ class AnnotationProvider # # Returns a Promise resolving to the store return value. query: (query) -> - return @registry.store.query(query) + return @registry['store'].query(query) # Public: Queries the store # From 4dc72900343044ae59a1fef22fffee677bf82b9c Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 11 Dec 2013 16:54:11 -0800 Subject: [PATCH 007/640] Documentation comments for Registry --- src/registry.coffee | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/registry.coffee b/src/registry.coffee index 07c4c7ef7..bcdc47c57 100644 --- a/src/registry.coffee +++ b/src/registry.coffee @@ -1,17 +1,32 @@ Evented = require('./events') +# Registry is a factory for annotator applications providing a simple runtime +# extension interface and application loader. It is used to pass settings to +# extension modules and provide a means by which extensions can export +# functionality to applications. class Registry extends Evented - @createApp: (appModule, settings) -> + + # Public: Create an instance of the application defined by the provided + # module. The application will receive a new registry instance whose settings + # may be provided as a second argument to this method. The registry will + # immediately invoke the run callback of the module. + @createApp: (appModule, settings={}) -> (new this(settings)).run(appModule) constructor: (@settings={}) -> super + # Public: Include a module. A module is any Object with a fuction property + # named 'configure`. This function is immediately invoked with the registry + # instance as the only argument. include: (module) -> module?.configure(this) this + # Public: Run an application. An application is a module with a function + # property named 'run'. The application is immediately included and its run + # callback invoked with the registry instance as the only argument. run: (app) -> if this.app throw new Error("This registry already has an application.") From fb7fc6c826450e84427a28407d02503d3e0f416d Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 11 Dec 2013 16:54:34 -0800 Subject: [PATCH 008/640] Registry bail on bad modules; pass itself to run --- src/registry.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/registry.coffee b/src/registry.coffee index bcdc47c57..d1915f224 100644 --- a/src/registry.coffee +++ b/src/registry.coffee @@ -21,7 +21,7 @@ class Registry extends Evented # named 'configure`. This function is immediately invoked with the registry # instance as the only argument. include: (module) -> - module?.configure(this) + module.configure(this) this # Public: Run an application. An application is a module with a function @@ -37,6 +37,6 @@ class Registry extends Evented app[k] = v this.app = app - app.run?() + app.run(this) module.exports = Registry From 7b1612582ab385fd5216a7ad0c45191730e3c0e0 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 11 Dec 2013 16:56:21 -0800 Subject: [PATCH 009/640] Registry error message tweak --- src/registry.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/registry.coffee b/src/registry.coffee index d1915f224..6fa3dba37 100644 --- a/src/registry.coffee +++ b/src/registry.coffee @@ -29,7 +29,7 @@ class Registry extends Evented # callback invoked with the registry instance as the only argument. run: (app) -> if this.app - throw new Error("This registry already has an application.") + throw new Error("Registry is already bound to a running application") this.include(app) From 57322a1091bd8b4c37d9db3a20848aab95bf744a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 11 Dec 2013 16:57:15 -0800 Subject: [PATCH 010/640] Pass Annotator options to Registry as settings --- src/annotator.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index 9d942bcf3..f11b09280 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -93,7 +93,7 @@ class Annotator extends Delegator return this unless Annotator.supported() # Create the registry and start the application - Registry.createApp(this) + Registry.createApp(this, options) # Wraps the children of @element in a @wrapper div. NOTE: This method will also # remove any script elements inside @element to prevent them re-executing. From 27540aec4af898b93849bfbb6dede2801273273d Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 11 Dec 2013 17:05:43 -0800 Subject: [PATCH 011/640] Set user storage plugin class from registry Annotator instances no longer inspect the options for a store because the default storage provider honors settings found on the registry. --- dev.html | 13 +++++-------- src/annotator.coffee | 10 +++------- src/storage.coffee | 9 ++++++++- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/dev.html b/dev.html index 9151bdd37..a2f37ba1b 100644 --- a/dev.html +++ b/dev.html @@ -89,15 +89,12 @@

Header Level 3

(function ($) { var elem = document.getElementById('airlock'); - devStore = new Annotator.Plugin.Store({ + devStore = { + type: Annotator.Plugin.Store, prefix: 'http://localhost:5000/', - loadFromSearch: { - uri: 'http://localhost/annotator/dev.html' - }, - annotationData: { - uri: 'http://localhost/annotator/dev.html' - } - }); + loadFromSearch: {uri: window.location}, + annotationData: {uri: window.location} + }; devAnnotator = new Annotator(elem, {store: devStore}) .addPlugin('Auth', { diff --git a/src/annotator.coffee b/src/annotator.coffee index f11b09280..e9b62fdbf 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -610,15 +610,11 @@ class Annotator extends Delegator isAnnotator: (element) -> !!$(element).parents().addBack().filter('[class^=annotator-]').not(@wrapper).length - configure: (registry) -> + configure: (@registry) -> registry.include(annotations) + registry.include(storage) - if @options.store - registry.store = @options.store - else - registry.include(storage) - - run: -> + run: (@registry) -> # Set up the core interface components this._setupDocumentEvents() unless @options.readOnly this._setupWrapper()._setupViewer()._setupEditor() diff --git a/src/storage.coffee b/src/storage.coffee index a529a9b6b..45b3cf540 100644 --- a/src/storage.coffee +++ b/src/storage.coffee @@ -2,7 +2,14 @@ class StorageProvider @configure: (registry) -> - registry['store'] ?= new this(registry) + klass = registry.settings.store?.type + + if typeof(klass) is 'function' + store = new klass(registry.settings.store) + else + store = new this(registry) + + registry['store'] ?= store constructor: (@registry) -> From b05dca04206b5ceab39f99748cf6bdbc8785a166 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 15:18:42 -0800 Subject: [PATCH 012/640] Unit tests for the new Registry class --- test/spec/registry_spec.coffee | 329 +++------------------------------ 1 file changed, 24 insertions(+), 305 deletions(-) diff --git a/test/spec/registry_spec.coffee b/test/spec/registry_spec.coffee index acecc68c7..9a9590e28 100644 --- a/test/spec/registry_spec.coffee +++ b/test/spec/registry_spec.coffee @@ -1,320 +1,39 @@ Registry = require('../../src/registry') -class MockStore - create: (data) -> - if data.i? - data.i *= 10 - dfd = new $.Deferred() - dfd.resolve($.extend({id: 123}, data)) - return dfd.promise() - - update: (data) -> - if data.i? - data.i *= 10 - dfd = new $.Deferred() - dfd.resolve($.extend({}, data)) - return dfd.promise() - - delete: (data) -> - if data.i? - data.i *= 10 - dfd = new $.Deferred() - dfd.resolve({}) - return dfd.promise() - - query: (data) -> - dfd = new $.Deferred() - dfd.resolve([{id: 1}, {id: 2}], {total:2}) - return dfd.promise() - describe 'Registry', -> - m = null + s = {} r = null + m = null beforeEach -> - m = new MockStore() - r = new Registry(m) - - it 'should take a Store plugin as its first constructor argument', -> - assert.equal(r.store, m) - - describe '#create()', -> - - it "should pass annotation data to the store's #create()", -> - sinon.spy(m, 'create') - - r.create({some: 'data'}) - assert(m.create.calledOnce, 'store .create() called once') - assert( - m.create.calledWith({some: 'data'}), - 'store .create() called with correct args' - ) - - m.create.reset() - - it "should return a promise resolving to the created annotation", (done) -> - ann = {some: 'data'} - r.create(ann) - .done (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {id: 123, some: 'data'}) - done() - .fail (obj, msg) -> - done(new Error("promise rejected: #{msg}")) - - - it "should publish beforeAnnotationCreated before passing to the store", (done) -> - ann = {i: 1} - r.subscribe('beforeAnnotationCreated', (a) -> a.i += 1) - r.create(ann) - .done (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {id: 123, i: 20}) - done() - - it "should publish annotationCreated once the store promise resolves", (done) -> - ann = {i: 1} - r.subscribe('annotationCreated', (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {id: 123, i: 10}) - done() - ) - r.create(ann) - .done((a) -> a.i += 1) - - it "should strip an annotation of any _local before passing to the store", -> - sinon.spy(m, 'create') - local = {foo: 'bar', numbers: [1,2,3]} - ann = {some: 'data', _local: local} - r.create(ann) - assert( - m.create.calledWith({some: 'data'}) - 'annotation _local stripped before store .create() call' - ) - - it "should leave _local in place when firing beforeAnnotationCreated", (done) -> - local = {foo: 'bar', numbers: [1,2,3]} - ann = {some: 'data', _local: local} - obj = null - r.subscribe('beforeAnnotationCreated', (a) -> obj = JSON.stringify(a)) - r.create(ann) - .done -> - assert.deepEqual(JSON.parse(obj), {some: 'data', _local: local}) - done() - - it "should reattach _local before firing annotationCreated", (done) -> - local = {foo: 'bar', numbers: [1,2,3]} - ann = {some: 'data', _local: local} - obj = null - r.subscribe('annotationCreated', (ann) -> obj = JSON.stringify(ann)) - r.create(ann) - .done -> - assert.deepEqual(JSON.parse(obj), {id: 123, some: 'data', _local: local}) - done() - - describe '#update()', -> - - it "should return a rejected promise if the data lacks an id", (done) -> - ann = {some: 'data'} - r.update(ann) - .done -> - done(new Error("promise unexpectedly resolved")) - .fail (ret, msg) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {some: 'data'}) - assert.include(msg, ' id ') - done() - - it "should pass annotation data to the store's #update()", -> - sinon.spy(m, 'update') - - r.update({id: 123, some: 'data'}) - assert(m.update.calledOnce, 'store .update() called once') - assert( - m.update.calledWith({id: 123, some: 'data'}), - 'store .update() called with correct args' - ) - - it "should return a promise resolving to the updated annotation", (done) -> - ann = {id: 123, some: 'data'} - r.update(ann) - .done (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {id: 123, some: 'data'}) - done() - .fail (obj, msg) -> - done(new Error("promise rejected: #{msg}")) - - it "should publish beforeAnnotationUpdated before passing to the store", (done) -> - ann = {id: 123, i: 1} - r.subscribe('beforeAnnotationUpdated', (a) -> a.i += 1) - r.update(ann) - .done (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {id: 123, i: 20}) - done() - - it "should publish annotationUpdated once the store promise resolves", (done) -> - ann = {id: 123, i: 1} - r.subscribe('annotationUpdated', (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {id: 123, i: 10}) - done() - ) - r.update(ann) - .done((a) -> a.i += 1) - - it "should strip an annotation of any _local before passing to the store", -> - sinon.spy(m, 'update') - local = {foo: 'bar', numbers: [1,2,3]} - ann = {id: 123, some: 'data', _local: local} - - r.update(ann) - - assert( - m.update.calledWith({id: 123, some: 'data'}) - 'annotation _local stripped before store .update() call' - ) - - it "should leave _local in place when firing beforeAnnotationUpdated", (done) -> - local = {foo: 'bar', numbers: [1,2,3]} - ann = {id: 123, some: 'data', _local: local} - cache = null - r.subscribe('beforeAnnotationUpdated', (a) -> cache = JSON.stringify(a)) - r.update(ann) - .done -> - assert.deepEqual(JSON.parse(cache), {id: 123, some: 'data', _local: local}) - done() - - it "should reattach _local before firing annotationUpdated", (done) -> - local = {foo: 'bar', numbers: [1,2,3]} - ann = {id: 123, some: 'data', _local: local} - cache = null - r.subscribe('annotationUpdated', (a) -> cache = JSON.stringify(a)) - r.update(ann) - .done -> - assert.deepEqual(JSON.parse(cache), {id: 123, some: 'data', _local: local}) - done() - - describe '#delete()', -> - - it "should return a rejected promise if the data lacks an id", (done) -> - ann = {some: 'data'} - r.delete(ann) - .done -> - done(new Error("promise unexpectedly resolved")) - .fail (ret, msg) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {some: 'data'}) - assert.include(msg, ' id ') - done() - - it "should pass annotation data to the store's #delete()", -> - sinon.spy(m, 'delete') - - r.delete({id: 123, some: 'data'}) - assert(m.delete.calledOnce, 'store .delete() called once') - assert( - m.delete.calledWith({id: 123, some: 'data'}), - 'store .delete() called with correct args' - ) - - it "should return a promise resolving to the deleted annotation object", (done) -> - ann = {id: 123, some: 'data'} - r.delete(ann) - .done (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {}) - done() - .fail (obj, msg) -> - done(new Error("promise rejected: #{msg}")) - - it "should publish beforeAnnotationDeleted before passing to the store", (done) -> - ann = {id: 123, some: 'data'} - cache = null - r.subscribe('beforeAnnotationDeleted', (a) -> cache = JSON.stringify(a)) - r.delete(ann) - .done -> - assert.deepEqual(JSON.parse(cache), {id: 123, some: 'data'}) - done() - - it "should publish annotationDeleted once the store promise resolves", (done) -> - ann = {id: 123, some: 'data'} - r.subscribe('annotationDeleted', (ret) -> - assert.equal(ret, ann) - assert.deepEqual(ret, {}) - done() - ) - r.delete(ann) - - it "should strip an annotation of any _local before passing to the store", -> - sinon.spy(m, 'delete') - local = {foo: 'bar', numbers: [1,2,3]} - r.delete({id: 123, some: 'data', _local: local}) - assert( - m.delete.calledWith({id: 123, some: 'data'}) - 'annotation _local stripped before store .delete() call' - ) - - it "should leave _local in place when firing beforeAnnotationDeleted", (done) -> - local = {foo: 'bar', numbers: [1,2,3]} - ann = {id: 123, some: 'data', _local: local} - cache = null - r.subscribe('beforeAnnotationDeleted', (a) -> cache = JSON.stringify(a)) - r.delete(ann) - .done -> - assert.deepEqual(JSON.parse(cache), {id: 123, some: 'data', _local: local}) - done() - - it "should reattach _local before firing annotationDeleted", (done) -> - local = {foo: 'bar', numbers: [1,2,3]} - ann = {id: 123, some: 'data', _local: local} - cache = null - r.subscribe('annotationDeleted', (a) -> cache = JSON.stringify(a)) - r.delete(ann) - .done -> - assert.deepEqual(JSON.parse(cache), {_local: local}) - done() - - describe '#query()', -> - - it "should pass query data to the store's #query()", -> - sinon.spy(m, 'query') + s = {} + r = new Registry(s) - r.query({foo: 'bar', type: 'giraffe'}) - assert(m.query.calledOnce, 'store .query() called once') - assert( - m.query.calledWith({foo: 'bar', type: 'giraffe'}), - 'store .query() called with correct args' - ) + m = + configure: sinon.spy((registry) -> registry['foo'] = 'bar') + run: sinon.stub() - m.query.reset() + it 'should take a settings Object as its first constructor argument', -> + assert.equal(r.settings, s) - it "should return a promise", (done) -> - r.query({foo: 'bar', type: 'giraffe'}) - .done () -> - done() - .fail (obj, msg) -> - done(new Error("promise rejected: #{msg}")) + describe '#include()', -> - describe '#load()', -> + it 'should invoke the configure method of the passed module with itself', -> + r.include(m) + assert(m.configure.calledWith(r)) - it "should pass query data to the store's #query()", -> - sinon.spy(m, 'query') + describe '#run()', -> - r.load({foo: 'bar', type: 'giraffe'}) - assert(m.query.calledOnce, 'store .query() called once') - assert( - m.query.calledWith({foo: 'bar', type: 'giraffe'}), - 'store .query() called with correct args' - ) + it 'should include the application module', -> + sinon.spy(r, 'include') + r.run(m) + assert(r.include.calledWith(m)) - m.query.reset() + it 'should extend the application with registry extensions', -> + r.run(m) + assert.equal(m['foo'], 'bar') - it "should return a promise", (done) -> - r.load({foo: 'bar', type: 'giraffe'}) - .done () -> - done() - .fail (obj, msg) -> - done(new Error("promise rejected: #{msg}")) + it 'should invoke the run method fo the passed module with itself', -> + r.run(m) + assert(m.run.calledWith(r)) From 0d707c1a77355615aad8214234332d02f0c90d31 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 15:59:35 -0800 Subject: [PATCH 013/640] Fix scope conflict in Annotator One unfortunate down-side of CoffeeScript not using the `var` keyword is that declarations in outer scopes can sometimes have unforeseen consequences due to conflicts with previously local-only names. --- src/annotator.coffee | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index e9b62fdbf..c202c53d1 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -7,8 +7,8 @@ Editor = require './editor' Notification = require './notification' Registry = require './registry' -annotations = require './annotations' -storage = require './storage' +AnnotationProvider = require './annotations' +StorageProvider = require './storage' _t = Util.TranslationString @@ -611,8 +611,8 @@ class Annotator extends Delegator !!$(element).parents().addBack().filter('[class^=annotator-]').not(@wrapper).length configure: (@registry) -> - registry.include(annotations) - registry.include(storage) + registry.include(AnnotationProvider) + registry.include(StorageProvider) run: (@registry) -> # Set up the core interface components From 09f2a5396ce6a07cdcd5fb8a2e04cf0f3cf9da1d Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 15:54:59 -0800 Subject: [PATCH 014/640] Update Annotator tests - cleanupAnnotation instead of delete Delete used to deal with highlights, now it's cleanupAnnotation - Add test to ensure default services are loaded This obsoletes NullStore --- test/spec/annotator_spec.coffee | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/test/spec/annotator_spec.coffee b/test/spec/annotator_spec.coffee index 807e1df12..5aef0f3d8 100644 --- a/test/spec/annotator_spec.coffee +++ b/test/spec/annotator_spec.coffee @@ -7,11 +7,8 @@ Range = Annotator.Range describe 'Annotator', -> annotator = null - mock = null - beforeEach -> annotator = new Annotator($('
')[0], { - store: new Annotator.Plugin.NullStore() - }) + beforeEach -> annotator = new Annotator($('
')[0]) afterEach -> $(document).unbind() describe "events", -> @@ -53,6 +50,10 @@ describe 'Annotator', -> sinon.stub(annotator, '_setupDocumentEvents').returns(annotator) sinon.stub(annotator, '_setupDynamicStyle').returns(annotator) + it 'should include the default modules', -> + assert.isObject(annotator['annotations'], 'annotations service exists') + assert.isObject(annotator['annotations'], 'storage service exists') + it "should have a jQuery wrapper as @element", -> Annotator.prototype.constructor.call(annotator, annotator.element[0]) assert.instanceOf(annotator.element, $) @@ -434,33 +435,32 @@ describe 'Annotator', -> it "should store the annotation in the highlighted element's data store", -> assert.equal(element.data('annotation'), annotation) - describe "when an annotation is deleted", -> + describe "cleanupAnnotation", -> annotation = null div = null beforeEach -> annotation = { text: "my annotation comment" - highlights: $('HatsGloves') + _local: + highlights: $('HatsGloves') } - div = $('
').append(annotation.highlights) + div = $('
').append(annotation._local.highlights) it "should remove the highlights from the DOM", -> - annotation.highlights.each -> + annotation._local.highlights.each -> assert.lengthOf($(this).parent(), 1) - annotator.annotations.delete(annotation) - .then -> - annotation.highlights.each -> - assert.lengthOf($(this).parent(), 0) + annotator.cleanupAnnotation(annotation) + annotation._local.highlights.each -> + assert.lengthOf($(this).parent(), 0) it "should leave the content of the highlights in place", -> - annotator.annotations.delete(annotation) - .then -> - assert.equal(div.html(), 'HatsGloves') + annotator.cleanupAnnotation(annotation) + assert.equal(div.html(), 'HatsGloves') it "should not choke when there are no highlights", -> - assert.doesNotThrow((-> annotator.annotations.delete({})), Error) + assert.doesNotThrow((-> annotator.cleanupAnnotation({})), Error) describe "loadAnnotations", -> beforeEach -> From 07d08afea652852dd235ddf7a829f3f185bd829b Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 16:00:39 -0800 Subject: [PATCH 015/640] Remove unnecessary => method bindings --- src/annotator.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index c202c53d1..588345956 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -310,7 +310,7 @@ class Annotator extends Delegator # annotation = annotator.setupAnnotation(annotation) # # Returns the initialised annotation. - setupAnnotation: (annotation) => + setupAnnotation: (annotation) -> root = @wrapper[0] normedRanges = [] @@ -465,7 +465,7 @@ class Annotator extends Delegator # Public: Waits for the @editor to submit or hide, returning a promise that # is resolved or rejected depending on whether the annotation was saved or # cancelled. - editAnnotation: (annotation, position) => + editAnnotation: (annotation, position) -> dfd = $.Deferred() resolve = dfd.resolve.bind(dfd, annotation) reject = dfd.reject.bind(dfd, annotation) From 484b082aa4be261775d3984059c0aca90e67c6eb Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 17:55:31 -0800 Subject: [PATCH 016/640] Add StorageProvider tests --- test/runner.html | 2 + test/spec/storage_spec.coffee | 120 ++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 test/spec/storage_spec.coffee diff --git a/test/runner.html b/test/runner.html index 6108323ad..624acaa7f 100644 --- a/test/runner.html +++ b/test/runner.html @@ -39,6 +39,8 @@ + + diff --git a/test/spec/storage_spec.coffee b/test/spec/storage_spec.coffee new file mode 100644 index 000000000..83e5c1dc8 --- /dev/null +++ b/test/spec/storage_spec.coffee @@ -0,0 +1,120 @@ +Registry = require('../../src/registry') +AnnotationProvider = require('../../src/annotations') +StorageProvider = require('../../src/storage') + + +describe 'StorageProvider', -> + a = null + m = null + r = null + ann = null + + beforeEach -> + r = new Registry() + .include(AnnotationProvider) + .include(StorageProvider) + a = r['annotations'] + m = r['store'] + ann = {id: 123, some: 'data'} + + describe '#::configure()', -> + + it "should register the base storage implementation by default", -> + assert.instanceOf(m, StorageProvider) + + it "should instantiate a provided implementation store settings", -> + + MockStore = sinon.spy() + + settings = + store: + type: MockStore + foo: 'bar' + + r = new Registry(settings) + .include(StorageProvider) + assert(MockStore.calledWithNew(), 'instatiated MockStore') + assert(MockStore.calledWith(settings.store), 'passed settings') + + describe '#update()', -> + + it "should pass annotation data to the store's #update()", -> + sinon.spy(m, 'update') + + a.update(ann) + assert(m.update.calledOnce, 'store .update() called once') + assert( + m.update.calledWith(ann), + 'store .update() called with correct args' + ) + + it "should return a promise resolving to the updated annotation", (done) -> + a.update(ann) + .done (ret) -> + assert.equal(ret, ann) + done() + .fail (obj, msg) -> + done(new Error("promise rejected: #{msg}")) + + describe '#delete()', -> + + it "should pass annotation data to the store's #delete()", -> + sinon.spy(m, 'delete') + + a.delete(ann) + assert(m.delete.calledOnce, 'store .delete() called once') + assert( + m.delete.calledWith(ann), + 'store .delete() called with correct args' + ) + + it "should return a promise resolving to the deleted annotation object", (done) -> + ann = {id: 123, some: 'data'} + a.delete(ann) + .done (ret) -> + assert.equal(ret, ann) + done() + .fail (obj, msg) -> + done(new Error("promise rejected: #{msg}")) + + describe '#query()', -> + + it "should pass query data to the store's #query()", -> + sinon.spy(m, 'query') + + a.query({foo: 'bar', type: 'giraffe'}) + assert(m.query.calledOnce, 'store .query() called once') + assert( + m.query.calledWith({foo: 'bar', type: 'giraffe'}), + 'store .query() called with correct args' + ) + + m.query.reset() + + it "should return a promise", (done) -> + a.query({foo: 'bar', type: 'giraffe'}) + .done () -> + done() + .fail (obj, msg) -> + done(new Error("promise rejected: #{msg}")) + + describe '#load()', -> + + it "should pass query data to the store's #query()", -> + sinon.spy(m, 'query') + + a.load({foo: 'bar', type: 'giraffe'}) + assert(m.query.calledOnce, 'store .query() called once') + assert( + m.query.calledWith({foo: 'bar', type: 'giraffe'}), + 'store .query() called with correct args' + ) + + m.query.reset() + + it "should return a promise", (done) -> + a.load({foo: 'bar', type: 'giraffe'}) + .done () -> + done() + .fail (obj, msg) -> + done(new Error("promise rejected: #{msg}")) From e3a548ab768ca1a6ab06827546ddc1416d1f5203 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 19:54:13 -0800 Subject: [PATCH 017/640] Include storage when annotations are included --- src/annotations.coffee | 4 ++++ src/annotator.coffee | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/annotations.coffee b/src/annotations.coffee index 503b00411..0aa92b93d 100644 --- a/src/annotations.coffee +++ b/src/annotations.coffee @@ -1,8 +1,12 @@ +StorageProvider = require('./storage') + + # Public: Provides CRUD methods for annotations which call corresponding registry hooks. class AnnotationProvider @configure: (registry) -> registry['annotations'] ?= new this(registry) + registry.include(StorageProvider) constructor: (@registry) -> diff --git a/src/annotator.coffee b/src/annotator.coffee index 588345956..9182d1aa5 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -8,7 +8,6 @@ Notification = require './notification' Registry = require './registry' AnnotationProvider = require './annotations' -StorageProvider = require './storage' _t = Util.TranslationString @@ -612,7 +611,6 @@ class Annotator extends Delegator configure: (@registry) -> registry.include(AnnotationProvider) - registry.include(StorageProvider) run: (@registry) -> # Set up the core interface components From f410ac978ae2ce722d764734a927ea926124318c Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Thu, 12 Dec 2013 19:53:55 -0800 Subject: [PATCH 018/640] Add AnnotationProvider tests --- test/runner.html | 1 + test/spec/annotations_spec.coffee | 116 ++++++++++++++++++++++++++++++ test/spec/storage_spec.coffee | 61 ++-------------- 3 files changed, 122 insertions(+), 56 deletions(-) create mode 100644 test/spec/annotations_spec.coffee diff --git a/test/runner.html b/test/runner.html index 624acaa7f..88a75739c 100644 --- a/test/runner.html +++ b/test/runner.html @@ -39,6 +39,7 @@ + diff --git a/test/spec/annotations_spec.coffee b/test/spec/annotations_spec.coffee new file mode 100644 index 000000000..fcc1f07c1 --- /dev/null +++ b/test/spec/annotations_spec.coffee @@ -0,0 +1,116 @@ +Registry = require('../../src/registry') +AnnotationProvider = require('../../src/annotations') + + +describe 'AnnotationProvider', -> + a = null + r = null + m = null + + beforeEach -> + r = new Registry() + .include(AnnotationProvider) + + a = r['annotations'] + m = r['store'] + + sinon.spy(m, 'create') + sinon.spy(m, 'update') + sinon.spy(m, 'delete') + sinon.spy(m, 'query') + + describe '#create()', -> + + it "should pass annotation data to the store's #create()", -> + + a.create({some: 'data'}) + assert(m.create.calledOnce, 'store .create() called once') + assert( + m.create.calledWith(sinon.match({some: 'data'})), + 'store .create() called with correct args' + ) + + it "should return a promise resolving to the created annotation", (done) -> + ann = {some: 'data'} + a.create(ann) + .done (ret) -> + assert.equal(ret, ann) + assert.property(ret, 'id', 'created annotation has an id') + done() + .fail (obj, msg) -> + done(new Error("promise rejected: #{msg}")) + + describe '#update()', -> + + it "should pass annotation data to the store's #update()", -> + + a.update({id: '123', some: 'data'}) + assert(m.update.calledOnce, 'store .update() called once') + assert( + m.update.calledWith(sinon.match({id: '123', some: 'data'})), + 'store .update() called with correct args' + ) + + it "should throw a TypeError if the data lacks an id", -> + ann = {some: 'data'} + assert.throws((-> a.update(ann)), TypeError, ' id ') + + describe '#delete()', -> + + it "should pass annotation data to the store's #delete()", -> + + a.delete({id: '123', some: 'data'}) + assert(m.delete.calledOnce, 'store .delete() called once') + assert( + m.delete.calledWith(sinon.match({id: '123', some: 'data'})), + 'store .delete() called with correct args' + ) + + it "should throw a TypeError if the data lacks an id", -> + ann = {some: 'data'} + assert.throws((-> a.delete(ann)), TypeError, ' id ') + + describe '#query()', -> + + it "should invoke the query method on the registered store service", -> + query = {url: 'foo'} + a.query(query) + assert(m.query.calledWith(query)) + + describe '#load()', -> + + it "should call the query method", -> + sinon.spy(a, 'query') + a.load({foo: 'bar', type: 'giraffe'}) + assert(a.query.calledWith, sinon.match({foo: 'bar', type: 'giraffe'})) + + describe '#_cycle()', -> + store_noop = (a) -> $.Deferred().resolve(a).promise() + local = null + ann = null + + beforeEach -> + local = {foo: 'bar', numbers: [1,2,3]} + ann = {some: 'data', _local: local} + m['bogus'] = sinon.spy(store_noop) + + it "should strip an annotation of any _local before passing to the store", -> + a._cycle(ann, 'bogus') + assert( + m.bogus.calledWith(sinon.match({some: 'data'})) + 'annotation _local stripped before store call' + ) + it "should pass annotation data to the store method", -> + a._cycle(ann, 'bogus') + assert(m.bogus.calledOnce, 'store method called once') + assert( + m.bogus.calledWith(sinon.match({some: 'data'})), + 'store method called with correct args' + ) + + it "should reattach _local after the store promise resolves", (done) -> + after = sinon.spy (ret) -> + after.calledWith(sinon.match({some: 'data', _local: local})) + done() + + a._cycle(ann, 'bogus').done(after) diff --git a/test/spec/storage_spec.coffee b/test/spec/storage_spec.coffee index 83e5c1dc8..aebb0c422 100644 --- a/test/spec/storage_spec.coffee +++ b/test/spec/storage_spec.coffee @@ -22,7 +22,7 @@ describe 'StorageProvider', -> it "should register the base storage implementation by default", -> assert.instanceOf(m, StorageProvider) - it "should instantiate a provided implementation store settings", -> + it "should instantiate a provided implementation with store settings", -> MockStore = sinon.spy() @@ -38,16 +38,6 @@ describe 'StorageProvider', -> describe '#update()', -> - it "should pass annotation data to the store's #update()", -> - sinon.spy(m, 'update') - - a.update(ann) - assert(m.update.calledOnce, 'store .update() called once') - assert( - m.update.calledWith(ann), - 'store .update() called with correct args' - ) - it "should return a promise resolving to the updated annotation", (done) -> a.update(ann) .done (ret) -> @@ -58,16 +48,6 @@ describe 'StorageProvider', -> describe '#delete()', -> - it "should pass annotation data to the store's #delete()", -> - sinon.spy(m, 'delete') - - a.delete(ann) - assert(m.delete.calledOnce, 'store .delete() called once') - assert( - m.delete.calledWith(ann), - 'store .delete() called with correct args' - ) - it "should return a promise resolving to the deleted annotation object", (done) -> ann = {id: 123, some: 'data'} a.delete(ann) @@ -79,42 +59,11 @@ describe 'StorageProvider', -> describe '#query()', -> - it "should pass query data to the store's #query()", -> - sinon.spy(m, 'query') - - a.query({foo: 'bar', type: 'giraffe'}) - assert(m.query.calledOnce, 'store .query() called once') - assert( - m.query.calledWith({foo: 'bar', type: 'giraffe'}), - 'store .query() called with correct args' - ) - - m.query.reset() - - it "should return a promise", (done) -> + it "should return a promise resolving to the results and metadata", (done) -> a.query({foo: 'bar', type: 'giraffe'}) - .done () -> - done() - .fail (obj, msg) -> - done(new Error("promise rejected: #{msg}")) - - describe '#load()', -> - - it "should pass query data to the store's #query()", -> - sinon.spy(m, 'query') - - a.load({foo: 'bar', type: 'giraffe'}) - assert(m.query.calledOnce, 'store .query() called once') - assert( - m.query.calledWith({foo: 'bar', type: 'giraffe'}), - 'store .query() called with correct args' - ) - - m.query.reset() - - it "should return a promise", (done) -> - a.load({foo: 'bar', type: 'giraffe'}) - .done () -> + .done (res, meta) -> + assert.isArray(res) + assert.isObject(meta) done() .fail (obj, msg) -> done(new Error("promise rejected: #{msg}")) From fce4ab27b51aad9441e2784dcec145e29651e6b9 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 13 Dec 2013 17:45:01 -0800 Subject: [PATCH 019/640] Use Delegator event helpers in Document --- src/plugin/document.coffee | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugin/document.coffee b/src/plugin/document.coffee index a9c314251..6d3819dbf 100644 --- a/src/plugin/document.coffee +++ b/src/plugin/document.coffee @@ -2,9 +2,10 @@ Annotator = require('annotator') class Annotator.Plugin.Document extends Annotator.Plugin + events: + 'beforeAnnotationCreated': 'beforeAnnotationCreated' pluginInit: -> - @annotator.subscribe('beforeAnnotationCreated', this.beforeAnnotationCreated) this.getDocumentMetadata() # returns the primary URI for the document being annotated From b881d9d3b14a70539a0c5564f839a2ee4676660a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 13 Dec 2013 17:45:24 -0800 Subject: [PATCH 020/640] Remove references to NullStore in specs --- test/spec/plugin/annotateitpermissions_spec.coffee | 4 +--- test/spec/plugin/permissions_spec.coffee | 4 +--- test/spec/plugin/tags_spec.coffee | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/test/spec/plugin/annotateitpermissions_spec.coffee b/test/spec/plugin/annotateitpermissions_spec.coffee index e9d373769..9abaf2f36 100644 --- a/test/spec/plugin/annotateitpermissions_spec.coffee +++ b/test/spec/plugin/annotateitpermissions_spec.coffee @@ -10,9 +10,7 @@ describe 'Annotator.Plugin.AnnotateItPermissions', -> beforeEach -> el = $("
").appendTo('body')[0] permissions = new AnnotateItPermissions(el) - annotator = new Annotator($('
')[0], { - store: new Annotator.Plugin.NullStore() - }) + annotator = new Annotator($('
')[0]) permissions.annotator = annotator permissions.pluginInit() diff --git a/test/spec/plugin/permissions_spec.coffee b/test/spec/plugin/permissions_spec.coffee index 98e5f0f22..996b4e1bf 100644 --- a/test/spec/plugin/permissions_spec.coffee +++ b/test/spec/plugin/permissions_spec.coffee @@ -9,9 +9,7 @@ describe 'Annotator.Plugin.Permissions', -> beforeEach -> el = $("
").appendTo('body')[0] - annotator = new Annotator($('
')[0], { - store: new Annotator.Plugin.NullStore() - }) + annotator = new Annotator($('
')[0]) permissions = new Permissions(el) permissions.annotator = annotator permissions.pluginInit() diff --git a/test/spec/plugin/tags_spec.coffee b/test/spec/plugin/tags_spec.coffee index c78f55e60..feac70729 100644 --- a/test/spec/plugin/tags_spec.coffee +++ b/test/spec/plugin/tags_spec.coffee @@ -8,9 +8,7 @@ describe 'Annotator.Plugin.Tags', -> beforeEach -> el = $("
")[0] - annotator = new Annotator($('
')[0], { - store: new Annotator.Plugin.NullStore() - }) + annotator = new Annotator($('
')[0]) plugin = new Tags(el) plugin.annotator = annotator plugin.pluginInit() From ae9dfe5bf36d71ddcf19b08210a8017db109109b Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 13 Dec 2013 17:45:39 -0800 Subject: [PATCH 021/640] Clean up Document spec, remove NullStore --- test/spec/plugin/document_spec.coffee | 110 +++++++++++--------------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/test/spec/plugin/document_spec.coffee b/test/spec/plugin/document_spec.coffee index 11b7c1dcf..60875736f 100644 --- a/test/spec/plugin/document_spec.coffee +++ b/test/spec/plugin/document_spec.coffee @@ -1,26 +1,23 @@ -Annotator = require('annotator') Document = require('../../../src/plugin/document') describe 'Annotator.Plugin.Document', -> $fix = null - annotator = null + plugin = null + metadata = null beforeEach -> - annotator = new Annotator($('
')[0], { - store: new Annotator.Plugin.NullStore() - }) - annotator.addPlugin('Document') + plugin = new Document($('
')[0]) + plugin.pluginInit() - afterEach -> $(document).unbind() + describe '#beforeAnnotationCreated', -> - it 'should have an annotator', -> - assert.ok(annotator) + it 'should add a document field to the annotation', -> + annotation = {} + plugin.beforeAnnotationCreated(annotation) + assert.property(annotation, 'document') - it 'should have Document plugin', -> - assert.ok('Document' of annotator.plugins) - - describe 'annotation should have some metadata', -> + describe '#getDocumentMetadata()', -> # add some metadata to the page head = $("head") head.append('') @@ -38,74 +35,63 @@ describe 'Annotator.Plugin.Document', -> head.append('') head.append('') - annotation = null - - beforeEach (done) -> - annotator.annotations.create({}) - .then (result) -> - annotation = result - done() - - it 'can create annotation', -> - assert.ok(annotation) - - it 'should have a document', -> - assert.ok(annotation.document) + beforeEach -> + metadata = plugin.getDocumentMetadata() it 'should have a title, derived from highwire metadata if possible', -> - assert.equal(annotation.document.title, 'Foo') + assert.equal(metadata.title, 'Foo') it 'should have links with absoulte hrefs and types', -> - assert.ok(annotation.document.link) - assert.equal(annotation.document.link.length, 7) - assert.match(annotation.document.link[0].href, /^.+runner.html#?(\?.*)?$/) - assert.equal(annotation.document.link[1].rel, "alternate") - assert.match(annotation.document.link[1].href, /^.+foo\.pdf$/) - assert.equal(annotation.document.link[1].type, "application/pdf") - assert.equal(annotation.document.link[2].rel, "alternate") - assert.match(annotation.document.link[2].href, /^.+foo\.doc$/) - assert.equal(annotation.document.link[2].type, "application/msword") - assert.equal(annotation.document.link[3].rel, "bookmark") - assert.equal(annotation.document.link[3].href, "http://example.com/bookmark") - assert.equal(annotation.document.link[4].href, "doi:10.1175/JCLI-D-11-00015.1") - assert.match(annotation.document.link[5].href, /.+foo\.pdf$/) - assert.equal(annotation.document.link[5].type, "application/pdf") - assert.equal(annotation.document.link[6].href, "doi:10.1175/JCLI-D-11-00015.1") + assert.ok(metadata.link) + assert.equal(metadata.link.length, 7) + assert.match(metadata.link[0].href, /^.+runner.html#?(\?.*)?$/) + assert.equal(metadata.link[1].rel, "alternate") + assert.match(metadata.link[1].href, /^.+foo\.pdf$/) + assert.equal(metadata.link[1].type, "application/pdf") + assert.equal(metadata.link[2].rel, "alternate") + assert.match(metadata.link[2].href, /^.+foo\.doc$/) + assert.equal(metadata.link[2].type, "application/msword") + assert.equal(metadata.link[3].rel, "bookmark") + assert.equal(metadata.link[3].href, "http://example.com/bookmark") + assert.equal(metadata.link[4].href, "doi:10.1175/JCLI-D-11-00015.1") + assert.match(metadata.link[5].href, /.+foo\.pdf$/) + assert.equal(metadata.link[5].type, "application/pdf") + assert.equal(metadata.link[6].href, "doi:10.1175/JCLI-D-11-00015.1") it 'should have highwire metadata', -> - assert.ok(annotation.document.highwire) - assert.deepEqual(annotation.document.highwire.pdf_url, ['foo.pdf']) - assert.deepEqual(annotation.document.highwire.doi, ['10.1175/JCLI-D-11-00015.1']) - assert.deepEqual(annotation.document.highwire.title, ['Foo']) + assert.ok(metadata.highwire) + assert.deepEqual(metadata.highwire.pdf_url, ['foo.pdf']) + assert.deepEqual(metadata.highwire.doi, ['10.1175/JCLI-D-11-00015.1']) + assert.deepEqual(metadata.highwire.title, ['Foo']) it 'should have dublincore metadata', -> - assert.ok(annotation.document.dc) - assert.deepEqual(annotation.document.dc.identifier, ["doi:10.1175/JCLI-D-11-00015.1", "isbn:123456789"]) - assert.deepEqual(annotation.document.dc.type, ["Article"]) + assert.ok(metadata.dc) + assert.deepEqual(metadata.dc.identifier, ["doi:10.1175/JCLI-D-11-00015.1", "isbn:123456789"]) + assert.deepEqual(metadata.dc.type, ["Article"]) it 'should have facebook metadata', -> - assert.ok(annotation.document.facebook) - assert.deepEqual(annotation.document.facebook.url, ["http://example.com/"]) + assert.ok(metadata.facebook) + assert.deepEqual(metadata.facebook.url, ["http://example.com/"]) it 'should have eprints metadata', -> - assert.ok(annotation.document.eprints) - assert.deepEqual(annotation.document.eprints.title, ['Computer Lib / Dream Machines']) + assert.ok(metadata.eprints) + assert.deepEqual(metadata.eprints.title, ['Computer Lib / Dream Machines']) it 'should have prism metadata', -> - assert.ok(annotation.document.prism) - assert.deepEqual(annotation.document.prism.title, ['Literary Machines']) + assert.ok(metadata.prism) + assert.deepEqual(metadata.prism.title, ['Literary Machines']) it 'should have twitter card metadata', -> - assert.ok(annotation.document.twitter) - assert.deepEqual(annotation.document.twitter.site, ['@okfn']) - - it 'should have unique uris', -> - uris = annotator.plugins.Document.uris() - assert.equal(uris.length, 5) + assert.ok(metadata.twitter) + assert.deepEqual(metadata.twitter.site, ['@okfn']) it 'should have a favicon', -> assert.equal( - annotation.document.favicon + metadata.favicon 'http://example.com/images/icon.ico' ) + describe '#uris()', -> + it 'should de-duplicate uris', -> + uris = plugin.uris() + assert.equal(uris.length, 5) From 60b50c06df114fbb565e11f6d3a806b58b066bf1 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 13 Dec 2013 17:58:06 -0800 Subject: [PATCH 022/640] Remove NullStore and jq plugin from kitchen spec --- test/spec/plugin/kitchensink_spec.coffee | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/test/spec/plugin/kitchensink_spec.coffee b/test/spec/plugin/kitchensink_spec.coffee index 2c7a52ca4..12484a458 100644 --- a/test/spec/plugin/kitchensink_spec.coffee +++ b/test/spec/plugin/kitchensink_spec.coffee @@ -24,22 +24,12 @@ describe 'Annotator::setupPlugins', -> it 'should added to the Annotator prototype', -> assert.equal(typeof Annotator::setupPlugins, 'function') - it 'should be callable via jQuery.fn.Annotator', -> - sinon.spy(Annotator.prototype, 'setupPlugins') - - $fix.annotator({ - store: new Annotator.Plugin.NullStore() - }).annotator('setupPlugins', {}, {Filter: {appendTo: h.fix()}}) - assert(Annotator::setupPlugins.calledOnce) - describe 'called with no parameters', -> _Showdown = null beforeEach -> _Showdown = window.Showdown - annotator = new Annotator(h.fix(), { - store: new Annotator.Plugin.NullStore() - }) + annotator = new Annotator(h.fix()) annotator.setupPlugins() afterEach -> window.Showdown = _Showdown @@ -71,9 +61,7 @@ describe 'Annotator::setupPlugins', -> describe 'called with AnnotateIt config', -> beforeEach -> - annotator = new Annotator(h.fix(), { - store: new Annotator.Plugin.NullStore() - }) + annotator = new Annotator(h.fix()) annotator.setupPlugins {}, Filter: appendTo: h.fix() @@ -88,9 +76,7 @@ describe 'Annotator::setupPlugins', -> assert.isDefined(annotator.plugins.Auth) describe 'called with plugin options', -> - beforeEach -> annotator = new Annotator(h.fix(), { - store: new Annotator.Plugin.NullStore() - }) + beforeEach -> annotator = new Annotator(h.fix()) it 'should override default plugin options', -> annotator.setupPlugins null, From b2090aa37f6be2ad302399df8b3eb5dde6cc96fd Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Wed, 4 Dec 2013 18:06:06 +0100 Subject: [PATCH 023/640] Update sourceMappingURL comment syntax Use non-deprecated format for sourceMappingURL comment. See: http://www.html5rocks.com/de/tutorials/developertools/sourcemaps/ --- tools/build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/build b/tools/build index ac6431825..230bc5a52 100755 --- a/tools/build +++ b/tools/build @@ -30,7 +30,7 @@ UglifyJS.AST_Node.warn_function = null; // Uglify a source with the supplied source map generator. // Returns {code::String, map::SourceMapGenerator} function uglify(src, srcMap) { - var comment = '//@ sourceMappingURL='; + var comment = '//# sourceMappingURL='; var inSrcMap = convert.fromJSON(srcMap.toString()); var filename = inSrcMap.getProperty('file').replace('.js', '.min.js'); From 29d709dab4f493ed5ca223fc4d0829a6fb16bf10 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Wed, 4 Dec 2013 18:07:17 +0100 Subject: [PATCH 024/640] tools/build: minor syntax tweaks --- tools/build | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/build b/tools/build index 230bc5a52..99e232579 100755 --- a/tools/build +++ b/tools/build @@ -88,7 +88,7 @@ exports.plugin = function (browserify, p) { var b = browserify({extensions: ['.coffee']}) .require('./src/namespace', {expose: 'annotator'}) .require(p, {expose: expose}) - .transform(coffeeify) + .transform(coffeeify); return annotator.loader(b); }; @@ -139,6 +139,7 @@ if (require.main === module) { default: console.error('Unrecognized target:', target); process.exit(64); + break; } } }); @@ -178,7 +179,7 @@ if (require.main === module) { fs.createReadStream('./css/annotator.css').pipe(dataURI.stdin); dataURI.stdout.pipe(through(write, end)); - function write (data) { src += data }; + function write (data) { src += data; } function end() { this.queue(null); @@ -224,7 +225,7 @@ if (require.main === module) { function pack(b, filename, options) { var deps = ''; - var ignore = /(_empty)|(fake_[a-z0-9]+).js$/ + var ignore = /(_empty)|(fake_[a-z0-9]+).js$/; var promise = q.defer(); if (depsOnly) { From 08ce83c88ea5e86cff567c2eef8ae2a3d9758478 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Wed, 22 Jan 2014 17:11:55 +0100 Subject: [PATCH 025/640] doc: Add initial sphinx documentation This commit adds an (essentially empty) sphinx project. This will be published on readthedocs.org. --- Makefile | 5 +- doc/.gitignore | 1 + doc/Makefile | 177 +++++++++++++++++++++++++++++++++ doc/conf.py | 261 +++++++++++++++++++++++++++++++++++++++++++++++++ doc/index.rst | 17 ++++ doc/make.bat | 242 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 doc/.gitignore create mode 100644 doc/Makefile create mode 100644 doc/conf.py create mode 100644 doc/index.rst create mode 100644 doc/make.bat diff --git a/Makefile b/Makefile index 59cc6dbbb..98664dabd 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,9 @@ test: develop: npm start +doc: + cd doc && $(MAKE) html + pkg/annotator.css: css/annotator.css $(BUILD) -c @@ -60,4 +63,4 @@ $(DEPDIR) $(PKGDIRS): -include $(PLUGIN_SRC:%.coffee=$(DEPDIR)/annotator.%.d) -include $(DEPDIR)/annotator-full.d -.PHONY: all annotator plugins annotator-full clean test develop pkg +.PHONY: all annotator plugins annotator-full clean test develop pkg doc diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 000000000..a485625d4 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1 @@ +/_build diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 000000000..be041725a --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,177 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) +$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Annotator.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Annotator.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Annotator" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Annotator" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 000000000..d66ae9ff7 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# +# Annotator documentation build configuration file, created by +# sphinx-quickstart on Wed Jan 22 17:02:43 2014. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import json +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.todo', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Annotator' +copyright = u'2014, The Annotator project contributors' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = json.load(open('../package.json'))['version'] +# The full version, including alpha/beta/rc tags. +release = version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Annotatordoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'Annotator.tex', u'Annotator Documentation', + u'The Annotator project contributors', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'annotator', u'Annotator Documentation', + [u'The Annotator project contributors'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Annotator', u'Annotator Documentation', + u'The Annotator project contributors', 'Annotator', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 000000000..b1e73c969 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,17 @@ +Welcome to Annotator's documentation! +===================================== + +.. highlight:: js + +Contents: + +.. toctree:: + :maxdepth: 2 + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 000000000..3534f740a --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,242 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. xml to make Docutils-native XML files + echo. pseudoxml to make pseudoxml-XML files for display purposes + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + + +%SPHINXBUILD% 2> nul +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\Annotator.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\Annotator.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdf" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "latexpdfja" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + cd %BUILDDIR%/latex + make all-pdf-ja + cd %BUILDDIR%/.. + echo. + echo.Build finished; the PDF files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +if "%1" == "xml" ( + %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The XML files are in %BUILDDIR%/xml. + goto end +) + +if "%1" == "pseudoxml" ( + %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. + goto end +) + +:end From a837ff510150d35a56e953549c2c67901f64039d Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Wed, 22 Jan 2014 17:37:31 +0100 Subject: [PATCH 026/640] doc: Import documentation (such as it is) from the wiki --- doc/annotation-format.rst | 44 +++++ doc/authentication.rst | 153 +++++++++++++++++ doc/getting-started.rst | 167 ++++++++++++++++++ doc/hacking/plugin-development.rst | 245 ++++++++++++++++++++++++++ doc/index.rst | 9 + doc/internationalization.rst | 61 +++++++ doc/plugins/auth.rst | 44 +++++ doc/plugins/filter.rst | 85 +++++++++ doc/plugins/index.rst | 12 ++ doc/plugins/permissions.rst | 267 +++++++++++++++++++++++++++++ doc/plugins/store.rst | 173 +++++++++++++++++++ doc/plugins/tags.rst | 46 +++++ doc/plugins/unsupported.rst | 43 +++++ doc/storage.rst | 219 +++++++++++++++++++++++ 14 files changed, 1568 insertions(+) create mode 100644 doc/annotation-format.rst create mode 100644 doc/authentication.rst create mode 100644 doc/getting-started.rst create mode 100644 doc/hacking/plugin-development.rst create mode 100644 doc/internationalization.rst create mode 100644 doc/plugins/auth.rst create mode 100644 doc/plugins/filter.rst create mode 100644 doc/plugins/index.rst create mode 100644 doc/plugins/permissions.rst create mode 100644 doc/plugins/store.rst create mode 100644 doc/plugins/tags.rst create mode 100644 doc/plugins/unsupported.rst create mode 100644 doc/storage.rst diff --git a/doc/annotation-format.rst b/doc/annotation-format.rst new file mode 100644 index 000000000..4f9a55d48 --- /dev/null +++ b/doc/annotation-format.rst @@ -0,0 +1,44 @@ +Annotation format +================= + +An annotation is a JSON document that contains a number of fields +describing the position and content of an annotation within a specified +document: + +.. code:: json + + { + "id": "39fc339cf058bd22176771b3e3187329", # unique id (added by backend) + "annotator_schema_version": "v1.0", # schema version: default v1.0 + "created": "2011-05-24T18:52:08.036814", # created datetime in iso8601 format (added by backend) + "updated": "2011-05-26T12:17:05.012544", # updated datetime in iso8601 format (added by backend) + "text": "A note I wrote", # content of annotation + "quote": "the text that was annotated", # the annotated text (added by frontend) + "uri": "http://example.com/", # URI of annotated document (added by frontend) + "ranges": [ # list of ranges covered by annotation (usually only one entry) + { + "start": "/p[69]/span/span", # (relative) XPath to start element + "end": "/p[70]/span/span", # (relative) XPath to end element + "startOffset": 0, # character offset within start element + "endOffset": 120 # character offset within end element + } + ], + "user": "alice", # user id of annotation owner (can also be an object with an 'id' property) + "consumer": "annotateit", # consumer key of backend + "tags": [ "review", "error" ], # list of tags (from Tags plugin) + "permissions": { # annotation permissions (from Permissions/AnnotateItPermissions plugin) + "read": ["group:__world__"], + "admin": [], + "update": [], + "delete": [] + } + } + +Note that this annotation includes some info stored by plugins (notably +the [[Permissions Plugin]] and [[Tags Plugin]]). + +This basic schema is **completely extensible**. It can be added to by +plugins, and any fields added by the frontend should be preserved by +backend implementations. For example, the [[Store Plugin]] (which adds +persistence of annotations) allow you to specify arbitrary additional +fields using the ``annotationData`` attribute. diff --git a/doc/authentication.rst b/doc/authentication.rst new file mode 100644 index 000000000..273bb8772 --- /dev/null +++ b/doc/authentication.rst @@ -0,0 +1,153 @@ +Authentication +============== + +What's the authentication system for? +------------------------------------- + +The simplest way to explain the role of the authentication system is by +example. Consider the following: + +1. Alice builds a website with documents which need annotating, DocLand. + +2. Alice registers DocLand with AnnotateIt, and receives a "consumer + key/secret" pair. + +3. Alice's users (Bob is one of them) login to her DocLand, and receive + an authentication token, which is a cryptographic combination of + (among other things) their unique user ID at DocLand, and DocLand's + "consumer secret". + +4. Bob's browser sends requests to AnnotateIt to save annotations, and + these include the authentication token as part of the payload. + +5. AnnotateIt can verify the Bob is a real user from DocLand, and thus + stores his annotation. + +So why go to all this trouble? Well, the point is really to save **you** +trouble. By implementing this authentication system (which shares key +ideas with the industry standard OAuth) you can provide your users with +the ability to annotate documents on your website without needing to +worry about implementing your own Annotator backend. You can use +`AnnotateIt `__ to provide the backend: all you +have to do is implement a token generator on your website (described +below). + +This is the simple explanation, but if you're in need of more technical +details, keep reading. + +Technical overview +------------------ + +How do we authorise users' browsers to create annotations on a +Consumer's behalf? There are three (and a half) entities involved: + +1. The Service Provider (SP; AnnotateIt in the above example) +2. The Consumer (C; DocLand) +3. The User (U; Bob), and the User Agent (UA; Bob's browser) + +Annotations are stored by the SP, which provides an API that the +Annotator's "Store" plugin understands. + +Text to be annotated, and configuration of the clientside Annotator, is +provided by the Consumer. + +Users will typically register with the Consumer -- we make no +assumptions about your user registration/authentication process other +than that it exists -- and the UA will, when visiting appropriate +sections of C's site, request an ``authToken`` from C. Typically, an +``authToken`` will only be provided if U is currently logged into C's +site. + +Technical specification +----------------------- + +It's unlikely you'll need to understand all of the following to get up +and running using AnnotateIt -- you can probably just copy and paste the +Python example given below -- but it's worth reading what follows if +you're doing anything unusual (such as giving out tokens to +unauthenticated users). + +The Annotator ``authToken`` is a type of `JSON Web +Token `__. +This document won't describe the details of the JWT specification, other +than to say that the token payload is signed by the consumer secret with +the HMAC-SHA256 algorithm, allowing the backend to verify that the +contents of the token haven't been interfered with while travelling from +the consumer. Numerous language implementations exist already +(`PyJWT `__, +`jwt `__ for Ruby, +`php-jwt `__, +`JWT-CodeIgniter `__...). + +The required contents of the token payload are: + ++-------------------+----------------------------------------------------------------------------------+------------------------------------------+ +| key | description | example | ++===================+==================================================================================+==========================================+ +| ``consumerKey`` | the consumer key issued by the backend store | ``"602368a0e905492fae87697edad14c3a"`` | ++-------------------+----------------------------------------------------------------------------------+------------------------------------------+ +| ``userId`` | the consumer's **unique** identifier for the user to whom the token was issued | ``"alice"`` | ++-------------------+----------------------------------------------------------------------------------+------------------------------------------+ +| ``issuedAt`` | the ISO8601 time at which the token was issued | ``"2012-03-23T10:51:18Z"`` | ++-------------------+----------------------------------------------------------------------------------+------------------------------------------+ +| ``ttl`` | the number of seconds after ``issuedAt`` for which the token is valid | ``86400`` | ++-------------------+----------------------------------------------------------------------------------+------------------------------------------+ + +You may wish the payload to contain other information (e.g. ``userRole`` +or ``userGroups``) and arbitrary additional keys may be added to the +token. This will only be useful if the Annotator client and the SP pay +attention to these keys. + +Lastly, note that the Annotator frontend does **not** verify the +authenticity of the tokens it receives. Only the SP is required to +verify authenticity of auth tokens before authorizing a request from the +Annotator frontend. + +For reference, here's a Python implementation of a token generator, +suitable for dropping straight into your +`Flask `__ or +`Django `__ project: + +.. code:: python + + import datetime + import jwt + + # Replace these with your details + CONSUMER_KEY = 'yourconsumerkey' + CONSUMER_SECRET = 'yourconsumersecret' + + # Only change this if you're sure you know what you're doing + CONSUMER_TTL = 86400 + + def generate_token(user_id): + return jwt.encode({ + 'consumerKey': CONSUMER_KEY, + 'userId': user_id, + 'issuedAt': _now().isoformat() + 'Z', + 'ttl': CONSUMER_TTL + }, CONSUMER_SECRET) + + def _now(): + return datetime.datetime.utcnow().replace(microsecond=0) + +Now all you need to do is expose an endpoint in your web application +that returns the token to logged-in users (say, +http://example.com/api/token), and you can set up the Annotator like so: + +.. code:: javascript + + $(body).annotator() + .annotator('setupPlugins', {tokenUrl: 'http://example.com/api/token'}); + +Colophon +-------- + +Original planning documents at: + +- http://lists.okfn.org/pipermail/okfn-help/2010-December/000977.html + +Rehashed in Feb 2012: + +- http://lists.okfn.org/pipermail/annotator-dev/2012-January/000188.html + diff --git a/doc/getting-started.rst b/doc/getting-started.rst new file mode 100644 index 000000000..3437bb1d3 --- /dev/null +++ b/doc/getting-started.rst @@ -0,0 +1,167 @@ +Getting started with Annotator +============================== + +The Annotator libraries +----------------------- + +To get the Annotator up and running on your website you'll need to +either link to a hosted version or deploy the Annotator source files +yourself. Details of both are provided below. + +NB: if you are using Wordpress there is also a `Annotator Wordpress +plugin `__ +which will take care of installing and integrating Annotator for you. + +Hosted Annotator Library +~~~~~~~~~~~~~~~~~~~~~~~~ + +For each Annotator release, we make available the following assets: + +:: + + http://assets.annotateit.org/annotator/{version}/annotator-full.min.js + http://assets.annotateit.org/annotator/{version}/annotator.min.js + http://assets.annotateit.org/annotator/{version}/annotator.{pluginname}.min.js + http://assets.annotateit.org/annotator/{version}/annotator.min.css + +Use ``annotator-full.min.js`` if you want to include both the core and +all plugins in a single file. Use ``annotator.min.js`` if you need only +the core. You can add individual plugins by including the relevant +``annotator.{pluginname}.min.js`` files. + +For example, a full version of the Annotator can be loaded with the +following code: + +.. code:: html + + + + +Deploy the Annotator Locally +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To do this visit the `download +area `__ and grab the latest +version. This contains the Annotator source code as well as the plugins +developed as part of the Annotator project. + +Including Annotator on your webpage +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You need to link the Annotator Javascript and CSS into the page. You +will also need to include jQuery >= 1.6. + +NOTE: Annotator requires jQuery 1.6 or greater. + +.. code:: html + + + + + +Setting up Annotator +-------------------- + +Setting up Annotator requires only a single line of code. Use jQuery to +select the element that you would like to annotate eg. +``
...
`` and call the ``.annotator()`` method on +it: + +.. code:: javascript + + jQuery(function ($) { + $('#content').annotator(); + }); + +Annotator will now be loaded on the ``#content`` element. Select some +text to see it in action. + +Options +------- + +You can optionally specify options: + +- readOnly - True to allow viewing annotations, but not creating or + editing them. Defaults to false. + +.. code:: javascript + + jQuery(function ($) { + $('#content').annotator({ + readOnly: true + }); + }); + +Setting up the default plugins +------------------------------ + +We include a special setup function in the ``annotator-full.min.js`` +file that installs all the default plugins for you automatically. To run +it just add a call to ``.annotator("setupPlugins")``. + +.. code:: javascript + + jQuery(function ($) { + $('#content').annotator() + .annotator('setupPlugins'); + }); + +This will set up the following: + +1. The [[Tags\|Tags Plugin]], [[Filter\|Filter Plugin]] & + [[Unsupported\|Unsupported Plugin]] plugins. +2. The [[Auth\|Auth Plugin]], [[Permissions\|Permissions Plugin]] and + [[Store\|Store Plugin]] plugins, for interaction with the `AnnotateIt + store `__. +3. If the `Showdown `__ library has + been included on the page the [[Markdown\|Markdown Plugin]] will also + be loaded. + +You can further customise the plugins by providing an object containing +options for individual plugins. Or to disable a plugin set it's +attribute to ``false``. + +.. code:: javascript + + jQuery(function ($) { + // Customise the default plugin options with the third argument. + $('#content').annotator() + .annotator('setupPlugins', {}, { + // Disable the tags plugin + Tags: false, + // Filter plugin options + Filter: { + addAnnotationFilter: false, // Turn off default annotation filter + filters: [{label: 'Quote', property: 'quote'}] // Add a quote filter + } + }); + }); + +Adding more plugins +------------------- + +To add a plugin first make sure that you're loading the script into the +page. Then call ``.annotator('addPlugin', 'PluginName')`` to load the +plugin. Options can also be passed to the plugin as additional +parameters after the plugin name. + +Here we add the tags plugin to the page: + +.. code:: javascript + + jQuery(function ($) { + $('#content').annotator() + .annotator('addPlugin', 'Tags'); + }); + +For more information on available plugins check the navigation to the +right of this article. Or to create your own check the [[creating a +plugin section\|Plugin Development]]. + +Saving annotations +------------------ + +In order to keep your annotations around longer than a single page view +you'll need to set up a store on your server or use an external service +like `AnnotateIt `__. For more information on +storing annotations check out the [[Store Plugin]] on the wiki. diff --git a/doc/hacking/plugin-development.rst b/doc/hacking/plugin-development.rst new file mode 100644 index 000000000..7731c7ea4 --- /dev/null +++ b/doc/hacking/plugin-development.rst @@ -0,0 +1,245 @@ +Plugin development +================== + +Getting Started +--------------- + +Building a plugin is very simple. Simply attach a function that creates +your plugin to the ``Annotator.Plugin`` namespace. The function will +receive the following arguments. + +- ``element``: The DOM element that is currently being annotated. + +Additional arguments (such as options) can be passed in by the user when +the plugin is added to the Annotator. These will be passed in after the +``element``. + +.. code:: javascript + + Annotator.Plugin.HelloWorld = function (element) { + var myPlugin = {}; + // Create your plugin here. Then return it. + return myPlugin; + }; + +Using Your Plugin +~~~~~~~~~~~~~~~~~ + +Adding your plugin to the annotator is the same as for all supported +plugins. Simply call "addPlugin" on the annotator and pass in the name +of the plugin and any options. For example: + +.. code:: javascript + + // Setup the annotator on the page. + var content = $('#content').annotator(); + + // Add your plugin. + content.annotator('addPlugin', 'HelloWorld' /*, any other options */); + +Setup +~~~~~ + +When the annotator creates your plugin it will take the following steps. + +1. Call your Plugin function passing in the annotated element plus any + additional arguments. (The Annotator calls the function with ``new`` + allowing you to use a constructor function if you wish). +2. Attaches the current instance of the Annotator to the ``.annotator`` + property of the plugin. +3. Calls ``.pluginInit()`` if the method exists on your plugin. + +pluginInit() +~~~~~~~~~~~~ + +If your plugin has a ``pluginInit()`` method it will be called after the +annotator has been attached to your plugin. You can use it to set up the +plugin. + +In this example we add a field to the viewer that contains the text +provided when the plugin was added. + +.. code:: javascript + + Annotator.Plugin.Message = function (element, message) { + var plugin = {}; + + plugin.pluginInit = function () { + this.annotator.viewer.addField({ + load: function (field, annotation) { + field.innerHTML = message; + } + }) + }; + + return plugin; + } + +Usage: + +.. code:: javascript + + // Setup the annotator on the page. + var content = $('#content').annotator(); + + // Add your plugin to the annotator and display the message "Hello World" + // in the viewer. + content.annotator('addPlugin', 'Message', 'Hello World'); + +Extending Annotator.Plugin +-------------------------- + +All supported Annotator plugins use a base "class" that has some useful +features such as event handling. To use this you simply need to extend +the ``Annotator.Plugin`` function. + +.. code:: javascript + + // This is now a constructor and needs to be called with `new`. + Annotator.Plugin.MyPlugin = function (element, options) { + + // Call the Annotator.Plugin constructor this sets up the .element and + // .options properties. + Annotator.Plugin.apply(this, arguments); + + // Set up the rest of your plugin. + }; + + // Set the plugin prototype. This gives us all of the Annotator.Plugin methods. + Annotator.Plugin.MyPlugin.prototype = new Annotator.Plugin(); + + // Now add your own custom methods. + Annotator.Plugin.MyPlugin.prototype.pluginInit = function () { + // Do something here. + }; + +If you're using jQuery you can make this process a lot neater. + +.. code:: javascript + + Annotator.Plugin.MyPlugin = function (element, options) { + // Same as before. + }; + + jQuery.extend(Annotator.Plugin.MyPlugin.prototype, new Annotator.Plugin(), { + events: {}, + options: { + // Any default options. + } + pluginInit: function () { + + }, + myCustomMethod: function () { + + } + }); + +Annotator.Plugin API +-------------------- + +The Annotator.Plugin provides the following methods and properties. + +element +~~~~~~~ + +This is the DOM element currently being annotated wrapped in a jQuery +wrapper. + +options +~~~~~~~ + +This is the options object, you can set default options when you create +the object and they will be overridden by those provided when the plugin +is created. + +events +~~~~~~ + +These can be either DOM events to be listened for within the +``.element`` or custom events defined by you. Custom events will not +receive the ``event`` property that is passed to DOM event listeners. +These are bound when the plugin is instantiated. + +publish(name, parameters) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Publish a custom event to all subscribers. + +- ``name``: The event name. +- ``parameters``: An array of parameters to pass to the subscriber. + +subscribe(name, callback) +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Subscribe to a custom event. This can be used to subscribe to your own +events or those broadcast by the annotator and other plugins. + +- ``name``: The event name. +- ``callback``: A callback to be fired when the event is published. The + callback will receive any arguments sent when the event is published. + +unsubscribe(name, callback) +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unsubscribe from an event. + +- ``name``: The event name. +- ``callback``: The callback to be unsubscribed. + +Annotator Events +---------------- + +The annotator fires the following events at key points in its operation. +You can subscribe to them using the ``.subscribe()`` method. This can be +called on either the ``.annotator`` object or if you're extending +``Annotator.Plugin`` the plugin instance itself. The events are as +follows: + +- ``beforeAnnotationCreated(annotation)``: called immediately before an + annotation is created. If you need to modify the annotation before it + is saved use this event. +- ``annotationCreated(annotation)``: called when the annotation is + created use this to store the annotations. +- ``beforeAnnotationUpdated(annotation)``: as above, but just before an + existing annotation is saved. +- ``annotationUpdated(annotation)``: as above, but for an existing + annotation which has just been edited. +- ``annotationDeleted(annotation)``: called when the user deletes an + annotation. +- ``annotationEditorShown(editor, annotation)``: called when the + annotation editor is presented to the user. +- ``annotationEditorHidden(editor)``: called when the annotation editor + is hidden, both when submitted and when editing is cancelled. +- ``annotationEditorSubmit(editor, annotation)``: called when the + annotation editor is submitted. +- ``annotationViewerShown(viewer, annotations)``: called when the + annotation viewer is shown and provides the annotations being + displayed. +- ``annotationViewerTextField(field, annotation)``: called when the + text field displaying the annotation comment in the viewer is + created. + +Example +~~~~~~~ + +A plugin that logs annotation activity to the console. + +.. code:: javascript + + Annotator.Plugin.StoreLogger = function (element) { + return { + pluginInit: function () { + this.annotator + .subscribe("annotationCreated", function (annotation) { + console.info("The annotation: %o has just been created!", annotation) + }) + .subscribe("annotationUpdated", function (annotation) { + console.info("The annotation: %o has just been updated!", annotation) + }) + .subscribe("annotationDeleted", function (annotation) { + console.info("The annotation: %o has just been deleted!", annotation) + }); + } + } + }; + diff --git a/doc/index.rst b/doc/index.rst index b1e73c969..9a7e20cf8 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -8,6 +8,15 @@ Contents: .. toctree:: :maxdepth: 2 + getting-started + annotation-format + authentication + storage + internationalization + + plugins/index + hacking/plugin-development + Indices and tables ================== diff --git a/doc/internationalization.rst b/doc/internationalization.rst new file mode 100644 index 000000000..4450e5a71 --- /dev/null +++ b/doc/internationalization.rst @@ -0,0 +1,61 @@ +Internationalisation and localisation (I18N, L10N) +================================================== + +Annotator now has rudimentary support for localisation of its interface. + +For users +--------- + +If you wish to use a provided translation, you need to add a ``link`` +tag pointing to the ``.po`` file, as well as include ``gettext.js`` +before you load the Annotator. For example, for a French translation: + +:: + + + + +This should be all you need to do to get the Annotator interface +displayed in French. + +For translators +--------------- + +We now use `Transifex `__ to manage localisation +efforts on Annotator. If you wish to contribute a translation you'll +first need to sign up for a free account at + +https://www.transifex.net/plans/signup/free/ + +Once you're signed up, you can go to + +https://www.transifex.net/projects/p/annotator/ + +and get translating! + +For developers +-------------- + +Any localisable string in the core of Annotator should be wrapped with a +call to the gettext function, ``_t``, e.g. + +:: + + console.log(_t("Hello, world!")) + +Any localisable string in an Annotator plugin should be wrapped with a +call to the gettext function, ``Annotator._t``, e.g. + +:: + + console.log(Annotator._t("Hello from a plugin!")) + +To update the localisation template (``locale/annotator.pot``), you +should run the ``i18n:update`` Cake task: + +:: + + cake i18n:update + +You should leave it up to individual translators to update their +individual ``.po`` files with the ``locale/l10n-update`` tool. diff --git a/doc/plugins/auth.rst b/doc/plugins/auth.rst new file mode 100644 index 000000000..a957a37da --- /dev/null +++ b/doc/plugins/auth.rst @@ -0,0 +1,44 @@ +``Auth`` plugin +=============== + +The Auth plugin complements the [[Store Plugin]] by providing +authentication for requests. This may be necessary if you are running +the Store on a separate domain or using a third party service like +annotateit.org. + +The plugin works by requesting an authentication token from the local +server and then provides this in all requests to the store. For more +details see the [[specification\|Authentication]]. + +Usage +----- + +Adding the Auth plugin to the annotator is very simple. Simply add the +annotator to the page using the ``.annotator()`` jQuery plugin. Then +call the ``.addPlugin()`` method eg. +``.annotator('addPlugin', 'Auth')``. + +.. code:: javascript + + var content = $('#content')); + content.annotator('addPlugin', 'Auth', { + tokenUrl: '/auth/token' + }); + +Options +------- + +The following options are available to the Auth plugin. + +- ``tokenUrl``: The URL to request the token from. Defaults to + ``/auth/token``. +- ``token``: An auth token. If this is present it will not be requested + from the server. Defaults to ``null``. +- ``autoFetch``: Whether to fetch the token when the plugin is loaded. + Defaults to ``true`` + +Token format +^^^^^^^^^^^^ + +For details of the token format, see the page on [[Annotator's +Authentication system\|Authentication]]. diff --git a/doc/plugins/filter.rst b/doc/plugins/filter.rst new file mode 100644 index 000000000..7fc770b0a --- /dev/null +++ b/doc/plugins/filter.rst @@ -0,0 +1,85 @@ +``Filter`` plugin +================= + +This plugin allows the user to navigate and filter the displayed +annotations. + +Interface Overview +------------------ + +The plugin adds a toolbar to the top of the window. This contains the +available filters that can be applied to the current annotations. + +Usage +----- + +Adding the Filter plugin to the annotator is very simple. Add the +annotator to the page using the ``.annotator()`` jQuery plugin. Then +call the ``.addPlugin()`` method by calling +``.annotator('addPlugin', 'Filter')``. + +.. code:: javascript + + var content = $('#content').annotator().annotator('addPlugin', 'Filter'); + +Options +~~~~~~~ + +There are several options available to customise the plugin. + +- ``filters``: This is an array of filter objects. These will be added + to the toolbar on load. +- ``addAnnotationFilter``: If ``true`` this will display the default + filter that searches the annotation text. + +Filters +~~~~~~~ + +Filters are very easy to create. The options require two properties a +``label`` and an annotation ``property`` to search for. For example if +we wanted to filter on an annotations quoted text we can create the +following filter. + +.. code:: javascript + + content.annotator('addPlugin', 'Filter', { + filters: [ + { + label: 'Quote', + property: 'quote' + } + ] + }); + +You can also customise the filter logic that determines if an annotation +should be filtered by providing an ``isFiltered`` function. This +function receives the contents of the filter input as well as the +annotation property. It should return ``true`` if the annotation should +remain highlighted. + +Heres an example that uses the ``annotation.tags`` property, which is an +array of tags: + +.. code:: javascript + + content.annotator('addPlugin', 'Filter', { + filters: [ + { + label: 'Tag', + property: 'tags', + isFiltered: function (input, tags) { + if (input && tags && tags.length) { + var keywords = input.split(/\s+/g); + for (var i = 0; i < keywords.length; i += 1) { + for (var j = 0; j < tags.length; j += 1) { + if (tags[j].indexOf(keywords[i]) !== -1) { + return true; + } + } + } + } + return false; + }} + ] + }); + diff --git a/doc/plugins/index.rst b/doc/plugins/index.rst new file mode 100644 index 000000000..f2029b66a --- /dev/null +++ b/doc/plugins/index.rst @@ -0,0 +1,12 @@ +Plugins +======= + +Annotator has a highly modular architecture, and a great deal of functionality +is provided by plugins. These pages document these plugins and how they work +together. + +.. toctree:: + :glob: + :maxdepth: 1 + + * diff --git a/doc/plugins/permissions.rst b/doc/plugins/permissions.rst new file mode 100644 index 000000000..c61b88963 --- /dev/null +++ b/doc/plugins/permissions.rst @@ -0,0 +1,267 @@ +``Permissions`` plugin +====================== + +This plugin handles setting the user and permissions properties on +annotations as well as providing some enhancements to the interface. + +Interface Overview +------------------ + +The following elements are added to the Annotator interface by this +plugin. + +Viewer +^^^^^^ + +The plugin adds a section to a viewed annotation displaying the name of +the user who created it. It also checks the annotation's permissions to +see if the current user can **edit**/**delete** the current annotation +and displays controls appropriately. + +Editor +^^^^^^ + +The plugin adds two fields with checkboxes to the annotation editor +(these are only displayed if the current user has **admin** permissions +on the annotation). One to allow anyone to view the annotation and one +to allow anyone to edit the annotation. + +Usage +----- + +Adding the permissions plugin to the annotator is very simple. Simply +add the annotator to the page using the ``.annotator()`` jQuery plugin +and retrieve the annotator object using ``.data('annotator')``. We now +add the plugin and pass an options object to set the current user. + +.. code:: javascript + + var annotator = $('#content').annotator().data('annotator'); + annotator.addPlugin('Permissions', { + user: 'Alice' + }); + +By default all annotations are publicly viewable/editable/deleteable. We +can set our own permissions using the options object. + +.. code:: javascript + + var annotator = $('#content').annotator().data('annotator'); + annotator.addPlugin('Permissions', { + user: 'Alice', + permissions: { + 'read': [], + 'update': ['Alice'], + 'delete': ['Alice'], + 'admin': ['Alice'] + } + }); + +Now only our current user can edit the annotations but anyone can view +them. + +Options +~~~~~~~ + +The options object allows you to completely define the way permissions +are handled for your site. + +- ``user``: The current user (required). +- ``permissions``: An object defining annotation permissions. +- ``userId``: A callback that returns the user id. +- ``userString``: A callback that returns the users name. +- ``userAuthorize``: A callback that allows custom authorisation. +- ``showViewPermissionsCheckbox``: If ``false`` hides the "Anyone can + view…" checkbox. +- ``showEditPermissionsCheckbox``: If ``false`` hides the "Anyone can + edit…" checkbox. + +user (required) +^^^^^^^^^^^^^^^ + +This value sets the current user and will be attached to all newly +created annotations. It can be as simple as a username string or if your +users objects are more complex an object literal. + +.. code:: javascript + + // Simple example. + annotator.addPlugin('Permissions', { + user: 'Alice' + }); + + // Complex example. + annotator.addPlugin('Permissions', { + user: { + id: 6, + username: 'Alice', + location: 'Brighton, UK' + } + }); + +If you do decide to use an object for your user as well as permissions +you'll need to also provide ``userId`` and ``userString`` callbacks. See +below for more information. + +permissions +^^^^^^^^^^^ + +Permissions set who is allowed to do what to your annotations. There are +four actions: + +- ``read``: Who can view the annotation +- ``update``: Who can edit the annotation +- ``delete``: Who can delete the annotation +- ``admin``: Who can change these permissions on the annotation + +Each action should be an array of tokens. An empty array means that +anyone can perform that action. Generally the token will just be the +users id. If you need something more complex (like groups) you can use +your own syntax and provide a ``userAuthorize`` callback with your +options. + +Here's a simple example of setting the permissions so that only the +current user can perform all actions: + +.. code:: javascript + + annotator.addPlugin('Permissions', { + user: 'Alice', + permissions: { + 'read': ['Alice'], + 'update': ['Alice'], + 'delete': ['Alice'], + 'admin': ['Alice'] + } + }); + +Or here is an example using numerical user ids: + +.. code:: javascript + + annotator.addPlugin('Permissions', { + user: {id: 6, name:'Alice'}, + permissions: { + 'read': [6], + 'update': [6], + 'delete': [6], + 'admin': [6] + } + }); + +userId(user) +^^^^^^^^^^^^ + +This is a callback that accepts a ``user`` parameter and returns the +identifier. By default this assumes you will be using strings for your +ids and simply returns the parameter. However if you are using a user +object you'll need to implement this: + +.. code:: javascript + + annotator.addPlugin('Permissions', { + user: {id: 6, name:'Alice'}, + userId: function (user) { + if (user && user.id) { + return user.id; + } + return user; + } + }); + // When called. + userId({id: 6, name:'Alice'}) // => Returns 6 + +NOTE: This function should handle ``null`` being passed as a parameter. +This is done when checking a globally editable annotation. + +userString(user) +^^^^^^^^^^^^^^^^ + +This is a callback that accepts a ``user`` parameter and returns the +human readable name for display. By default this assumes you will be +using a string to represent your users name and id so simply returns the +parameter. However if you are using a user object you'll need to +implement this: + +.. code:: javascript + + annotator.addPlugin('Permissions', { + user: {id: 6, name:'Alice'}, + userString: function (user) { + if (user && user.name) { + return user.name; + } + return user; + } + }); + // When called. + userString({id: 6, name:'Alice'}) // => Returns 'Alice' + +userAuthorize(action, annotation, user) +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This is another callback that allows you to implement your own +authorization logic. It receives three arguments: + +- ``action``: Action that is being checked, 'update', 'delete' or + 'admin'. 'create' does not call this callback +- ``annotation``: The entire annotation object; note that the + permissions subobject is at ``annotation.permissions`` +- ``user``: current user, as passed in to the permissions plugin + +Your function will check to see if the user can perform an action based +on these values. + +The default implementation assumes that the user is a simple string and +the tokens used (within ``annotation.permissions``) are also strings so +simply checks that the user is one of the tokens for the current action. + +.. code:: javascript + + // This is the default implementation as an example. + annotator.addPlugin('Permissions', { + user: 'Alice', + userAuthorize: function(action, annotation, user) { + var token, tokens, _i, _len; + if (annotation.permissions) { + tokens = annotation.permissions[action] || []; + if (tokens.length === 0) { + return true; + } + for (_i = 0, _len = tokens.length; _i < _len; _i++) { + token = tokens[_i]; + if (this.userId(user) === token) { + return true; + } + } + return false; + } else if (annotation.user) { + if (user) { + return this.userId(user) === this.userId(annotation.user); + } else { + return false; + } + } + return true; + }, + }); + // When called. + userAuthorize('update', aliceAnnotation, 'Alice') // => Returns true + userAuthorize('Alice', bobAnnotation, 'Bob') // => Returns false + +.. raw:: html + + + +A more complex example might involve you wanting to have a groups +property on your user object. If the user is a member of the 'Admin' +group they can perform any action on the annotation. + +// When called by a normal user. userAuthorize('update', +adminAnnotation, { id: 1, group: 'user' }) // => Returns false + +// When called by an admin. userAuthorize('update', adminAnnotation, { +id: 2, group: 'Admin' }) // => Returns true + +// When called by the owner. userAuthorize('update', regularAnnotation, +ownerOfRegularAnnotation) // => Returns true \`\`\` diff --git a/doc/plugins/store.rst b/doc/plugins/store.rst new file mode 100644 index 000000000..35f677ae3 --- /dev/null +++ b/doc/plugins/store.rst @@ -0,0 +1,173 @@ +``Store`` plugin +================ + +This plugin sends annotations (serialised as JSON) to the server at key +events broadcast by the annotator. + +Actions +------- + +The following actions are performed by the annotator. + +- ``read``: GETs all annotations. Called when plugin loads or + ``.loadAnnotations()`` is called. Server should return an array of + annotations serialised as JSON. +- ``create``: POSTs an annotation (serialised as JSON) to the server. + Called when the annotator publishes the "annotationCreated" event. + The annotation is updated with any data (such as a newly created id) + returned from the server. +- ``update``: PUTs an annotation (serialised as JSON) on the server + under its id. Called when the annotator publishes the + "annotationUpdated" event. The annotation is updated with any data + (such as a newly created id) returned from the server. +- ``destroy``: Issues a DELETE request to server for the annotation. +- ``search``: GETs all annotations relevant to the query. Should return + a JSON object with a ``rows`` property containing an array of + annotations. + +Stores +------ + +For an example store check out our +`annotator-store `__ project on +GitHub which you can use or examine as the basis for your own store. If +you're looking to get up and running quickly then +`annotateit.org `__ will store your annotations +remotely under your account. + +Interface Overview +------------------ + +This plugin adds no additional UI to the Annotator but will display +error notifications if a request to the store fails. + +Usage +----- + +Adding the store plugin to the annotator is very simple. Simply add the +annotator to the page using the ``.annotator()`` jQuery plugin and +retrieve the annotator object using ``.data('annotator')``. Then add the +``Store`` plugin. + +.. code:: javascript + + var content = $('#content').annotator(); + content.annotator('addPlugin', 'Store', { + // The endpoint of the store on your server. + prefix: '/store/endpoint', + + // Attach the uri of the current page to all annotations to allow search. + annotationData: { + 'uri': 'http://this/document/only' + }, + + // This will perform a "search" action rather than "read" when the plugin + // loads. Will request the last 20 annotations for the current url. + // eg. /store/endpoint/search?limit=20&uri=http://this/document/only + loadFromSearch: { + 'limit': 20, + 'uri': 'http://this/document/only' + } + }); + +Options +~~~~~~~ + +The following options are made available for customisation of the store. + +- ``prefix``: The store endpoint. +- ``annotationData``: An object literal containing any data to attach + to the annotation on submission. +- ``loadFromSearch``: Search options for using the "search" action. +- ``urls``: Custom URL paths. +- ``showViewPermissionsCheckbox``: If ``true`` will display the "anyone + can view this annotation" checkbox. +- ``showEditPermissionsCheckbox``: If ``true`` will display the "anyone + can edit this annotation" checkbox. + +prefix +^^^^^^ + +This is the API endpoint. If the server supports Cross Origin Resource +Sharing (CORS) a full URL can be used here. Defaults to ``/store``. + +NOTE: The trailing slash should be omitted. + +Example: + +.. code:: javascript + + $('#content').annotator('addPlugin', 'Store', { + prefix: '/store/endpoint' + }); + +annotationData +^^^^^^^^^^^^^^ + +Custom meta data that will be attached to every annotation that is sent +to the server. This *will* override previous values. + +Example: + +.. code:: javascript + + $('#content').annotator('addPlugin', 'Store', { + // Attach a uri property to every annotation sent to the server. + annotationData: { + 'uri': 'http://this/document/only' + } + }); + +loadFromSearch +^^^^^^^^^^^^^^ + +An object literal containing query string parameters to query the store. +If ``loadFromSearch`` is set, then we load the first batch of +annotations from the 'search' URL as set in ``options.urls`` instead of +the registry path 'prefix/read'. Defaults to ``false``. + +Example: + +.. code:: javascript + + $('#content').annotator('addPlugin', 'Store', { + loadFromSearch: { + 'limit': 0, + 'all_fields': 1, + 'uri': 'http://this/document/only' + } + }); + +urls +^^^^ + +The server URLs for each available action (excluding ``prefix``). These +URLs can point anywhere but must respond to the appropriate HTTP method. +The ``:id`` token can be used anywhere in the URL and will be replaced +with the annotation id. + +Methods for actions are as follows: + +:: + + read: GET + create: POST + update: PUT + destroy: DELETE + search: GET + +Example: + +.. code:: javascript + + $('#content').annotator('addPlugin', 'Store', { + urls: { + // These are the default URLs. + create: '/annotations', + read: '/annotations/:id', + update: '/annotations/:id', + destroy: '/annotations/:id', + search: '/search' + } + }): + diff --git a/doc/plugins/tags.rst b/doc/plugins/tags.rst new file mode 100644 index 000000000..18ad9542b --- /dev/null +++ b/doc/plugins/tags.rst @@ -0,0 +1,46 @@ +``Tags`` plugin +=============== + +This plugin allows the user to tag their annotations with keywords. + +Interface Overview +------------------ + +The following elements are added to the Annotator interface by this +plugin. + +Viewer +^^^^^^ + +The plugin adds a section to a viewed annotation displaying any tags +that have been added. + +Editor +^^^^^^ + +The plugin adds an input field to the editor allowing the user to enter +a space separated list of tags. + +Usage +----- + +Adding the tags plugin to the annotator is very simple. Simply add the +annotator to the page using the ``.annotator()`` jQuery plugin. Then +call the ``.addPlugin()`` method by calling +``.annotator('addPlugin', 'Tags')``. + +.. code:: javascript + + var content = $('#content').annotator().annotator('addPlugin', 'Tags'); + +Options +~~~~~~~ + +*There are no options available for this plugin* + +Adding autocompletion of tags +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +See `this +example `__ +using jQueryUI autocomplete. diff --git a/doc/plugins/unsupported.rst b/doc/plugins/unsupported.rst new file mode 100644 index 000000000..ada690a5b --- /dev/null +++ b/doc/plugins/unsupported.rst @@ -0,0 +1,43 @@ +``Unsupported`` plugin +====================== + +The Annotator only supports browsers that have the +``window.getSelection()`` method (for a table of support please see +`this Quirksmode +article `__). This +plugin provides a notification to users of these unsupported browsers +letting them know that the plugin has not loaded. + +Usage +----- + +Adding the unsupported plugin to the annotator is very simple. Simply +add the annotator to the page using the ``.annotator()`` jQuery plugin. +Then call the ``.addPlugin()`` method eg. +``.annotator('addPlugin', 'Unsupported')``. + +.. code:: javascript + + var content = $('#content').annotator(); + content.annotator('addPlugin', 'Unsupported'); + +Options +~~~~~~~ + +You can provide options + +- ``message``: A customised message that you wish to display to users. + +message +^^^^^^^ + +The message that you wish to display to users. + +.. code:: javascript + + var annotator = $('#content').annotator().data('annotator'); + + annotator.addPlugin('Unsupported', { + message: "We're sorry the Annotator is not supported by this browser" + }); + diff --git a/doc/storage.rst b/doc/storage.rst new file mode 100644 index 000000000..bdb9e4f7b --- /dev/null +++ b/doc/storage.rst @@ -0,0 +1,219 @@ +Storage +======= + +Some kind of storage is needed to save your annotations after you leave +a web page. + +To do this you can use the [[Store plugin]] and a remote JSON API. This +page describes the API expected by the Store plugin, and implemented by +the `reference backend `__. It +is this backend that runs the `AnnotateIt `__ web +service. + +Core storage API +---------------- + +The storage API is defined in terms of a ``prefix`` and a number of +endpoints. It attempts to follow the principles of +`REST `__, +and emits JSON documents to be parsed by the Annotator. Each of the +following endpoints for the storage API is expected to be found on the +web at ``prefix`` + ``path``. For example, if the prefix were +``http://example.com/api``, then the **index** endpoint would be found +at ``http://example.com/api/annotations``. + +General rules are those common to most REST APIs. If a resource cannot +be found, return ``404 NOT FOUND``. If an action is not permitted for +the current user, return ``401 NOT AUTHORIZED``, otherwise return +``200 OK``. Send JSON text with the header +``Content-Type: application/json``. + +Below you can find details of the six core endpoints, **root**, +**index**, **create**, **read**, **update**, **delete**, as well as an +optional **search** API. + +.. raw:: html + +

+ +WARNING: + +.. raw:: html + +

+ +The spec below requires you return ``303 SEE OTHER`` from the **create** +and **update** endpoints. Ideally this *is* what you'd do, but +unfortunately most modern browsers (Firefox and Webkit) still make a +hash of CORS requests when they include redirects. A simple workaround +for the time being is to return ``200 OK`` and the JSON annotation that +*would* be returned by the **read** endpoint in the body of the +**create** and **update** responses. See bugs in +`Chromium `__ +and `Webkit `__. + +root +~~~~ + +- method: ``GET`` +- path: ``/`` +- returns: object containing store metadata, including API version + +Example: + +:: + + $ curl http://example.com/api/ + { + "name": "Annotator Store API", + "version": "2.0.0" + } + +index +~~~~~ + +- method: ``GET`` +- path: ``/annotations`` +- returns: a list of all annotation objects + +Example (see [[Annotation Format]] for details of the format of +individual annotations): + +.. code:: json + + $ curl http://example.com/api/annotations + [ + { + "text": "Example annotation text", + "ranges": [ ... ], + ... + }, + { + "text": "Another annotation", + "ranges": [ ... ], + ... + }, + ... + ] + +create +~~~~~~ + +- method: ``POST`` +- path: ``/annotations`` +- receives: an annotation object, sent with + ``Content-Type: application/json`` +- returns: ``303 SEE OTHER`` redirect to the appropriate **read** + endpoint + +Example: + +:: + + $ curl -i -X POST \ + -H 'Content-Type: application/json' \ + -d '{"text": "Annotation text"}' \ + http://example.com/api/annotations + HTTP/1.0 303 SEE OTHER + Location: http://example.com/api/annotations/d41d8cd98f00b204e9800998ecf8427e + ... + +read +~~~~ + +- method: ``GET`` +- path: ``/annotations/`` +- returns: an annotation object + +Example: + +:: + + $ curl http://example.com/api/annotations/d41d8cd98f00b204e9800998ecf8427e + { + "id": "d41d8cd98f00b204e9800998ecf8427e", + "text": "Annotation text", + ... + } + +update +~~~~~~ + +- method: ``PUT`` +- path: ``/annotations/`` +- receives: a (partial) annotation object, sent with + ``Content-Type: application/json`` +- returns: ``303 SEE OTHER`` redirect to the appropriate **read** + endpoint + +Example: + +:: + + $ curl -i -X PUT \ + -H 'Content-Type: application/json' \ + -d '{"text": "Updated annotation text"}' \ + http://example.com/api/annotations/d41d8cd98f00b204e9800998ecf8427e + HTTP/1.0 303 SEE OTHER + Location: http://example.com/api/annotations/d41d8cd98f00b204e9800998ecf8427e + ... + +delete +~~~~~~ + +- method: ``DELETE`` +- path: ``/annotations/`` +- returns: ``204 NO CONTENT``, and -- obviously -- no content + +:: + + $ curl -i -X DELETE http://example.com/api/annotations/d41d8cd98f00b204e9800998ecf8427e + HTTP/1.0 204 NO CONTENT + Content-Length: 0 + +Search API +---------- + +You may also choose to implement a search API, which can be used by the +Store plugin's ``loadFromSearch`` configuration option. + +search +~~~~~~ + +- method: ``GET`` +- path: ``/search?text=foobar`` +- returns: an object with ``total`` and ``rows`` fields. ``total`` is + an integer denoting the *total* number of annotations matched by the + search, while ``rows`` is a list containing what might be a subset of + these annotations. +- If implemented, this method should also support the ``limit`` and + ``offset`` query parameters for paging through results. + +:: + + $ curl http://example.com/api/search?text=annotation + { + "total": 43127, + "rows": [ + { + "id": "d41d8cd98f00b204e9800998ecf8427e", + "text": "Updated annotation text", + ... + }, + ... + ] + } + +Storage Implementations +----------------------- + +- Reference backend, a Python Flask app: + https://github.com/okfn/annotator-store (in particular, see + `store.py `__, + although be aware that this file also deals with authentication and + authorization, making the code a good deal more complex than would be + required to implement what is described above). +- PHP (Silex) and MongoDB-based basic implementation: + https://github.com/julien-c/annotator-php (in particular, see + `index.php `__). + From af4ea75cec7c9d51b54e4d0972cb99d3f6fe45c1 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 22 Jan 2014 09:28:16 -0800 Subject: [PATCH 027/640] Bump version in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 53a0d4baa..f66d9adca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "annotator", - "version": "1.2.8", + "version": "2.0.0", "description": "Inline annotation for the web. Select text, images, or (nearly) anything else, and add your notes.", "repository": { "type": "git", From 6c70f0e2f58cd7560d2078396bd2dfdf3f7ac02f Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Wed, 22 Jan 2014 19:47:14 +0100 Subject: [PATCH 028/640] Make it clear that 2.0.0 is not ready yet! --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f66d9adca..d062f3be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "annotator", - "version": "2.0.0", + "version": "2.0.0dev", "description": "Inline annotation for the web. Select text, images, or (nearly) anything else, and add your notes.", "repository": { "type": "git", From 6081cc1e3f917c6494bf38076aed41dd0577876b Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 22 Jan 2014 15:31:54 -0800 Subject: [PATCH 029/640] Stick to semver for version format --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d062f3be0..f28de1b0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "annotator", - "version": "2.0.0dev", + "version": "2.0.0-dev", "description": "Inline annotation for the web. Select text, images, or (nearly) anything else, and add your notes.", "repository": { "type": "git", From 6c212d60f4ada943bd0e6a17afa2e1f86c95f114 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 29 Jan 2014 14:26:44 -0800 Subject: [PATCH 030/640] Prevent buttons from submitting page forms Setting 'type="button"' on the button tags in our UI elements prevents browsers from treating them as the default submit button. This ensures that an annotator loaded inside a form element doesn't cause accidental form submissions. Tested in FF, Chrome, and IE9. Fix #147 --- src/annotator.coffee | 2 +- src/plugin/filter.coffee | 6 +++--- src/viewer.coffee | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index 9182d1aa5..ab07c56b3 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -34,7 +34,7 @@ class Annotator extends Delegator ".annotator-hl mouseout": "startViewerHideTimer" html: - adder: '
' + adder: '
' wrapper: '
' options: # Configuration options diff --git a/src/plugin/filter.coffee b/src/plugin/filter.coffee index b14dfa4e4..05732a625 100644 --- a/src/plugin/filter.coffee +++ b/src/plugin/filter.coffee @@ -24,8 +24,8 @@ class Annotator.Plugin.Filter extends Annotator.Plugin
""" + Annotator._t('Navigate:') + """ - - + + """ + Annotator._t('Filter by:') + """
@@ -34,7 +34,7 @@ class Annotator.Plugin.Filter extends Annotator.Plugin - + """ diff --git a/src/viewer.coffee b/src/viewer.coffee index ba8d38da0..475db1563 100644 --- a/src/viewer.coffee +++ b/src/viewer.coffee @@ -29,8 +29,8 @@ class Viewer extends Widget
  • View as webpage - - + +
  • """ From 98e9116d8ef442c0f7f7f6c5ac9d878e4c3a0dcb Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Wed, 29 Jan 2014 14:41:31 -0800 Subject: [PATCH 031/640] Small updates to HACKING.markdown for new build --- HACKING.markdown | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/HACKING.markdown b/HACKING.markdown index b4fb45b90..0378d2c2d 100644 --- a/HACKING.markdown +++ b/HACKING.markdown @@ -25,7 +25,7 @@ developing. The tests can be found in `test/spec/`. You can run the tests in your browser (using `test/runner.html`), but while you're working it's probably easiest to -run the tests using `make test` from the root of the repository. This will +run the tests using `npm test` from the root of the repository. This will require [PhantomJS][phantom]. For inline documentation we use [TomDoc][tom]. It's a Ruby specification but it @@ -44,7 +44,6 @@ Building the packaged version of Annotator involves running the appropriate `make` task. For example: $ make # build everything - $ make develop # converts CoffeeScript to JavaScript by calling ./tools/build $ make bookmarklet # build the bookmarklet $ make annotator plugins # build annotator and individual plugin files. From 6cae6569deda3457a41d67c30f1a137b369eaae1 Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Thu, 30 Jan 2014 21:12:08 +0200 Subject: [PATCH 032/640] use admonitions --- doc/getting-started.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/getting-started.rst b/doc/getting-started.rst index 3437bb1d3..17e8a1b28 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -8,9 +8,11 @@ To get the Annotator up and running on your website you'll need to either link to a hosted version or deploy the Annotator source files yourself. Details of both are provided below. -NB: if you are using Wordpress there is also a `Annotator Wordpress -plugin `__ -which will take care of installing and integrating Annotator for you. +.. admonition:: NB + + If you are using Wordpress there is also a `Annotator Wordpress + plugin `__ + which will take care of installing and integrating Annotator for you. Hosted Annotator Library ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -48,10 +50,9 @@ developed as part of the Annotator project. Including Annotator on your webpage ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You need to link the Annotator Javascript and CSS into the page. You -will also need to include jQuery >= 1.6. +You need to link the Annotator Javascript and CSS into the page. -NOTE: Annotator requires jQuery 1.6 or greater. +.. note:: Annotator requires jQuery 1.6 or greater. .. code:: html From 00664c2d38a8b2f60eb933d262e5b0301c6404ea Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Thu, 30 Jan 2014 21:29:55 +0200 Subject: [PATCH 033/640] fix various local refs --- doc/annotation-format.rst | 4 ++-- doc/getting-started.rst | 16 ++++++++-------- doc/plugins/auth.rst | 8 ++++---- doc/storage.rst | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/doc/annotation-format.rst b/doc/annotation-format.rst index 4f9a55d48..df87de066 100644 --- a/doc/annotation-format.rst +++ b/doc/annotation-format.rst @@ -35,10 +35,10 @@ document: } Note that this annotation includes some info stored by plugins (notably -the [[Permissions Plugin]] and [[Tags Plugin]]). +the :doc:`plugins/permissions` and :doc:`plugins/tags`). This basic schema is **completely extensible**. It can be added to by plugins, and any fields added by the frontend should be preserved by -backend implementations. For example, the [[Store Plugin]] (which adds +backend implementations. For example, the :doc:`plugins/store` (which adds persistence of annotations) allow you to specify arbitrary additional fields using the ``annotationData`` attribute. diff --git a/doc/getting-started.rst b/doc/getting-started.rst index 17e8a1b28..26aacdb6c 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -109,10 +109,10 @@ it just add a call to ``.annotator("setupPlugins")``. This will set up the following: -1. The [[Tags\|Tags Plugin]], [[Filter\|Filter Plugin]] & - [[Unsupported\|Unsupported Plugin]] plugins. -2. The [[Auth\|Auth Plugin]], [[Permissions\|Permissions Plugin]] and - [[Store\|Store Plugin]] plugins, for interaction with the `AnnotateIt +1. The :doc:`Tags `, :doc:`Filter ` & + :doc:`Unsupported ` plugins. +2. The :doc:`Auth `, :doc:`Permissions ` and + :doc:`Store ` plugins, for interaction with the `AnnotateIt store `__. 3. If the `Showdown `__ library has been included on the page the [[Markdown\|Markdown Plugin]] will also @@ -155,9 +155,9 @@ Here we add the tags plugin to the page: .annotator('addPlugin', 'Tags'); }); -For more information on available plugins check the navigation to the -right of this article. Or to create your own check the [[creating a -plugin section\|Plugin Development]]. +For more information on available plugins check the navigation to the right of +this article. Or to create your own check the :doc:`creating a plugin section +`. Saving annotations ------------------ @@ -165,4 +165,4 @@ Saving annotations In order to keep your annotations around longer than a single page view you'll need to set up a store on your server or use an external service like `AnnotateIt `__. For more information on -storing annotations check out the [[Store Plugin]] on the wiki. +storing annotations check out the :doc:`Store Plugin ` on the wiki. diff --git a/doc/plugins/auth.rst b/doc/plugins/auth.rst index a957a37da..4b0d0f62a 100644 --- a/doc/plugins/auth.rst +++ b/doc/plugins/auth.rst @@ -1,14 +1,14 @@ ``Auth`` plugin =============== -The Auth plugin complements the [[Store Plugin]] by providing +The Auth plugin complements the :doc:`store` by providing authentication for requests. This may be necessary if you are running the Store on a separate domain or using a third party service like annotateit.org. The plugin works by requesting an authentication token from the local server and then provides this in all requests to the store. For more -details see the [[specification\|Authentication]]. +details see the :doc:`specification <../authentication>`. Usage ----- @@ -40,5 +40,5 @@ The following options are available to the Auth plugin. Token format ^^^^^^^^^^^^ -For details of the token format, see the page on [[Annotator's -Authentication system\|Authentication]]. +For details of the token format, see the page on :doc:`Annotator's +Authentication system <../authentication>`. diff --git a/doc/storage.rst b/doc/storage.rst index bdb9e4f7b..e54299815 100644 --- a/doc/storage.rst +++ b/doc/storage.rst @@ -4,7 +4,7 @@ Storage Some kind of storage is needed to save your annotations after you leave a web page. -To do this you can use the [[Store plugin]] and a remote JSON API. This +To do this you can use the :doc:`plugins/store` and a remote JSON API. This page describes the API expected by the Store plugin, and implemented by the `reference backend `__. It is this backend that runs the `AnnotateIt `__ web @@ -76,7 +76,7 @@ index - path: ``/annotations`` - returns: a list of all annotation objects -Example (see [[Annotation Format]] for details of the format of +Example (see :doc:`annotation-format` for details of the format of individual annotations): .. code:: json From 21ff72ac4948b6e71d23317f93f1e2dfa270b38b Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Thu, 30 Jan 2014 21:31:20 +0200 Subject: [PATCH 034/640] use definition lists for options --- doc/getting-started.rst | 5 +-- doc/hacking/plugin-development.rst | 50 ++++++++++++++++-------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/doc/getting-started.rst b/doc/getting-started.rst index 26aacdb6c..c0c6e2038 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -82,8 +82,9 @@ Options You can optionally specify options: -- readOnly - True to allow viewing annotations, but not creating or - editing them. Defaults to false. +``readOnly`` + True to allow viewing annotations, but not creating or editing them. + Defaults to ``false``. .. code:: javascript diff --git a/doc/hacking/plugin-development.rst b/doc/hacking/plugin-development.rst index 7731c7ea4..778a454e7 100644 --- a/doc/hacking/plugin-development.rst +++ b/doc/hacking/plugin-development.rst @@ -8,7 +8,8 @@ Building a plugin is very simple. Simply attach a function that creates your plugin to the ``Annotator.Plugin`` namespace. The function will receive the following arguments. -- ``element``: The DOM element that is currently being annotated. +``element`` + The DOM element that is currently being annotated. Additional arguments (such as options) can be passed in by the user when the plugin is added to the Annotator. These will be passed in after the @@ -195,29 +196,30 @@ called on either the ``.annotator`` object or if you're extending ``Annotator.Plugin`` the plugin instance itself. The events are as follows: -- ``beforeAnnotationCreated(annotation)``: called immediately before an - annotation is created. If you need to modify the annotation before it - is saved use this event. -- ``annotationCreated(annotation)``: called when the annotation is - created use this to store the annotations. -- ``beforeAnnotationUpdated(annotation)``: as above, but just before an - existing annotation is saved. -- ``annotationUpdated(annotation)``: as above, but for an existing - annotation which has just been edited. -- ``annotationDeleted(annotation)``: called when the user deletes an - annotation. -- ``annotationEditorShown(editor, annotation)``: called when the - annotation editor is presented to the user. -- ``annotationEditorHidden(editor)``: called when the annotation editor - is hidden, both when submitted and when editing is cancelled. -- ``annotationEditorSubmit(editor, annotation)``: called when the - annotation editor is submitted. -- ``annotationViewerShown(viewer, annotations)``: called when the - annotation viewer is shown and provides the annotations being - displayed. -- ``annotationViewerTextField(field, annotation)``: called when the - text field displaying the annotation comment in the viewer is - created. +``beforeAnnotationCreated(annotation)`` + called immediately before an annotation is created. If you need to modify + the annotation before it is saved use this event. +``annotationCreated(annotation)`` + called when the annotation is created use this to store the annotations. +``beforeAnnotationUpdated(annotation)`` + as above, but just before an existing annotation is saved. +``annotationUpdated(annotation)`` + as above, but for an existing annotation which has just been edited. +``annotationDeleted(annotation)`` + called when the user deletes an annotation. +``annotationEditorShown(editor, annotation)`` + called when the annotation editor is presented to the user. +``annotationEditorHidden(editor)`` + called when the annotation editor is hidden, both when submitted and when + editing is cancelled. +``annotationEditorSubmit(editor, annotation)`` + called when the annotation editor is submitted. +``annotationViewerShown(viewer, annotations)`` + called when the annotation viewer is shown and provides the annotations + being displayed. +``annotationViewerTextField(field, annotation)`` + called when the text field displaying the annotation comment in the viewer + is created. Example ~~~~~~~ From faeaa217c123d8f9b92bb20048ddc7b750eb0603 Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Thu, 30 Jan 2014 21:31:41 +0200 Subject: [PATCH 035/640] minor sphinx formatting change --- doc/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/getting-started.rst b/doc/getting-started.rst index c0c6e2038..3a65e6f69 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -29,7 +29,7 @@ For each Annotator release, we make available the following assets: Use ``annotator-full.min.js`` if you want to include both the core and all plugins in a single file. Use ``annotator.min.js`` if you need only the core. You can add individual plugins by including the relevant -``annotator.{pluginname}.min.js`` files. +:samp:`annotator.{pluginname}.min.js` files. For example, a full version of the Annotator can be loaded with the following code: From 8bf092be6db2d9cb9a9c27758f621046baf3430f Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Fri, 31 Jan 2014 18:58:19 +0200 Subject: [PATCH 036/640] admonition directive does not render properly with rtd theme --- doc/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/getting-started.rst b/doc/getting-started.rst index 3a65e6f69..69b4be40f 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -8,7 +8,7 @@ To get the Annotator up and running on your website you'll need to either link to a hosted version or deploy the Annotator source files yourself. Details of both are provided below. -.. admonition:: NB +.. note:: If you are using Wordpress there is also a `Annotator Wordpress plugin `__ From f58fc20f4f54fabb365e59a8c0b57c308d44da8f Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Fri, 31 Jan 2014 19:05:01 +0200 Subject: [PATCH 037/640] remove empty lines at end of files --- doc/authentication.rst | 1 - doc/hacking/plugin-development.rst | 1 - doc/index.rst | 1 - doc/plugins/filter.rst | 1 - doc/plugins/store.rst | 1 - doc/plugins/unsupported.rst | 1 - doc/storage.rst | 1 - 7 files changed, 7 deletions(-) diff --git a/doc/authentication.rst b/doc/authentication.rst index 273bb8772..94df29da7 100644 --- a/doc/authentication.rst +++ b/doc/authentication.rst @@ -150,4 +150,3 @@ Original planning documents at: Rehashed in Feb 2012: - http://lists.okfn.org/pipermail/annotator-dev/2012-January/000188.html - diff --git a/doc/hacking/plugin-development.rst b/doc/hacking/plugin-development.rst index 778a454e7..3105de3e8 100644 --- a/doc/hacking/plugin-development.rst +++ b/doc/hacking/plugin-development.rst @@ -244,4 +244,3 @@ A plugin that logs annotation activity to the console. } } }; - diff --git a/doc/index.rst b/doc/index.rst index 9a7e20cf8..5d96b32b2 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -23,4 +23,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/doc/plugins/filter.rst b/doc/plugins/filter.rst index 7fc770b0a..95e5a7c0c 100644 --- a/doc/plugins/filter.rst +++ b/doc/plugins/filter.rst @@ -82,4 +82,3 @@ array of tags: }} ] }); - diff --git a/doc/plugins/store.rst b/doc/plugins/store.rst index 35f677ae3..7834392f8 100644 --- a/doc/plugins/store.rst +++ b/doc/plugins/store.rst @@ -170,4 +170,3 @@ Example: search: '/search' } }): - diff --git a/doc/plugins/unsupported.rst b/doc/plugins/unsupported.rst index ada690a5b..2d3147a83 100644 --- a/doc/plugins/unsupported.rst +++ b/doc/plugins/unsupported.rst @@ -40,4 +40,3 @@ The message that you wish to display to users. annotator.addPlugin('Unsupported', { message: "We're sorry the Annotator is not supported by this browser" }); - diff --git a/doc/storage.rst b/doc/storage.rst index e54299815..041e6c2cc 100644 --- a/doc/storage.rst +++ b/doc/storage.rst @@ -216,4 +216,3 @@ Storage Implementations - PHP (Silex) and MongoDB-based basic implementation: https://github.com/julien-c/annotator-php (in particular, see `index.php `__). - From 85abe73b1439af6b8c7284304456cdb85940e0a0 Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Fri, 31 Jan 2014 19:07:13 +0200 Subject: [PATCH 038/640] copy over the markdown plugin docs --- doc/getting-started.rst | 2 +- doc/plugins/markdown.rst | 41 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 doc/plugins/markdown.rst diff --git a/doc/getting-started.rst b/doc/getting-started.rst index 3a65e6f69..15a49b553 100644 --- a/doc/getting-started.rst +++ b/doc/getting-started.rst @@ -116,7 +116,7 @@ This will set up the following: :doc:`Store ` plugins, for interaction with the `AnnotateIt store `__. 3. If the `Showdown `__ library has - been included on the page the [[Markdown\|Markdown Plugin]] will also + been included on the page the :doc:`plugins/markdown` will also be loaded. You can further customise the plugins by providing an object containing diff --git a/doc/plugins/markdown.rst b/doc/plugins/markdown.rst new file mode 100644 index 000000000..b225b807c --- /dev/null +++ b/doc/plugins/markdown.rst @@ -0,0 +1,41 @@ +``Markdown`` Plugin +=================== + +The Markdown plugin allows you to use +`Markdown `__ in your +annotation comments. It will then render them in the Viewer. + +Requirements +------------ + +This plugin requires that the +`Showdown `__ Markdown library be +loaded in the page before the plugin is added to the annotator. To do +this simply +`download `__ +the showdown.js and include it on your page before the annotator. + +.. code:: html + + + + + + +Usage +----- + +Adding the Markdown plugin to the annotator is very simple. Simply add +the annotator to the page using the ``.annotator()`` jQuery plugin and +retrieve the annotator object using ``.data('annotator')``. Then add the +``Markdown`` plugin. + +.. code:: javascript + + var content = $('#content').annotator(); + content.annotator('addPlugin', 'Markdown'); + +Options +~~~~~~~ + +*There are no options available for this plugin* From 6b5c458da4c0b0988defc7059693b406cd8862e1 Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Fri, 31 Jan 2014 19:13:33 +0200 Subject: [PATCH 039/640] add _static/ hierarchy since it's mentioned in the conf.py --- doc/_static/.placeholder | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 doc/_static/.placeholder diff --git a/doc/_static/.placeholder b/doc/_static/.placeholder new file mode 100644 index 000000000..e69de29bb From 4e35938789a9e4f58c6e5502500ae958ad0ccc07 Mon Sep 17 00:00:00 2001 From: Mikhail Sobolev Date: Fri, 31 Jan 2014 19:26:23 +0200 Subject: [PATCH 040/640] use sphinx_rtd_theme locally if availble * do not set it up if the build happens at readthedocs.org (https://github.com/snide/sphinx_rtd_theme#using-this-theme-locally-then-building-on-read-the-docs) * do not set it up if not available locally --- doc/conf.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/doc/conf.py b/doc/conf.py index d66ae9ff7..79ea8886a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -16,6 +16,17 @@ import sys import os +# By default, we do not want to use the RTD theme +sphinx_rtd_theme = None + +# If the docs are built on readthedocs, it will be used by default +if os.environ.get('READTHEDOCS') != 'True': + try: + import sphinx_rtd_theme + except ImportError: + # Now we know for sure we do not have it + pass + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. @@ -101,7 +112,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' if sphinx_rtd_theme else 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -109,7 +120,10 @@ #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +if sphinx_rtd_theme: + html_theme_path = [ + sphinx_rtd_theme.get_html_theme_path() + ] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". From 269fe1c6519e99256d463961b0ded1901a6d044d Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Fri, 31 Jan 2014 21:52:01 +0000 Subject: [PATCH 041/640] annotator: don't call handleError for non-errors Cancelling an editing action rejects a promise, causing handleError to be called. This probably isn't desireable, seeing as it currently calls console.error(...). In addition (and more immediately important) console.log(...) and friends can't accept non-serializable inputs such as annotation _local objects in mocha-phantomjs, causing the tests to fail. Instead, we only actually hook up the handleError promise rejection handler for real errors that come from the storage layer. --- src/annotator.coffee | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index ab07c56b3..73b0a6f50 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -688,6 +688,8 @@ class Annotator extends Delegator this.editAnnotation(annotation, position) .then (annotation) => this.annotations.create(annotation) + # Handle storage errors + .fail(handleError) # Clean up the highlights .done (annotation) => @@ -696,8 +698,8 @@ class Annotator extends Delegator .done (annotation) => this.publish('annotationCreated', [annotation]) - # Handle errors - .fail(this.cleanupAnnotation, handleError) + # Clean up (if, for example, editing was cancelled, or storage failed) + .fail(this.cleanupAnnotation) # Annotator#viewer callback function. Displays the Annotator#editor in the # positions of the Annotator#viewer and loads the passed annotation for @@ -719,13 +721,12 @@ class Annotator extends Delegator this.editAnnotation(annotation, position) .then (annotation) => this.annotations.update(annotation) + # Handle storage errors + .fail(handleError) .done (annotation) => this.publish('annotationUpdated', [annotation]) - # Handle errors - .fail(handleError) - # Create namespace for Annotator plugins class Annotator.Plugin extends Delegator constructor: (element, options) -> From 3bcc16b5a201283a8f9a60b7f22f7b9f81e4abb4 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Fri, 31 Jan 2014 22:13:12 +0000 Subject: [PATCH 042/640] Add @sa2ajj to AUTHORS --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 4ea353360..c179f264b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -14,6 +14,7 @@ Brian Long Jean-Bernard Marcon Chris Nitchie Rufus Pollock +Mikhail Sobolev Nick Stenning Ed Summers Deepak Thukral From a99b7fa297f800d483885a52e92343a02e8e4bfb Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 31 Jan 2014 00:18:25 -0800 Subject: [PATCH 043/640] update test dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f28de1b0c..a98ec9f8a 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "coffee-script": "~1.6.0", "uglify-js": "~2.3.6", "uglifycss": "~0.0.5", - "mocha": "~1.9.0", - "mocha-phantomjs": "~2.0.1", - "chai": "~1.5.0", + "mocha": "~1.12.0", + "mocha-phantomjs": "~3.3.1", + "chai": "~1.7.0", "sinon": "~1.6.0", "jwt-simple": "~0.1.0", "iso8601": "~1.1.1", @@ -33,7 +33,7 @@ }, "scripts": { "start": "./tools/serve", - "test": "./tools/test -R spec" + "test": "./tools/test" }, "browser": "./lib/annotator" } From 40a4a0598b945396899d91731acddd6d12b08b5a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 31 Jan 2014 14:22:59 -0800 Subject: [PATCH 044/640] Update test dependency documentation --- HACKING.markdown | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/HACKING.markdown b/HACKING.markdown index 0378d2c2d..6dcaafcda 100644 --- a/HACKING.markdown +++ b/HACKING.markdown @@ -26,7 +26,9 @@ developing. The tests can be found in `test/spec/`. You can run the tests in your browser (using `test/runner.html`), but while you're working it's probably easiest to run the tests using `npm test` from the root of the repository. This will -require [PhantomJS][phantom]. +require [PhantomJS][phantom] and the mocha runner: + + $ npm install -g phantomjs mocha-phantomjs For inline documentation we use [TomDoc][tom]. It's a Ruby specification but it also works nicely with CoffeeScript. From 82b36a3589a34b3235ce931a84a1ab82840776a9 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 31 Jan 2014 14:24:52 -0800 Subject: [PATCH 045/640] Fix cleanupAnnotation test --- test/spec/annotator_spec.coffee | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/spec/annotator_spec.coffee b/test/spec/annotator_spec.coffee index 5aef0f3d8..807f8aa4d 100644 --- a/test/spec/annotator_spec.coffee +++ b/test/spec/annotator_spec.coffee @@ -448,11 +448,12 @@ describe 'Annotator', -> div = $('
    ').append(annotation._local.highlights) it "should remove the highlights from the DOM", -> - annotation._local.highlights.each -> + highlights = annotation._local.highlights + highlights.each -> assert.lengthOf($(this).parent(), 1) annotator.cleanupAnnotation(annotation) - annotation._local.highlights.each -> + highlights.each -> assert.lengthOf($(this).parent(), 0) it "should leave the content of the highlights in place", -> From 7d6bd20d86102b4401e17d07f2ce5b6051e2d37a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Fri, 31 Jan 2014 14:23:55 -0800 Subject: [PATCH 046/640] delete highlights field in cleanupAnnotation --- src/annotator.coffee | 1 + test/spec/annotator_spec.coffee | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/annotator.coffee b/src/annotator.coffee index 73b0a6f50..4790ca055 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -350,6 +350,7 @@ class Annotator extends Delegator if annotation._local?.highlights? for h in annotation._local.highlights when h.parentNode? $(h).replaceWith(h.childNodes) + delete annotation._local.highlights annotation diff --git a/test/spec/annotator_spec.coffee b/test/spec/annotator_spec.coffee index 807f8aa4d..43cc1e126 100644 --- a/test/spec/annotator_spec.coffee +++ b/test/spec/annotator_spec.coffee @@ -456,6 +456,8 @@ describe 'Annotator', -> highlights.each -> assert.lengthOf($(this).parent(), 0) + assert.isUndefined(annotation._local.highlights, "highlights property removed") + it "should leave the content of the highlights in place", -> annotator.cleanupAnnotation(annotation) assert.equal(div.html(), 'HatsGloves') From 403e43ecc645c39ae2981e698d495b783ce24cb3 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 4 Feb 2014 00:54:06 -0800 Subject: [PATCH 047/640] Use backbone-events-standalone Remove the events module in favor of using a standalone copy of Backbone Events from npm as a mixin for Delegator. --- package.json | 1 + src/class.coffee | 69 ++++++++++++++++++++++++++--- src/events.coffee | 87 ------------------------------------- src/registry.coffee | 6 +-- test/runner.html | 1 - test/spec/class_spec.coffee | 45 ++++++++++++++++++- 6 files changed, 109 insertions(+), 100 deletions(-) delete mode 100644 src/events.coffee diff --git a/package.json b/package.json index a98ec9f8a..22f2e8950 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "through": "~2.3.4" }, "devDependencies": { + "backbone-events-standalone": "~0.2.1", "coffee-script": "~1.6.0", "uglify-js": "~2.3.6", "uglifycss": "~0.0.5", diff --git a/src/class.coffee b/src/class.coffee index a5901bd44..83db6b278 100644 --- a/src/class.coffee +++ b/src/class.coffee @@ -1,12 +1,10 @@ Util = require './util' -Evented = require('./events') - # Public: Delegator is the base class that all of Annotators objects inherit # from. It provides basic functionality such as instance options, event # delegation and pub/sub methods. -class Delegator extends Evented +class Delegator # Public: Events object. This contains a key/pair hash of events/methods that # should be bound. See Delegator#addEvents() for usage. events: {} @@ -39,7 +37,6 @@ class Delegator extends Evented # registry of created closures, used to enable event unbinding. @_closures = {} - this.on = this.subscribe this.addEvents() # Public: binds the function names in the @events Object to their events. @@ -139,6 +136,64 @@ class Delegator extends Evented this + # Public: Fires an event and calls all subscribed callbacks with parameters + # provided. This is essentially an alias of $(this).triggerHandler() but + # should be used to fire custom events. + # + # NOTE: Events fired using .publish() will not bubble up the DOM. + # + # event - A String event name. + # params - An Array of parameters to provide to callbacks. + # + # Examples + # + # instance.subscribe('annotation:save', (msg) -> console.log(msg)) + # instance.publish('annotation:save', ['Hello World']) + # # => Outputs "Hello World" + # + # Returns itself. + publish: (name, args=[]) -> + this.trigger.apply(this, [name, args...]) + + # Public: Listens for custom event which when published will call the provided + # callback. This is essentially a wrapper around $(this).bind() but removes + # the event parameter that jQuery event callbacks always recieve. These + # parameters are unnessecary for custom events. + # + # event - A String event name. + # callback - A callback function called when the event is published. + # + # Examples + # + # instance.subscribe('annotation:save', (msg) -> console.log(msg)) + # instance.publish('annotation:save', ['Hello World']) + # # => Outputs "Hello World" + # + # Returns itself. + subscribe: (event, callback, context=this) -> + this.on(event, callback, context) + + # Public: Unsubscribes a callback from an event. The callback will no longer + # be called when the event is published. + # + # event - A String event name. + # callback - A callback function to be removed. + # + # Examples + # + # callback = (msg) -> console.log(msg) + # instance.subscribe('annotation:save', callback) + # instance.publish('annotation:save', ['Hello World']) + # # => Outputs "Hello World" + # + # instance.unsubscribe('annotation:save', callback) + # instance.publish('annotation:save', ['Hello Again']) + # # => No output. + # + # Returns itself. + unsubscribe: (event, callback, context=this) -> + this.off(event, callback, context) + # Parse the @events object of a Delegator into an array of objects containing # string-valued "selector", "event", and "func" keys. @@ -182,5 +237,9 @@ Delegator._isCustomEvent = (event) -> $.inArray(event, Delegator.natives) == -1 +# Mix in backbone events +BackboneEvents = require 'backbone-events-standalone' +BackboneEvents.mixin(Delegator::) + # Export Delegator object -module.exports = Delegator \ No newline at end of file +module.exports = Delegator diff --git a/src/events.coffee b/src/events.coffee deleted file mode 100644 index 66d147285..000000000 --- a/src/events.coffee +++ /dev/null @@ -1,87 +0,0 @@ -# TODO: replace this with Backbone.Events or similar -class Evented - # Public: Fires an event and calls all subscribed callbacks with any parameters - # provided. This is essentially an alias of $(this).triggerHandler() but - # should be used to fire custom events. - # - # NOTE: Events fired using .publish() will not bubble up the DOM. - # - # event - A String event name. - # params - An Array of parameters to provide to callbacks. - # - # Examples - # - # instance.subscribe('annotation:save', (msg) -> console.log(msg)) - # instance.publish('annotation:save', ['Hello World']) - # # => Outputs "Hello World" - # - # Returns itself. - publish: () -> - $(this).triggerHandler.apply($(this), arguments) - this - - # Public: Listens for custom event which when published will call the provided - # callback. This is essentially a wrapper around $(this).bind() but removes - # the event parameter that jQuery event callbacks always recieve. These - # parameters are unnessecary for custom events. - # - # event - A String event name. - # callback - A callback function called when the event is published. - # - # Examples - # - # instance.subscribe('annotation:save', (msg) -> console.log(msg)) - # instance.publish('annotation:save', ['Hello World']) - # # => Outputs "Hello World" - # - # Returns itself. - subscribe: (event, callback, context=this) -> - closure = -> callback.apply(context, [].slice.call(arguments, 1)) - - # Ensure both functions have the same unique id so that jQuery will accept - # callback when unbinding closure. - closure.guid = callback.guid = ($.guid += 1) - - $(this).bind(event, closure) - this - - # Public: Unsubscribes a callback from an event. The callback will no longer - # be called when the event is published. - # - # event - A String event name. - # callback - A callback function to be removed. - # - # Examples - # - # callback = (msg) -> console.log(msg) - # instance.subscribe('annotation:save', callback) - # instance.publish('annotation:save', ['Hello World']) - # # => Outputs "Hello World" - # - # instance.unsubscribe('annotation:save', callback) - # instance.publish('annotation:save', ['Hello Again']) - # # => No output. - # - # Returns itself. - unsubscribe: -> - $(this).unbind.apply($(this), arguments) - this - - # Public: Alias for subscribe - on: -> this.subscribe(arguments...) - - # Public: Alias for unsubscribe - off: -> this.unsubscribe(arguments...) - - # Public: Alias for trigger - trigger: -> this.publish(arguments...) - - # Public: Like subscribe, but unsubscribes automatically at first callback. - once: (event, callback, context) -> - closure = => - this.unsubscribe event, closure - callback.apply(context, arguments) - this.subscribe event, closure, context - - -module.exports = Evented \ No newline at end of file diff --git a/src/registry.coffee b/src/registry.coffee index 6fa3dba37..df1d1981e 100644 --- a/src/registry.coffee +++ b/src/registry.coffee @@ -1,11 +1,8 @@ -Evented = require('./events') - - # Registry is a factory for annotator applications providing a simple runtime # extension interface and application loader. It is used to pass settings to # extension modules and provide a means by which extensions can export # functionality to applications. -class Registry extends Evented +class Registry # Public: Create an instance of the application defined by the provided # module. The application will receive a new registry instance whose settings @@ -15,7 +12,6 @@ class Registry extends Evented (new this(settings)).run(appModule) constructor: (@settings={}) -> - super # Public: Include a module. A module is any Object with a fuction property # named 'configure`. This function is immediately invoked with the registry diff --git a/test/runner.html b/test/runner.html index 88a75739c..bb1f85fa9 100644 --- a/test/runner.html +++ b/test/runner.html @@ -30,7 +30,6 @@ - diff --git a/test/spec/class_spec.coffee b/test/spec/class_spec.coffee index 51fe4d407..e79e9c81d 100644 --- a/test/spec/class_spec.coffee +++ b/test/spec/class_spec.coffee @@ -79,5 +79,46 @@ describe 'Delegator', -> assert.deepEqual(delegator.returns, []) - it ".on() should be an alias of .subscribe()", -> - assert.strictEqual(delegator.on, delegator.subscribe) + it ".subscribe() subscribes listeners", -> + res = [] + delegator.subscribe('foo', -> res.push('bar')) + assert.deepEqual(res, []) + delegator.publish('foo') + assert.deepEqual(res, ['bar']) + + it "passes args from .publish() to listeners", -> + res = [] + delegator.subscribe('foo', (x, y, z) -> res.push(z, y, x)) + assert.deepEqual(res, []) + delegator.publish('foo', [1, 2, 3]) + assert.deepEqual(res, [3, 2, 1]) + + it "invokes the callback in the context of the object by default", -> + res = null + delegator.subscribe('foo', (-> res = this)) + delegator.publish('foo') + assert.equal(res, delegator) + + it "invokes the callback with a context if provided", -> + res = null + sentinel = {} + delegator.subscribe('foo', (-> res = this), sentinel) + delegator.publish('foo') + assert.equal(res, sentinel) + + it ".unsubscribe() unsubscribes listeners", -> + res = [] + cbk = -> res.push('bar') + delegator.subscribe('foo', cbk) + delegator.unsubscribe('foo', cbk) + delegator.publish('foo') + assert.deepEqual(res, []) + + it ".unsubscribe() only unsubscribes listeners passed", -> + res = [] + cbk = -> res.push('bar') + delegator.subscribe('foo', -> res.push('baz')) + delegator.subscribe('foo', cbk) + delegator.unsubscribe('foo', cbk) + delegator.publish('foo') + assert.deepEqual(res, ['baz']) From 2e3245b7e5187476975270105a29f679e363465a Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 4 Feb 2014 00:55:42 -0800 Subject: [PATCH 048/640] Add Annotator.Extend Use backbone-extend-standalone to provide a class extension feature for non-coffee environments. --- package.json | 1 + src/annotator.coffee | 1 + 2 files changed, 2 insertions(+) diff --git a/package.json b/package.json index 22f2e8950..15e97b351 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "devDependencies": { "backbone-events-standalone": "~0.2.1", + "backbone-extend-standalone": "~0.1.2", "coffee-script": "~1.6.0", "uglify-js": "~2.3.6", "uglifycss": "~0.0.5", diff --git a/src/annotator.coffee b/src/annotator.coffee index 4790ca055..3bec2c462 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -775,6 +775,7 @@ Annotator.Widget = Widget Annotator.Viewer = Viewer Annotator.Editor = Editor Annotator.Notification = Notification +Annotator.Extend = require 'backbone-extend-standalone' # Attach notification methods to the Annotation object notification = new Notification From 22ebb1f8bfb80945f3a9c3dd7a9a289869616631 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 4 Feb 2014 14:19:12 -0800 Subject: [PATCH 049/640] Remove excessive docstrings for alias methods --- src/class.coffee | 53 +++++------------------------------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/src/class.coffee b/src/class.coffee index 83db6b278..2211b4d9a 100644 --- a/src/class.coffee +++ b/src/class.coffee @@ -137,60 +137,17 @@ class Delegator this # Public: Fires an event and calls all subscribed callbacks with parameters - # provided. This is essentially an alias of $(this).triggerHandler() but - # should be used to fire custom events. - # - # NOTE: Events fired using .publish() will not bubble up the DOM. - # - # event - A String event name. - # params - An Array of parameters to provide to callbacks. - # - # Examples - # - # instance.subscribe('annotation:save', (msg) -> console.log(msg)) - # instance.publish('annotation:save', ['Hello World']) - # # => Outputs "Hello World" - # - # Returns itself. + # provided. This is essentially an alias to Backbone.Events .trigger() + # except that the arguments are passed in an Array as the second parameter + # rather than using a variable number of arguments. publish: (name, args=[]) -> this.trigger.apply(this, [name, args...]) - # Public: Listens for custom event which when published will call the provided - # callback. This is essentially a wrapper around $(this).bind() but removes - # the event parameter that jQuery event callbacks always recieve. These - # parameters are unnessecary for custom events. - # - # event - A String event name. - # callback - A callback function called when the event is published. - # - # Examples - # - # instance.subscribe('annotation:save', (msg) -> console.log(msg)) - # instance.publish('annotation:save', ['Hello World']) - # # => Outputs "Hello World" - # - # Returns itself. + # Public: An alias for .on() from Backbone.Events subscribe: (event, callback, context=this) -> this.on(event, callback, context) - # Public: Unsubscribes a callback from an event. The callback will no longer - # be called when the event is published. - # - # event - A String event name. - # callback - A callback function to be removed. - # - # Examples - # - # callback = (msg) -> console.log(msg) - # instance.subscribe('annotation:save', callback) - # instance.publish('annotation:save', ['Hello World']) - # # => Outputs "Hello World" - # - # instance.unsubscribe('annotation:save', callback) - # instance.publish('annotation:save', ['Hello Again']) - # # => No output. - # - # Returns itself. + # Public: An alias for .off() from Backbone.Events unsubscribe: (event, callback, context=this) -> this.off(event, callback, context) From 9dfff136255dae063f96874ad66b0ea50c1ebf6d Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Sat, 15 Feb 2014 14:46:32 -0800 Subject: [PATCH 050/640] Move .Extend to .extend and add docs --- src/annotator.coffee | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/annotator.coffee b/src/annotator.coffee index 3bec2c462..f5c14624f 100644 --- a/src/annotator.coffee +++ b/src/annotator.coffee @@ -1,3 +1,5 @@ +extend = require 'backbone-extend-standalone' + Delegator = require './class' Range = require './range' Util = require './util' @@ -94,6 +96,31 @@ class Annotator extends Delegator # Create the registry and start the application Registry.createApp(this, options) + # Public: Creates a subclass of Annotator. + # + # See the documentation from Backbone: http://backbonejs.org/#Model-extend + # + # Examples + # + # var ExtendedAnnotator = Annotator.extend({ + # setupAnnotation: function (annotation) { + # // Invoke the built-in implementation + # try { + # Annotator.prototype.setupAnnotation.call(this, annotation); + # } catch (e) { + # if (e instanceof Annotator.Range.RangeError) { + # // Try to locate the Annotation using the quote + # } else { + # throw e; + # } + # } + # + # return annotation; + # }); + # + # var annotator = new ExtendedAnnotator(document.body, /* {options} */); + @extend: extend + # Wraps the children of @element in a @wrapper div. NOTE: This method will also # remove any script elements inside @element to prevent them re-executing. # @@ -775,7 +802,6 @@ Annotator.Widget = Widget Annotator.Viewer = Viewer Annotator.Editor = Editor Annotator.Notification = Notification -Annotator.Extend = require 'backbone-extend-standalone' # Attach notification methods to the Annotation object notification = new Notification From ee943c68141a8e435fed4dfbaf13a6be11970abb Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 25 Feb 2014 16:31:21 -0800 Subject: [PATCH 051/640] Browserify v3 Update various dev dependencies and build process. - Some basic browserify dependent module substitutions such as coffeeify for caching-coffeeify. - Reworked plugin loader into a browserify plugin. This can probably be evolved further to a nice pattern for namespace plugins to browserify from the CLI. Maybe a separate module. The reason it had to be in this change is that watchify with browserify v3 tries errors dealing with the stream input, which makes sense. - Fixes build warnings with watchify --- index.js | 72 +++++++++++++++--------------------- package.json | 18 ++++----- tools/build | 75 +++++++++++++++---------------------- tools/serve | 102 ++++++++++++++++++++++++++++++--------------------- tools/test | 4 -- 5 files changed, 128 insertions(+), 143 deletions(-) diff --git a/index.js b/index.js index 367c0c1ef..2b2f653fe 100644 --- a/index.js +++ b/index.js @@ -1,48 +1,34 @@ +var fs = require('fs'); +var path = require('path'); var through = require('through'); -// Addd a loader script to a browserify instance which exports the annotator -// namespace and then requires any other exposed modules. Use it on a browserify -// instance to create standalone bundles for annotator extensions and plugins -// by exposing the annotator/lib/namespace module as annotator instead of -// declaring annotator as external. -function loader(b) { - var exposed = ['annotator']; - var loader = through(expose); - - loader.pause(); - loader.write("module.exports = require('annotator');\n"); - - b.add(loader); - b.transform(function (file) { - return through(expose); - }); - - // Browserify doesn't honor streams as entry points so we need to force it. - b._entries.push(loader); - - return b; - - function expose(data) { - this.queue(data); - if (this === loader) return; - - var done = true; - for (var m in b._mapped) { - if (exposed.indexOf(m) == -1) { - done = false; - exposed.push(m); - loader.write("require('" + m + "');\n"); - } - } - - if (Object.keys(exposed).length == Object.keys(b._expose).length) { - if (!done) { - loader.resume(); - loader.end(); +/** + * Populate the Annotator namespace by require()'ing any exposed plugins. + * + * Given a browserify bundle transform any module exposed as 'annotator' + * by appending `require()` calls all other exposed modules in the bundle. + * + * @param {object} b - A browserify bundle. + */ +module.exports = function (b) { + if (b[__filename]) return b; + else b[__filename] = true; + + return b + .transform(function (file) { + if (b._mapped['annotator'] == file) { + return through(null, function () { + this.queue('\n'); + for (var m in b._mapped) { + if (m == 'annotator') continue; + this.queue("require('" + m + "');\n"); + } + this.queue(null); + }); + } else { + return through(); } - } - } + }) + ; } - -exports.loader = loader; diff --git a/package.json b/package.json index 15e97b351..5b8c35e14 100644 --- a/package.json +++ b/package.json @@ -12,23 +12,23 @@ "devDependencies": { "backbone-events-standalone": "~0.2.1", "backbone-extend-standalone": "~0.1.2", - "coffee-script": "~1.6.0", - "uglify-js": "~2.3.6", + "coffee-script": "~1.6.3", + "uglify-js": "~2.4.12", "uglifycss": "~0.0.5", - "mocha": "~1.12.0", - "mocha-phantomjs": "~3.3.1", - "chai": "~1.7.0", + "mocha": "~1.12.1", + "mocha-phantomjs": "~3.3.2", + "chai": "~1.7.2", "sinon": "~1.6.0", "jwt-simple": "~0.1.0", "iso8601": "~1.1.1", "connect": "~2.10.1", - "browserify": "~2.35.2", - "caching-coffeeify": "~0.3.0", + "browserify": "~3.30.1", + "coffeeify": "~0.6.0", "convert-source-map": "~0.3.1", "glob": "~3.2.6", "kew": "~0.2.2", - "source-map": "~0.1.31", - "watchify": "~0.3.0" + "source-map": "~0.1.32", + "watchify": "~0.6.1" }, "engines": { "node": ">=0.8 <0.12" diff --git a/tools/build b/tools/build index 99e232579..089a1a9e2 100755 --- a/tools/build +++ b/tools/build @@ -3,18 +3,16 @@ var child_process = require('child_process'); var fs = require('fs'); var path = require('path'); -var browserify = require('browserify'); var coffee = require('coffee-script'); -var coffeeify = require('caching-coffeeify'); +var coffeeify = require('coffeeify'); var convert = require('convert-source-map'); var glob = require('glob'); -var q = require('kew'); var sourceMap = require('source-map'); var through = require('through'); var UglifyJS = require('uglify-js'); var UglifyCSS = require('uglifycss'); -var annotator = require('../'); +var loader = require('../'); var SourceMapConsumer = sourceMap.SourceMapConsumer; var SourceMapGenerator = sourceMap.SourceMapGenerator; @@ -71,36 +69,21 @@ function uglify(src, srcMap) { } -// The core Annotator library -exports.annotator = function (browserify) { - return browserify({extensions: ['.coffee']}) - .require('./src/annotator', {entry: true}) - .transform(coffeeify) - ; +// Convenience browserify factory for coffee +exports.browserify = function (opts) { + if (!opts) opts = {}; + if (Array.isArray(opts)) opts = {entries: opts} + if (typeof(opts) === 'string') opts = {entries: [opts]} + if (!opts.extensions) opts.extensions = []; + opts.extensions.push('.coffee'); + return require('browserify')(opts).transform(coffeeify); }; // The plugin bundles -exports.plugin = function (browserify, p) { - var name = path.basename(p).replace('.coffee', ''); - var expose = ['annotator', 'plugin', name].join('-').replace('.js', ''); - - var b = browserify({extensions: ['.coffee']}) - .require('./src/namespace', {expose: 'annotator'}) - .require(p, {expose: expose}) - .transform(coffeeify); - - return annotator.loader(b); -}; - - -// Test files -exports.test = function (browserify, t) { - return browserify({entries: [t], extensions: ['.coffee']}) - .external('annotator') - .external('helpers') - .transform(coffeeify) - ; +exports.plugin = function (b, p) { + var name = path.basename(p).replace('.coffee', '') + return b.require(p, {expose: ['annotator', 'plugin', name].join('-')}); }; @@ -193,40 +176,40 @@ if (require.main === module) { } function core () { - pack(exports.annotator(browserify), 'pkg/annotator.js', { - debug: true, - standalone: 'Annotator' + pack(exports.browserify('./src/annotator'), 'pkg/annotator.js', { + debug: true + , standalone: 'Annotator' }); } function plugin(name) { var source = './src/plugin/' + name + '.coffee'; var target = 'pkg/annotator.' + path.basename(name) + '.js'; - pack(exports.plugin(browserify, source), target, { - debug: true, - standalone: 'Annotator' + var b = exports.browserify().plugin(loader).require('./src/namespace', { + entry: true + , expose: 'annotator' + }); + pack(exports.plugin(b, source), target, { + debug: true + , standalone: 'Annotator' }); } function full() { - var b = annotator.loader(browserify({extensions: '.coffee'})); - b.require('./src/annotator', {expose: 'annotator'}); - b.transform(coffeeify); - glob.sync('./src/plugin/*.coffee').forEach(function (p) { - var name = path.basename(p).replace('.coffee', ''); - var expose = ['annotator', 'plugin', name].join('-').replace('.js', ''); - b.require(p, {expose: expose}); + var b = exports.browserify().plugin(loader).require('./src/annotator', { + entry: true + , expose: 'annotator' }); + glob.sync('./src/plugin/*.coffee').reduce(exports.plugin, b); pack(b, 'pkg/annotator-full.js', { - debug: true, - standalone: 'Annotator' + debug: true + , standalone: 'Annotator' }); } function pack(b, filename, options) { var deps = ''; var ignore = /(_empty)|(fake_[a-z0-9]+).js$/; - var promise = q.defer(); if (depsOnly) { options.noParse = b._noParse; diff --git a/tools/serve b/tools/serve index 339a3fc6c..c72c6a27c 100755 --- a/tools/serve +++ b/tools/serve @@ -1,55 +1,49 @@ #!/usr/bin/env node var path = require('path'); -var coffeeify = require('caching-coffeeify'); var connect = require('connect'); var convert = require('convert-source-map'); var jwt = require('jwt-simple'); -var iso8601 = require('iso8601'); var glob = require('glob'); var q = require('kew') +var through = require('through'); var watchify = require('watchify') var build = require('./build'); +var loader = require('../'); var CONSUMER_KEY = 'mockconsumer'; var CONSUMER_SECRET = 'mockconsumersecret'; var CONSUMER_TTL = 1800; -function browserify(opts, xopts) { - var b = require('browserify')(opts, xopts); - b.serve = serve.bind(b); +function browserify(opts) { + var b = build.browserify(opts); + b.serve = serve; + b.watch = watch; return b; } // Serve a browserify bundle with connect middleware -function serve(options) { - var promise = null; +function serve(app, location) { var bundle = this.bundle.bind(this); - var self = this; - - // A function which fulfills the promise of building a bundle. - // Based on connect-browserify. - var build = function () { - promise = q.defer(); - self.bundle(options, function (err, result) { - if (err) { - console.error(err); - promise.reject(err); - } else { - promise.resolve(result); - } + var promise = q.defer(); + + this.bundle = function (opts, cb) { + var p = promise; + return bundle(opts, function (err, result) { + if (p == null) return; + if (err) return p.reject(err); + p.resolve(result); }); - } + }; - // Build and rebuild on changes - build() - watchify(this).on('update', build); + this.on('update', function () { + promise = q.defer(); + }); - // Return middleware to serve the bundle - return function (req, res, next) { + app.use(location, function (req, res, next) { res.setHeader('Content-Type', 'application/javascript'); promise.then(function (result) { var src = convert.removeComments(result); @@ -66,11 +60,21 @@ function serve(options) { // Add all the sources and set the root relative to the workspace srcMap.setProperty('sources', sources); - res.write(src.slice(0,-2)); + res.write(src); res.write(srcMap.toComment()); res.end('\n;'); }).fail(next); - } + }); + + return this; +} + + +// Watch a bundle and rebuild changes +function watch(options) { + watchify(this).on('update', this.bundle.bind(this, options)); + this.bundle(options); + return this; } @@ -101,33 +105,49 @@ app.use('/api/token', function(request, response) { // Core -app.use('/lib/annotator.js', build.annotator(browserify).serve({ - debug: true, - standalone: 'Annotator' -})); +browserify('./src/annotator') + .serve(app, '/lib/annotator.js') + .watch({ + debug: true, + standalone: 'Annotator' + }) +; // Plugins glob.sync('./src/plugin/*.coffee').forEach(function (p) { - var b = build.plugin(browserify, p); - var path = p.replace('./src', '/lib').replace('.coffee', '.js'); - app.use(path, b.serve({debug: true, standalone: 'Annotator'})); + var location = '/lib/plugin/' + path.basename(p).replace('.coffee', '.js'); + var b = browserify().plugin(loader).require('./src/namespace', { + entry: true + , expose: 'annotator' + }); + build.plugin(b, p) + .serve(app, location) + .watch({ + debug: true + , standalone: 'Annotator' + }) + ; }); // Tests glob.sync('./test/**/*.coffee').forEach(function (t) { if (t == './test/helpers.coffee') { - var b = browserify({extensions: '.coffee'}) - .require('./test/helpers', {expose: 'helpers'}) + browserify(t) + .require(t, {expose: 'helpers'}) .require('./src/annotator', {expose: 'annotator'}) - .transform(coffeeify) + .serve(app, '/test/helpers.js') + .watch({debug: true}) ; - app.use('/test/helpers.js', b.serve({debug: true})); } else { - var b = build.test(browserify, t).external('annotator'); var path = t.replace('./', '/').replace('.coffee', '.js'); - app.use(path, b.serve({debug: true})); + browserify(t) + .external('annotator') + .external('helpers') + .serve(app, path) + .watch({debug: true}) + ; } }); diff --git a/tools/test b/tools/test index 684a87704..3deed7239 100755 --- a/tools/test +++ b/tools/test @@ -1,9 +1,5 @@ #!/usr/bin/env node var child_process = require('child_process'); -var fs = require('fs'); - -var coffeeify = require('caching-coffeeify'); -var glob = require('glob'); var app = require('./serve'); From 10fcc041e82fd50a02fd13baacdf2f2eee4ed878 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 25 Feb 2014 16:38:59 -0800 Subject: [PATCH 052/640] Log pack failures during build --- tools/build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/build b/tools/build index 089a1a9e2..102ed837a 100755 --- a/tools/build +++ b/tools/build @@ -224,8 +224,8 @@ if (require.main === module) { } } else { b.bundle(options, function (err, result) { - if (err) throw err; - var src = convert.removeComments(result).slice(0, -3); + if (err) throw 'Error building ' + filename + ': ' + err; + var src = convert.removeComments(result); var srcMap = convert.fromSource(result).toObject(); // Make the source file paths relative From 91b814b14a8a20e62ff17ab438ad0f6c6a42947c Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 25 Feb 2014 19:45:27 -0800 Subject: [PATCH 053/640] Support inter-plugin dependencies --- package.json | 1 + src/plugin/annotateitpermissions.coffee | 6 +++++- tools/build | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5b8c35e14..626bf6677 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "devDependencies": { "backbone-events-standalone": "~0.2.1", "backbone-extend-standalone": "~0.1.2", + "browser-resolve": "~1.2.1", "coffee-script": "~1.6.3", "uglify-js": "~2.4.12", "uglifycss": "~0.0.5", diff --git a/src/plugin/annotateitpermissions.coffee b/src/plugin/annotateitpermissions.coffee index 60dab1942..c40a049dd 100644 --- a/src/plugin/annotateitpermissions.coffee +++ b/src/plugin/annotateitpermissions.coffee @@ -1,4 +1,8 @@ Annotator = require('annotator') +Permissions = ( + Annotator.Plugin.Permissions or + require('annotator-plugin-permissions') +) # Public: Plugin for managing user permissions under the rather more specialised @@ -13,7 +17,7 @@ Annotator = require('annotator') # new Annotator.plugin.AnnotateItPermissions(annotator.element) # # Returns a new instance of the AnnotateItPermissions Object. -class Annotator.Plugin.AnnotateItPermissions extends Annotator.Plugin.Permissions +class Annotator.Plugin.AnnotateItPermissions extends Permissions # A Object literal of default options for the class. options: diff --git a/tools/build b/tools/build index 102ed837a..a6bc33d5e 100755 --- a/tools/build +++ b/tools/build @@ -76,6 +76,17 @@ exports.browserify = function (opts) { if (typeof(opts) === 'string') opts = {entries: [opts]} if (!opts.extensions) opts.extensions = []; opts.extensions.push('.coffee'); + opts.resolve = (function (__resolve) { + return function (pkg, opts, cb) { + var m = pkg.match(/^annotator-plugin-(.+)/); + if (m) { + try { + return __resolve('./' + m[1], opts, cb); + } catch (e) {} + } + return __resolve(pkg, opts, cb); + } + })(opts.resolve || require('browser-resolve')); return require('browserify')(opts).transform(coffeeify); }; From efc08f9ecb148b183336951e268b7c4b1e656135 Mon Sep 17 00:00:00 2001 From: Randall Leeds Date: Tue, 25 Feb 2014 19:56:55 -0800 Subject: [PATCH 054/640] enable node v0.10 in travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index d706ac2f9..cc4dba29d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ language: node_js node_js: - "0.8" + - "0.10" From d600191176278dd1f6f53db7c65e6d34176a9b12 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Sun, 2 Mar 2014 12:42:06 +0100 Subject: [PATCH 055/640] Clean up .gitignore Moving subdirectory gitignores in here makes it possible for tools that use gitignore files (like ag[1]) to ignore those directories when searching through the code. [1]: https://github.com/ggreer/the_silver_searcher --- .gitignore | 5 +---- doc/.gitignore | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 doc/.gitignore diff --git a/.gitignore b/.gitignore index c53954d0a..d3c5be96e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,3 @@ -/.redo -*.redo?.tmp .DS_Store -.cake_task_cache -*.sublime-workspace node_modules +doc/_build diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index a485625d4..000000000 --- a/doc/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/_build From 533cf9d5ba236cd19f90ad055d84dfa62a5e3553 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Sun, 2 Mar 2014 13:49:04 +0100 Subject: [PATCH 056/640] preamble: Don't double-dev things. When the version in package.json is X.Y.Z-dev, we were generating version strings like X.Y.Z-dev-dev-abcdef123 --- tools/preamble | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/preamble b/tools/preamble index 42fdc81c9..25126caa2 100755 --- a/tools/preamble +++ b/tools/preamble @@ -7,7 +7,7 @@ YEAR=$(date -u "+%Y") if [ -n "$VERSION" ]; then VERSION="v${VERSION}" else - VERSION="v$(echo "$(grep version ../package.json | cut -d\" -f4)-dev-$(git describe --always)")" + VERSION="v$(echo "$(grep version ../package.json | cut -d\" -f4)-$(git describe --always)")" fi # print preamble, substituting variables From 677d176e0f10e6681e5482ab6d13269aa0883031 Mon Sep 17 00:00:00 2001 From: Nick Stenning Date: Sun, 2 Mar 2014 14:49:33 +0100 Subject: [PATCH 057/640] Clean up stray comment characters --- test/runner.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runner.html b/test/runner.html index bb1f85fa9..76a37637d 100644 --- a/test/runner.html +++ b/test/runner.html @@ -50,7 +50,7 @@ - --> + - - - - - - - - - diff --git a/contrib/bookmarklet/test/spec/bookmarklet.js b/contrib/bookmarklet/test/spec/bookmarklet.js deleted file mode 100644 index 2853c943f..000000000 --- a/contrib/bookmarklet/test/spec/bookmarklet.js +++ /dev/null @@ -1,259 +0,0 @@ -describe("bookmarklet", function () { - var bookmarklet; - - beforeEach(function () { - runBookmarklet(); - - bookmarklet = window._annotator.bookmarklet; - - // Prevent Notifications from being fired. - spyOn(bookmarklet.notification, 'show'); - spyOn(bookmarklet.notification, 'message'); - spyOn(bookmarklet.notification, 'hide'); - spyOn(bookmarklet.notification, 'remove'); - }); - - afterEach(function () { - // Remove all traces of the bookmarklet. - delete window._annotator; - jQuery('.annotator-bm-status, .annotator-notice').remove(); - }); - - describe("init()", function () { - beforeEach(function () { - spyOn(bookmarklet, 'loadjQuery'); - spyOn(bookmarklet, 'load'); - }); - - it("should display a notification telling the user the page is loading", function () { - bookmarklet.init(); - expect(bookmarklet.notification.show).toHaveBeenCalled(); - }); - - it("should call load() if jQuery is on the page", function () { - var jQuery = window.jQuery; - window.jQuery = { - sub: jasmine.createSpy('jQuery.sub()'), - proxy: jasmine.createSpy('jQuery.proxy()') - }; - window.jQuery.sub.andReturn(window.jQuery); - - bookmarklet.init(); - expect(bookmarklet.load).toHaveBeenCalled(); - expect(window.jQuery.sub).toHaveBeenCalled(); - - window.jQuery = jQuery; - }); - - it("should call jQueryLoad() if jQuery is not on the page", function () { - var _$ = window.jQuery.noConflict(true); - - bookmarklet.init(); - expect(bookmarklet.loadjQuery).toHaveBeenCalled(); - - window.jQuery = _$; - }); - - it("should display a notification if the bookmarklet has loaded", function () { - window._annotator.instance = {}; - window._annotator.Annotator = { - showNotification: jasmine.createSpy() - }; - - bookmarklet.init(); - expect(window._annotator.Annotator.showNotification).toHaveBeenCalled(); - }); - }); - - describe("load()", function () { - beforeEach(function () { - spyOn(bookmarklet, 'setup'); - bookmarklet.init(); - }); - - afterEach(function () { - var head = document.getElementsByTagName('head')[0]; - var stylesheets = document.getElementsByTagName('link'); - }); - - it("should append the stylesheet to the head", function () { - var stylesheets = document.getElementsByTagName('link'), - count = stylesheets.length; - - bookmarklet.load(function () {}); - - expect(stylesheets.length).toEqual(count + 1); - }); - - it("should load the annotator script and call the callback", function () { - var callback = jasmine.createSpy('callback'); - bookmarklet.load(callback); - waitsFor(function () { - return callback.wasCalled; - }); - runs(function () { - expect(callback).toHaveBeenCalled(); - }); - }); - }); - - describe("loadjQuery()", function () { - it("should load jQuery into the page and call bookmarklet.load()", function () { - spyOn(bookmarklet, 'load'); - - bookmarklet.loadjQuery(); - waitsFor(function () { - return !!bookmarklet.load.wasCalled; - }); - runs(function () { - expect(bookmarklet.load).toHaveBeenCalled(); - }); - }); - }); - - describe("setup()", function () { - beforeEach(function () { - bookmarklet.setup(); - }); - - afterEach(function () { - window._annotator.jQuery('#fixtures') - .empty() - .removeData('annotator') - .removeData('annotator:headers'); - }); - - it("should export useful values to window._annotator", function () { - expect(window._annotator.Annotator).toBeTruthy(); - expect(window._annotator.instance).toBeTruthy(); - expect(window._annotator.jQuery).toBeTruthy(); - expect(window._annotator.element).toBeTruthy(); - }); - - it("should add the plugins to the annotator instance", function () { - var instance = window._annotator.instance, - plugins = instance.plugins; - - expect(plugins.Store).toBeTruthy(); - expect(plugins.Permissions).toBeTruthy(); - expect(plugins.Unsupported).toBeTruthy(); - expect(instance.element.data('annotator:headers')).toBeTruthy(); - }); - - it ("should add the tags plugin if options.tags is true", function () { - var instance = window._annotator.instance, - plugins = instance.plugins; - - expect(plugins.Tags).toBeTruthy(); - }); - - it("should display a loaded notification", function () { - expect(bookmarklet.notification.message).toHaveBeenCalled(); - }); - }); - - describe("permissionsOptions()", function () { - it("should return an object literal", function () { - expect(typeof bookmarklet.permissionsOptions()).toEqual('object'); - }); - - it("should retrieve user and permissions from config", function () { - spyOn(bookmarklet, 'config'); - spyOn(jQuery, 'extend'); - bookmarklet.permissionsOptions(); - expect(jQuery.extend).toHaveBeenCalled(); - expect(bookmarklet.config).toHaveBeenCalledWith('permissions'); - }); - - it("should have a userId method that returns the user id", function () { - var userId = bookmarklet.permissionsOptions().userId; - - expect(userId({id: 'myId'})).toEqual('myId'); - expect(userId({})).toEqual(''); - expect(userId(null)).toEqual(''); - }); - - it("should have a userString method that returns the username", function () { - var userString = bookmarklet.permissionsOptions().userString; - - expect(userString({name: 'bill'})).toEqual('bill'); - expect(userString({})).toEqual(''); - expect(userString(null)).toEqual(''); - }); - }); - - describe("storeOptions()", function () { - it("should return an object literal", function () { - expect(typeof bookmarklet.storeOptions()).toEqual('object'); - }); - - it("should retrieve store prefix from config", function () { - spyOn(bookmarklet, 'config'); - bookmarklet.storeOptions(); - expect(bookmarklet.config).toHaveBeenCalledWith('store.prefix'); - }); - - it("should have set a uri property", function () { - var uri = bookmarklet.storeOptions().annotationData.uri; - expect(uri).toBeTruthy(); - }); - }); -}); - -describe("bookmarklet.notification", function () { - var notification; - - runBookmarklet(); - notification = window._annotator.bookmarklet.notification; - - it("should have an Element property", function () { - expect(notification.element).toBeTruthy(); - }); - - describe("show", function () { - it("should set the top style of the element", function () { - notification.show(); - expect(notification.element.style.top).toEqual("0px"); - }); - - it("should call notification.message", function () { - spyOn(notification, 'message'); - notification.show('hello', 'red'); - expect(notification.message).toHaveBeenCalledWith('hello', 'red'); - }); - }); - - describe("hide", function () { - it("should set the top style of the element", function () { - notification.hide(); - expect(notification.element.style.top).not.toEqual("0px"); - }); - }); - - describe("message", function () { - it("should set the innerHTML of the element", function () { - notification.message('hello'); - expect(notification.element.innerHTML).toEqual("hello"); - }); - - it("should set the bottomBorderColor of the element", function () { - var current = notification.element.style.borderBottomColor; - notification.message('hello', '#fff'); - expect(notification.element.style.borderBottomColor).not.toEqual(current); - }); - }); - - describe("append", function () { - it("should append the element to the document.body", function () { - notification.append(); - expect(notification.element.parentNode).toEqual(document.body); - }); - }); - - describe("remove", function () { - it("should remove the element from the document.body", function () { - notification.remove(); - expect(notification.element.parentNode).toBeFalsy(); - }); - }); -}); diff --git a/contrib/bookmarklet/test/template.html b/contrib/bookmarklet/test/template.html deleted file mode 100644 index 0b499532d..000000000 --- a/contrib/bookmarklet/test/template.html +++ /dev/null @@ -1,61 +0,0 @@ - - - - Jasmine Test Runner - - - - - - - - - - - - - - - diff --git a/contrib/bookmarklet/src/bookmarklet.js b/src/bootstrap.js similarity index 81% rename from contrib/bookmarklet/src/bookmarklet.js rename to src/bootstrap.js index fc63c4ed7..0b59d2cb6 100644 --- a/contrib/bookmarklet/src/bookmarklet.js +++ b/src/bootstrap.js @@ -1,3 +1,55 @@ +/* +Annotator Bookmarklet +===================== + +A Javascript bookmarklet wrapper around the Annotator plugin. This allows the +user to load the annotator into any web page and post the annotations to a +server (by default this is [AnnotateIt][#annotateit]). + +The bookmarklet version of the annotator has the following plugins loaded: + + - [Auth][#wiki-auth]: authenticates with [AnnotateIt][#annotateit] + - [Store][#wiki-store]: saves to [AnnotateIt][#annotateit] + - [Permissions][#wiki-permissions] + - [Unsupported][#wiki-unsupported]: displays a notification if the bookmarklet is run on an + unsupported browser + +and optionally, the [Tags plugin][#wiki-tags]. + +Configuration +------------- + +In order to configure the bookmarklet for your needs it accepts `config` hash of +options. These are set in the _config.json_ file. There's an example in the +repository (see _config.example.json_). The options are as follows: + +### externals + + - `jQuery`: A URL to a hosted version of jQuery. This will default to the latest + minor version of 1.7 hosted on Google's CDN. + - `source`: The generated Annotator Javascript source code (see Development) + - `styles`: The generated Annotator CSS source code (see Development) + +### auth + +Settings for the [Auth plugin][#wiki-auth] + +- `tokenUrl`: The URL of the auth token generator to use (default: http://annotateit.org/api/token) + +### store + +Settings for the [Store plugin][#wiki-store]. + + - `prefix`: The prefix URL for the store (default: http://annotateit.org/api) + +### permissions + +Settings for the [Permissions plugin][#wiki-permissions]. + +#### tags + +If this is set to `true` the [Tags plugin][#wiki-tags] will be loaded. +*/ (function (options, window, document) { "use strict"; @@ -139,7 +191,7 @@ loadjQuery: function () { var script = document.createElement('script'), - fallback = 'https://ajax.googleapis.com/ajax/libs/jquery/1.7/jquery.js', + fallback = 'https://ajax.googleapis.com/ajax/libs/jquery/1.9/jquery.js', timer; timer = setTimeout(function () { @@ -164,7 +216,7 @@ }, load: function (callback) { - var annotatorSource = this.config('externals.source', 'http://assets.annotateit.org/bookmarklet/annotator.min.js'), + var annotatorSource = this.config('externals.source', 'http://assets.annotateit.org/bookmarklet/annotator-bookmarklet.min.js'), annotatorStyles = this.config('externals.styles', 'http://assets.annotateit.org/bookmarklet/annotator.min.css'); head.appendChild(jQuery('', { @@ -269,9 +321,4 @@ // always check window.jQuery. jQuery = window.jQuery; } -}( - -// Leave __config__ on a line of its own -__config__ - -, this, this.document)); +}(window._annotatorConfig, window, window.document)); diff --git a/test/runner.html b/test/runner.html index 76a37637d..6bad81671 100644 --- a/test/runner.html +++ b/test/runner.html @@ -52,6 +52,8 @@ + + @@ -107,7 +106,7 @@

    Header Level 3

    console.log(tok); }) - }(jQuery)); + }(Annotator.Util.$)); diff --git a/lib/vendor/jquery.js b/lib/vendor/jquery.js deleted file mode 100644 index e2c203fe9..000000000 --- a/lib/vendor/jquery.js +++ /dev/null @@ -1,9597 +0,0 @@ -/*! - * jQuery JavaScript Library v1.9.1 - * http://jquery.com/ - * - * Includes Sizzle.js - * http://sizzlejs.com/ - * - * Copyright 2005, 2012 jQuery Foundation, Inc. and other contributors - * Released under the MIT license - * http://jquery.org/license - * - * Date: 2013-2-4 - */ -(function( window, undefined ) { - -// Can't do this because several apps including ASP.NET trace -// the stack via arguments.caller.callee and Firefox dies if -// you try to trace through "use strict" call chains. (#13335) -// Support: Firefox 18+ -//"use strict"; -var - // The deferred used on DOM ready - readyList, - - // A central reference to the root jQuery(document) - rootjQuery, - - // Support: IE<9 - // For `typeof node.method` instead of `node.method !== undefined` - core_strundefined = typeof undefined, - - // Use the correct document accordingly with window argument (sandbox) - document = window.document, - location = window.location, - - // Map over jQuery in case of overwrite - _jQuery = window.jQuery, - - // Map over the $ in case of overwrite - _$ = window.$, - - // [[Class]] -> type pairs - class2type = {}, - - // List of deleted data cache ids, so we can reuse them - core_deletedIds = [], - - core_version = "1.9.1", - - // Save a reference to some core methods - core_concat = core_deletedIds.concat, - core_push = core_deletedIds.push, - core_slice = core_deletedIds.slice, - core_indexOf = core_deletedIds.indexOf, - core_toString = class2type.toString, - core_hasOwn = class2type.hasOwnProperty, - core_trim = core_version.trim, - - // Define a local copy of jQuery - jQuery = function( selector, context ) { - // The jQuery object is actually just the init constructor 'enhanced' - return new jQuery.fn.init( selector, context, rootjQuery ); - }, - - // Used for matching numbers - core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, - - // Used for splitting on whitespace - core_rnotwhite = /\S+/g, - - // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) - rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, - - // A simple way to check for HTML strings - // Prioritize #id over to avoid XSS via location.hash (#9521) - // Strict HTML recognition (#11290: must start with <) - rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/, - - // Match a standalone tag - rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, - - // JSON RegExp - rvalidchars = /^[\],:{}\s]*$/, - rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, - rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, - rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, - - // Matches dashed string for camelizing - rmsPrefix = /^-ms-/, - rdashAlpha = /-([\da-z])/gi, - - // Used by jQuery.camelCase as callback to replace() - fcamelCase = function( all, letter ) { - return letter.toUpperCase(); - }, - - // The ready event handler - completed = function( event ) { - - // readyState === "complete" is good enough for us to call the dom ready in oldIE - if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { - detach(); - jQuery.ready(); - } - }, - // Clean-up method for dom ready events - detach = function() { - if ( document.addEventListener ) { - document.removeEventListener( "DOMContentLoaded", completed, false ); - window.removeEventListener( "load", completed, false ); - - } else { - document.detachEvent( "onreadystatechange", completed ); - window.detachEvent( "onload", completed ); - } - }; - -jQuery.fn = jQuery.prototype = { - // The current version of jQuery being used - jquery: core_version, - - constructor: jQuery, - init: function( selector, context, rootjQuery ) { - var match, elem; - - // HANDLE: $(""), $(null), $(undefined), $(false) - if ( !selector ) { - return this; - } - - // Handle HTML strings - if ( typeof selector === "string" ) { - if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) { - // Assume that strings that start and end with <> are HTML and skip the regex check - match = [ null, selector, null ]; - - } else { - match = rquickExpr.exec( selector ); - } - - // Match html or make sure no context is specified for #id - if ( match && (match[1] || !context) ) { - - // HANDLE: $(html) -> $(array) - if ( match[1] ) { - context = context instanceof jQuery ? context[0] : context; - - // scripts is true for back-compat - jQuery.merge( this, jQuery.parseHTML( - match[1], - context && context.nodeType ? context.ownerDocument || context : document, - true - ) ); - - // HANDLE: $(html, props) - if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { - for ( match in context ) { - // Properties of context are called as methods if possible - if ( jQuery.isFunction( this[ match ] ) ) { - this[ match ]( context[ match ] ); - - // ...and otherwise set as attributes - } else { - this.attr( match, context[ match ] ); - } - } - } - - return this; - - // HANDLE: $(#id) - } else { - elem = document.getElementById( match[2] ); - - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE and Opera return items - // by name instead of ID - if ( elem.id !== match[2] ) { - return rootjQuery.find( selector ); - } - - // Otherwise, we inject the element directly into the jQuery object - this.length = 1; - this[0] = elem; - } - - this.context = document; - this.selector = selector; - return this; - } - - // HANDLE: $(expr, $(...)) - } else if ( !context || context.jquery ) { - return ( context || rootjQuery ).find( selector ); - - // HANDLE: $(expr, context) - // (which is just equivalent to: $(context).find(expr) - } else { - return this.constructor( context ).find( selector ); - } - - // HANDLE: $(DOMElement) - } else if ( selector.nodeType ) { - this.context = this[0] = selector; - this.length = 1; - return this; - - // HANDLE: $(function) - // Shortcut for document ready - } else if ( jQuery.isFunction( selector ) ) { - return rootjQuery.ready( selector ); - } - - if ( selector.selector !== undefined ) { - this.selector = selector.selector; - this.context = selector.context; - } - - return jQuery.makeArray( selector, this ); - }, - - // Start with an empty selector - selector: "", - - // The default length of a jQuery object is 0 - length: 0, - - // The number of elements contained in the matched element set - size: function() { - return this.length; - }, - - toArray: function() { - return core_slice.call( this ); - }, - - // Get the Nth element in the matched element set OR - // Get the whole matched element set as a clean array - get: function( num ) { - return num == null ? - - // Return a 'clean' array - this.toArray() : - - // Return just the object - ( num < 0 ? this[ this.length + num ] : this[ num ] ); - }, - - // Take an array of elements and push it onto the stack - // (returning the new matched element set) - pushStack: function( elems ) { - - // Build a new jQuery matched element set - var ret = jQuery.merge( this.constructor(), elems ); - - // Add the old object onto the stack (as a reference) - ret.prevObject = this; - ret.context = this.context; - - // Return the newly-formed element set - return ret; - }, - - // Execute a callback for every element in the matched set. - // (You can seed the arguments with an array of args, but this is - // only used internally.) - each: function( callback, args ) { - return jQuery.each( this, callback, args ); - }, - - ready: function( fn ) { - // Add the callback - jQuery.ready.promise().done( fn ); - - return this; - }, - - slice: function() { - return this.pushStack( core_slice.apply( this, arguments ) ); - }, - - first: function() { - return this.eq( 0 ); - }, - - last: function() { - return this.eq( -1 ); - }, - - eq: function( i ) { - var len = this.length, - j = +i + ( i < 0 ? len : 0 ); - return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); - }, - - map: function( callback ) { - return this.pushStack( jQuery.map(this, function( elem, i ) { - return callback.call( elem, i, elem ); - })); - }, - - end: function() { - return this.prevObject || this.constructor(null); - }, - - // For internal use only. - // Behaves like an Array's method, not like a jQuery method. - push: core_push, - sort: [].sort, - splice: [].splice -}; - -// Give the init function the jQuery prototype for later instantiation -jQuery.fn.init.prototype = jQuery.fn; - -jQuery.extend = jQuery.fn.extend = function() { - var src, copyIsArray, copy, name, options, clone, - target = arguments[0] || {}, - i = 1, - length = arguments.length, - deep = false; - - // Handle a deep copy situation - if ( typeof target === "boolean" ) { - deep = target; - target = arguments[1] || {}; - // skip the boolean and the target - i = 2; - } - - // Handle case when target is a string or something (possible in deep copy) - if ( typeof target !== "object" && !jQuery.isFunction(target) ) { - target = {}; - } - - // extend jQuery itself if only one argument is passed - if ( length === i ) { - target = this; - --i; - } - - for ( ; i < length; i++ ) { - // Only deal with non-null/undefined values - if ( (options = arguments[ i ]) != null ) { - // Extend the base object - for ( name in options ) { - src = target[ name ]; - copy = options[ name ]; - - // Prevent never-ending loop - if ( target === copy ) { - continue; - } - - // Recurse if we're merging plain objects or arrays - if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { - if ( copyIsArray ) { - copyIsArray = false; - clone = src && jQuery.isArray(src) ? src : []; - - } else { - clone = src && jQuery.isPlainObject(src) ? src : {}; - } - - // Never move original objects, clone them - target[ name ] = jQuery.extend( deep, clone, copy ); - - // Don't bring in undefined values - } else if ( copy !== undefined ) { - target[ name ] = copy; - } - } - } - } - - // Return the modified object - return target; -}; - -jQuery.extend({ - noConflict: function( deep ) { - if ( window.$ === jQuery ) { - window.$ = _$; - } - - if ( deep && window.jQuery === jQuery ) { - window.jQuery = _jQuery; - } - - return jQuery; - }, - - // Is the DOM ready to be used? Set to true once it occurs. - isReady: false, - - // A counter to track how many items to wait for before - // the ready event fires. See #6781 - readyWait: 1, - - // Hold (or release) the ready event - holdReady: function( hold ) { - if ( hold ) { - jQuery.readyWait++; - } else { - jQuery.ready( true ); - } - }, - - // Handle when the DOM is ready - ready: function( wait ) { - - // Abort if there are pending holds or we're already ready - if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { - return; - } - - // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). - if ( !document.body ) { - return setTimeout( jQuery.ready ); - } - - // Remember that the DOM is ready - jQuery.isReady = true; - - // If a normal DOM Ready event fired, decrement, and wait if need be - if ( wait !== true && --jQuery.readyWait > 0 ) { - return; - } - - // If there are functions bound, to execute - readyList.resolveWith( document, [ jQuery ] ); - - // Trigger any bound ready events - if ( jQuery.fn.trigger ) { - jQuery( document ).trigger("ready").off("ready"); - } - }, - - // See test/unit/core.js for details concerning isFunction. - // Since version 1.3, DOM methods and functions like alert - // aren't supported. They return false on IE (#2968). - isFunction: function( obj ) { - return jQuery.type(obj) === "function"; - }, - - isArray: Array.isArray || function( obj ) { - return jQuery.type(obj) === "array"; - }, - - isWindow: function( obj ) { - return obj != null && obj == obj.window; - }, - - isNumeric: function( obj ) { - return !isNaN( parseFloat(obj) ) && isFinite( obj ); - }, - - type: function( obj ) { - if ( obj == null ) { - return String( obj ); - } - return typeof obj === "object" || typeof obj === "function" ? - class2type[ core_toString.call(obj) ] || "object" : - typeof obj; - }, - - isPlainObject: function( obj ) { - // Must be an Object. - // Because of IE, we also have to check the presence of the constructor property. - // Make sure that DOM nodes and window objects don't pass through, as well - if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { - return false; - } - - try { - // Not own constructor property must be Object - if ( obj.constructor && - !core_hasOwn.call(obj, "constructor") && - !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { - return false; - } - } catch ( e ) { - // IE8,9 Will throw exceptions on certain host objects #9897 - return false; - } - - // Own properties are enumerated firstly, so to speed up, - // if last one is own, then all properties are own. - - var key; - for ( key in obj ) {} - - return key === undefined || core_hasOwn.call( obj, key ); - }, - - isEmptyObject: function( obj ) { - var name; - for ( name in obj ) { - return false; - } - return true; - }, - - error: function( msg ) { - throw new Error( msg ); - }, - - // data: string of html - // context (optional): If specified, the fragment will be created in this context, defaults to document - // keepScripts (optional): If true, will include scripts passed in the html string - parseHTML: function( data, context, keepScripts ) { - if ( !data || typeof data !== "string" ) { - return null; - } - if ( typeof context === "boolean" ) { - keepScripts = context; - context = false; - } - context = context || document; - - var parsed = rsingleTag.exec( data ), - scripts = !keepScripts && []; - - // Single tag - if ( parsed ) { - return [ context.createElement( parsed[1] ) ]; - } - - parsed = jQuery.buildFragment( [ data ], context, scripts ); - if ( scripts ) { - jQuery( scripts ).remove(); - } - return jQuery.merge( [], parsed.childNodes ); - }, - - parseJSON: function( data ) { - // Attempt to parse using the native JSON parser first - if ( window.JSON && window.JSON.parse ) { - return window.JSON.parse( data ); - } - - if ( data === null ) { - return data; - } - - if ( typeof data === "string" ) { - - // Make sure leading/trailing whitespace is removed (IE can't handle it) - data = jQuery.trim( data ); - - if ( data ) { - // Make sure the incoming data is actual JSON - // Logic borrowed from http://json.org/json2.js - if ( rvalidchars.test( data.replace( rvalidescape, "@" ) - .replace( rvalidtokens, "]" ) - .replace( rvalidbraces, "")) ) { - - return ( new Function( "return " + data ) )(); - } - } - } - - jQuery.error( "Invalid JSON: " + data ); - }, - - // Cross-browser xml parsing - parseXML: function( data ) { - var xml, tmp; - if ( !data || typeof data !== "string" ) { - return null; - } - try { - if ( window.DOMParser ) { // Standard - tmp = new DOMParser(); - xml = tmp.parseFromString( data , "text/xml" ); - } else { // IE - xml = new ActiveXObject( "Microsoft.XMLDOM" ); - xml.async = "false"; - xml.loadXML( data ); - } - } catch( e ) { - xml = undefined; - } - if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { - jQuery.error( "Invalid XML: " + data ); - } - return xml; - }, - - noop: function() {}, - - // Evaluates a script in a global context - // Workarounds based on findings by Jim Driscoll - // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context - globalEval: function( data ) { - if ( data && jQuery.trim( data ) ) { - // We use execScript on Internet Explorer - // We use an anonymous function so that context is window - // rather than jQuery in Firefox - ( window.execScript || function( data ) { - window[ "eval" ].call( window, data ); - } )( data ); - } - }, - - // Convert dashed to camelCase; used by the css and data modules - // Microsoft forgot to hump their vendor prefix (#9572) - camelCase: function( string ) { - return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); - }, - - nodeName: function( elem, name ) { - return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); - }, - - // args is for internal usage only - each: function( obj, callback, args ) { - var value, - i = 0, - length = obj.length, - isArray = isArraylike( obj ); - - if ( args ) { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.apply( obj[ i ], args ); - - if ( value === false ) { - break; - } - } - } - - // A special, fast, case for the most common use of each - } else { - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } else { - for ( i in obj ) { - value = callback.call( obj[ i ], i, obj[ i ] ); - - if ( value === false ) { - break; - } - } - } - } - - return obj; - }, - - // Use native String.trim function wherever possible - trim: core_trim && !core_trim.call("\uFEFF\xA0") ? - function( text ) { - return text == null ? - "" : - core_trim.call( text ); - } : - - // Otherwise use our own trimming functionality - function( text ) { - return text == null ? - "" : - ( text + "" ).replace( rtrim, "" ); - }, - - // results is for internal usage only - makeArray: function( arr, results ) { - var ret = results || []; - - if ( arr != null ) { - if ( isArraylike( Object(arr) ) ) { - jQuery.merge( ret, - typeof arr === "string" ? - [ arr ] : arr - ); - } else { - core_push.call( ret, arr ); - } - } - - return ret; - }, - - inArray: function( elem, arr, i ) { - var len; - - if ( arr ) { - if ( core_indexOf ) { - return core_indexOf.call( arr, elem, i ); - } - - len = arr.length; - i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; - - for ( ; i < len; i++ ) { - // Skip accessing in sparse arrays - if ( i in arr && arr[ i ] === elem ) { - return i; - } - } - } - - return -1; - }, - - merge: function( first, second ) { - var l = second.length, - i = first.length, - j = 0; - - if ( typeof l === "number" ) { - for ( ; j < l; j++ ) { - first[ i++ ] = second[ j ]; - } - } else { - while ( second[j] !== undefined ) { - first[ i++ ] = second[ j++ ]; - } - } - - first.length = i; - - return first; - }, - - grep: function( elems, callback, inv ) { - var retVal, - ret = [], - i = 0, - length = elems.length; - inv = !!inv; - - // Go through the array, only saving the items - // that pass the validator function - for ( ; i < length; i++ ) { - retVal = !!callback( elems[ i ], i ); - if ( inv !== retVal ) { - ret.push( elems[ i ] ); - } - } - - return ret; - }, - - // arg is for internal usage only - map: function( elems, callback, arg ) { - var value, - i = 0, - length = elems.length, - isArray = isArraylike( elems ), - ret = []; - - // Go through the array, translating each of the items to their - if ( isArray ) { - for ( ; i < length; i++ ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - - // Go through every key on the object, - } else { - for ( i in elems ) { - value = callback( elems[ i ], i, arg ); - - if ( value != null ) { - ret[ ret.length ] = value; - } - } - } - - // Flatten any nested arrays - return core_concat.apply( [], ret ); - }, - - // A global GUID counter for objects - guid: 1, - - // Bind a function to a context, optionally partially applying any - // arguments. - proxy: function( fn, context ) { - var args, proxy, tmp; - - if ( typeof context === "string" ) { - tmp = fn[ context ]; - context = fn; - fn = tmp; - } - - // Quick check to determine if target is callable, in the spec - // this throws a TypeError, but we will just return undefined. - if ( !jQuery.isFunction( fn ) ) { - return undefined; - } - - // Simulated bind - args = core_slice.call( arguments, 2 ); - proxy = function() { - return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); - }; - - // Set the guid of unique handler to the same of original handler, so it can be removed - proxy.guid = fn.guid = fn.guid || jQuery.guid++; - - return proxy; - }, - - // Multifunctional method to get and set values of a collection - // The value/s can optionally be executed if it's a function - access: function( elems, fn, key, value, chainable, emptyGet, raw ) { - var i = 0, - length = elems.length, - bulk = key == null; - - // Sets many values - if ( jQuery.type( key ) === "object" ) { - chainable = true; - for ( i in key ) { - jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); - } - - // Sets one value - } else if ( value !== undefined ) { - chainable = true; - - if ( !jQuery.isFunction( value ) ) { - raw = true; - } - - if ( bulk ) { - // Bulk operations run against the entire set - if ( raw ) { - fn.call( elems, value ); - fn = null; - - // ...except when executing function values - } else { - bulk = fn; - fn = function( elem, key, value ) { - return bulk.call( jQuery( elem ), value ); - }; - } - } - - if ( fn ) { - for ( ; i < length; i++ ) { - fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); - } - } - } - - return chainable ? - elems : - - // Gets - bulk ? - fn.call( elems ) : - length ? fn( elems[0], key ) : emptyGet; - }, - - now: function() { - return ( new Date() ).getTime(); - } -}); - -jQuery.ready.promise = function( obj ) { - if ( !readyList ) { - - readyList = jQuery.Deferred(); - - // Catch cases where $(document).ready() is called after the browser event has already occurred. - // we once tried to use readyState "interactive" here, but it caused issues like the one - // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 - if ( document.readyState === "complete" ) { - // Handle it asynchronously to allow scripts the opportunity to delay ready - setTimeout( jQuery.ready ); - - // Standards-based browsers support DOMContentLoaded - } else if ( document.addEventListener ) { - // Use the handy event callback - document.addEventListener( "DOMContentLoaded", completed, false ); - - // A fallback to window.onload, that will always work - window.addEventListener( "load", completed, false ); - - // If IE event model is used - } else { - // Ensure firing before onload, maybe late but safe also for iframes - document.attachEvent( "onreadystatechange", completed ); - - // A fallback to window.onload, that will always work - window.attachEvent( "onload", completed ); - - // If IE and not a frame - // continually check to see if the document is ready - var top = false; - - try { - top = window.frameElement == null && document.documentElement; - } catch(e) {} - - if ( top && top.doScroll ) { - (function doScrollCheck() { - if ( !jQuery.isReady ) { - - try { - // Use the trick by Diego Perini - // http://javascript.nwbox.com/IEContentLoaded/ - top.doScroll("left"); - } catch(e) { - return setTimeout( doScrollCheck, 50 ); - } - - // detach all dom ready events - detach(); - - // and execute any waiting functions - jQuery.ready(); - } - })(); - } - } - } - return readyList.promise( obj ); -}; - -// Populate the class2type map -jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { - class2type[ "[object " + name + "]" ] = name.toLowerCase(); -}); - -function isArraylike( obj ) { - var length = obj.length, - type = jQuery.type( obj ); - - if ( jQuery.isWindow( obj ) ) { - return false; - } - - if ( obj.nodeType === 1 && length ) { - return true; - } - - return type === "array" || type !== "function" && - ( length === 0 || - typeof length === "number" && length > 0 && ( length - 1 ) in obj ); -} - -// All jQuery objects should point back to these -rootjQuery = jQuery(document); -// String to Object options format cache -var optionsCache = {}; - -// Convert String-formatted options into Object-formatted ones and store in cache -function createOptions( options ) { - var object = optionsCache[ options ] = {}; - jQuery.each( options.match( core_rnotwhite ) || [], function( _, flag ) { - object[ flag ] = true; - }); - return object; -} - -/* - * Create a callback list using the following parameters: - * - * options: an optional list of space-separated options that will change how - * the callback list behaves or a more traditional option object - * - * By default a callback list will act like an event callback list and can be - * "fired" multiple times. - * - * Possible options: - * - * once: will ensure the callback list can only be fired once (like a Deferred) - * - * memory: will keep track of previous values and will call any callback added - * after the list has been fired right away with the latest "memorized" - * values (like a Deferred) - * - * unique: will ensure a callback can only be added once (no duplicate in the list) - * - * stopOnFalse: interrupt callings when a callback returns false - * - */ -jQuery.Callbacks = function( options ) { - - // Convert options from String-formatted to Object-formatted if needed - // (we check in cache first) - options = typeof options === "string" ? - ( optionsCache[ options ] || createOptions( options ) ) : - jQuery.extend( {}, options ); - - var // Flag to know if list is currently firing - firing, - // Last fire value (for non-forgettable lists) - memory, - // Flag to know if list was already fired - fired, - // End of the loop when firing - firingLength, - // Index of currently firing callback (modified by remove if needed) - firingIndex, - // First callback to fire (used internally by add and fireWith) - firingStart, - // Actual callback list - list = [], - // Stack of fire calls for repeatable lists - stack = !options.once && [], - // Fire callbacks - fire = function( data ) { - memory = options.memory && data; - fired = true; - firingIndex = firingStart || 0; - firingStart = 0; - firingLength = list.length; - firing = true; - for ( ; list && firingIndex < firingLength; firingIndex++ ) { - if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { - memory = false; // To prevent further calls using add - break; - } - } - firing = false; - if ( list ) { - if ( stack ) { - if ( stack.length ) { - fire( stack.shift() ); - } - } else if ( memory ) { - list = []; - } else { - self.disable(); - } - } - }, - // Actual Callbacks object - self = { - // Add a callback or a collection of callbacks to the list - add: function() { - if ( list ) { - // First, we save the current length - var start = list.length; - (function add( args ) { - jQuery.each( args, function( _, arg ) { - var type = jQuery.type( arg ); - if ( type === "function" ) { - if ( !options.unique || !self.has( arg ) ) { - list.push( arg ); - } - } else if ( arg && arg.length && type !== "string" ) { - // Inspect recursively - add( arg ); - } - }); - })( arguments ); - // Do we need to add the callbacks to the - // current firing batch? - if ( firing ) { - firingLength = list.length; - // With memory, if we're not firing then - // we should call right away - } else if ( memory ) { - firingStart = start; - fire( memory ); - } - } - return this; - }, - // Remove a callback from the list - remove: function() { - if ( list ) { - jQuery.each( arguments, function( _, arg ) { - var index; - while( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { - list.splice( index, 1 ); - // Handle firing indexes - if ( firing ) { - if ( index <= firingLength ) { - firingLength--; - } - if ( index <= firingIndex ) { - firingIndex--; - } - } - } - }); - } - return this; - }, - // Check if a given callback is in the list. - // If no argument is given, return whether or not list has callbacks attached. - has: function( fn ) { - return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); - }, - // Remove all callbacks from the list - empty: function() { - list = []; - return this; - }, - // Have the list do nothing anymore - disable: function() { - list = stack = memory = undefined; - return this; - }, - // Is it disabled? - disabled: function() { - return !list; - }, - // Lock the list in its current state - lock: function() { - stack = undefined; - if ( !memory ) { - self.disable(); - } - return this; - }, - // Is it locked? - locked: function() { - return !stack; - }, - // Call all callbacks with the given context and arguments - fireWith: function( context, args ) { - args = args || []; - args = [ context, args.slice ? args.slice() : args ]; - if ( list && ( !fired || stack ) ) { - if ( firing ) { - stack.push( args ); - } else { - fire( args ); - } - } - return this; - }, - // Call all the callbacks with the given arguments - fire: function() { - self.fireWith( this, arguments ); - return this; - }, - // To know if the callbacks have already been called at least once - fired: function() { - return !!fired; - } - }; - - return self; -}; -jQuery.extend({ - - Deferred: function( func ) { - var tuples = [ - // action, add listener, listener list, final state - [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], - [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], - [ "notify", "progress", jQuery.Callbacks("memory") ] - ], - state = "pending", - promise = { - state: function() { - return state; - }, - always: function() { - deferred.done( arguments ).fail( arguments ); - return this; - }, - then: function( /* fnDone, fnFail, fnProgress */ ) { - var fns = arguments; - return jQuery.Deferred(function( newDefer ) { - jQuery.each( tuples, function( i, tuple ) { - var action = tuple[ 0 ], - fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; - // deferred[ done | fail | progress ] for forwarding actions to newDefer - deferred[ tuple[1] ](function() { - var returned = fn && fn.apply( this, arguments ); - if ( returned && jQuery.isFunction( returned.promise ) ) { - returned.promise() - .done( newDefer.resolve ) - .fail( newDefer.reject ) - .progress( newDefer.notify ); - } else { - newDefer[ action + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); - } - }); - }); - fns = null; - }).promise(); - }, - // Get a promise for this deferred - // If obj is provided, the promise aspect is added to the object - promise: function( obj ) { - return obj != null ? jQuery.extend( obj, promise ) : promise; - } - }, - deferred = {}; - - // Keep pipe for back-compat - promise.pipe = promise.then; - - // Add list-specific methods - jQuery.each( tuples, function( i, tuple ) { - var list = tuple[ 2 ], - stateString = tuple[ 3 ]; - - // promise[ done | fail | progress ] = list.add - promise[ tuple[1] ] = list.add; - - // Handle state - if ( stateString ) { - list.add(function() { - // state = [ resolved | rejected ] - state = stateString; - - // [ reject_list | resolve_list ].disable; progress_list.lock - }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); - } - - // deferred[ resolve | reject | notify ] - deferred[ tuple[0] ] = function() { - deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); - return this; - }; - deferred[ tuple[0] + "With" ] = list.fireWith; - }); - - // Make the deferred a promise - promise.promise( deferred ); - - // Call given func if any - if ( func ) { - func.call( deferred, deferred ); - } - - // All done! - return deferred; - }, - - // Deferred helper - when: function( subordinate /* , ..., subordinateN */ ) { - var i = 0, - resolveValues = core_slice.call( arguments ), - length = resolveValues.length, - - // the count of uncompleted subordinates - remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, - - // the master Deferred. If resolveValues consist of only a single Deferred, just use that. - deferred = remaining === 1 ? subordinate : jQuery.Deferred(), - - // Update function for both resolve and progress values - updateFunc = function( i, contexts, values ) { - return function( value ) { - contexts[ i ] = this; - values[ i ] = arguments.length > 1 ? core_slice.call( arguments ) : value; - if( values === progressValues ) { - deferred.notifyWith( contexts, values ); - } else if ( !( --remaining ) ) { - deferred.resolveWith( contexts, values ); - } - }; - }, - - progressValues, progressContexts, resolveContexts; - - // add listeners to Deferred subordinates; treat others as resolved - if ( length > 1 ) { - progressValues = new Array( length ); - progressContexts = new Array( length ); - resolveContexts = new Array( length ); - for ( ; i < length; i++ ) { - if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { - resolveValues[ i ].promise() - .done( updateFunc( i, resolveContexts, resolveValues ) ) - .fail( deferred.reject ) - .progress( updateFunc( i, progressContexts, progressValues ) ); - } else { - --remaining; - } - } - } - - // if we're not waiting on anything, resolve the master - if ( !remaining ) { - deferred.resolveWith( resolveContexts, resolveValues ); - } - - return deferred.promise(); - } -}); -jQuery.support = (function() { - - var support, all, a, - input, select, fragment, - opt, eventName, isSupported, i, - div = document.createElement("div"); - - // Setup - div.setAttribute( "className", "t" ); - div.innerHTML = "
    a"; - - // Support tests won't run in some limited or non-browser environments - all = div.getElementsByTagName("*"); - a = div.getElementsByTagName("a")[ 0 ]; - if ( !all || !a || !all.length ) { - return {}; - } - - // First batch of tests - select = document.createElement("select"); - opt = select.appendChild( document.createElement("option") ); - input = div.getElementsByTagName("input")[ 0 ]; - - a.style.cssText = "top:1px;float:left;opacity:.5"; - support = { - // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) - getSetAttribute: div.className !== "t", - - // IE strips leading whitespace when .innerHTML is used - leadingWhitespace: div.firstChild.nodeType === 3, - - // Make sure that tbody elements aren't automatically inserted - // IE will insert them into empty tables - tbody: !div.getElementsByTagName("tbody").length, - - // Make sure that link elements get serialized correctly by innerHTML - // This requires a wrapper element in IE - htmlSerialize: !!div.getElementsByTagName("link").length, - - // Get the style information from getAttribute - // (IE uses .cssText instead) - style: /top/.test( a.getAttribute("style") ), - - // Make sure that URLs aren't manipulated - // (IE normalizes it by default) - hrefNormalized: a.getAttribute("href") === "/a", - - // Make sure that element opacity exists - // (IE uses filter instead) - // Use a regex to work around a WebKit issue. See #5145 - opacity: /^0.5/.test( a.style.opacity ), - - // Verify style float existence - // (IE uses styleFloat instead of cssFloat) - cssFloat: !!a.style.cssFloat, - - // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) - checkOn: !!input.value, - - // Make sure that a selected-by-default option has a working selected property. - // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) - optSelected: opt.selected, - - // Tests for enctype support on a form (#6743) - enctype: !!document.createElement("form").enctype, - - // Makes sure cloning an html5 element does not cause problems - // Where outerHTML is undefined, this still works - html5Clone: document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>", - - // jQuery.support.boxModel DEPRECATED in 1.8 since we don't support Quirks Mode - boxModel: document.compatMode === "CSS1Compat", - - // Will be defined later - deleteExpando: true, - noCloneEvent: true, - inlineBlockNeedsLayout: false, - shrinkWrapBlocks: false, - reliableMarginRight: true, - boxSizingReliable: true, - pixelPosition: false - }; - - // Make sure checked status is properly cloned - input.checked = true; - support.noCloneChecked = input.cloneNode( true ).checked; - - // Make sure that the options inside disabled selects aren't marked as disabled - // (WebKit marks them as disabled) - select.disabled = true; - support.optDisabled = !opt.disabled; - - // Support: IE<9 - try { - delete div.test; - } catch( e ) { - support.deleteExpando = false; - } - - // Check if we can trust getAttribute("value") - input = document.createElement("input"); - input.setAttribute( "value", "" ); - support.input = input.getAttribute( "value" ) === ""; - - // Check if an input maintains its value after becoming a radio - input.value = "t"; - input.setAttribute( "type", "radio" ); - support.radioValue = input.value === "t"; - - // #11217 - WebKit loses check when the name is after the checked attribute - input.setAttribute( "checked", "t" ); - input.setAttribute( "name", "t" ); - - fragment = document.createDocumentFragment(); - fragment.appendChild( input ); - - // Check if a disconnected checkbox will retain its checked - // value of true after appended to the DOM (IE6/7) - support.appendChecked = input.checked; - - // WebKit doesn't clone checked state correctly in fragments - support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Opera does not clone events (and typeof div.attachEvent === undefined). - // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() - if ( div.attachEvent ) { - div.attachEvent( "onclick", function() { - support.noCloneEvent = false; - }); - - div.cloneNode( true ).click(); - } - - // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP), test/csp.php - for ( i in { submit: true, change: true, focusin: true }) { - div.setAttribute( eventName = "on" + i, "t" ); - - support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; - } - - div.style.backgroundClip = "content-box"; - div.cloneNode( true ).style.backgroundClip = ""; - support.clearCloneStyle = div.style.backgroundClip === "content-box"; - - // Run tests that need a body at doc ready - jQuery(function() { - var container, marginDiv, tds, - divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", - body = document.getElementsByTagName("body")[0]; - - if ( !body ) { - // Return for frameset docs that don't have a body - return; - } - - container = document.createElement("div"); - container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; - - body.appendChild( container ).appendChild( div ); - - // Support: IE8 - // Check if table cells still have offsetWidth/Height when they are set - // to display:none and there are still other visible table cells in a - // table row; if so, offsetWidth/Height are not reliable for use when - // determining if an element has been hidden directly using - // display:none (it is still safe to use offsets if a parent element is - // hidden; don safety goggles and see bug #4512 for more information). - div.innerHTML = "
    t
    "; - tds = div.getElementsByTagName("td"); - tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; - isSupported = ( tds[ 0 ].offsetHeight === 0 ); - - tds[ 0 ].style.display = ""; - tds[ 1 ].style.display = "none"; - - // Support: IE8 - // Check if empty table cells still have offsetWidth/Height - support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); - - // Check box-sizing and margin behavior - div.innerHTML = ""; - div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; - support.boxSizing = ( div.offsetWidth === 4 ); - support.doesNotIncludeMarginInBodyOffset = ( body.offsetTop !== 1 ); - - // Use window.getComputedStyle because jsdom on node.js will break without it. - if ( window.getComputedStyle ) { - support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; - support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; - - // Check if div with explicit width and no margin-right incorrectly - // gets computed margin-right based on width of container. (#3333) - // Fails in WebKit before Feb 2011 nightlies - // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right - marginDiv = div.appendChild( document.createElement("div") ); - marginDiv.style.cssText = div.style.cssText = divReset; - marginDiv.style.marginRight = marginDiv.style.width = "0"; - div.style.width = "1px"; - - support.reliableMarginRight = - !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); - } - - if ( typeof div.style.zoom !== core_strundefined ) { - // Support: IE<8 - // Check if natively block-level elements act like inline-block - // elements when setting their display to 'inline' and giving - // them layout - div.innerHTML = ""; - div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; - support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); - - // Support: IE6 - // Check if elements with layout shrink-wrap their children - div.style.display = "block"; - div.innerHTML = "
    "; - div.firstChild.style.width = "5px"; - support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); - - if ( support.inlineBlockNeedsLayout ) { - // Prevent IE 6 from affecting layout for positioned elements #11048 - // Prevent IE from shrinking the body in IE 7 mode #12869 - // Support: IE<8 - body.style.zoom = 1; - } - } - - body.removeChild( container ); - - // Null elements to avoid leaks in IE - container = div = tds = marginDiv = null; - }); - - // Null elements to avoid leaks in IE - all = select = fragment = opt = a = input = null; - - return support; -})(); - -var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, - rmultiDash = /([A-Z])/g; - -function internalData( elem, name, data, pvt /* Internal Use Only */ ){ - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var thisCache, ret, - internalKey = jQuery.expando, - getByName = typeof name === "string", - - // We have to handle DOM nodes and JS objects differently because IE6-7 - // can't GC object references properly across the DOM-JS boundary - isNode = elem.nodeType, - - // Only DOM nodes need the global jQuery cache; JS object data is - // attached directly to the object so GC can occur automatically - cache = isNode ? jQuery.cache : elem, - - // Only defining an ID for JS objects if its cache already exists allows - // the code to shortcut on the same path as a DOM node with no cache - id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; - - // Avoid doing any more work than we need to when trying to get data on an - // object that has no data at all - if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && getByName && data === undefined ) { - return; - } - - if ( !id ) { - // Only DOM nodes need a new unique ID for each element since their data - // ends up in the global cache - if ( isNode ) { - elem[ internalKey ] = id = core_deletedIds.pop() || jQuery.guid++; - } else { - id = internalKey; - } - } - - if ( !cache[ id ] ) { - cache[ id ] = {}; - - // Avoids exposing jQuery metadata on plain JS objects when the object - // is serialized using JSON.stringify - if ( !isNode ) { - cache[ id ].toJSON = jQuery.noop; - } - } - - // An object can be passed to jQuery.data instead of a key/value pair; this gets - // shallow copied over onto the existing cache - if ( typeof name === "object" || typeof name === "function" ) { - if ( pvt ) { - cache[ id ] = jQuery.extend( cache[ id ], name ); - } else { - cache[ id ].data = jQuery.extend( cache[ id ].data, name ); - } - } - - thisCache = cache[ id ]; - - // jQuery data() is stored in a separate object inside the object's internal data - // cache in order to avoid key collisions between internal data and user-defined - // data. - if ( !pvt ) { - if ( !thisCache.data ) { - thisCache.data = {}; - } - - thisCache = thisCache.data; - } - - if ( data !== undefined ) { - thisCache[ jQuery.camelCase( name ) ] = data; - } - - // Check for both converted-to-camel and non-converted data property names - // If a data property was specified - if ( getByName ) { - - // First Try to find as-is property data - ret = thisCache[ name ]; - - // Test for null|undefined property data - if ( ret == null ) { - - // Try to find the camelCased property - ret = thisCache[ jQuery.camelCase( name ) ]; - } - } else { - ret = thisCache; - } - - return ret; -} - -function internalRemoveData( elem, name, pvt ) { - if ( !jQuery.acceptData( elem ) ) { - return; - } - - var i, l, thisCache, - isNode = elem.nodeType, - - // See jQuery.data for more information - cache = isNode ? jQuery.cache : elem, - id = isNode ? elem[ jQuery.expando ] : jQuery.expando; - - // If there is already no cache entry for this object, there is no - // purpose in continuing - if ( !cache[ id ] ) { - return; - } - - if ( name ) { - - thisCache = pvt ? cache[ id ] : cache[ id ].data; - - if ( thisCache ) { - - // Support array or space separated string names for data keys - if ( !jQuery.isArray( name ) ) { - - // try the string as a key before any manipulation - if ( name in thisCache ) { - name = [ name ]; - } else { - - // split the camel cased version by spaces unless a key with the spaces exists - name = jQuery.camelCase( name ); - if ( name in thisCache ) { - name = [ name ]; - } else { - name = name.split(" "); - } - } - } else { - // If "name" is an array of keys... - // When data is initially created, via ("key", "val") signature, - // keys will be converted to camelCase. - // Since there is no way to tell _how_ a key was added, remove - // both plain key and camelCase key. #12786 - // This will only penalize the array argument path. - name = name.concat( jQuery.map( name, jQuery.camelCase ) ); - } - - for ( i = 0, l = name.length; i < l; i++ ) { - delete thisCache[ name[i] ]; - } - - // If there is no data left in the cache, we want to continue - // and let the cache object itself get destroyed - if ( !( pvt ? isEmptyDataObject : jQuery.isEmptyObject )( thisCache ) ) { - return; - } - } - } - - // See jQuery.data for more information - if ( !pvt ) { - delete cache[ id ].data; - - // Don't destroy the parent cache unless the internal data object - // had been the only thing left in it - if ( !isEmptyDataObject( cache[ id ] ) ) { - return; - } - } - - // Destroy the cache - if ( isNode ) { - jQuery.cleanData( [ elem ], true ); - - // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) - } else if ( jQuery.support.deleteExpando || cache != cache.window ) { - delete cache[ id ]; - - // When all else fails, null - } else { - cache[ id ] = null; - } -} - -jQuery.extend({ - cache: {}, - - // Unique for each copy of jQuery on the page - // Non-digits removed to match rinlinejQuery - expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), - - // The following elements throw uncatchable exceptions if you - // attempt to add expando properties to them. - noData: { - "embed": true, - // Ban all objects except for Flash (which handle expandos) - "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", - "applet": true - }, - - hasData: function( elem ) { - elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; - return !!elem && !isEmptyDataObject( elem ); - }, - - data: function( elem, name, data ) { - return internalData( elem, name, data ); - }, - - removeData: function( elem, name ) { - return internalRemoveData( elem, name ); - }, - - // For internal use only. - _data: function( elem, name, data ) { - return internalData( elem, name, data, true ); - }, - - _removeData: function( elem, name ) { - return internalRemoveData( elem, name, true ); - }, - - // A method for determining if a DOM node can handle the data expando - acceptData: function( elem ) { - // Do not set data on non-element because it will not be cleared (#8335). - if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { - return false; - } - - var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; - - // nodes accept data unless otherwise specified; rejection can be conditional - return !noData || noData !== true && elem.getAttribute("classid") === noData; - } -}); - -jQuery.fn.extend({ - data: function( key, value ) { - var attrs, name, - elem = this[0], - i = 0, - data = null; - - // Gets all values - if ( key === undefined ) { - if ( this.length ) { - data = jQuery.data( elem ); - - if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { - attrs = elem.attributes; - for ( ; i < attrs.length; i++ ) { - name = attrs[i].name; - - if ( !name.indexOf( "data-" ) ) { - name = jQuery.camelCase( name.slice(5) ); - - dataAttr( elem, name, data[ name ] ); - } - } - jQuery._data( elem, "parsedAttrs", true ); - } - } - - return data; - } - - // Sets multiple values - if ( typeof key === "object" ) { - return this.each(function() { - jQuery.data( this, key ); - }); - } - - return jQuery.access( this, function( value ) { - - if ( value === undefined ) { - // Try to fetch any internally stored data first - return elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; - } - - this.each(function() { - jQuery.data( this, key, value ); - }); - }, null, value, arguments.length > 1, null, true ); - }, - - removeData: function( key ) { - return this.each(function() { - jQuery.removeData( this, key ); - }); - } -}); - -function dataAttr( elem, key, data ) { - // If nothing was found internally, try to fetch any - // data from the HTML5 data-* attribute - if ( data === undefined && elem.nodeType === 1 ) { - - var name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); - - data = elem.getAttribute( name ); - - if ( typeof data === "string" ) { - try { - data = data === "true" ? true : - data === "false" ? false : - data === "null" ? null : - // Only convert to a number if it doesn't change the string - +data + "" === data ? +data : - rbrace.test( data ) ? jQuery.parseJSON( data ) : - data; - } catch( e ) {} - - // Make sure we set the data so it isn't changed later - jQuery.data( elem, key, data ); - - } else { - data = undefined; - } - } - - return data; -} - -// checks a cache object for emptiness -function isEmptyDataObject( obj ) { - var name; - for ( name in obj ) { - - // if the public data object is empty, the private is still empty - if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { - continue; - } - if ( name !== "toJSON" ) { - return false; - } - } - - return true; -} -jQuery.extend({ - queue: function( elem, type, data ) { - var queue; - - if ( elem ) { - type = ( type || "fx" ) + "queue"; - queue = jQuery._data( elem, type ); - - // Speed up dequeue by getting out quickly if this is just a lookup - if ( data ) { - if ( !queue || jQuery.isArray(data) ) { - queue = jQuery._data( elem, type, jQuery.makeArray(data) ); - } else { - queue.push( data ); - } - } - return queue || []; - } - }, - - dequeue: function( elem, type ) { - type = type || "fx"; - - var queue = jQuery.queue( elem, type ), - startLength = queue.length, - fn = queue.shift(), - hooks = jQuery._queueHooks( elem, type ), - next = function() { - jQuery.dequeue( elem, type ); - }; - - // If the fx queue is dequeued, always remove the progress sentinel - if ( fn === "inprogress" ) { - fn = queue.shift(); - startLength--; - } - - hooks.cur = fn; - if ( fn ) { - - // Add a progress sentinel to prevent the fx queue from being - // automatically dequeued - if ( type === "fx" ) { - queue.unshift( "inprogress" ); - } - - // clear up the last queue stop function - delete hooks.stop; - fn.call( elem, next, hooks ); - } - - if ( !startLength && hooks ) { - hooks.empty.fire(); - } - }, - - // not intended for public consumption - generates a queueHooks object, or returns the current one - _queueHooks: function( elem, type ) { - var key = type + "queueHooks"; - return jQuery._data( elem, key ) || jQuery._data( elem, key, { - empty: jQuery.Callbacks("once memory").add(function() { - jQuery._removeData( elem, type + "queue" ); - jQuery._removeData( elem, key ); - }) - }); - } -}); - -jQuery.fn.extend({ - queue: function( type, data ) { - var setter = 2; - - if ( typeof type !== "string" ) { - data = type; - type = "fx"; - setter--; - } - - if ( arguments.length < setter ) { - return jQuery.queue( this[0], type ); - } - - return data === undefined ? - this : - this.each(function() { - var queue = jQuery.queue( this, type, data ); - - // ensure a hooks for this queue - jQuery._queueHooks( this, type ); - - if ( type === "fx" && queue[0] !== "inprogress" ) { - jQuery.dequeue( this, type ); - } - }); - }, - dequeue: function( type ) { - return this.each(function() { - jQuery.dequeue( this, type ); - }); - }, - // Based off of the plugin by Clint Helfers, with permission. - // http://blindsignals.com/index.php/2009/07/jquery-delay/ - delay: function( time, type ) { - time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; - type = type || "fx"; - - return this.queue( type, function( next, hooks ) { - var timeout = setTimeout( next, time ); - hooks.stop = function() { - clearTimeout( timeout ); - }; - }); - }, - clearQueue: function( type ) { - return this.queue( type || "fx", [] ); - }, - // Get a promise resolved when queues of a certain type - // are emptied (fx is the type by default) - promise: function( type, obj ) { - var tmp, - count = 1, - defer = jQuery.Deferred(), - elements = this, - i = this.length, - resolve = function() { - if ( !( --count ) ) { - defer.resolveWith( elements, [ elements ] ); - } - }; - - if ( typeof type !== "string" ) { - obj = type; - type = undefined; - } - type = type || "fx"; - - while( i-- ) { - tmp = jQuery._data( elements[ i ], type + "queueHooks" ); - if ( tmp && tmp.empty ) { - count++; - tmp.empty.add( resolve ); - } - } - resolve(); - return defer.promise( obj ); - } -}); -var nodeHook, boolHook, - rclass = /[\t\r\n]/g, - rreturn = /\r/g, - rfocusable = /^(?:input|select|textarea|button|object)$/i, - rclickable = /^(?:a|area)$/i, - rboolean = /^(?:checked|selected|autofocus|autoplay|async|controls|defer|disabled|hidden|loop|multiple|open|readonly|required|scoped)$/i, - ruseDefault = /^(?:checked|selected)$/i, - getSetAttribute = jQuery.support.getSetAttribute, - getSetInput = jQuery.support.input; - -jQuery.fn.extend({ - attr: function( name, value ) { - return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); - }, - - removeAttr: function( name ) { - return this.each(function() { - jQuery.removeAttr( this, name ); - }); - }, - - prop: function( name, value ) { - return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); - }, - - removeProp: function( name ) { - name = jQuery.propFix[ name ] || name; - return this.each(function() { - // try/catch handles cases where IE balks (such as removing a property on window) - try { - this[ name ] = undefined; - delete this[ name ]; - } catch( e ) {} - }); - }, - - addClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).addClass( value.call( this, j, this.className ) ); - }); - } - - if ( proceed ) { - // The disjunction here is for better compressibility (see removeClass) - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - " " - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - if ( cur.indexOf( " " + clazz + " " ) < 0 ) { - cur += clazz + " "; - } - } - elem.className = jQuery.trim( cur ); - - } - } - } - - return this; - }, - - removeClass: function( value ) { - var classes, elem, cur, clazz, j, - i = 0, - len = this.length, - proceed = arguments.length === 0 || typeof value === "string" && value; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( j ) { - jQuery( this ).removeClass( value.call( this, j, this.className ) ); - }); - } - if ( proceed ) { - classes = ( value || "" ).match( core_rnotwhite ) || []; - - for ( ; i < len; i++ ) { - elem = this[ i ]; - // This expression is here for better compressibility (see addClass) - cur = elem.nodeType === 1 && ( elem.className ? - ( " " + elem.className + " " ).replace( rclass, " " ) : - "" - ); - - if ( cur ) { - j = 0; - while ( (clazz = classes[j++]) ) { - // Remove *all* instances - while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { - cur = cur.replace( " " + clazz + " ", " " ); - } - } - elem.className = value ? jQuery.trim( cur ) : ""; - } - } - } - - return this; - }, - - toggleClass: function( value, stateVal ) { - var type = typeof value, - isBool = typeof stateVal === "boolean"; - - if ( jQuery.isFunction( value ) ) { - return this.each(function( i ) { - jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); - }); - } - - return this.each(function() { - if ( type === "string" ) { - // toggle individual class names - var className, - i = 0, - self = jQuery( this ), - state = stateVal, - classNames = value.match( core_rnotwhite ) || []; - - while ( (className = classNames[ i++ ]) ) { - // check each className given, space separated list - state = isBool ? state : !self.hasClass( className ); - self[ state ? "addClass" : "removeClass" ]( className ); - } - - // Toggle whole class name - } else if ( type === core_strundefined || type === "boolean" ) { - if ( this.className ) { - // store className if set - jQuery._data( this, "__className__", this.className ); - } - - // If the element has a class name or if we're passed "false", - // then remove the whole classname (if there was one, the above saved it). - // Otherwise bring back whatever was previously saved (if anything), - // falling back to the empty string if nothing was stored. - this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; - } - }); - }, - - hasClass: function( selector ) { - var className = " " + selector + " ", - i = 0, - l = this.length; - for ( ; i < l; i++ ) { - if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { - return true; - } - } - - return false; - }, - - val: function( value ) { - var ret, hooks, isFunction, - elem = this[0]; - - if ( !arguments.length ) { - if ( elem ) { - hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; - - if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { - return ret; - } - - ret = elem.value; - - return typeof ret === "string" ? - // handle most common string cases - ret.replace(rreturn, "") : - // handle cases where value is null/undef or number - ret == null ? "" : ret; - } - - return; - } - - isFunction = jQuery.isFunction( value ); - - return this.each(function( i ) { - var val, - self = jQuery(this); - - if ( this.nodeType !== 1 ) { - return; - } - - if ( isFunction ) { - val = value.call( this, i, self.val() ); - } else { - val = value; - } - - // Treat null/undefined as ""; convert numbers to string - if ( val == null ) { - val = ""; - } else if ( typeof val === "number" ) { - val += ""; - } else if ( jQuery.isArray( val ) ) { - val = jQuery.map(val, function ( value ) { - return value == null ? "" : value + ""; - }); - } - - hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; - - // If set returns undefined, fall back to normal setting - if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { - this.value = val; - } - }); - } -}); - -jQuery.extend({ - valHooks: { - option: { - get: function( elem ) { - // attributes.value is undefined in Blackberry 4.7 but - // uses .value. See #6932 - var val = elem.attributes.value; - return !val || val.specified ? elem.value : elem.text; - } - }, - select: { - get: function( elem ) { - var value, option, - options = elem.options, - index = elem.selectedIndex, - one = elem.type === "select-one" || index < 0, - values = one ? null : [], - max = one ? index + 1 : options.length, - i = index < 0 ? - max : - one ? index : 0; - - // Loop through all the selected options - for ( ; i < max; i++ ) { - option = options[ i ]; - - // oldIE doesn't update selected after form reset (#2551) - if ( ( option.selected || i === index ) && - // Don't return options that are disabled or in a disabled optgroup - ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && - ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { - - // Get the specific value for the option - value = jQuery( option ).val(); - - // We don't need an array for one selects - if ( one ) { - return value; - } - - // Multi-Selects return an array - values.push( value ); - } - } - - return values; - }, - - set: function( elem, value ) { - var values = jQuery.makeArray( value ); - - jQuery(elem).find("option").each(function() { - this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; - }); - - if ( !values.length ) { - elem.selectedIndex = -1; - } - return values; - } - } - }, - - attr: function( elem, name, value ) { - var hooks, notxml, ret, - nType = elem.nodeType; - - // don't get/set attributes on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - // Fallback to prop when attributes are not supported - if ( typeof elem.getAttribute === core_strundefined ) { - return jQuery.prop( elem, name, value ); - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - // All attributes are lowercase - // Grab necessary hook if one is defined - if ( notxml ) { - name = name.toLowerCase(); - hooks = jQuery.attrHooks[ name ] || ( rboolean.test( name ) ? boolHook : nodeHook ); - } - - if ( value !== undefined ) { - - if ( value === null ) { - jQuery.removeAttr( elem, name ); - - } else if ( hooks && notxml && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - elem.setAttribute( name, value + "" ); - return value; - } - - } else if ( hooks && notxml && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - - // In IE9+, Flash objects don't have .getAttribute (#12945) - // Support: IE9+ - if ( typeof elem.getAttribute !== core_strundefined ) { - ret = elem.getAttribute( name ); - } - - // Non-existent attributes return null, we normalize to undefined - return ret == null ? - undefined : - ret; - } - }, - - removeAttr: function( elem, value ) { - var name, propName, - i = 0, - attrNames = value && value.match( core_rnotwhite ); - - if ( attrNames && elem.nodeType === 1 ) { - while ( (name = attrNames[i++]) ) { - propName = jQuery.propFix[ name ] || name; - - // Boolean attributes get special treatment (#10870) - if ( rboolean.test( name ) ) { - // Set corresponding property to false for boolean attributes - // Also clear defaultChecked/defaultSelected (if appropriate) for IE<8 - if ( !getSetAttribute && ruseDefault.test( name ) ) { - elem[ jQuery.camelCase( "default-" + name ) ] = - elem[ propName ] = false; - } else { - elem[ propName ] = false; - } - - // See #9699 for explanation of this approach (setting first, then removal) - } else { - jQuery.attr( elem, name, "" ); - } - - elem.removeAttribute( getSetAttribute ? name : propName ); - } - } - }, - - attrHooks: { - type: { - set: function( elem, value ) { - if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { - // Setting the type on a radio button after the value resets the value in IE6-9 - // Reset value to default in case type is set after value during creation - var val = elem.value; - elem.setAttribute( "type", value ); - if ( val ) { - elem.value = val; - } - return value; - } - } - } - }, - - propFix: { - tabindex: "tabIndex", - readonly: "readOnly", - "for": "htmlFor", - "class": "className", - maxlength: "maxLength", - cellspacing: "cellSpacing", - cellpadding: "cellPadding", - rowspan: "rowSpan", - colspan: "colSpan", - usemap: "useMap", - frameborder: "frameBorder", - contenteditable: "contentEditable" - }, - - prop: function( elem, name, value ) { - var ret, hooks, notxml, - nType = elem.nodeType; - - // don't get/set properties on text, comment and attribute nodes - if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { - return; - } - - notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); - - if ( notxml ) { - // Fix name and attach hooks - name = jQuery.propFix[ name ] || name; - hooks = jQuery.propHooks[ name ]; - } - - if ( value !== undefined ) { - if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { - return ret; - - } else { - return ( elem[ name ] = value ); - } - - } else { - if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { - return ret; - - } else { - return elem[ name ]; - } - } - }, - - propHooks: { - tabIndex: { - get: function( elem ) { - // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set - // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ - var attributeNode = elem.getAttributeNode("tabindex"); - - return attributeNode && attributeNode.specified ? - parseInt( attributeNode.value, 10 ) : - rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? - 0 : - undefined; - } - } - } -}); - -// Hook for boolean attributes -boolHook = { - get: function( elem, name ) { - var - // Use .prop to determine if this attribute is understood as boolean - prop = jQuery.prop( elem, name ), - - // Fetch it accordingly - attr = typeof prop === "boolean" && elem.getAttribute( name ), - detail = typeof prop === "boolean" ? - - getSetInput && getSetAttribute ? - attr != null : - // oldIE fabricates an empty string for missing boolean attributes - // and conflates checked/selected into attroperties - ruseDefault.test( name ) ? - elem[ jQuery.camelCase( "default-" + name ) ] : - !!attr : - - // fetch an attribute node for properties not recognized as boolean - elem.getAttributeNode( name ); - - return detail && detail.value !== false ? - name.toLowerCase() : - undefined; - }, - set: function( elem, value, name ) { - if ( value === false ) { - // Remove boolean attributes when set to false - jQuery.removeAttr( elem, name ); - } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { - // IE<8 needs the *property* name - elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); - - // Use defaultChecked and defaultSelected for oldIE - } else { - elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; - } - - return name; - } -}; - -// fix oldIE value attroperty -if ( !getSetInput || !getSetAttribute ) { - jQuery.attrHooks.value = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return jQuery.nodeName( elem, "input" ) ? - - // Ignore the value *property* by using defaultValue - elem.defaultValue : - - ret && ret.specified ? ret.value : undefined; - }, - set: function( elem, value, name ) { - if ( jQuery.nodeName( elem, "input" ) ) { - // Does not return so that setAttribute is also used - elem.defaultValue = value; - } else { - // Use nodeHook if defined (#1954); otherwise setAttribute is fine - return nodeHook && nodeHook.set( elem, value, name ); - } - } - }; -} - -// IE6/7 do not support getting/setting some attributes with get/setAttribute -if ( !getSetAttribute ) { - - // Use this for any attribute in IE6/7 - // This fixes almost every IE6/7 issue - nodeHook = jQuery.valHooks.button = { - get: function( elem, name ) { - var ret = elem.getAttributeNode( name ); - return ret && ( name === "id" || name === "name" || name === "coords" ? ret.value !== "" : ret.specified ) ? - ret.value : - undefined; - }, - set: function( elem, value, name ) { - // Set the existing or create a new attribute node - var ret = elem.getAttributeNode( name ); - if ( !ret ) { - elem.setAttributeNode( - (ret = elem.ownerDocument.createAttribute( name )) - ); - } - - ret.value = value += ""; - - // Break association with cloned elements by also using setAttribute (#9646) - return name === "value" || value === elem.getAttribute( name ) ? - value : - undefined; - } - }; - - // Set contenteditable to false on removals(#10429) - // Setting to empty string throws an error as an invalid value - jQuery.attrHooks.contenteditable = { - get: nodeHook.get, - set: function( elem, value, name ) { - nodeHook.set( elem, value === "" ? false : value, name ); - } - }; - - // Set width and height to auto instead of 0 on empty string( Bug #8150 ) - // This is for removals - jQuery.each([ "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - set: function( elem, value ) { - if ( value === "" ) { - elem.setAttribute( name, "auto" ); - return value; - } - } - }); - }); -} - - -// Some attributes require a special call on IE -// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx -if ( !jQuery.support.hrefNormalized ) { - jQuery.each([ "href", "src", "width", "height" ], function( i, name ) { - jQuery.attrHooks[ name ] = jQuery.extend( jQuery.attrHooks[ name ], { - get: function( elem ) { - var ret = elem.getAttribute( name, 2 ); - return ret == null ? undefined : ret; - } - }); - }); - - // href/src property should get the full normalized URL (#10299/#12915) - jQuery.each([ "href", "src" ], function( i, name ) { - jQuery.propHooks[ name ] = { - get: function( elem ) { - return elem.getAttribute( name, 4 ); - } - }; - }); -} - -if ( !jQuery.support.style ) { - jQuery.attrHooks.style = { - get: function( elem ) { - // Return undefined in the case of empty string - // Note: IE uppercases css property names, but if we were to .toLowerCase() - // .cssText, that would destroy case senstitivity in URL's, like in "background" - return elem.style.cssText || undefined; - }, - set: function( elem, value ) { - return ( elem.style.cssText = value + "" ); - } - }; -} - -// Safari mis-reports the default selected property of an option -// Accessing the parent's selectedIndex property fixes it -if ( !jQuery.support.optSelected ) { - jQuery.propHooks.selected = jQuery.extend( jQuery.propHooks.selected, { - get: function( elem ) { - var parent = elem.parentNode; - - if ( parent ) { - parent.selectedIndex; - - // Make sure that it also works with optgroups, see #5701 - if ( parent.parentNode ) { - parent.parentNode.selectedIndex; - } - } - return null; - } - }); -} - -// IE6/7 call enctype encoding -if ( !jQuery.support.enctype ) { - jQuery.propFix.enctype = "encoding"; -} - -// Radios and checkboxes getter/setter -if ( !jQuery.support.checkOn ) { - jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = { - get: function( elem ) { - // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified - return elem.getAttribute("value") === null ? "on" : elem.value; - } - }; - }); -} -jQuery.each([ "radio", "checkbox" ], function() { - jQuery.valHooks[ this ] = jQuery.extend( jQuery.valHooks[ this ], { - set: function( elem, value ) { - if ( jQuery.isArray( value ) ) { - return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); - } - } - }); -}); -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|contextmenu)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !(events = elemData.events) ) { - events = elemData.events = {}; - } - if ( !(eventHandle = elemData.handle) ) { - eventHandle = elemData.handle = function( e ) { - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - // jQuery(...).bind("mouseover mouseout", fn); - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend({ - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join(".") - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !(handlers = events[ type ]) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - - if ( !elemData || !(events = elemData.events) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( core_rnotwhite ) || [""]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[t] ) || []; - type = origType = tmp[1]; - namespaces = ( tmp[2] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = core_hasOwn.call( event, "type" ) ? event.type : event, - namespaces = core_hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf(".") >= 0 ) { - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split("."); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf(":") < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - event.isTrigger = true; - event.namespace = namespaces.join("."); - event.namespace_re = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === (elem.ownerDocument || document) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === false ) { - event.preventDefault(); - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( (!special._default || special._default.apply( elem.ownerDocument, data ) === false) && - !(type === "click" && jQuery.nodeName( elem, "a" )) && jQuery.acceptData( elem ) ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, ret, handleObj, matched, j, - handlerQueue = [], - args = core_slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[0] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or - // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). - if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) - .apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( (event.result = ret) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var sel, handleObj, matches, i, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // Avoid non-left-click bubbling in Firefox (#3861) - if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { - - for ( ; cur != this; cur = cur.parentNode || this ) { - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && (cur.disabled !== true || event.type !== "click") ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) >= 0 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push({ elem: cur, handlers: matches }); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); - } - - return handlerQueue; - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Chrome 23+, Safari? - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split(" "), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: "button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - special: { - load: { - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - click: { - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - } - }, - focus: { - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== document.activeElement && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === document.activeElement && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - - beforeunload: { - postDispatch: function( event ) { - - // Even when returnValue equals to undefined Firefox will still show alert - if ( event.result !== undefined ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - simulate: function( type, elem, event, bubble ) { - // Piggyback on a donor event to simulate a different one. - // Fake originalEvent to avoid donor's stopPropagation, but if the - // simulated event prevents default then we do the same on the donor. - var e = jQuery.extend( - new jQuery.Event(), - event, - { type: type, - isSimulated: true, - originalEvent: {} - } - ); - if ( bubble ) { - jQuery.event.trigger( e, null, elem ); - } else { - jQuery.event.dispatch.call( elem, e ); - } - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle, false ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, to properly expose it to GC - if ( typeof elem[ name ] === core_strundefined ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; - -jQuery.Event = function( src, props ) { - // Allow instantiation without the 'new' keyword - if ( !(this instanceof jQuery.Event) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = ( src.defaultPrevented || src.returnValue === false || - src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - if ( !e ) { - return; - } - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - this.isImmediatePropagationStopped = returnTrue; - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -jQuery.each({ - mouseenter: "mouseover", - mouseleave: "mouseout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mousenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || (related !== target && !jQuery.contains( target, related )) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -}); - -// IE submit delegation -if ( !jQuery.support.submitBubbles ) { - - jQuery.event.special.submit = { - setup: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; - if ( form && !jQuery._data( form, "submitBubbles" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submit_bubble = true; - }); - jQuery._data( form, "submitBubbles", true ); - } - }); - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - // If form was submitted by the user, bubble the event up the tree - if ( event._submit_bubble ) { - delete event._submit_bubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event, true ); - } - } - }, - - teardown: function() { - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !jQuery.support.changeBubbles ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._just_changed = true; - } - }); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._just_changed && !event.isTrigger ) { - this._just_changed = false; - } - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event, true ); - }); - } - return false; - } - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event, true ); - } - }); - jQuery._data( elem, "changeBubbles", true ); - } - }); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - -// Create "bubbling" focus and blur events -if ( !jQuery.support.focusinBubbles ) { - jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler while someone wants focusin/focusout - var attaches = 0, - handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - if ( attaches++ === 0 ) { - document.addEventListener( orig, handler, true ); - } - }, - teardown: function() { - if ( --attaches === 0 ) { - document.removeEventListener( orig, handler, true ); - } - } - }; - }); -} - -jQuery.fn.extend({ - - on: function( types, selector, data, fn, /*INTERNAL*/ one ) { - var type, origFn; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - this.on( type, selector, data, types[ type ], one ); - } - return this; - } - - if ( data == null && fn == null ) { - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return this; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return this.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - }); - }, - one: function( types, selector, data, fn ) { - return this.on( types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each(function() { - jQuery.event.remove( this, types, fn, selector ); - }); - }, - - bind: function( types, data, fn ) { - return this.on( types, null, data, fn ); - }, - unbind: function( types, fn ) { - return this.off( types, null, fn ); - }, - - delegate: function( selector, types, data, fn ) { - return this.on( types, selector, data, fn ); - }, - undelegate: function( selector, types, fn ) { - // ( namespace ) or ( selector, types [, fn] ) - return arguments.length === 1 ? this.off( selector, "**" ) : this.off( types, selector || "**", fn ); - }, - - trigger: function( type, data ) { - return this.each(function() { - jQuery.event.trigger( type, data, this ); - }); - }, - triggerHandler: function( type, data ) { - var elem = this[0]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -}); -/*! - * Sizzle CSS Selector Engine - * Copyright 2012 jQuery Foundation and other contributors - * Released under the MIT license - * http://sizzlejs.com/ - */ -(function( window, undefined ) { - -var i, - cachedruns, - Expr, - getText, - isXML, - compile, - hasDuplicate, - outermostContext, - - // Local document vars - setDocument, - document, - docElem, - documentIsXML, - rbuggyQSA, - rbuggyMatches, - matches, - contains, - sortOrder, - - // Instance-specific data - expando = "sizzle" + -(new Date()), - preferredDoc = window.document, - support = {}, - dirruns = 0, - done = 0, - classCache = createCache(), - tokenCache = createCache(), - compilerCache = createCache(), - - // General-purpose constants - strundefined = typeof undefined, - MAX_NEGATIVE = 1 << 31, - - // Array methods - arr = [], - pop = arr.pop, - push = arr.push, - slice = arr.slice, - // Use a stripped-down indexOf if we can't use a native one - indexOf = arr.indexOf || function( elem ) { - var i = 0, - len = this.length; - for ( ; i < len; i++ ) { - if ( this[i] === elem ) { - return i; - } - } - return -1; - }, - - - // Regular expressions - - // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace - whitespace = "[\\x20\\t\\r\\n\\f]", - // http://www.w3.org/TR/css3-syntax/#characters - characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", - - // Loosely modeled on CSS identifier characters - // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors - // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier - identifier = characterEncoding.replace( "w", "w#" ), - - // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors - operators = "([*^$|!~]?=)", - attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + - "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", - - // Prefer arguments quoted, - // then not containing pseudos/brackets, - // then attribute selectors/non-parenthetical expressions, - // then anything else - // These preferences are here to reduce the number of selectors - // needing tokenize in the PSEUDO preFilter - pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", - - // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter - rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), - - rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), - rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ), - rpseudo = new RegExp( pseudos ), - ridentifier = new RegExp( "^" + identifier + "$" ), - - matchExpr = { - "ID": new RegExp( "^#(" + characterEncoding + ")" ), - "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), - "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ), - "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), - "ATTR": new RegExp( "^" + attributes ), - "PSEUDO": new RegExp( "^" + pseudos ), - "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + - "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + - "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), - // For use in libraries implementing .is() - // We use this for POS matching in `select` - "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + - whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) - }, - - rsibling = /[\x20\t\r\n\f]*[+~]/, - - rnative = /^[^{]+\{\s*\[native code/, - - // Easily-parseable/retrievable ID or TAG or CLASS selectors - rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, - - rinputs = /^(?:input|select|textarea|button)$/i, - rheader = /^h\d$/i, - - rescape = /'|\\/g, - rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g, - - // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters - runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g, - funescape = function( _, escaped ) { - var high = "0x" + escaped - 0x10000; - // NaN means non-codepoint - return high !== high ? - escaped : - // BMP codepoint - high < 0 ? - String.fromCharCode( high + 0x10000 ) : - // Supplemental Plane codepoint (surrogate pair) - String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); - }; - -// Use a stripped-down slice if we can't use a native one -try { - slice.call( preferredDoc.documentElement.childNodes, 0 )[0].nodeType; -} catch ( e ) { - slice = function( i ) { - var elem, - results = []; - while ( (elem = this[i++]) ) { - results.push( elem ); - } - return results; - }; -} - -/** - * For feature detection - * @param {Function} fn The function to test for native support - */ -function isNative( fn ) { - return rnative.test( fn + "" ); -} - -/** - * Create key-value caches of limited size - * @returns {Function(string, Object)} Returns the Object data after storing it on itself with - * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) - * deleting the oldest entry - */ -function createCache() { - var cache, - keys = []; - - return (cache = function( key, value ) { - // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) - if ( keys.push( key += " " ) > Expr.cacheLength ) { - // Only keep the most recent entries - delete cache[ keys.shift() ]; - } - return (cache[ key ] = value); - }); -} - -/** - * Mark a function for special use by Sizzle - * @param {Function} fn The function to mark - */ -function markFunction( fn ) { - fn[ expando ] = true; - return fn; -} - -/** - * Support testing using an element - * @param {Function} fn Passed the created div and expects a boolean result - */ -function assert( fn ) { - var div = document.createElement("div"); - - try { - return fn( div ); - } catch (e) { - return false; - } finally { - // release memory in IE - div = null; - } -} - -function Sizzle( selector, context, results, seed ) { - var match, elem, m, nodeType, - // QSA vars - i, groups, old, nid, newContext, newSelector; - - if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { - setDocument( context ); - } - - context = context || document; - results = results || []; - - if ( !selector || typeof selector !== "string" ) { - return results; - } - - if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { - return []; - } - - if ( !documentIsXML && !seed ) { - - // Shortcuts - if ( (match = rquickExpr.exec( selector )) ) { - // Speed-up: Sizzle("#ID") - if ( (m = match[1]) ) { - if ( nodeType === 9 ) { - elem = context.getElementById( m ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - if ( elem && elem.parentNode ) { - // Handle the case where IE, Opera, and Webkit return items - // by name instead of ID - if ( elem.id === m ) { - results.push( elem ); - return results; - } - } else { - return results; - } - } else { - // Context is not a document - if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && - contains( context, elem ) && elem.id === m ) { - results.push( elem ); - return results; - } - } - - // Speed-up: Sizzle("TAG") - } else if ( match[2] ) { - push.apply( results, slice.call(context.getElementsByTagName( selector ), 0) ); - return results; - - // Speed-up: Sizzle(".CLASS") - } else if ( (m = match[3]) && support.getByClassName && context.getElementsByClassName ) { - push.apply( results, slice.call(context.getElementsByClassName( m ), 0) ); - return results; - } - } - - // QSA path - if ( support.qsa && !rbuggyQSA.test(selector) ) { - old = true; - nid = expando; - newContext = context; - newSelector = nodeType === 9 && selector; - - // qSA works strangely on Element-rooted queries - // We can work around this by specifying an extra ID on the root - // and working up from there (Thanks to Andrew Dupont for the technique) - // IE 8 doesn't work on object elements - if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { - groups = tokenize( selector ); - - if ( (old = context.getAttribute("id")) ) { - nid = old.replace( rescape, "\\$&" ); - } else { - context.setAttribute( "id", nid ); - } - nid = "[id='" + nid + "'] "; - - i = groups.length; - while ( i-- ) { - groups[i] = nid + toSelector( groups[i] ); - } - newContext = rsibling.test( selector ) && context.parentNode || context; - newSelector = groups.join(","); - } - - if ( newSelector ) { - try { - push.apply( results, slice.call( newContext.querySelectorAll( - newSelector - ), 0 ) ); - return results; - } catch(qsaError) { - } finally { - if ( !old ) { - context.removeAttribute("id"); - } - } - } - } - } - - // All others - return select( selector.replace( rtrim, "$1" ), context, results, seed ); -} - -/** - * Detect xml - * @param {Element|Object} elem An element or a document - */ -isXML = Sizzle.isXML = function( elem ) { - // documentElement is verified for cases where it doesn't yet exist - // (such as loading iframes in IE - #4833) - var documentElement = elem && (elem.ownerDocument || elem).documentElement; - return documentElement ? documentElement.nodeName !== "HTML" : false; -}; - -/** - * Sets document-related variables once based on the current document - * @param {Element|Object} [doc] An element or document object to use to set the document - * @returns {Object} Returns the current document - */ -setDocument = Sizzle.setDocument = function( node ) { - var doc = node ? node.ownerDocument || node : preferredDoc; - - // If no document and documentElement is available, return - if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { - return document; - } - - // Set our document - document = doc; - docElem = doc.documentElement; - - // Support tests - documentIsXML = isXML( doc ); - - // Check if getElementsByTagName("*") returns only elements - support.tagNameNoComments = assert(function( div ) { - div.appendChild( doc.createComment("") ); - return !div.getElementsByTagName("*").length; - }); - - // Check if attributes should be retrieved by attribute nodes - support.attributes = assert(function( div ) { - div.innerHTML = ""; - var type = typeof div.lastChild.getAttribute("multiple"); - // IE8 returns a string for some attributes even when not present - return type !== "boolean" && type !== "string"; - }); - - // Check if getElementsByClassName can be trusted - support.getByClassName = assert(function( div ) { - // Opera can't find a second classname (in 9.6) - div.innerHTML = ""; - if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) { - return false; - } - - // Safari 3.2 caches class attributes and doesn't catch changes - div.lastChild.className = "e"; - return div.getElementsByClassName("e").length === 2; - }); - - // Check if getElementById returns elements by name - // Check if getElementsByName privileges form controls or returns elements by ID - support.getByName = assert(function( div ) { - // Inject content - div.id = expando + 0; - div.innerHTML = "
    "; - docElem.insertBefore( div, docElem.firstChild ); - - // Test - var pass = doc.getElementsByName && - // buggy browsers will return fewer than the correct 2 - doc.getElementsByName( expando ).length === 2 + - // buggy browsers will return more than the correct 0 - doc.getElementsByName( expando + 0 ).length; - support.getIdNotName = !doc.getElementById( expando ); - - // Cleanup - docElem.removeChild( div ); - - return pass; - }); - - // IE6/7 return modified attributes - Expr.attrHandle = assert(function( div ) { - div.innerHTML = ""; - return div.firstChild && typeof div.firstChild.getAttribute !== strundefined && - div.firstChild.getAttribute("href") === "#"; - }) ? - {} : - { - "href": function( elem ) { - return elem.getAttribute( "href", 2 ); - }, - "type": function( elem ) { - return elem.getAttribute("type"); - } - }; - - // ID find and filter - if ( support.getIdNotName ) { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && !documentIsXML ) { - var m = context.getElementById( id ); - // Check parentNode to catch when Blackberry 4.6 returns - // nodes that are no longer in the document #6963 - return m && m.parentNode ? [m] : []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - return elem.getAttribute("id") === attrId; - }; - }; - } else { - Expr.find["ID"] = function( id, context ) { - if ( typeof context.getElementById !== strundefined && !documentIsXML ) { - var m = context.getElementById( id ); - - return m ? - m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ? - [m] : - undefined : - []; - } - }; - Expr.filter["ID"] = function( id ) { - var attrId = id.replace( runescape, funescape ); - return function( elem ) { - var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); - return node && node.value === attrId; - }; - }; - } - - // Tag - Expr.find["TAG"] = support.tagNameNoComments ? - function( tag, context ) { - if ( typeof context.getElementsByTagName !== strundefined ) { - return context.getElementsByTagName( tag ); - } - } : - function( tag, context ) { - var elem, - tmp = [], - i = 0, - results = context.getElementsByTagName( tag ); - - // Filter out possible comments - if ( tag === "*" ) { - while ( (elem = results[i++]) ) { - if ( elem.nodeType === 1 ) { - tmp.push( elem ); - } - } - - return tmp; - } - return results; - }; - - // Name - Expr.find["NAME"] = support.getByName && function( tag, context ) { - if ( typeof context.getElementsByName !== strundefined ) { - return context.getElementsByName( name ); - } - }; - - // Class - Expr.find["CLASS"] = support.getByClassName && function( className, context ) { - if ( typeof context.getElementsByClassName !== strundefined && !documentIsXML ) { - return context.getElementsByClassName( className ); - } - }; - - // QSA and matchesSelector support - - // matchesSelector(:active) reports false when true (IE9/Opera 11.5) - rbuggyMatches = []; - - // qSa(:focus) reports false when true (Chrome 21), - // no need to also add to buggyMatches since matches checks buggyQSA - // A support test would require too much code (would include document ready) - rbuggyQSA = [ ":focus" ]; - - if ( (support.qsa = isNative(doc.querySelectorAll)) ) { - // Build QSA regex - // Regex strategy adopted from Diego Perini - assert(function( div ) { - // Select is set to empty string on purpose - // This is to test IE's treatment of not explictly - // setting a boolean content attribute, - // since its presence should be enough - // http://bugs.jquery.com/ticket/12359 - div.innerHTML = ""; - - // IE8 - Some boolean attributes are not treated correctly - if ( !div.querySelectorAll("[selected]").length ) { - rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" ); - } - - // Webkit/Opera - :checked should return selected option elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":checked").length ) { - rbuggyQSA.push(":checked"); - } - }); - - assert(function( div ) { - - // Opera 10-12/IE8 - ^= $= *= and empty values - // Should not select anything - div.innerHTML = ""; - if ( div.querySelectorAll("[i^='']").length ) { - rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" ); - } - - // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) - // IE8 throws error here and will not see later tests - if ( !div.querySelectorAll(":enabled").length ) { - rbuggyQSA.push( ":enabled", ":disabled" ); - } - - // Opera 10-11 does not throw on post-comma invalid pseudos - div.querySelectorAll("*,:x"); - rbuggyQSA.push(",.*:"); - }); - } - - if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector || - docElem.mozMatchesSelector || - docElem.webkitMatchesSelector || - docElem.oMatchesSelector || - docElem.msMatchesSelector) )) ) { - - assert(function( div ) { - // Check to see if it's possible to do matchesSelector - // on a disconnected node (IE 9) - support.disconnectedMatch = matches.call( div, "div" ); - - // This should fail with an exception - // Gecko does not error, returns false instead - matches.call( div, "[s!='']:x" ); - rbuggyMatches.push( "!=", pseudos ); - }); - } - - rbuggyQSA = new RegExp( rbuggyQSA.join("|") ); - rbuggyMatches = new RegExp( rbuggyMatches.join("|") ); - - // Element contains another - // Purposefully does not implement inclusive descendent - // As in, an element does not contain itself - contains = isNative(docElem.contains) || docElem.compareDocumentPosition ? - function( a, b ) { - var adown = a.nodeType === 9 ? a.documentElement : a, - bup = b && b.parentNode; - return a === bup || !!( bup && bup.nodeType === 1 && ( - adown.contains ? - adown.contains( bup ) : - a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 - )); - } : - function( a, b ) { - if ( b ) { - while ( (b = b.parentNode) ) { - if ( b === a ) { - return true; - } - } - } - return false; - }; - - // Document order sorting - sortOrder = docElem.compareDocumentPosition ? - function( a, b ) { - var compare; - - if ( a === b ) { - hasDuplicate = true; - return 0; - } - - if ( (compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b )) ) { - if ( compare & 1 || a.parentNode && a.parentNode.nodeType === 11 ) { - if ( a === doc || contains( preferredDoc, a ) ) { - return -1; - } - if ( b === doc || contains( preferredDoc, b ) ) { - return 1; - } - return 0; - } - return compare & 4 ? -1 : 1; - } - - return a.compareDocumentPosition ? -1 : 1; - } : - function( a, b ) { - var cur, - i = 0, - aup = a.parentNode, - bup = b.parentNode, - ap = [ a ], - bp = [ b ]; - - // Exit early if the nodes are identical - if ( a === b ) { - hasDuplicate = true; - return 0; - - // Parentless nodes are either documents or disconnected - } else if ( !aup || !bup ) { - return a === doc ? -1 : - b === doc ? 1 : - aup ? -1 : - bup ? 1 : - 0; - - // If the nodes are siblings, we can do a quick check - } else if ( aup === bup ) { - return siblingCheck( a, b ); - } - - // Otherwise we need full lists of their ancestors for comparison - cur = a; - while ( (cur = cur.parentNode) ) { - ap.unshift( cur ); - } - cur = b; - while ( (cur = cur.parentNode) ) { - bp.unshift( cur ); - } - - // Walk down the tree looking for a discrepancy - while ( ap[i] === bp[i] ) { - i++; - } - - return i ? - // Do a sibling check if the nodes have a common ancestor - siblingCheck( ap[i], bp[i] ) : - - // Otherwise nodes in our document sort first - ap[i] === preferredDoc ? -1 : - bp[i] === preferredDoc ? 1 : - 0; - }; - - // Always assume the presence of duplicates if sort doesn't - // pass them to our comparison function (as in Google Chrome). - hasDuplicate = false; - [0, 0].sort( sortOrder ); - support.detectDuplicates = hasDuplicate; - - return document; -}; - -Sizzle.matches = function( expr, elements ) { - return Sizzle( expr, null, null, elements ); -}; - -Sizzle.matchesSelector = function( elem, expr ) { - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - // Make sure that attribute selectors are quoted - expr = expr.replace( rattributeQuotes, "='$1']" ); - - // rbuggyQSA always contains :focus, so no need for an existence check - if ( support.matchesSelector && !documentIsXML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) { - try { - var ret = matches.call( elem, expr ); - - // IE 9's matchesSelector returns false on disconnected nodes - if ( ret || support.disconnectedMatch || - // As well, disconnected nodes are said to be in a document - // fragment in IE 9 - elem.document && elem.document.nodeType !== 11 ) { - return ret; - } - } catch(e) {} - } - - return Sizzle( expr, document, null, [elem] ).length > 0; -}; - -Sizzle.contains = function( context, elem ) { - // Set document vars if needed - if ( ( context.ownerDocument || context ) !== document ) { - setDocument( context ); - } - return contains( context, elem ); -}; - -Sizzle.attr = function( elem, name ) { - var val; - - // Set document vars if needed - if ( ( elem.ownerDocument || elem ) !== document ) { - setDocument( elem ); - } - - if ( !documentIsXML ) { - name = name.toLowerCase(); - } - if ( (val = Expr.attrHandle[ name ]) ) { - return val( elem ); - } - if ( documentIsXML || support.attributes ) { - return elem.getAttribute( name ); - } - return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ? - name : - val && val.specified ? val.value : null; -}; - -Sizzle.error = function( msg ) { - throw new Error( "Syntax error, unrecognized expression: " + msg ); -}; - -// Document sorting and removing duplicates -Sizzle.uniqueSort = function( results ) { - var elem, - duplicates = [], - i = 1, - j = 0; - - // Unless we *know* we can detect duplicates, assume their presence - hasDuplicate = !support.detectDuplicates; - results.sort( sortOrder ); - - if ( hasDuplicate ) { - for ( ; (elem = results[i]); i++ ) { - if ( elem === results[ i - 1 ] ) { - j = duplicates.push( i ); - } - } - while ( j-- ) { - results.splice( duplicates[ j ], 1 ); - } - } - - return results; -}; - -function siblingCheck( a, b ) { - var cur = b && a, - diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE ); - - // Use IE sourceIndex if available on both nodes - if ( diff ) { - return diff; - } - - // Check if b follows a - if ( cur ) { - while ( (cur = cur.nextSibling) ) { - if ( cur === b ) { - return -1; - } - } - } - - return a ? 1 : -1; -} - -// Returns a function to use in pseudos for input types -function createInputPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === type; - }; -} - -// Returns a function to use in pseudos for buttons -function createButtonPseudo( type ) { - return function( elem ) { - var name = elem.nodeName.toLowerCase(); - return (name === "input" || name === "button") && elem.type === type; - }; -} - -// Returns a function to use in pseudos for positionals -function createPositionalPseudo( fn ) { - return markFunction(function( argument ) { - argument = +argument; - return markFunction(function( seed, matches ) { - var j, - matchIndexes = fn( [], seed.length, argument ), - i = matchIndexes.length; - - // Match elements found at the specified indexes - while ( i-- ) { - if ( seed[ (j = matchIndexes[i]) ] ) { - seed[j] = !(matches[j] = seed[j]); - } - } - }); - }); -} - -/** - * Utility function for retrieving the text value of an array of DOM nodes - * @param {Array|Element} elem - */ -getText = Sizzle.getText = function( elem ) { - var node, - ret = "", - i = 0, - nodeType = elem.nodeType; - - if ( !nodeType ) { - // If no nodeType, this is expected to be an array - for ( ; (node = elem[i]); i++ ) { - // Do not traverse comment nodes - ret += getText( node ); - } - } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { - // Use textContent for elements - // innerText usage removed for consistency of new lines (see #11153) - if ( typeof elem.textContent === "string" ) { - return elem.textContent; - } else { - // Traverse its children - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - ret += getText( elem ); - } - } - } else if ( nodeType === 3 || nodeType === 4 ) { - return elem.nodeValue; - } - // Do not include comment or processing instruction nodes - - return ret; -}; - -Expr = Sizzle.selectors = { - - // Can be adjusted by the user - cacheLength: 50, - - createPseudo: markFunction, - - match: matchExpr, - - find: {}, - - relative: { - ">": { dir: "parentNode", first: true }, - " ": { dir: "parentNode" }, - "+": { dir: "previousSibling", first: true }, - "~": { dir: "previousSibling" } - }, - - preFilter: { - "ATTR": function( match ) { - match[1] = match[1].replace( runescape, funescape ); - - // Move the given value to match[3] whether quoted or unquoted - match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape ); - - if ( match[2] === "~=" ) { - match[3] = " " + match[3] + " "; - } - - return match.slice( 0, 4 ); - }, - - "CHILD": function( match ) { - /* matches from matchExpr["CHILD"] - 1 type (only|nth|...) - 2 what (child|of-type) - 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) - 4 xn-component of xn+y argument ([+-]?\d*n|) - 5 sign of xn-component - 6 x of xn-component - 7 sign of y-component - 8 y of y-component - */ - match[1] = match[1].toLowerCase(); - - if ( match[1].slice( 0, 3 ) === "nth" ) { - // nth-* requires argument - if ( !match[3] ) { - Sizzle.error( match[0] ); - } - - // numeric x and y parameters for Expr.filter.CHILD - // remember that false/true cast respectively to 0/1 - match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); - match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); - - // other types prohibit arguments - } else if ( match[3] ) { - Sizzle.error( match[0] ); - } - - return match; - }, - - "PSEUDO": function( match ) { - var excess, - unquoted = !match[5] && match[2]; - - if ( matchExpr["CHILD"].test( match[0] ) ) { - return null; - } - - // Accept quoted arguments as-is - if ( match[4] ) { - match[2] = match[4]; - - // Strip excess characters from unquoted arguments - } else if ( unquoted && rpseudo.test( unquoted ) && - // Get excess from tokenize (recursively) - (excess = tokenize( unquoted, true )) && - // advance to the next closing parenthesis - (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { - - // excess is a negative index - match[0] = match[0].slice( 0, excess ); - match[2] = unquoted.slice( 0, excess ); - } - - // Return only captures needed by the pseudo filter method (type and argument) - return match.slice( 0, 3 ); - } - }, - - filter: { - - "TAG": function( nodeName ) { - if ( nodeName === "*" ) { - return function() { return true; }; - } - - nodeName = nodeName.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; - }; - }, - - "CLASS": function( className ) { - var pattern = classCache[ className + " " ]; - - return pattern || - (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && - classCache( className, function( elem ) { - return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" ); - }); - }, - - "ATTR": function( name, operator, check ) { - return function( elem ) { - var result = Sizzle.attr( elem, name ); - - if ( result == null ) { - return operator === "!="; - } - if ( !operator ) { - return true; - } - - result += ""; - - return operator === "=" ? result === check : - operator === "!=" ? result !== check : - operator === "^=" ? check && result.indexOf( check ) === 0 : - operator === "*=" ? check && result.indexOf( check ) > -1 : - operator === "$=" ? check && result.slice( -check.length ) === check : - operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : - operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : - false; - }; - }, - - "CHILD": function( type, what, argument, first, last ) { - var simple = type.slice( 0, 3 ) !== "nth", - forward = type.slice( -4 ) !== "last", - ofType = what === "of-type"; - - return first === 1 && last === 0 ? - - // Shortcut for :nth-*(n) - function( elem ) { - return !!elem.parentNode; - } : - - function( elem, context, xml ) { - var cache, outerCache, node, diff, nodeIndex, start, - dir = simple !== forward ? "nextSibling" : "previousSibling", - parent = elem.parentNode, - name = ofType && elem.nodeName.toLowerCase(), - useCache = !xml && !ofType; - - if ( parent ) { - - // :(first|last|only)-(child|of-type) - if ( simple ) { - while ( dir ) { - node = elem; - while ( (node = node[ dir ]) ) { - if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { - return false; - } - } - // Reverse direction for :only-* (if we haven't yet done so) - start = dir = type === "only" && !start && "nextSibling"; - } - return true; - } - - start = [ forward ? parent.firstChild : parent.lastChild ]; - - // non-xml :nth-child(...) stores cache data on `parent` - if ( forward && useCache ) { - // Seek `elem` from a previously-cached index - outerCache = parent[ expando ] || (parent[ expando ] = {}); - cache = outerCache[ type ] || []; - nodeIndex = cache[0] === dirruns && cache[1]; - diff = cache[0] === dirruns && cache[2]; - node = nodeIndex && parent.childNodes[ nodeIndex ]; - - while ( (node = ++nodeIndex && node && node[ dir ] || - - // Fallback to seeking `elem` from the start - (diff = nodeIndex = 0) || start.pop()) ) { - - // When found, cache indexes on `parent` and break - if ( node.nodeType === 1 && ++diff && node === elem ) { - outerCache[ type ] = [ dirruns, nodeIndex, diff ]; - break; - } - } - - // Use previously-cached element index if available - } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { - diff = cache[1]; - - // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) - } else { - // Use the same loop as above to seek `elem` from the start - while ( (node = ++nodeIndex && node && node[ dir ] || - (diff = nodeIndex = 0) || start.pop()) ) { - - if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { - // Cache the index of each encountered element - if ( useCache ) { - (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; - } - - if ( node === elem ) { - break; - } - } - } - } - - // Incorporate the offset, then check against cycle size - diff -= last; - return diff === first || ( diff % first === 0 && diff / first >= 0 ); - } - }; - }, - - "PSEUDO": function( pseudo, argument ) { - // pseudo-class names are case-insensitive - // http://www.w3.org/TR/selectors/#pseudo-classes - // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters - // Remember that setFilters inherits from pseudos - var args, - fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || - Sizzle.error( "unsupported pseudo: " + pseudo ); - - // The user may use createPseudo to indicate that - // arguments are needed to create the filter function - // just as Sizzle does - if ( fn[ expando ] ) { - return fn( argument ); - } - - // But maintain support for old signatures - if ( fn.length > 1 ) { - args = [ pseudo, pseudo, "", argument ]; - return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? - markFunction(function( seed, matches ) { - var idx, - matched = fn( seed, argument ), - i = matched.length; - while ( i-- ) { - idx = indexOf.call( seed, matched[i] ); - seed[ idx ] = !( matches[ idx ] = matched[i] ); - } - }) : - function( elem ) { - return fn( elem, 0, args ); - }; - } - - return fn; - } - }, - - pseudos: { - // Potentially complex pseudos - "not": markFunction(function( selector ) { - // Trim the selector passed to compile - // to avoid treating leading and trailing - // spaces as combinators - var input = [], - results = [], - matcher = compile( selector.replace( rtrim, "$1" ) ); - - return matcher[ expando ] ? - markFunction(function( seed, matches, context, xml ) { - var elem, - unmatched = matcher( seed, null, xml, [] ), - i = seed.length; - - // Match elements unmatched by `matcher` - while ( i-- ) { - if ( (elem = unmatched[i]) ) { - seed[i] = !(matches[i] = elem); - } - } - }) : - function( elem, context, xml ) { - input[0] = elem; - matcher( input, null, xml, results ); - return !results.pop(); - }; - }), - - "has": markFunction(function( selector ) { - return function( elem ) { - return Sizzle( selector, elem ).length > 0; - }; - }), - - "contains": markFunction(function( text ) { - return function( elem ) { - return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; - }; - }), - - // "Whether an element is represented by a :lang() selector - // is based solely on the element's language value - // being equal to the identifier C, - // or beginning with the identifier C immediately followed by "-". - // The matching of C against the element's language value is performed case-insensitively. - // The identifier C does not have to be a valid language name." - // http://www.w3.org/TR/selectors/#lang-pseudo - "lang": markFunction( function( lang ) { - // lang value must be a valid identifider - if ( !ridentifier.test(lang || "") ) { - Sizzle.error( "unsupported lang: " + lang ); - } - lang = lang.replace( runescape, funescape ).toLowerCase(); - return function( elem ) { - var elemLang; - do { - if ( (elemLang = documentIsXML ? - elem.getAttribute("xml:lang") || elem.getAttribute("lang") : - elem.lang) ) { - - elemLang = elemLang.toLowerCase(); - return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; - } - } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); - return false; - }; - }), - - // Miscellaneous - "target": function( elem ) { - var hash = window.location && window.location.hash; - return hash && hash.slice( 1 ) === elem.id; - }, - - "root": function( elem ) { - return elem === docElem; - }, - - "focus": function( elem ) { - return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); - }, - - // Boolean properties - "enabled": function( elem ) { - return elem.disabled === false; - }, - - "disabled": function( elem ) { - return elem.disabled === true; - }, - - "checked": function( elem ) { - // In CSS3, :checked should return both checked and selected elements - // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked - var nodeName = elem.nodeName.toLowerCase(); - return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); - }, - - "selected": function( elem ) { - // Accessing this property makes selected-by-default - // options in Safari work properly - if ( elem.parentNode ) { - elem.parentNode.selectedIndex; - } - - return elem.selected === true; - }, - - // Contents - "empty": function( elem ) { - // http://www.w3.org/TR/selectors/#empty-pseudo - // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)), - // not comment, processing instructions, or others - // Thanks to Diego Perini for the nodeName shortcut - // Greater than "@" means alpha characters (specifically not starting with "#" or "?") - for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { - if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { - return false; - } - } - return true; - }, - - "parent": function( elem ) { - return !Expr.pseudos["empty"]( elem ); - }, - - // Element/input types - "header": function( elem ) { - return rheader.test( elem.nodeName ); - }, - - "input": function( elem ) { - return rinputs.test( elem.nodeName ); - }, - - "button": function( elem ) { - var name = elem.nodeName.toLowerCase(); - return name === "input" && elem.type === "button" || name === "button"; - }, - - "text": function( elem ) { - var attr; - // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) - // use getAttribute instead to test this case - return elem.nodeName.toLowerCase() === "input" && - elem.type === "text" && - ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); - }, - - // Position-in-collection - "first": createPositionalPseudo(function() { - return [ 0 ]; - }), - - "last": createPositionalPseudo(function( matchIndexes, length ) { - return [ length - 1 ]; - }), - - "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { - return [ argument < 0 ? argument + length : argument ]; - }), - - "even": createPositionalPseudo(function( matchIndexes, length ) { - var i = 0; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "odd": createPositionalPseudo(function( matchIndexes, length ) { - var i = 1; - for ( ; i < length; i += 2 ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; --i >= 0; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }), - - "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { - var i = argument < 0 ? argument + length : argument; - for ( ; ++i < length; ) { - matchIndexes.push( i ); - } - return matchIndexes; - }) - } -}; - -// Add button/input type pseudos -for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { - Expr.pseudos[ i ] = createInputPseudo( i ); -} -for ( i in { submit: true, reset: true } ) { - Expr.pseudos[ i ] = createButtonPseudo( i ); -} - -function tokenize( selector, parseOnly ) { - var matched, match, tokens, type, - soFar, groups, preFilters, - cached = tokenCache[ selector + " " ]; - - if ( cached ) { - return parseOnly ? 0 : cached.slice( 0 ); - } - - soFar = selector; - groups = []; - preFilters = Expr.preFilter; - - while ( soFar ) { - - // Comma and first run - if ( !matched || (match = rcomma.exec( soFar )) ) { - if ( match ) { - // Don't consume trailing commas as valid - soFar = soFar.slice( match[0].length ) || soFar; - } - groups.push( tokens = [] ); - } - - matched = false; - - // Combinators - if ( (match = rcombinators.exec( soFar )) ) { - matched = match.shift(); - tokens.push( { - value: matched, - // Cast descendant combinators to space - type: match[0].replace( rtrim, " " ) - } ); - soFar = soFar.slice( matched.length ); - } - - // Filters - for ( type in Expr.filter ) { - if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || - (match = preFilters[ type ]( match ))) ) { - matched = match.shift(); - tokens.push( { - value: matched, - type: type, - matches: match - } ); - soFar = soFar.slice( matched.length ); - } - } - - if ( !matched ) { - break; - } - } - - // Return the length of the invalid excess - // if we're just parsing - // Otherwise, throw an error or return tokens - return parseOnly ? - soFar.length : - soFar ? - Sizzle.error( selector ) : - // Cache the tokens - tokenCache( selector, groups ).slice( 0 ); -} - -function toSelector( tokens ) { - var i = 0, - len = tokens.length, - selector = ""; - for ( ; i < len; i++ ) { - selector += tokens[i].value; - } - return selector; -} - -function addCombinator( matcher, combinator, base ) { - var dir = combinator.dir, - checkNonElements = base && dir === "parentNode", - doneName = done++; - - return combinator.first ? - // Check against closest ancestor/preceding element - function( elem, context, xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - return matcher( elem, context, xml ); - } - } - } : - - // Check against all ancestor/preceding elements - function( elem, context, xml ) { - var data, cache, outerCache, - dirkey = dirruns + " " + doneName; - - // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching - if ( xml ) { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - if ( matcher( elem, context, xml ) ) { - return true; - } - } - } - } else { - while ( (elem = elem[ dir ]) ) { - if ( elem.nodeType === 1 || checkNonElements ) { - outerCache = elem[ expando ] || (elem[ expando ] = {}); - if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { - if ( (data = cache[1]) === true || data === cachedruns ) { - return data === true; - } - } else { - cache = outerCache[ dir ] = [ dirkey ]; - cache[1] = matcher( elem, context, xml ) || cachedruns; - if ( cache[1] === true ) { - return true; - } - } - } - } - } - }; -} - -function elementMatcher( matchers ) { - return matchers.length > 1 ? - function( elem, context, xml ) { - var i = matchers.length; - while ( i-- ) { - if ( !matchers[i]( elem, context, xml ) ) { - return false; - } - } - return true; - } : - matchers[0]; -} - -function condense( unmatched, map, filter, context, xml ) { - var elem, - newUnmatched = [], - i = 0, - len = unmatched.length, - mapped = map != null; - - for ( ; i < len; i++ ) { - if ( (elem = unmatched[i]) ) { - if ( !filter || filter( elem, context, xml ) ) { - newUnmatched.push( elem ); - if ( mapped ) { - map.push( i ); - } - } - } - } - - return newUnmatched; -} - -function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { - if ( postFilter && !postFilter[ expando ] ) { - postFilter = setMatcher( postFilter ); - } - if ( postFinder && !postFinder[ expando ] ) { - postFinder = setMatcher( postFinder, postSelector ); - } - return markFunction(function( seed, results, context, xml ) { - var temp, i, elem, - preMap = [], - postMap = [], - preexisting = results.length, - - // Get initial elements from seed or context - elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), - - // Prefilter to get matcher input, preserving a map for seed-results synchronization - matcherIn = preFilter && ( seed || !selector ) ? - condense( elems, preMap, preFilter, context, xml ) : - elems, - - matcherOut = matcher ? - // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, - postFinder || ( seed ? preFilter : preexisting || postFilter ) ? - - // ...intermediate processing is necessary - [] : - - // ...otherwise use results directly - results : - matcherIn; - - // Find primary matches - if ( matcher ) { - matcher( matcherIn, matcherOut, context, xml ); - } - - // Apply postFilter - if ( postFilter ) { - temp = condense( matcherOut, postMap ); - postFilter( temp, [], context, xml ); - - // Un-match failing elements by moving them back to matcherIn - i = temp.length; - while ( i-- ) { - if ( (elem = temp[i]) ) { - matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); - } - } - } - - if ( seed ) { - if ( postFinder || preFilter ) { - if ( postFinder ) { - // Get the final matcherOut by condensing this intermediate into postFinder contexts - temp = []; - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) ) { - // Restore matcherIn since elem is not yet a final match - temp.push( (matcherIn[i] = elem) ); - } - } - postFinder( null, (matcherOut = []), temp, xml ); - } - - // Move matched elements from seed to results to keep them synchronized - i = matcherOut.length; - while ( i-- ) { - if ( (elem = matcherOut[i]) && - (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { - - seed[temp] = !(results[temp] = elem); - } - } - } - - // Add elements to results, through postFinder if defined - } else { - matcherOut = condense( - matcherOut === results ? - matcherOut.splice( preexisting, matcherOut.length ) : - matcherOut - ); - if ( postFinder ) { - postFinder( null, results, matcherOut, xml ); - } else { - push.apply( results, matcherOut ); - } - } - }); -} - -function matcherFromTokens( tokens ) { - var checkContext, matcher, j, - len = tokens.length, - leadingRelative = Expr.relative[ tokens[0].type ], - implicitRelative = leadingRelative || Expr.relative[" "], - i = leadingRelative ? 1 : 0, - - // The foundational matcher ensures that elements are reachable from top-level context(s) - matchContext = addCombinator( function( elem ) { - return elem === checkContext; - }, implicitRelative, true ), - matchAnyContext = addCombinator( function( elem ) { - return indexOf.call( checkContext, elem ) > -1; - }, implicitRelative, true ), - matchers = [ function( elem, context, xml ) { - return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( - (checkContext = context).nodeType ? - matchContext( elem, context, xml ) : - matchAnyContext( elem, context, xml ) ); - } ]; - - for ( ; i < len; i++ ) { - if ( (matcher = Expr.relative[ tokens[i].type ]) ) { - matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; - } else { - matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); - - // Return special upon seeing a positional matcher - if ( matcher[ expando ] ) { - // Find the next relative operator (if any) for proper handling - j = ++i; - for ( ; j < len; j++ ) { - if ( Expr.relative[ tokens[j].type ] ) { - break; - } - } - return setMatcher( - i > 1 && elementMatcher( matchers ), - i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ), - matcher, - i < j && matcherFromTokens( tokens.slice( i, j ) ), - j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), - j < len && toSelector( tokens ) - ); - } - matchers.push( matcher ); - } - } - - return elementMatcher( matchers ); -} - -function matcherFromGroupMatchers( elementMatchers, setMatchers ) { - // A counter to specify which element is currently being matched - var matcherCachedRuns = 0, - bySet = setMatchers.length > 0, - byElement = elementMatchers.length > 0, - superMatcher = function( seed, context, xml, results, expandContext ) { - var elem, j, matcher, - setMatched = [], - matchedCount = 0, - i = "0", - unmatched = seed && [], - outermost = expandContext != null, - contextBackup = outermostContext, - // We must always have either seed elements or context - elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), - // Use integer dirruns iff this is the outermost matcher - dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); - - if ( outermost ) { - outermostContext = context !== document && context; - cachedruns = matcherCachedRuns; - } - - // Add elements passing elementMatchers directly to results - // Keep `i` a string if there are no elements so `matchedCount` will be "00" below - for ( ; (elem = elems[i]) != null; i++ ) { - if ( byElement && elem ) { - j = 0; - while ( (matcher = elementMatchers[j++]) ) { - if ( matcher( elem, context, xml ) ) { - results.push( elem ); - break; - } - } - if ( outermost ) { - dirruns = dirrunsUnique; - cachedruns = ++matcherCachedRuns; - } - } - - // Track unmatched elements for set filters - if ( bySet ) { - // They will have gone through all possible matchers - if ( (elem = !matcher && elem) ) { - matchedCount--; - } - - // Lengthen the array for every element, matched or not - if ( seed ) { - unmatched.push( elem ); - } - } - } - - // Apply set filters to unmatched elements - matchedCount += i; - if ( bySet && i !== matchedCount ) { - j = 0; - while ( (matcher = setMatchers[j++]) ) { - matcher( unmatched, setMatched, context, xml ); - } - - if ( seed ) { - // Reintegrate element matches to eliminate the need for sorting - if ( matchedCount > 0 ) { - while ( i-- ) { - if ( !(unmatched[i] || setMatched[i]) ) { - setMatched[i] = pop.call( results ); - } - } - } - - // Discard index placeholder values to get only actual matches - setMatched = condense( setMatched ); - } - - // Add matches to results - push.apply( results, setMatched ); - - // Seedless set matches succeeding multiple successful matchers stipulate sorting - if ( outermost && !seed && setMatched.length > 0 && - ( matchedCount + setMatchers.length ) > 1 ) { - - Sizzle.uniqueSort( results ); - } - } - - // Override manipulation of globals by nested matchers - if ( outermost ) { - dirruns = dirrunsUnique; - outermostContext = contextBackup; - } - - return unmatched; - }; - - return bySet ? - markFunction( superMatcher ) : - superMatcher; -} - -compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) { - var i, - setMatchers = [], - elementMatchers = [], - cached = compilerCache[ selector + " " ]; - - if ( !cached ) { - // Generate a function of recursive functions that can be used to check each element - if ( !group ) { - group = tokenize( selector ); - } - i = group.length; - while ( i-- ) { - cached = matcherFromTokens( group[i] ); - if ( cached[ expando ] ) { - setMatchers.push( cached ); - } else { - elementMatchers.push( cached ); - } - } - - // Cache the compiled function - cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); - } - return cached; -}; - -function multipleContexts( selector, contexts, results ) { - var i = 0, - len = contexts.length; - for ( ; i < len; i++ ) { - Sizzle( selector, contexts[i], results ); - } - return results; -} - -function select( selector, context, results, seed ) { - var i, tokens, token, type, find, - match = tokenize( selector ); - - if ( !seed ) { - // Try to minimize operations if there is only one group - if ( match.length === 1 ) { - - // Take a shortcut and set the context if the root selector is an ID - tokens = match[0] = match[0].slice( 0 ); - if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && - context.nodeType === 9 && !documentIsXML && - Expr.relative[ tokens[1].type ] ) { - - context = Expr.find["ID"]( token.matches[0].replace( runescape, funescape ), context )[0]; - if ( !context ) { - return results; - } - - selector = selector.slice( tokens.shift().value.length ); - } - - // Fetch a seed set for right-to-left matching - i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; - while ( i-- ) { - token = tokens[i]; - - // Abort if we hit a combinator - if ( Expr.relative[ (type = token.type) ] ) { - break; - } - if ( (find = Expr.find[ type ]) ) { - // Search, expanding context for leading sibling combinators - if ( (seed = find( - token.matches[0].replace( runescape, funescape ), - rsibling.test( tokens[0].type ) && context.parentNode || context - )) ) { - - // If seed is empty or no tokens remain, we can return early - tokens.splice( i, 1 ); - selector = seed.length && toSelector( tokens ); - if ( !selector ) { - push.apply( results, slice.call( seed, 0 ) ); - return results; - } - - break; - } - } - } - } - } - - // Compile and execute a filtering function - // Provide `match` to avoid retokenization if we modified the selector above - compile( selector, match )( - seed, - context, - documentIsXML, - results, - rsibling.test( selector ) - ); - return results; -} - -// Deprecated -Expr.pseudos["nth"] = Expr.pseudos["eq"]; - -// Easy API for creating new setFilters -function setFilters() {} -Expr.filters = setFilters.prototype = Expr.pseudos; -Expr.setFilters = new setFilters(); - -// Initialize with the default document -setDocument(); - -// Override sizzle attribute retrieval -Sizzle.attr = jQuery.attr; -jQuery.find = Sizzle; -jQuery.expr = Sizzle.selectors; -jQuery.expr[":"] = jQuery.expr.pseudos; -jQuery.unique = Sizzle.uniqueSort; -jQuery.text = Sizzle.getText; -jQuery.isXMLDoc = Sizzle.isXML; -jQuery.contains = Sizzle.contains; - - -})( window ); -var runtil = /Until$/, - rparentsprev = /^(?:parents|prev(?:Until|All))/, - isSimple = /^.[^:#\[\.,]*$/, - rneedsContext = jQuery.expr.match.needsContext, - // methods guaranteed to produce a unique set when starting from a unique set - guaranteedUnique = { - children: true, - contents: true, - next: true, - prev: true - }; - -jQuery.fn.extend({ - find: function( selector ) { - var i, ret, self, - len = this.length; - - if ( typeof selector !== "string" ) { - self = this; - return this.pushStack( jQuery( selector ).filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( self[ i ], this ) ) { - return true; - } - } - }) ); - } - - ret = []; - for ( i = 0; i < len; i++ ) { - jQuery.find( selector, this[ i ], ret ); - } - - // Needed because $( selector, context ) becomes $( context ).find( selector ) - ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); - ret.selector = ( this.selector ? this.selector + " " : "" ) + selector; - return ret; - }, - - has: function( target ) { - var i, - targets = jQuery( target, this ), - len = targets.length; - - return this.filter(function() { - for ( i = 0; i < len; i++ ) { - if ( jQuery.contains( this, targets[i] ) ) { - return true; - } - } - }); - }, - - not: function( selector ) { - return this.pushStack( winnow(this, selector, false) ); - }, - - filter: function( selector ) { - return this.pushStack( winnow(this, selector, true) ); - }, - - is: function( selector ) { - return !!selector && ( - typeof selector === "string" ? - // If this is a positional/relative selector, check membership in the returned set - // so $("p:first").is("p:last") won't return true for a doc with two "p". - rneedsContext.test( selector ) ? - jQuery( selector, this.context ).index( this[0] ) >= 0 : - jQuery.filter( selector, this ).length > 0 : - this.filter( selector ).length > 0 ); - }, - - closest: function( selectors, context ) { - var cur, - i = 0, - l = this.length, - ret = [], - pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? - jQuery( selectors, context || this.context ) : - 0; - - for ( ; i < l; i++ ) { - cur = this[i]; - - while ( cur && cur.ownerDocument && cur !== context && cur.nodeType !== 11 ) { - if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { - ret.push( cur ); - break; - } - cur = cur.parentNode; - } - } - - return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); - }, - - // Determine the position of an element within - // the matched set of elements - index: function( elem ) { - - // No argument, return index in parent - if ( !elem ) { - return ( this[0] && this[0].parentNode ) ? this.first().prevAll().length : -1; - } - - // index in selector - if ( typeof elem === "string" ) { - return jQuery.inArray( this[0], jQuery( elem ) ); - } - - // Locate the position of the desired element - return jQuery.inArray( - // If it receives a jQuery object, the first element is used - elem.jquery ? elem[0] : elem, this ); - }, - - add: function( selector, context ) { - var set = typeof selector === "string" ? - jQuery( selector, context ) : - jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), - all = jQuery.merge( this.get(), set ); - - return this.pushStack( jQuery.unique(all) ); - }, - - addBack: function( selector ) { - return this.add( selector == null ? - this.prevObject : this.prevObject.filter(selector) - ); - } -}); - -jQuery.fn.andSelf = jQuery.fn.addBack; - -function sibling( cur, dir ) { - do { - cur = cur[ dir ]; - } while ( cur && cur.nodeType !== 1 ); - - return cur; -} - -jQuery.each({ - parent: function( elem ) { - var parent = elem.parentNode; - return parent && parent.nodeType !== 11 ? parent : null; - }, - parents: function( elem ) { - return jQuery.dir( elem, "parentNode" ); - }, - parentsUntil: function( elem, i, until ) { - return jQuery.dir( elem, "parentNode", until ); - }, - next: function( elem ) { - return sibling( elem, "nextSibling" ); - }, - prev: function( elem ) { - return sibling( elem, "previousSibling" ); - }, - nextAll: function( elem ) { - return jQuery.dir( elem, "nextSibling" ); - }, - prevAll: function( elem ) { - return jQuery.dir( elem, "previousSibling" ); - }, - nextUntil: function( elem, i, until ) { - return jQuery.dir( elem, "nextSibling", until ); - }, - prevUntil: function( elem, i, until ) { - return jQuery.dir( elem, "previousSibling", until ); - }, - siblings: function( elem ) { - return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); - }, - children: function( elem ) { - return jQuery.sibling( elem.firstChild ); - }, - contents: function( elem ) { - return jQuery.nodeName( elem, "iframe" ) ? - elem.contentDocument || elem.contentWindow.document : - jQuery.merge( [], elem.childNodes ); - } -}, function( name, fn ) { - jQuery.fn[ name ] = function( until, selector ) { - var ret = jQuery.map( this, fn, until ); - - if ( !runtil.test( name ) ) { - selector = until; - } - - if ( selector && typeof selector === "string" ) { - ret = jQuery.filter( selector, ret ); - } - - ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; - - if ( this.length > 1 && rparentsprev.test( name ) ) { - ret = ret.reverse(); - } - - return this.pushStack( ret ); - }; -}); - -jQuery.extend({ - filter: function( expr, elems, not ) { - if ( not ) { - expr = ":not(" + expr + ")"; - } - - return elems.length === 1 ? - jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : - jQuery.find.matches(expr, elems); - }, - - dir: function( elem, dir, until ) { - var matched = [], - cur = elem[ dir ]; - - while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { - if ( cur.nodeType === 1 ) { - matched.push( cur ); - } - cur = cur[dir]; - } - return matched; - }, - - sibling: function( n, elem ) { - var r = []; - - for ( ; n; n = n.nextSibling ) { - if ( n.nodeType === 1 && n !== elem ) { - r.push( n ); - } - } - - return r; - } -}); - -// Implement the identical functionality for filter and not -function winnow( elements, qualifier, keep ) { - - // Can't pass null or undefined to indexOf in Firefox 4 - // Set to 0 to skip string check - qualifier = qualifier || 0; - - if ( jQuery.isFunction( qualifier ) ) { - return jQuery.grep(elements, function( elem, i ) { - var retVal = !!qualifier.call( elem, i, elem ); - return retVal === keep; - }); - - } else if ( qualifier.nodeType ) { - return jQuery.grep(elements, function( elem ) { - return ( elem === qualifier ) === keep; - }); - - } else if ( typeof qualifier === "string" ) { - var filtered = jQuery.grep(elements, function( elem ) { - return elem.nodeType === 1; - }); - - if ( isSimple.test( qualifier ) ) { - return jQuery.filter(qualifier, filtered, !keep); - } else { - qualifier = jQuery.filter( qualifier, filtered ); - } - } - - return jQuery.grep(elements, function( elem ) { - return ( jQuery.inArray( elem, qualifier ) >= 0 ) === keep; - }); -} -function createSafeFragment( document ) { - var list = nodeNames.split( "|" ), - safeFrag = document.createDocumentFragment(); - - if ( safeFrag.createElement ) { - while ( list.length ) { - safeFrag.createElement( - list.pop() - ); - } - } - return safeFrag; -} - -var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + - "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", - rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), - rleadingWhitespace = /^\s+/, - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, - rtagName = /<([\w:]+)/, - rtbody = /\s*$/g, - - // We have to close these tags to support XHTML (#13200) - wrapMap = { - option: [ 1, "" ], - legend: [ 1, "
    ", "
    " ], - area: [ 1, "", "" ], - param: [ 1, "", "" ], - thead: [ 1, "", "
    " ], - tr: [ 2, "", "
    " ], - col: [ 2, "", "
    " ], - td: [ 3, "", "
    " ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] - }, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement("div") ); - -wrapMap.optgroup = wrapMap.option; -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - -jQuery.fn.extend({ - text: function( value ) { - return jQuery.access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( value ) ); - }, null, value, arguments.length ); - }, - - wrapAll: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapAll( html.call(this, i) ); - }); - } - - if ( this[0] ) { - // The elements to wrap the target around - var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); - - if ( this[0].parentNode ) { - wrap.insertBefore( this[0] ); - } - - wrap.map(function() { - var elem = this; - - while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { - elem = elem.firstChild; - } - - return elem; - }).append( this ); - } - - return this; - }, - - wrapInner: function( html ) { - if ( jQuery.isFunction( html ) ) { - return this.each(function(i) { - jQuery(this).wrapInner( html.call(this, i) ); - }); - } - - return this.each(function() { - var self = jQuery( this ), - contents = self.contents(); - - if ( contents.length ) { - contents.wrapAll( html ); - - } else { - self.append( html ); - } - }); - }, - - wrap: function( html ) { - var isFunction = jQuery.isFunction( html ); - - return this.each(function(i) { - jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); - }); - }, - - unwrap: function() { - return this.parent().each(function() { - if ( !jQuery.nodeName( this, "body" ) ) { - jQuery( this ).replaceWith( this.childNodes ); - } - }).end(); - }, - - append: function() { - return this.domManip(arguments, true, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.appendChild( elem ); - } - }); - }, - - prepend: function() { - return this.domManip(arguments, true, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - this.insertBefore( elem, this.firstChild ); - } - }); - }, - - before: function() { - return this.domManip( arguments, false, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - }); - }, - - after: function() { - return this.domManip( arguments, false, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - }); - }, - - // keepData is for internal use only--do not document - remove: function( selector, keepData ) { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - if ( !selector || jQuery.filter( selector, [ elem ] ).length > 0 ) { - if ( !keepData && elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem ) ); - } - - if ( elem.parentNode ) { - if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { - setGlobalEval( getAll( elem, "script" ) ); - } - elem.parentNode.removeChild( elem ); - } - } - } - - return this; - }, - - empty: function() { - var elem, - i = 0; - - for ( ; (elem = this[i]) != null; i++ ) { - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function () { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - }); - }, - - html: function( value ) { - return jQuery.access( this, function( value ) { - var elem = this[0] || {}, - i = 0, - l = this.length; - - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { - - value = value.replace( rxhtmlTag, "<$1>" ); - - try { - for (; i < l; i++ ) { - // Remove element nodes and prevent memory leaks - elem = this[i] || {}; - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch(e) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function( value ) { - var isFunc = jQuery.isFunction( value ); - - // Make sure that the elements are removed from the DOM before they are inserted - // this can help fix replacing a parent with child elements - if ( !isFunc && typeof value !== "string" ) { - value = jQuery( value ).not( this ).detach(); - } - - return this.domManip( [ value ], true, function( elem ) { - var next = this.nextSibling, - parent = this.parentNode; - - if ( parent ) { - jQuery( this ).remove(); - parent.insertBefore( elem, next ); - } - }); - }, - - detach: function( selector ) { - return this.remove( selector, true ); - }, - - domManip: function( args, table, callback ) { - - // Flatten any nested arrays - args = core_concat.apply( [], args ); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = this.length, - set = this, - iNoClone = l - 1, - value = args[0], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || !( l <= 1 || typeof value !== "string" || jQuery.support.checkClone || !rchecked.test( value ) ) ) { - return this.each(function( index ) { - var self = set.eq( index ); - if ( isFunction ) { - args[0] = value.call( this, index, table ? self.html() : undefined ); - } - self.domManip( args, table, callback ); - }); - } - - if ( l ) { - fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - if ( first ) { - table = table && jQuery.nodeName( first, "tr" ); - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( - table && jQuery.nodeName( this[i], "table" ) ? - findOrAppend( this[i], "tbody" ) : - this[i], - node, - i - ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { - - if ( node.src ) { - // Hope ajax is available... - jQuery.ajax({ - url: node.src, - type: "GET", - dataType: "script", - async: false, - global: false, - "throws": true - }); - } else { - jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return this; - } -}); - -function findOrAppend( elem, tag ) { - return elem.getElementsByTagName( tag )[0] || elem.appendChild( elem.ownerDocument.createElement( tag ) ); -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - var attr = elem.getAttributeNode("type"); - elem.type = ( attr && attr.specified ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[1]; - } else { - elem.removeAttribute("type"); - } - return elem; -} - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; (elem = elems[i]) != null; i++ ) { - jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); - } -} - -function cloneCopyEvent( src, dest ) { - - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -jQuery.each({ - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - i = 0, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone(true); - jQuery( insert[i] )[ original ]( elems ); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - core_push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -}); - -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - -// Used in buildFragment, fixes the defaultChecked property -function fixDefaultChecked( elem ) { - if ( manipulation_rcheckableType.test( elem.type ) ) { - elem.defaultChecked = elem.checked; - } -} - -jQuery.extend({ - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); - - if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( (!jQuery.support.noCloneEvent || !jQuery.support.noCloneChecked) && - (elem.nodeType === 1 || elem.nodeType === 11) && !jQuery.isXMLDoc(elem) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; (node = srcElements[i]) != null; ++i ) { - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[i] ) { - fixCloneNodeIssues( node, destElements[i] ); - } - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0; (node = srcElements[i]) != null; i++ ) { - cloneCopyEvent( node, destElements[i] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - buildFragment: function( elems, context, scripts, selection ) { - var j, elem, contains, - tmp, tag, tbody, wrap, - l = elems.length, - - // Ensure a safe fragment - safe = createSafeFragment( context ), - - nodes = [], - i = 0; - - for ( ; i < l; i++ ) { - elem = elems[ i ]; - - if ( elem || elem === 0 ) { - - // Add nodes directly - if ( jQuery.type( elem ) === "object" ) { - jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); - - // Convert non-html into a text node - } else if ( !rhtml.test( elem ) ) { - nodes.push( context.createTextNode( elem ) ); - - // Convert html into DOM nodes - } else { - tmp = tmp || safe.appendChild( context.createElement("div") ); - - // Deserialize a standard representation - tag = ( rtagName.exec( elem ) || ["", ""] )[1].toLowerCase(); - wrap = wrapMap[ tag ] || wrapMap._default; - - tmp.innerHTML = wrap[1] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[2]; - - // Descend through wrappers to the right content - j = wrap[0]; - while ( j-- ) { - tmp = tmp.lastChild; - } - - // Manually add leading whitespace removed by IE - if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { - nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); - } - - // Remove IE's autoinserted from table fragments - if ( !jQuery.support.tbody ) { - - // String was a , *may* have spurious - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : - - // String was a bare or - wrap[1] === "
    " && !rtbody.test( elem ) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { - elem.removeChild( tbody ); - } - } - } - - jQuery.merge( nodes, tmp.childNodes ); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !jQuery.support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } - - i = 0; - while ( (elem = nodes[ i++ ]) ) { - - // #4087 - If origin and destination elements are the same, and this is - // that element, do not do anything - if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( (elem = tmp[ j++ ]) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - tmp = null; - - return safe; - }, - - cleanData: function( elems, /* internal */ acceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - deleteExpando = jQuery.support.deleteExpando, - special = jQuery.event.special; - - for ( ; (elem = elems[i]) != null; i++ ) { - - if ( acceptData || jQuery.acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { - - delete cache[ id ]; - - // IE does not allow us to delete expando properties from nodes, - // nor does it have a removeAttribute function on Document nodes; - // we must handle all of these cases - if ( deleteExpando ) { - delete elem[ internalKey ]; - - } else if ( typeof elem.removeAttribute !== core_strundefined ) { - elem.removeAttribute( internalKey ); - - } else { - elem[ internalKey ] = null; - } - - core_deletedIds.push( id ); - } - } - } - } - } -}); -var iframe, getStyles, curCSS, - ralpha = /alpha\([^)]*\)/i, - ropacity = /opacity\s*=\s*([^)]*)/, - rposition = /^(top|right|bottom|left)$/, - // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" - // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display - rdisplayswap = /^(none|table(?!-c[ea]).+)/, - rmargin = /^margin/, - rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), - rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), - rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), - elemdisplay = { BODY: "block" }, - - cssShow = { position: "absolute", visibility: "hidden", display: "block" }, - cssNormalTransform = { - letterSpacing: 0, - fontWeight: 400 - }, - - cssExpand = [ "Top", "Right", "Bottom", "Left" ], - cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; - -// return a css property mapped to a potentially vendor prefixed property -function vendorPropName( style, name ) { - - // shortcut for names that are not vendor prefixed - if ( name in style ) { - return name; - } - - // check for vendor prefixed names - var capName = name.charAt(0).toUpperCase() + name.slice(1), - origName = name, - i = cssPrefixes.length; - - while ( i-- ) { - name = cssPrefixes[ i ] + capName; - if ( name in style ) { - return name; - } - } - - return origName; -} - -function isHidden( elem, el ) { - // isHidden might be called from jQuery#filter function; - // in that case, element will be second argument - elem = el || elem; - return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); -} - -function showHide( elements, show ) { - var display, elem, hidden, - values = [], - index = 0, - length = elements.length; - - for ( ; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - - values[ index ] = jQuery._data( elem, "olddisplay" ); - display = elem.style.display; - if ( show ) { - // Reset the inline display of this element to learn if it is - // being hidden by cascaded rules or not - if ( !values[ index ] && display === "none" ) { - elem.style.display = ""; - } - - // Set elements which have been overridden with display: none - // in a stylesheet to whatever the default browser style is - // for such an element - if ( elem.style.display === "" && isHidden( elem ) ) { - values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); - } - } else { - - if ( !values[ index ] ) { - hidden = isHidden( elem ); - - if ( display && display !== "none" || !hidden ) { - jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); - } - } - } - } - - // Set the display of most of the elements in a second loop - // to avoid the constant reflow - for ( index = 0; index < length; index++ ) { - elem = elements[ index ]; - if ( !elem.style ) { - continue; - } - if ( !show || elem.style.display === "none" || elem.style.display === "" ) { - elem.style.display = show ? values[ index ] || "" : "none"; - } - } - - return elements; -} - -jQuery.fn.extend({ - css: function( name, value ) { - return jQuery.access( this, function( elem, name, value ) { - var len, styles, - map = {}, - i = 0; - - if ( jQuery.isArray( name ) ) { - styles = getStyles( elem ); - len = name.length; - - for ( ; i < len; i++ ) { - map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); - } - - return map; - } - - return value !== undefined ? - jQuery.style( elem, name, value ) : - jQuery.css( elem, name ); - }, name, value, arguments.length > 1 ); - }, - show: function() { - return showHide( this, true ); - }, - hide: function() { - return showHide( this ); - }, - toggle: function( state ) { - var bool = typeof state === "boolean"; - - return this.each(function() { - if ( bool ? state : isHidden( this ) ) { - jQuery( this ).show(); - } else { - jQuery( this ).hide(); - } - }); - } -}); - -jQuery.extend({ - // Add in style property hooks for overriding the default - // behavior of getting and setting a style property - cssHooks: { - opacity: { - get: function( elem, computed ) { - if ( computed ) { - // We should always get a number back from opacity - var ret = curCSS( elem, "opacity" ); - return ret === "" ? "1" : ret; - } - } - } - }, - - // Exclude the following css properties to add px - cssNumber: { - "columnCount": true, - "fillOpacity": true, - "fontWeight": true, - "lineHeight": true, - "opacity": true, - "orphans": true, - "widows": true, - "zIndex": true, - "zoom": true - }, - - // Add in properties whose names you wish to fix before - // setting or getting the value - cssProps: { - // normalize float css property - "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" - }, - - // Get and set the style property on a DOM Node - style: function( elem, name, value, extra ) { - // Don't set styles on text and comment nodes - if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { - return; - } - - // Make sure that we're working with the right name - var ret, type, hooks, - origName = jQuery.camelCase( name ), - style = elem.style; - - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // Check if we're setting a value - if ( value !== undefined ) { - type = typeof value; - - // convert relative number strings (+= or -=) to relative numbers. #7345 - if ( type === "string" && (ret = rrelNum.exec( value )) ) { - value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); - // Fixes bug #9237 - type = "number"; - } - - // Make sure that NaN and null values aren't set. See: #7116 - if ( value == null || type === "number" && isNaN( value ) ) { - return; - } - - // If a number was passed in, add 'px' to the (except for certain CSS properties) - if ( type === "number" && !jQuery.cssNumber[ origName ] ) { - value += "px"; - } - - // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, - // but it would mean to define eight (for every problematic property) identical functions - if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { - style[ name ] = "inherit"; - } - - // If a hook was provided, use that value, otherwise just set the specified value - if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { - - // Wrapped to prevent IE from throwing errors when 'invalid' values are provided - // Fixes bug #5509 - try { - style[ name ] = value; - } catch(e) {} - } - - } else { - // If a hook was provided get the non-computed value from there - if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { - return ret; - } - - // Otherwise just get the value from the style object - return style[ name ]; - } - }, - - css: function( elem, name, extra, styles ) { - var num, val, hooks, - origName = jQuery.camelCase( name ); - - // Make sure that we're working with the right name - name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); - - // gets hook for the prefixed version - // followed by the unprefixed version - hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; - - // If a hook was provided get the computed value from there - if ( hooks && "get" in hooks ) { - val = hooks.get( elem, true, extra ); - } - - // Otherwise, if a way to get the computed value exists, use that - if ( val === undefined ) { - val = curCSS( elem, name, styles ); - } - - //convert "normal" to computed value - if ( val === "normal" && name in cssNormalTransform ) { - val = cssNormalTransform[ name ]; - } - - // Return, converting to number if forced or a qualifier was provided and val looks numeric - if ( extra === "" || extra ) { - num = parseFloat( val ); - return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; - } - return val; - }, - - // A method for quickly swapping in/out CSS properties to get correct calculations - swap: function( elem, options, callback, args ) { - var ret, name, - old = {}; - - // Remember the old values, and insert the new ones - for ( name in options ) { - old[ name ] = elem.style[ name ]; - elem.style[ name ] = options[ name ]; - } - - ret = callback.apply( elem, args || [] ); - - // Revert the old values - for ( name in options ) { - elem.style[ name ] = old[ name ]; - } - - return ret; - } -}); - -// NOTE: we've included the "window" in window.getComputedStyle -// because jsdom on node.js will break without it. -if ( window.getComputedStyle ) { - getStyles = function( elem ) { - return window.getComputedStyle( elem, null ); - }; - - curCSS = function( elem, name, _computed ) { - var width, minWidth, maxWidth, - computed = _computed || getStyles( elem ), - - // getPropertyValue is only needed for .css('filter') in IE9, see #12537 - ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, - style = elem.style; - - if ( computed ) { - - if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { - ret = jQuery.style( elem, name ); - } - - // A tribute to the "awesome hack by Dean Edwards" - // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right - // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels - // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values - if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { - - // Remember the original values - width = style.width; - minWidth = style.minWidth; - maxWidth = style.maxWidth; - - // Put in the new values to get a computed value out - style.minWidth = style.maxWidth = style.width = ret; - ret = computed.width; - - // Revert the changed values - style.width = width; - style.minWidth = minWidth; - style.maxWidth = maxWidth; - } - } - - return ret; - }; -} else if ( document.documentElement.currentStyle ) { - getStyles = function( elem ) { - return elem.currentStyle; - }; - - curCSS = function( elem, name, _computed ) { - var left, rs, rsLeft, - computed = _computed || getStyles( elem ), - ret = computed ? computed[ name ] : undefined, - style = elem.style; - - // Avoid setting ret to empty string here - // so we don't default to auto - if ( ret == null && style && style[ name ] ) { - ret = style[ name ]; - } - - // From the awesome hack by Dean Edwards - // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 - - // If we're not dealing with a regular pixel number - // but a number that has a weird ending, we need to convert it to pixels - // but not position css attributes, as those are proportional to the parent element instead - // and we can't measure the parent instead because it might trigger a "stacking dolls" problem - if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { - - // Remember the original values - left = style.left; - rs = elem.runtimeStyle; - rsLeft = rs && rs.left; - - // Put in the new values to get a computed value out - if ( rsLeft ) { - rs.left = elem.currentStyle.left; - } - style.left = name === "fontSize" ? "1em" : ret; - ret = style.pixelLeft + "px"; - - // Revert the changed values - style.left = left; - if ( rsLeft ) { - rs.left = rsLeft; - } - } - - return ret === "" ? "auto" : ret; - }; -} - -function setPositiveNumber( elem, value, subtract ) { - var matches = rnumsplit.exec( value ); - return matches ? - // Guard against undefined "subtract", e.g., when used as in cssHooks - Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : - value; -} - -function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { - var i = extra === ( isBorderBox ? "border" : "content" ) ? - // If we already have the right measurement, avoid augmentation - 4 : - // Otherwise initialize for horizontal or vertical properties - name === "width" ? 1 : 0, - - val = 0; - - for ( ; i < 4; i += 2 ) { - // both box models exclude margin, so add it if we want it - if ( extra === "margin" ) { - val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); - } - - if ( isBorderBox ) { - // border-box includes padding, so remove it if we want content - if ( extra === "content" ) { - val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - } - - // at this point, extra isn't border nor margin, so remove border - if ( extra !== "margin" ) { - val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } else { - // at this point, extra isn't content, so add padding - val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); - - // at this point, extra isn't content nor padding, so add border - if ( extra !== "padding" ) { - val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); - } - } - } - - return val; -} - -function getWidthOrHeight( elem, name, extra ) { - - // Start with offset property, which is equivalent to the border-box value - var valueIsBorderBox = true, - val = name === "width" ? elem.offsetWidth : elem.offsetHeight, - styles = getStyles( elem ), - isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; - - // some non-html elements return undefined for offsetWidth, so check for null/undefined - // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 - // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 - if ( val <= 0 || val == null ) { - // Fall back to computed then uncomputed css if necessary - val = curCSS( elem, name, styles ); - if ( val < 0 || val == null ) { - val = elem.style[ name ]; - } - - // Computed unit is not pixels. Stop here and return. - if ( rnumnonpx.test(val) ) { - return val; - } - - // we need the check for style in case a browser which returns unreliable values - // for getComputedStyle silently falls back to the reliable elem.style - valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); - - // Normalize "", auto, and prepare for extra - val = parseFloat( val ) || 0; - } - - // use the active box-sizing model to add/subtract irrelevant styles - return ( val + - augmentWidthOrHeight( - elem, - name, - extra || ( isBorderBox ? "border" : "content" ), - valueIsBorderBox, - styles - ) - ) + "px"; -} - -// Try to determine the default display value of an element -function css_defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - // Use the already-created iframe if possible - iframe = ( iframe || - jQuery("