├── README.markdown ├── docs ├── Makefile ├── conf.py ├── index.rst ├── make.bat └── usage.rst ├── examples └── index.html ├── jquery.stopwatch.js └── test ├── index.html ├── qunit ├── qunit.css └── qunit.js └── test.js /README.markdown: -------------------------------------------------------------------------------- 1 | # Simple count-up jQuery plugin 2 | 3 | ## Summary 4 | 5 | A jQuery plugin that renders a count-up clock from a defined start time. Supports `start`, `stop`, `toggle`, `reset`, and custom 'tick' event. 6 | 7 | ## Usage 8 | 9 | * Initialise and start a default timer 10 | 11 | ```js 12 | $('').stopwatch().stopwatch('start') 13 | ``` 14 | 15 | 16 | * Initialise and bind start/stop to click 17 | 18 | ```js 19 | $('').stopwatch().click(function(){ 20 | $(this).stopwatch('toggle') 21 | }) 22 | ``` 23 | 24 | * Bind to tick event and reset when 10 seconds has elapsed 25 | 26 | ```js 27 | $('').stopwatch().bind('tick.stopwatch', function(e, elapsed){ 28 | if (elapsed >= 10000) { 29 | $(this).stopwatch('reset'); 30 | } 31 | }).stopwatch('start') 32 | ``` 33 | 34 | * Start at non-zero elapsed time 35 | 36 | ```js 37 | $('').stopwatch({startTime: 10000000}).stopwatch('start') 38 | ``` 39 | 40 | ## Formatting 41 | 42 | Provided by the [jintervals](https://github.com/nene/jintervals) lib. If you don't 43 | include this library, the default output format `HH:MM:SS` is used. 44 | 45 | ### Formats 46 | 47 | Pass a format pattern as the `format` option. jsintervals provides a rich syntax for formatting 48 | time intervals, perhaps best illustrated by some examples. 49 | 50 | ```js 51 | '{MM}:{ss}' --> 01:05 52 | '{MMM}:{ss}' --> 001:05 53 | '{M}m, {s}s' --> 1m, 5s 54 | '{Minutes} and {seconds}' --> 1 minute and 5 seconds 55 | '{Greatest} ago' --> 1 minute ago 56 | ``` 57 | 58 | The full documentation of the syntax is at http://code.google.com/p/jintervals/wiki/Documentation. 59 | 60 | ### Custom formatter 61 | 62 | A formatter function can be supplied as `formatter` in options. It receives `milliseconds` and 63 | `options` and must return a string. 64 | 65 | ## Licence 66 | 67 | Copyright (c) 2012 Rob Cowie. Licensed under the MIT license. -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/jQueryStopwatch.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/jQueryStopwatch.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/jQueryStopwatch" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/jQueryStopwatch" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # jQuery Stopwatch documentation build configuration file, created by 4 | # sphinx-quickstart on Thu Oct 13 14:02:25 2011. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = [] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'jQuery Stopwatch' 44 | copyright = u'2011, Rob Cowie' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.0.3' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.0.3' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'jQueryStopwatchdoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'jQueryStopwatch.tex', u'jQuery Stopwatch Documentation', 187 | u'Rob Cowie', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'jquerystopwatch', u'jQuery Stopwatch Documentation', 217 | [u'Rob Cowie'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'jQueryStopwatch', u'jQuery Stopwatch Documentation', u'Rob Cowie', 231 | 'jQueryStopwatch', 'One line description of project.', 'Miscellaneous'), 232 | ] 233 | 234 | # Documents to append as an appendix to all manuals. 235 | #texinfo_appendices = [] 236 | 237 | # If false, no module index is generated. 238 | #texinfo_domain_indices = True 239 | 240 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 241 | #texinfo_show_urls = 'footnote' 242 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: javascript 2 | 3 | Welcome to jQuery Stopwatch's documentation 4 | =========================================== 5 | 6 | An easy to use jquery plugin to render and control a stopwatch (count-up) clock. 7 | 8 | Starting a timer is as easy as 9 | 10 | :: 11 | 12 | $('').stopwatch().stopwatch('start'); 13 | 14 | See some more :ref:`usage examples` -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\jQueryStopwatch.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\jQueryStopwatch.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/usage.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: javascript 2 | 3 | .. _usage: 4 | 5 | Some usage examples 6 | =================== 7 | 8 | 1. Initialise and start a default timer 9 | --------------------------------------- 10 | 11 | :: 12 | 13 | $('').stopwatch().stopwatch('start'); 14 | 15 | 16 | 2. Initialise and bind start/stop to click 17 | ------------------------------------------ 18 | 19 | :: 20 | 21 | $('').stopwatch().click(function(){ 22 | $(this).stopwatch('toggle') 23 | }); 24 | 25 | 26 | 3. Bind to ``tick`` event and reset when 10 seconds has elapsed 27 | --------------------------------------------------------------- 28 | 29 | :: 30 | 31 | $('').stopwatch().bind('tick.stopwatch', function(e, elapsed){ 32 | if (elapsed >= 10000) { 33 | $(this).stopwatch('reset') 34 | } 35 | }).stopwatch('start'); 36 | 37 | 38 | 4. Start at non-zero elapsed time 39 | --------------------------------- 40 | 41 | :: 42 | 43 | $('').stopwatch({startTime:10000000}).stopwatch('start'); -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | jquery.stopwatch.js demo 13 | 14 | 15 | 16 | 17 | 54 | 55 | 56 | 57 | 58 |
59 |
60 |

jquery.stopwatch.js Examples

61 |
62 | 63 |
64 |
65 |
Count up from now
66 |
00:00:00
67 |
68 |
69 |
Click to toggle (start/stop)
70 |
00:00:00
71 |
72 |
73 |
Click to reset
74 |
00:00:00
75 |
76 |
77 |
Count up from seed time (10000000 milliseconds)
78 |
00:00:00
79 |
80 |
81 |
2 second update interval
82 |
00:00:00
83 |
84 |
85 |
Custom format using jintervals
86 |
00:00:00
87 |
88 |
89 | 90 |
91 |
92 | 93 | 94 | 95 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /jquery.stopwatch.js: -------------------------------------------------------------------------------- 1 | (function( $ ){ 2 | 3 | function incrementer(ct, increment) { 4 | return function() { ct+=increment; return ct; }; 5 | } 6 | 7 | function pad2(number) { 8 | return (number < 10 ? '0' : '') + number; 9 | } 10 | 11 | function defaultFormatMilliseconds(millis) { 12 | var x, seconds, minutes, hours; 13 | x = millis / 1000; 14 | seconds = Math.floor(x % 60); 15 | x /= 60; 16 | minutes = Math.floor(x % 60); 17 | x /= 60; 18 | hours = Math.floor(x % 24); 19 | // x /= 24; 20 | // days = Math.floor(x); 21 | return [pad2(hours), pad2(minutes), pad2(seconds)].join(':'); 22 | } 23 | 24 | //NOTE: This is a the 'lazy func def' pattern described at http://michaux.ca/articles/lazy-function-definition-pattern 25 | function formatMilliseconds(millis, data) { 26 | // Use jintervals if available, else default formatter 27 | var formatter; 28 | if (typeof jintervals == 'function') { 29 | formatter = function(millis, data){return jintervals(millis/1000, data.format);}; 30 | } else { 31 | formatter = defaultFormatMilliseconds; 32 | } 33 | formatMilliseconds = function(millis, data) { 34 | return formatter(millis, data); 35 | }; 36 | return formatMilliseconds(millis, data); 37 | } 38 | 39 | var methods = { 40 | 41 | init: function(options) { 42 | var defaults = { 43 | updateInterval: 1000, 44 | startTime: 0, 45 | format: '{HH}:{MM}:{SS}', 46 | formatter: formatMilliseconds 47 | }; 48 | 49 | // if (options) { $.extend(settings, options); } 50 | 51 | return this.each(function() { 52 | var $this = $(this), 53 | data = $this.data('stopwatch'); 54 | 55 | // If the plugin hasn't been initialized yet 56 | if (!data) { 57 | // Setup the stopwatch data 58 | var settings = $.extend({}, defaults, options); 59 | data = settings; 60 | data.active = false; 61 | data.target = $this; 62 | data.elapsed = settings.startTime; 63 | // create counter 64 | data.incrementer = incrementer(data.startTime, data.updateInterval); 65 | data.tick_function = function() { 66 | var millis = data.incrementer(); 67 | data.elapsed = millis; 68 | data.target.trigger('tick.stopwatch', [millis]); 69 | data.target.stopwatch('render'); 70 | }; 71 | $this.data('stopwatch', data); 72 | } 73 | 74 | }); 75 | }, 76 | 77 | start: function() { 78 | return this.each(function() { 79 | var $this = $(this), 80 | data = $this.data('stopwatch'); 81 | // Mark as active 82 | data.active = true; 83 | data.timerID = setInterval(data.tick_function, data.updateInterval); 84 | $this.data('stopwatch', data); 85 | }); 86 | }, 87 | 88 | stop: function() { 89 | return this.each(function() { 90 | var $this = $(this), 91 | data = $this.data('stopwatch'); 92 | clearInterval(data.timerID); 93 | data.active = false; 94 | $this.data('stopwatch', data); 95 | }); 96 | }, 97 | 98 | destroy: function() { 99 | return this.each(function(){ 100 | var $this = $(this), 101 | data = $this.data('stopwatch'); 102 | $this.stopwatch('stop').unbind('.stopwatch').removeData('stopwatch'); 103 | }); 104 | }, 105 | 106 | render: function() { 107 | var $this = $(this), 108 | data = $this.data('stopwatch'); 109 | $this.html(data.formatter(data.elapsed, data)); 110 | }, 111 | 112 | getTime: function() { 113 | var $this = $(this), 114 | data = $this.data('stopwatch'); 115 | return data.elapsed; 116 | }, 117 | 118 | toggle: function() { 119 | return this.each(function() { 120 | var $this = $(this); 121 | var data = $this.data('stopwatch'); 122 | if (data.active) { 123 | $this.stopwatch('stop'); 124 | } else { 125 | $this.stopwatch('start'); 126 | } 127 | }); 128 | }, 129 | 130 | reset: function() { 131 | return this.each(function() { 132 | var $this = $(this); 133 | data = $this.data('stopwatch'); 134 | data.incrementer = incrementer(data.startTime, data.updateInterval); 135 | data.elapsed = data.startTime; 136 | $this.data('stopwatch', data); 137 | }); 138 | } 139 | }; 140 | 141 | 142 | // Define the function 143 | $.fn.stopwatch = function( method ) { 144 | if (methods[method]) { 145 | return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 )); 146 | } else if (typeof method === 'object' || !method) { 147 | return methods.init.apply(this, arguments); 148 | } else { 149 | $.error( 'Method ' + method + ' does not exist on jQuery.stopwatch' ); 150 | } 151 | }; 152 | 153 | })( jQuery ); -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | QUnit Test Suite 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | 18 |
19 |
jquery-stopwatch Tests
20 |
21 |

22 |
23 |

24 |
    25 |
26 |
test markup
27 |
28 |
29 |
30 | 31 | -------------------------------------------------------------------------------- /test/qunit/qunit.css: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | /** Font Family and Sizes */ 12 | 13 | #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult { 14 | font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; 15 | } 16 | 17 | #qunit-testrunner-toolbar, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } 18 | #qunit-tests { font-size: smaller; } 19 | 20 | 21 | /** Resets */ 22 | 23 | #qunit-tests, #qunit-tests ol, #qunit-header, #qunit-banner, #qunit-userAgent, #qunit-testresult { 24 | margin: 0; 25 | padding: 0; 26 | } 27 | 28 | 29 | /** Header */ 30 | 31 | #qunit-header { 32 | padding: 0.5em 0 0.5em 1em; 33 | 34 | color: #8699a4; 35 | background-color: #0d3349; 36 | 37 | font-size: 1.5em; 38 | line-height: 1em; 39 | font-weight: normal; 40 | 41 | border-radius: 15px 15px 0 0; 42 | -moz-border-radius: 15px 15px 0 0; 43 | -webkit-border-top-right-radius: 15px; 44 | -webkit-border-top-left-radius: 15px; 45 | } 46 | 47 | #qunit-header a { 48 | text-decoration: none; 49 | color: #c2ccd1; 50 | } 51 | 52 | #qunit-header a:hover, 53 | #qunit-header a:focus { 54 | color: #fff; 55 | } 56 | 57 | #qunit-banner { 58 | height: 5px; 59 | } 60 | 61 | #qunit-testrunner-toolbar { 62 | padding: 0.5em 0 0.5em 2em; 63 | color: #5E740B; 64 | background-color: #eee; 65 | } 66 | 67 | #qunit-userAgent { 68 | padding: 0.5em 0 0.5em 2.5em; 69 | background-color: #2b81af; 70 | color: #fff; 71 | text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; 72 | } 73 | 74 | 75 | /** Tests: Pass/Fail */ 76 | 77 | #qunit-tests { 78 | list-style-position: inside; 79 | } 80 | 81 | #qunit-tests li { 82 | padding: 0.4em 0.5em 0.4em 2.5em; 83 | border-bottom: 1px solid #fff; 84 | list-style-position: inside; 85 | } 86 | 87 | #qunit-tests.hidepass li.pass, #qunit-tests.hidepass li.running { 88 | display: none; 89 | } 90 | 91 | #qunit-tests li strong { 92 | cursor: pointer; 93 | } 94 | 95 | #qunit-tests li a { 96 | padding: 0.5em; 97 | color: #c2ccd1; 98 | text-decoration: none; 99 | } 100 | #qunit-tests li a:hover, 101 | #qunit-tests li a:focus { 102 | color: #000; 103 | } 104 | 105 | #qunit-tests ol { 106 | margin-top: 0.5em; 107 | padding: 0.5em; 108 | 109 | background-color: #fff; 110 | 111 | border-radius: 15px; 112 | -moz-border-radius: 15px; 113 | -webkit-border-radius: 15px; 114 | 115 | box-shadow: inset 0px 2px 13px #999; 116 | -moz-box-shadow: inset 0px 2px 13px #999; 117 | -webkit-box-shadow: inset 0px 2px 13px #999; 118 | } 119 | 120 | #qunit-tests table { 121 | border-collapse: collapse; 122 | margin-top: .2em; 123 | } 124 | 125 | #qunit-tests th { 126 | text-align: right; 127 | vertical-align: top; 128 | padding: 0 .5em 0 0; 129 | } 130 | 131 | #qunit-tests td { 132 | vertical-align: top; 133 | } 134 | 135 | #qunit-tests pre { 136 | margin: 0; 137 | white-space: pre-wrap; 138 | word-wrap: break-word; 139 | } 140 | 141 | #qunit-tests del { 142 | background-color: #e0f2be; 143 | color: #374e0c; 144 | text-decoration: none; 145 | } 146 | 147 | #qunit-tests ins { 148 | background-color: #ffcaca; 149 | color: #500; 150 | text-decoration: none; 151 | } 152 | 153 | /*** Test Counts */ 154 | 155 | #qunit-tests b.counts { color: black; } 156 | #qunit-tests b.passed { color: #5E740B; } 157 | #qunit-tests b.failed { color: #710909; } 158 | 159 | #qunit-tests li li { 160 | margin: 0.5em; 161 | padding: 0.4em 0.5em 0.4em 0.5em; 162 | background-color: #fff; 163 | border-bottom: none; 164 | list-style-position: inside; 165 | } 166 | 167 | /*** Passing Styles */ 168 | 169 | #qunit-tests li li.pass { 170 | color: #5E740B; 171 | background-color: #fff; 172 | border-left: 26px solid #C6E746; 173 | } 174 | 175 | #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } 176 | #qunit-tests .pass .test-name { color: #366097; } 177 | 178 | #qunit-tests .pass .test-actual, 179 | #qunit-tests .pass .test-expected { color: #999999; } 180 | 181 | #qunit-banner.qunit-pass { background-color: #C6E746; } 182 | 183 | /*** Failing Styles */ 184 | 185 | #qunit-tests li li.fail { 186 | color: #710909; 187 | background-color: #fff; 188 | border-left: 26px solid #EE5757; 189 | } 190 | 191 | #qunit-tests > li:last-child { 192 | border-radius: 0 0 15px 15px; 193 | -moz-border-radius: 0 0 15px 15px; 194 | -webkit-border-bottom-right-radius: 15px; 195 | -webkit-border-bottom-left-radius: 15px; 196 | } 197 | 198 | #qunit-tests .fail { color: #000000; background-color: #EE5757; } 199 | #qunit-tests .fail .test-name, 200 | #qunit-tests .fail .module-name { color: #000000; } 201 | 202 | #qunit-tests .fail .test-actual { color: #EE5757; } 203 | #qunit-tests .fail .test-expected { color: green; } 204 | 205 | #qunit-banner.qunit-fail { background-color: #EE5757; } 206 | 207 | 208 | /** Result */ 209 | 210 | #qunit-testresult { 211 | padding: 0.5em 0.5em 0.5em 2.5em; 212 | 213 | color: #2b81af; 214 | background-color: #D2E0E6; 215 | 216 | border-bottom: 1px solid white; 217 | } 218 | 219 | /** Fixture */ 220 | 221 | #qunit-fixture { 222 | position: absolute; 223 | top: -10000px; 224 | left: -10000px; 225 | } -------------------------------------------------------------------------------- /test/qunit/qunit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * QUnit - A JavaScript Unit Testing Framework 3 | * 4 | * http://docs.jquery.com/QUnit 5 | * 6 | * Copyright (c) 2011 John Resig, Jörn Zaefferer 7 | * Dual licensed under the MIT (MIT-LICENSE.txt) 8 | * or GPL (GPL-LICENSE.txt) licenses. 9 | */ 10 | 11 | (function(window) { 12 | 13 | var defined = { 14 | setTimeout: typeof window.setTimeout !== "undefined", 15 | sessionStorage: (function() { 16 | try { 17 | return !!sessionStorage.getItem; 18 | } catch (e) { 19 | return false; 20 | } 21 | })() 22 | }; 23 | 24 | var testId = 0; 25 | 26 | var Test = function(name, testName, expected, testEnvironmentArg, async, callback) { 27 | this.name = name; 28 | this.testName = testName; 29 | this.expected = expected; 30 | this.testEnvironmentArg = testEnvironmentArg; 31 | this.async = async; 32 | this.callback = callback; 33 | this.assertions = []; 34 | }; 35 | Test.prototype = { 36 | init: function() { 37 | var tests = id("qunit-tests"); 38 | if (tests) { 39 | var b = document.createElement("strong"); 40 | b.innerHTML = "Running " + this.name; 41 | var li = document.createElement("li"); 42 | li.appendChild(b); 43 | li.className = "running"; 44 | li.id = this.id = "test-output" + testId++; 45 | tests.appendChild(li); 46 | } 47 | }, 48 | setup: function() { 49 | if (this.module != config.previousModule) { 50 | if (config.previousModule) { 51 | QUnit.moduleDone({ 52 | name: config.previousModule, 53 | failed: config.moduleStats.bad, 54 | passed: config.moduleStats.all - config.moduleStats.bad, 55 | total: config.moduleStats.all 56 | }); 57 | } 58 | config.previousModule = this.module; 59 | config.moduleStats = { 60 | all: 0, 61 | bad: 0 62 | }; 63 | QUnit.moduleStart({ 64 | name: this.module 65 | }); 66 | } 67 | 68 | config.current = this; 69 | this.testEnvironment = extend({ 70 | setup: function() {}, 71 | teardown: function() {} 72 | }, this.moduleTestEnvironment); 73 | if (this.testEnvironmentArg) { 74 | extend(this.testEnvironment, this.testEnvironmentArg); 75 | } 76 | 77 | QUnit.testStart({ 78 | name: this.testName 79 | }); 80 | 81 | // allow utility functions to access the current test environment 82 | // TODO why?? 83 | QUnit.current_testEnvironment = this.testEnvironment; 84 | 85 | try { 86 | if (!config.pollution) { 87 | saveGlobal(); 88 | } 89 | 90 | this.testEnvironment.setup.call(this.testEnvironment); 91 | } catch (e) { 92 | QUnit.ok(false, "Setup failed on " + this.testName + ": " + e.message); 93 | } 94 | }, 95 | run: function() { 96 | if (this.async) { 97 | QUnit.stop(); 98 | } 99 | 100 | if (config.notrycatch) { 101 | this.callback.call(this.testEnvironment); 102 | return; 103 | } 104 | try { 105 | this.callback.call(this.testEnvironment); 106 | } catch (e) { 107 | fail("Test " + this.testName + " died, exception and test follows", e, this.callback); 108 | QUnit.ok(false, "Died on test #" + (this.assertions.length + 1) + ": " + e.message + " - " + QUnit.jsDump.parse(e)); 109 | // else next test will carry the responsibility 110 | saveGlobal(); 111 | 112 | // Restart the tests if they're blocking 113 | if (config.blocking) { 114 | start(); 115 | } 116 | } 117 | }, 118 | teardown: function() { 119 | try { 120 | this.testEnvironment.teardown.call(this.testEnvironment); 121 | checkPollution(); 122 | } catch (e) { 123 | QUnit.ok(false, "Teardown failed on " + this.testName + ": " + e.message); 124 | } 125 | }, 126 | finish: function() { 127 | if (this.expected && this.expected != this.assertions.length) { 128 | QUnit.ok(false, "Expected " + this.expected + " assertions, but " + this.assertions.length + " were run"); 129 | } 130 | 131 | var good = 0, 132 | bad = 0, 133 | tests = id("qunit-tests"); 134 | 135 | config.stats.all += this.assertions.length; 136 | config.moduleStats.all += this.assertions.length; 137 | 138 | if (tests) { 139 | var ol = document.createElement("ol"); 140 | 141 | for (var i = 0; i < this.assertions.length; i++) { 142 | var assertion = this.assertions[i]; 143 | 144 | var li = document.createElement("li"); 145 | li.className = assertion.result ? "pass" : "fail"; 146 | li.innerHTML = assertion.message || (assertion.result ? "okay" : "failed"); 147 | ol.appendChild(li); 148 | 149 | if (assertion.result) { 150 | good++; 151 | } else { 152 | bad++; 153 | config.stats.bad++; 154 | config.moduleStats.bad++; 155 | } 156 | } 157 | 158 | // store result when possible 159 | if (QUnit.config.reorder && defined.sessionStorage) { 160 | if (bad) { 161 | sessionStorage.setItem("qunit-" + this.module + "-" + this.testName, bad); 162 | } else { 163 | sessionStorage.removeItem("qunit-" + this.module + "-" + this.testName); 164 | } 165 | } 166 | 167 | if (bad == 0) { 168 | ol.style.display = "none"; 169 | } 170 | 171 | var b = document.createElement("strong"); 172 | b.innerHTML = this.name + " (" + bad + ", " + good + ", " + this.assertions.length + ")"; 173 | 174 | var a = document.createElement("a"); 175 | a.innerHTML = "Rerun"; 176 | a.href = QUnit.url({ 177 | filter: getText([b]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") 178 | }); 179 | 180 | addEvent(b, "click", function() { 181 | var next = b.nextSibling.nextSibling, 182 | display = next.style.display; 183 | next.style.display = display === "none" ? "block" : "none"; 184 | }); 185 | 186 | addEvent(b, "dblclick", function(e) { 187 | var target = e && e.target ? e.target : window.event.srcElement; 188 | if (target.nodeName.toLowerCase() == "span" || target.nodeName.toLowerCase() == "b") { 189 | target = target.parentNode; 190 | } 191 | if (window.location && target.nodeName.toLowerCase() === "strong") { 192 | window.location = QUnit.url({ 193 | filter: getText([target]).replace(/\([^)]+\)$/, "").replace(/(^\s*|\s*$)/g, "") 194 | }); 195 | } 196 | }); 197 | 198 | var li = id(this.id); 199 | li.className = bad ? "fail" : "pass"; 200 | li.removeChild(li.firstChild); 201 | li.appendChild(b); 202 | li.appendChild(a); 203 | li.appendChild(ol); 204 | 205 | } else { 206 | for (var i = 0; i < this.assertions.length; i++) { 207 | if (!this.assertions[i].result) { 208 | bad++; 209 | config.stats.bad++; 210 | config.moduleStats.bad++; 211 | } 212 | } 213 | } 214 | 215 | try { 216 | QUnit.reset(); 217 | } catch (e) { 218 | fail("reset() failed, following Test " + this.testName + ", exception and reset fn follows", e, QUnit.reset); 219 | } 220 | 221 | QUnit.testDone({ 222 | name: this.testName, 223 | failed: bad, 224 | passed: this.assertions.length - bad, 225 | total: this.assertions.length 226 | }); 227 | }, 228 | 229 | queue: function() { 230 | var test = this; 231 | synchronize(function() { 232 | test.init(); 233 | }); 234 | 235 | function run() { 236 | // each of these can by async 237 | synchronize(function() { 238 | test.setup(); 239 | }); 240 | synchronize(function() { 241 | test.run(); 242 | }); 243 | synchronize(function() { 244 | test.teardown(); 245 | }); 246 | synchronize(function() { 247 | test.finish(); 248 | }); 249 | } 250 | // defer when previous test run passed, if storage is available 251 | var bad = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem("qunit-" + this.module + "-" + this.testName); 252 | if (bad) { 253 | run(); 254 | } else { 255 | synchronize(run); 256 | }; 257 | } 258 | 259 | }; 260 | 261 | var QUnit = { 262 | 263 | // call on start of module test to prepend name to all tests 264 | module: function(name, testEnvironment) { 265 | config.currentModule = name; 266 | config.currentModuleTestEnviroment = testEnvironment; 267 | }, 268 | 269 | asyncTest: function(testName, expected, callback) { 270 | if (arguments.length === 2) { 271 | callback = expected; 272 | expected = 0; 273 | } 274 | 275 | QUnit.test(testName, expected, callback, true); 276 | }, 277 | 278 | test: function(testName, expected, callback, async) { 279 | var name = '' + testName + '', 280 | testEnvironmentArg; 281 | 282 | if (arguments.length === 2) { 283 | callback = expected; 284 | expected = null; 285 | } 286 | // is 2nd argument a testEnvironment? 287 | if (expected && typeof expected === 'object') { 288 | testEnvironmentArg = expected; 289 | expected = null; 290 | } 291 | 292 | if (config.currentModule) { 293 | name = '' + config.currentModule + ": " + name; 294 | } 295 | 296 | if (!validTest(config.currentModule + ": " + testName)) { 297 | return; 298 | } 299 | 300 | var test = new Test(name, testName, expected, testEnvironmentArg, async, callback); 301 | test.module = config.currentModule; 302 | test.moduleTestEnvironment = config.currentModuleTestEnviroment; 303 | test.queue(); 304 | }, 305 | 306 | /** 307 | * Specify the number of expected assertions to gurantee that failed test (no assertions are run at all) don't slip through. 308 | */ 309 | expect: function(asserts) { 310 | config.current.expected = asserts; 311 | }, 312 | 313 | /** 314 | * Asserts true. 315 | * @example ok( "asdfasdf".length > 5, "There must be at least 5 chars" ); 316 | */ 317 | ok: function(a, msg) { 318 | a = !! a; 319 | var details = { 320 | result: a, 321 | message: msg 322 | }; 323 | msg = escapeHtml(msg); 324 | QUnit.log(details); 325 | config.current.assertions.push({ 326 | result: a, 327 | message: msg 328 | }); 329 | }, 330 | 331 | /** 332 | * Checks that the first two arguments are equal, with an optional message. 333 | * Prints out both actual and expected values. 334 | * 335 | * Prefered to ok( actual == expected, message ) 336 | * 337 | * @example equal( format("Received {0} bytes.", 2), "Received 2 bytes." ); 338 | * 339 | * @param Object actual 340 | * @param Object expected 341 | * @param String message (optional) 342 | */ 343 | equal: function(actual, expected, message) { 344 | QUnit.push(expected == actual, actual, expected, message); 345 | }, 346 | 347 | notEqual: function(actual, expected, message) { 348 | QUnit.push(expected != actual, actual, expected, message); 349 | }, 350 | 351 | deepEqual: function(actual, expected, message) { 352 | QUnit.push(QUnit.equiv(actual, expected), actual, expected, message); 353 | }, 354 | 355 | notDeepEqual: function(actual, expected, message) { 356 | QUnit.push(!QUnit.equiv(actual, expected), actual, expected, message); 357 | }, 358 | 359 | strictEqual: function(actual, expected, message) { 360 | QUnit.push(expected === actual, actual, expected, message); 361 | }, 362 | 363 | notStrictEqual: function(actual, expected, message) { 364 | QUnit.push(expected !== actual, actual, expected, message); 365 | }, 366 | 367 | raises: function(block, expected, message) { 368 | var actual, ok = false; 369 | 370 | if (typeof expected === 'string') { 371 | message = expected; 372 | expected = null; 373 | } 374 | 375 | try { 376 | block(); 377 | } catch (e) { 378 | actual = e; 379 | } 380 | 381 | if (actual) { 382 | // we don't want to validate thrown error 383 | if (!expected) { 384 | ok = true; 385 | // expected is a regexp 386 | } else if (QUnit.objectType(expected) === "regexp") { 387 | ok = expected.test(actual); 388 | // expected is a constructor 389 | } else if (actual instanceof expected) { 390 | ok = true; 391 | // expected is a validation function which returns true is validation passed 392 | } else if (expected.call({}, actual) === true) { 393 | ok = true; 394 | } 395 | } 396 | 397 | QUnit.ok(ok, message); 398 | }, 399 | 400 | start: function() { 401 | config.semaphore--; 402 | if (config.semaphore > 0) { 403 | // don't start until equal number of stop-calls 404 | return; 405 | } 406 | if (config.semaphore < 0) { 407 | // ignore if start is called more often then stop 408 | config.semaphore = 0; 409 | } 410 | // A slight delay, to avoid any current callbacks 411 | if (defined.setTimeout) { 412 | window.setTimeout(function() { 413 | if (config.timeout) { 414 | clearTimeout(config.timeout); 415 | } 416 | 417 | config.blocking = false; 418 | process(); 419 | }, 13); 420 | } else { 421 | config.blocking = false; 422 | process(); 423 | } 424 | }, 425 | 426 | stop: function(timeout) { 427 | config.semaphore++; 428 | config.blocking = true; 429 | 430 | if (timeout && defined.setTimeout) { 431 | clearTimeout(config.timeout); 432 | config.timeout = window.setTimeout(function() { 433 | QUnit.ok(false, "Test timed out"); 434 | QUnit.start(); 435 | }, timeout); 436 | } 437 | } 438 | }; 439 | 440 | // Backwards compatibility, deprecated 441 | QUnit.equals = QUnit.equal; 442 | QUnit.same = QUnit.deepEqual; 443 | 444 | // Maintain internal state 445 | var config = { 446 | // The queue of tests to run 447 | queue: [], 448 | 449 | // block until document ready 450 | blocking: true, 451 | 452 | // by default, run previously failed tests first 453 | // very useful in combination with "Hide passed tests" checked 454 | reorder: true, 455 | 456 | noglobals: false, 457 | notrycatch: false 458 | }; 459 | 460 | // Load paramaters 461 | (function() { 462 | var location = window.location || { 463 | search: "", 464 | protocol: "file:" 465 | }, 466 | params = location.search.slice(1).split("&"), 467 | length = params.length, 468 | urlParams = {}, 469 | current; 470 | 471 | if (params[0]) { 472 | for (var i = 0; i < length; i++) { 473 | current = params[i].split("="); 474 | current[0] = decodeURIComponent(current[0]); 475 | // allow just a key to turn on a flag, e.g., test.html?noglobals 476 | current[1] = current[1] ? decodeURIComponent(current[1]) : true; 477 | urlParams[current[0]] = current[1]; 478 | if (current[0] in config) { 479 | config[current[0]] = current[1]; 480 | } 481 | } 482 | } 483 | 484 | QUnit.urlParams = urlParams; 485 | config.filter = urlParams.filter; 486 | 487 | // Figure out if we're running the tests from a server or not 488 | QUnit.isLocal = !! (location.protocol === 'file:'); 489 | })(); 490 | 491 | // Expose the API as global variables, unless an 'exports' 492 | // object exists, in that case we assume we're in CommonJS 493 | if (typeof exports === "undefined" || typeof require === "undefined") { 494 | extend(window, QUnit); 495 | window.QUnit = QUnit; 496 | } else { 497 | extend(exports, QUnit); 498 | exports.QUnit = QUnit; 499 | } 500 | 501 | // define these after exposing globals to keep them in these QUnit namespace only 502 | extend(QUnit, { 503 | config: config, 504 | 505 | // Initialize the configuration options 506 | init: function() { 507 | extend(config, { 508 | stats: { 509 | all: 0, 510 | bad: 0 511 | }, 512 | moduleStats: { 513 | all: 0, 514 | bad: 0 515 | }, 516 | started: +new Date, 517 | updateRate: 1000, 518 | blocking: false, 519 | autostart: true, 520 | autorun: false, 521 | filter: "", 522 | queue: [], 523 | semaphore: 0 524 | }); 525 | 526 | var tests = id("qunit-tests"), 527 | banner = id("qunit-banner"), 528 | result = id("qunit-testresult"); 529 | 530 | if (tests) { 531 | tests.innerHTML = ""; 532 | } 533 | 534 | if (banner) { 535 | banner.className = ""; 536 | } 537 | 538 | if (result) { 539 | result.parentNode.removeChild(result); 540 | } 541 | 542 | if (tests) { 543 | result = document.createElement("p"); 544 | result.id = "qunit-testresult"; 545 | result.className = "result"; 546 | tests.parentNode.insertBefore(result, tests); 547 | result.innerHTML = 'Running...
 '; 548 | } 549 | }, 550 | 551 | /** 552 | * Resets the test setup. Useful for tests that modify the DOM. 553 | * 554 | * If jQuery is available, uses jQuery's html(), otherwise just innerHTML. 555 | */ 556 | reset: function() { 557 | if (window.jQuery) { 558 | jQuery("#qunit-fixture").html(config.fixture); 559 | } else { 560 | var main = id('qunit-fixture'); 561 | if (main) { 562 | main.innerHTML = config.fixture; 563 | } 564 | } 565 | }, 566 | 567 | /** 568 | * Trigger an event on an element. 569 | * 570 | * @example triggerEvent( document.body, "click" ); 571 | * 572 | * @param DOMElement elem 573 | * @param String type 574 | */ 575 | triggerEvent: function(elem, type, event) { 576 | if (document.createEvent) { 577 | event = document.createEvent("MouseEvents"); 578 | event.initMouseEvent(type, true, true, elem.ownerDocument.defaultView, 0, 0, 0, 0, 0, false, false, false, false, 0, null); 579 | elem.dispatchEvent(event); 580 | 581 | } else if (elem.fireEvent) { 582 | elem.fireEvent("on" + type); 583 | } 584 | }, 585 | 586 | // Safe object type checking 587 | is: function(type, obj) { 588 | return QUnit.objectType(obj) == type; 589 | }, 590 | 591 | objectType: function(obj) { 592 | if (typeof obj === "undefined") { 593 | return "undefined"; 594 | 595 | // consider: typeof null === object 596 | } 597 | if (obj === null) { 598 | return "null"; 599 | } 600 | 601 | var type = Object.prototype.toString.call(obj).match(/^\[object\s(.*)\]$/)[1] || ''; 602 | 603 | switch (type) { 604 | case 'Number': 605 | if (isNaN(obj)) { 606 | return "nan"; 607 | } else { 608 | return "number"; 609 | } 610 | case 'String': 611 | case 'Boolean': 612 | case 'Array': 613 | case 'Date': 614 | case 'RegExp': 615 | case 'Function': 616 | return type.toLowerCase(); 617 | } 618 | if (typeof obj === "object") { 619 | return "object"; 620 | } 621 | return undefined; 622 | }, 623 | 624 | push: function(result, actual, expected, message) { 625 | var details = { 626 | result: result, 627 | message: message, 628 | actual: actual, 629 | expected: expected 630 | }; 631 | 632 | message = escapeHtml(message) || (result ? "okay" : "failed"); 633 | message = '' + message + ""; 634 | expected = escapeHtml(QUnit.jsDump.parse(expected)); 635 | actual = escapeHtml(QUnit.jsDump.parse(actual)); 636 | var output = message + ''; 637 | if (actual != expected) { 638 | output += ''; 639 | output += ''; 640 | } 641 | if (!result) { 642 | var source = sourceFromStacktrace(); 643 | if (source) { 644 | details.source = source; 645 | output += ''; 646 | } 647 | } 648 | output += "
Expected:
' + expected + '
Result:
' + actual + '
Diff:
' + QUnit.diff(expected, actual) + '
Source:
' + escapeHtml(source) + '
"; 649 | 650 | QUnit.log(details); 651 | 652 | config.current.assertions.push({ 653 | result: !! result, 654 | message: output 655 | }); 656 | }, 657 | 658 | url: function(params) { 659 | params = extend(extend({}, QUnit.urlParams), params); 660 | var querystring = "?", 661 | key; 662 | for (key in params) { 663 | querystring += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]) + "&"; 664 | } 665 | return window.location.pathname + querystring.slice(0, -1); 666 | }, 667 | 668 | // Logging callbacks; all receive a single argument with the listed properties 669 | // run test/logs.html for any related changes 670 | begin: function() {}, 671 | // done: { failed, passed, total, runtime } 672 | done: function() {}, 673 | // log: { result, actual, expected, message } 674 | log: function() {}, 675 | // testStart: { name } 676 | testStart: function() {}, 677 | // testDone: { name, failed, passed, total } 678 | testDone: function() {}, 679 | // moduleStart: { name } 680 | moduleStart: function() {}, 681 | // moduleDone: { name, failed, passed, total } 682 | moduleDone: function() {} 683 | }); 684 | 685 | if (typeof document === "undefined" || document.readyState === "complete") { 686 | config.autorun = true; 687 | } 688 | 689 | addEvent(window, "load", function() { 690 | QUnit.begin({}); 691 | 692 | // Initialize the config, saving the execution queue 693 | var oldconfig = extend({}, config); 694 | QUnit.init(); 695 | extend(config, oldconfig); 696 | 697 | config.blocking = false; 698 | 699 | var userAgent = id("qunit-userAgent"); 700 | if (userAgent) { 701 | userAgent.innerHTML = navigator.userAgent; 702 | } 703 | var banner = id("qunit-header"); 704 | if (banner) { 705 | banner.innerHTML = ' ' + banner.innerHTML + ' ' + '' + ''; 708 | addEvent(banner, "change", function(event) { 709 | var params = {}; 710 | params[event.target.name] = event.target.checked ? true : undefined; 711 | window.location = QUnit.url(params); 712 | }); 713 | } 714 | 715 | var toolbar = id("qunit-testrunner-toolbar"); 716 | if (toolbar) { 717 | var filter = document.createElement("input"); 718 | filter.type = "checkbox"; 719 | filter.id = "qunit-filter-pass"; 720 | addEvent(filter, "click", function() { 721 | var ol = document.getElementById("qunit-tests"); 722 | if (filter.checked) { 723 | ol.className = ol.className + " hidepass"; 724 | } else { 725 | var tmp = " " + ol.className.replace(/[\n\t\r]/g, " ") + " "; 726 | ol.className = tmp.replace(/ hidepass /, " "); 727 | } 728 | if (defined.sessionStorage) { 729 | if (filter.checked) { 730 | sessionStorage.setItem("qunit-filter-passed-tests", "true"); 731 | } else { 732 | sessionStorage.removeItem("qunit-filter-passed-tests"); 733 | } 734 | } 735 | }); 736 | if (defined.sessionStorage && sessionStorage.getItem("qunit-filter-passed-tests")) { 737 | filter.checked = true; 738 | var ol = document.getElementById("qunit-tests"); 739 | ol.className = ol.className + " hidepass"; 740 | } 741 | toolbar.appendChild(filter); 742 | 743 | var label = document.createElement("label"); 744 | label.setAttribute("for", "qunit-filter-pass"); 745 | label.innerHTML = "Hide passed tests"; 746 | toolbar.appendChild(label); 747 | } 748 | 749 | var main = id('qunit-fixture'); 750 | if (main) { 751 | config.fixture = main.innerHTML; 752 | } 753 | 754 | if (config.autostart) { 755 | QUnit.start(); 756 | } 757 | }); 758 | 759 | function done() { 760 | config.autorun = true; 761 | 762 | // Log the last module results 763 | if (config.currentModule) { 764 | QUnit.moduleDone({ 765 | name: config.currentModule, 766 | failed: config.moduleStats.bad, 767 | passed: config.moduleStats.all - config.moduleStats.bad, 768 | total: config.moduleStats.all 769 | }); 770 | } 771 | 772 | var banner = id("qunit-banner"), 773 | tests = id("qunit-tests"), 774 | runtime = +new Date - config.started, 775 | passed = config.stats.all - config.stats.bad, 776 | html = ['Tests completed in ', runtime, ' milliseconds.
', '', passed, ' tests of ', config.stats.all, ' passed, ', config.stats.bad, ' failed.'].join(''); 777 | 778 | if (banner) { 779 | banner.className = (config.stats.bad ? "qunit-fail" : "qunit-pass"); 780 | } 781 | 782 | if (tests) { 783 | id("qunit-testresult").innerHTML = html; 784 | } 785 | 786 | if (typeof document !== "undefined" && document.title) { 787 | // show ✖ for good, ✔ for bad suite result in title 788 | // use escape sequences in case file gets loaded with non-utf-8-charset 789 | document.title = (config.stats.bad ? "\u2716" : "\u2714") + " " + document.title; 790 | } 791 | 792 | QUnit.done({ 793 | failed: config.stats.bad, 794 | passed: passed, 795 | total: config.stats.all, 796 | runtime: runtime 797 | }); 798 | } 799 | 800 | function validTest(name) { 801 | var filter = config.filter, 802 | run = false; 803 | 804 | if (!filter) { 805 | return true; 806 | } 807 | 808 | var not = filter.charAt(0) === "!"; 809 | if (not) { 810 | filter = filter.slice(1); 811 | } 812 | 813 | if (name.indexOf(filter) !== -1) { 814 | return !not; 815 | } 816 | 817 | if (not) { 818 | run = true; 819 | } 820 | 821 | return run; 822 | } 823 | 824 | // so far supports only Firefox, Chrome and Opera (buggy) 825 | // could be extended in the future to use something like https://github.com/csnover/TraceKit 826 | 827 | 828 | function sourceFromStacktrace() { 829 | try { 830 | throw new Error(); 831 | } catch (e) { 832 | if (e.stacktrace) { 833 | // Opera 834 | return e.stacktrace.split("\n")[6]; 835 | } else if (e.stack) { 836 | // Firefox, Chrome 837 | return e.stack.split("\n")[4]; 838 | } 839 | } 840 | } 841 | 842 | function escapeHtml(s) { 843 | if (!s) { 844 | return ""; 845 | } 846 | s = s + ""; 847 | return s.replace(/[\&"<>\\]/g, function(s) { 848 | switch (s) { 849 | case "&": 850 | return "&"; 851 | case "\\": 852 | return "\\\\"; 853 | case '"': 854 | return '\"'; 855 | case "<": 856 | return "<"; 857 | case ">": 858 | return ">"; 859 | default: 860 | return s; 861 | } 862 | }); 863 | } 864 | 865 | function synchronize(callback) { 866 | config.queue.push(callback); 867 | 868 | if (config.autorun && !config.blocking) { 869 | process(); 870 | } 871 | } 872 | 873 | function process() { 874 | var start = (new Date()).getTime(); 875 | 876 | while (config.queue.length && !config.blocking) { 877 | if (config.updateRate <= 0 || (((new Date()).getTime() - start) < config.updateRate)) { 878 | config.queue.shift()(); 879 | } else { 880 | window.setTimeout(process, 13); 881 | break; 882 | } 883 | } 884 | if (!config.blocking && !config.queue.length) { 885 | done(); 886 | } 887 | } 888 | 889 | function saveGlobal() { 890 | config.pollution = []; 891 | 892 | if (config.noglobals) { 893 | for (var key in window) { 894 | config.pollution.push(key); 895 | } 896 | } 897 | } 898 | 899 | function checkPollution(name) { 900 | var old = config.pollution; 901 | saveGlobal(); 902 | 903 | var newGlobals = diff(config.pollution, old); 904 | if (newGlobals.length > 0) { 905 | ok(false, "Introduced global variable(s): " + newGlobals.join(", ")); 906 | } 907 | 908 | var deletedGlobals = diff(old, config.pollution); 909 | if (deletedGlobals.length > 0) { 910 | ok(false, "Deleted global variable(s): " + deletedGlobals.join(", ")); 911 | } 912 | } 913 | 914 | // returns a new Array with the elements that are in a but not in b 915 | 916 | 917 | function diff(a, b) { 918 | var result = a.slice(); 919 | for (var i = 0; i < result.length; i++) { 920 | for (var j = 0; j < b.length; j++) { 921 | if (result[i] === b[j]) { 922 | result.splice(i, 1); 923 | i--; 924 | break; 925 | } 926 | } 927 | } 928 | return result; 929 | } 930 | 931 | function fail(message, exception, callback) { 932 | if (typeof console !== "undefined" && console.error && console.warn) { 933 | console.error(message); 934 | console.error(exception); 935 | console.warn(callback.toString()); 936 | 937 | } else if (window.opera && opera.postError) { 938 | opera.postError(message, exception, callback.toString); 939 | } 940 | } 941 | 942 | function extend(a, b) { 943 | for (var prop in b) { 944 | if (b[prop] === undefined) { 945 | delete a[prop]; 946 | } else { 947 | a[prop] = b[prop]; 948 | } 949 | } 950 | 951 | return a; 952 | } 953 | 954 | function addEvent(elem, type, fn) { 955 | if (elem.addEventListener) { 956 | elem.addEventListener(type, fn, false); 957 | } else if (elem.attachEvent) { 958 | elem.attachEvent("on" + type, fn); 959 | } else { 960 | fn(); 961 | } 962 | } 963 | 964 | function id(name) { 965 | return !!(typeof document !== "undefined" && document && document.getElementById) && document.getElementById(name); 966 | } 967 | 968 | // Test for equality any JavaScript type. 969 | // Discussions and reference: http://philrathe.com/articles/equiv 970 | // Test suites: http://philrathe.com/tests/equiv 971 | // Author: Philippe Rathé 972 | QUnit.equiv = function() { 973 | 974 | var innerEquiv; // the real equiv function 975 | var callers = []; // stack to decide between skip/abort functions 976 | var parents = []; // stack to avoiding loops from circular referencing 977 | // Call the o related callback with the given arguments. 978 | 979 | 980 | function bindCallbacks(o, callbacks, args) { 981 | var prop = QUnit.objectType(o); 982 | if (prop) { 983 | if (QUnit.objectType(callbacks[prop]) === "function") { 984 | return callbacks[prop].apply(callbacks, args); 985 | } else { 986 | return callbacks[prop]; // or undefined 987 | } 988 | } 989 | } 990 | 991 | var callbacks = function() { 992 | 993 | // for string, boolean, number and null 994 | 995 | 996 | function useStrictEquality(b, a) { 997 | if (b instanceof a.constructor || a instanceof b.constructor) { 998 | // to catch short annotaion VS 'new' annotation of a declaration 999 | // e.g. var i = 1; 1000 | // var j = new Number(1); 1001 | return a == b; 1002 | } else { 1003 | return a === b; 1004 | } 1005 | } 1006 | 1007 | return { 1008 | "string": useStrictEquality, 1009 | "boolean": useStrictEquality, 1010 | "number": useStrictEquality, 1011 | "null": useStrictEquality, 1012 | "undefined": useStrictEquality, 1013 | 1014 | "nan": function(b) { 1015 | return isNaN(b); 1016 | }, 1017 | 1018 | "date": function(b, a) { 1019 | return QUnit.objectType(b) === "date" && a.valueOf() === b.valueOf(); 1020 | }, 1021 | 1022 | "regexp": function(b, a) { 1023 | return QUnit.objectType(b) === "regexp" && a.source === b.source && // the regex itself 1024 | a.global === b.global && // and its modifers (gmi) ... 1025 | a.ignoreCase === b.ignoreCase && a.multiline === b.multiline; 1026 | }, 1027 | 1028 | // - skip when the property is a method of an instance (OOP) 1029 | // - abort otherwise, 1030 | // initial === would have catch identical references anyway 1031 | "function": function() { 1032 | var caller = callers[callers.length - 1]; 1033 | return caller !== Object && typeof caller !== "undefined"; 1034 | }, 1035 | 1036 | "array": function(b, a) { 1037 | var i, j, loop; 1038 | var len; 1039 | 1040 | // b could be an object literal here 1041 | if (!(QUnit.objectType(b) === "array")) { 1042 | return false; 1043 | } 1044 | 1045 | len = a.length; 1046 | if (len !== b.length) { // safe and faster 1047 | return false; 1048 | } 1049 | 1050 | //track reference to avoid circular references 1051 | parents.push(a); 1052 | for (i = 0; i < len; i++) { 1053 | loop = false; 1054 | for (j = 0; j < parents.length; j++) { 1055 | if (parents[j] === a[i]) { 1056 | loop = true; //dont rewalk array 1057 | } 1058 | } 1059 | if (!loop && !innerEquiv(a[i], b[i])) { 1060 | parents.pop(); 1061 | return false; 1062 | } 1063 | } 1064 | parents.pop(); 1065 | return true; 1066 | }, 1067 | 1068 | "object": function(b, a) { 1069 | var i, j, loop; 1070 | var eq = true; // unless we can proove it 1071 | var aProperties = [], 1072 | bProperties = []; // collection of strings 1073 | // comparing constructors is more strict than using instanceof 1074 | if (a.constructor !== b.constructor) { 1075 | return false; 1076 | } 1077 | 1078 | // stack constructor before traversing properties 1079 | callers.push(a.constructor); 1080 | //track reference to avoid circular references 1081 | parents.push(a); 1082 | 1083 | for (i in a) { // be strict: don't ensures hasOwnProperty and go deep 1084 | loop = false; 1085 | for (j = 0; j < parents.length; j++) { 1086 | if (parents[j] === a[i]) loop = true; //don't go down the same path twice 1087 | } 1088 | aProperties.push(i); // collect a's properties 1089 | if (!loop && !innerEquiv(a[i], b[i])) { 1090 | eq = false; 1091 | break; 1092 | } 1093 | } 1094 | 1095 | callers.pop(); // unstack, we are done 1096 | parents.pop(); 1097 | 1098 | for (i in b) { 1099 | bProperties.push(i); // collect b's properties 1100 | } 1101 | 1102 | // Ensures identical properties name 1103 | return eq && innerEquiv(aProperties.sort(), bProperties.sort()); 1104 | } 1105 | }; 1106 | }(); 1107 | 1108 | innerEquiv = function() { // can take multiple arguments 1109 | var args = Array.prototype.slice.apply(arguments); 1110 | if (args.length < 2) { 1111 | return true; // end transition 1112 | } 1113 | 1114 | return (function(a, b) { 1115 | if (a === b) { 1116 | return true; // catch the most you can 1117 | } else if (a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || QUnit.objectType(a) !== QUnit.objectType(b)) { 1118 | return false; // don't lose time with error prone cases 1119 | } else { 1120 | return bindCallbacks(a, callbacks, [b, a]); 1121 | } 1122 | 1123 | // apply transition with (1..n) arguments 1124 | })(args[0], args[1]) && arguments.callee.apply(this, args.splice(1, args.length - 1)); 1125 | }; 1126 | 1127 | return innerEquiv; 1128 | 1129 | }(); 1130 | 1131 | /** 1132 | * jsDump 1133 | * Copyright (c) 2008 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 1134 | * Licensed under BSD (http://www.opensource.org/licenses/bsd-license.php) 1135 | * Date: 5/15/2008 1136 | * @projectDescription Advanced and extensible data dumping for Javascript. 1137 | * @version 1.0.0 1138 | * @author Ariel Flesler 1139 | * @link {http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html} 1140 | */ 1141 | QUnit.jsDump = (function() { 1142 | function quote(str) { 1143 | return '"' + str.toString().replace(/"/g, '\\"') + '"'; 1144 | }; 1145 | 1146 | function literal(o) { 1147 | return o + ''; 1148 | }; 1149 | 1150 | function join(pre, arr, post) { 1151 | var s = jsDump.separator(), 1152 | base = jsDump.indent(), 1153 | inner = jsDump.indent(1); 1154 | if (arr.join) arr = arr.join(',' + s + inner); 1155 | if (!arr) return pre + post; 1156 | return [pre, inner + arr, base + post].join(s); 1157 | }; 1158 | 1159 | function array(arr) { 1160 | var i = arr.length, 1161 | ret = Array(i); 1162 | this.up(); 1163 | while (i--) 1164 | ret[i] = this.parse(arr[i]); 1165 | this.down(); 1166 | return join('[', ret, ']'); 1167 | }; 1168 | 1169 | var reName = /^function (\w+)/; 1170 | 1171 | var jsDump = { 1172 | parse: function(obj, type) { //type is used mostly internally, you can fix a (custom)type in advance 1173 | var parser = this.parsers[type || this.typeOf(obj)]; 1174 | type = typeof parser; 1175 | 1176 | return type == 'function' ? parser.call(this, obj) : type == 'string' ? parser : this.parsers.error; 1177 | }, 1178 | typeOf: function(obj) { 1179 | var type; 1180 | if (obj === null) { 1181 | type = "null"; 1182 | } else if (typeof obj === "undefined") { 1183 | type = "undefined"; 1184 | } else if (QUnit.is("RegExp", obj)) { 1185 | type = "regexp"; 1186 | } else if (QUnit.is("Date", obj)) { 1187 | type = "date"; 1188 | } else if (QUnit.is("Function", obj)) { 1189 | type = "function"; 1190 | } else if (typeof obj.setInterval !== undefined && typeof obj.document !== "undefined" && typeof obj.nodeType === "undefined") { 1191 | type = "window"; 1192 | } else if (obj.nodeType === 9) { 1193 | type = "document"; 1194 | } else if (obj.nodeType) { 1195 | type = "node"; 1196 | } else if (typeof obj === "object" && typeof obj.length === "number" && obj.length >= 0) { 1197 | type = "array"; 1198 | } else { 1199 | type = typeof obj; 1200 | } 1201 | return type; 1202 | }, 1203 | separator: function() { 1204 | return this.multiline ? this.HTML ? '
' : '\n' : this.HTML ? ' ' : ' '; 1205 | }, 1206 | indent: function(extra) { // extra can be a number, shortcut for increasing-calling-decreasing 1207 | if (!this.multiline) return ''; 1208 | var chr = this.indentChar; 1209 | if (this.HTML) chr = chr.replace(/\t/g, ' ').replace(/ /g, ' '); 1210 | return Array(this._depth_ + (extra || 0)).join(chr); 1211 | }, 1212 | up: function(a) { 1213 | this._depth_ += a || 1; 1214 | }, 1215 | down: function(a) { 1216 | this._depth_ -= a || 1; 1217 | }, 1218 | setParser: function(name, parser) { 1219 | this.parsers[name] = parser; 1220 | }, 1221 | // The next 3 are exposed so you can use them 1222 | quote: quote, 1223 | literal: literal, 1224 | join: join, 1225 | // 1226 | _depth_: 1, 1227 | // This is the list of parsers, to modify them, use jsDump.setParser 1228 | parsers: { 1229 | window: '[Window]', 1230 | document: '[Document]', 1231 | error: '[ERROR]', 1232 | //when no parser is found, shouldn't happen 1233 | unknown: '[Unknown]', 1234 | 'null': 'null', 1235 | 'undefined': 'undefined', 1236 | 'function': function(fn) { 1237 | var ret = 'function', 1238 | name = 'name' in fn ? fn.name : (reName.exec(fn) || [])[1]; //functions never have name in IE 1239 | if (name) ret += ' ' + name; 1240 | ret += '('; 1241 | 1242 | ret = [ret, QUnit.jsDump.parse(fn, 'functionArgs'), '){'].join(''); 1243 | return join(ret, QUnit.jsDump.parse(fn, 'functionCode'), '}'); 1244 | }, 1245 | array: array, 1246 | nodelist: array, 1247 | arguments: array, 1248 | object: function(map) { 1249 | var ret = []; 1250 | QUnit.jsDump.up(); 1251 | for (var key in map) 1252 | ret.push(QUnit.jsDump.parse(key, 'key') + ': ' + QUnit.jsDump.parse(map[key])); 1253 | QUnit.jsDump.down(); 1254 | return join('{', ret, '}'); 1255 | }, 1256 | node: function(node) { 1257 | var open = QUnit.jsDump.HTML ? '<' : '<', 1258 | close = QUnit.jsDump.HTML ? '>' : '>'; 1259 | 1260 | var tag = node.nodeName.toLowerCase(), 1261 | ret = open + tag; 1262 | 1263 | for (var a in QUnit.jsDump.DOMAttrs) { 1264 | var val = node[QUnit.jsDump.DOMAttrs[a]]; 1265 | if (val) ret += ' ' + a + '=' + QUnit.jsDump.parse(val, 'attribute'); 1266 | } 1267 | return ret + close + open + '/' + tag + close; 1268 | }, 1269 | functionArgs: function(fn) { //function calls it internally, it's the arguments part of the function 1270 | var l = fn.length; 1271 | if (!l) return ''; 1272 | 1273 | var args = Array(l); 1274 | while (l--) 1275 | args[l] = String.fromCharCode(97 + l); //97 is 'a' 1276 | return ' ' + args.join(', ') + ' '; 1277 | }, 1278 | key: quote, 1279 | //object calls it internally, the key part of an item in a map 1280 | functionCode: '[code]', 1281 | //function calls it internally, it's the content of the function 1282 | attribute: quote, 1283 | //node calls it internally, it's an html attribute value 1284 | string: quote, 1285 | date: quote, 1286 | regexp: literal, 1287 | //regex 1288 | number: literal, 1289 | 'boolean': literal 1290 | }, 1291 | DOMAttrs: { //attributes to dump from nodes, name=>realName 1292 | id: 'id', 1293 | name: 'name', 1294 | 'class': 'className' 1295 | }, 1296 | HTML: false, 1297 | //if true, entities are escaped ( <, >, \t, space and \n ) 1298 | indentChar: ' ', 1299 | //indentation unit 1300 | multiline: true //if true, items in a collection, are separated by a \n, else just a space. 1301 | }; 1302 | 1303 | return jsDump; 1304 | })(); 1305 | 1306 | // from Sizzle.js 1307 | 1308 | 1309 | function getText(elems) { 1310 | var ret = "", 1311 | elem; 1312 | 1313 | for (var i = 0; elems[i]; i++) { 1314 | elem = elems[i]; 1315 | 1316 | // Get the text from text nodes and CDATA nodes 1317 | if (elem.nodeType === 3 || elem.nodeType === 4) { 1318 | ret += elem.nodeValue; 1319 | 1320 | // Traverse everything else, except comment nodes 1321 | } else if (elem.nodeType !== 8) { 1322 | ret += getText(elem.childNodes); 1323 | } 1324 | } 1325 | 1326 | return ret; 1327 | }; 1328 | 1329 | /* 1330 | * Javascript Diff Algorithm 1331 | * By John Resig (http://ejohn.org/) 1332 | * Modified by Chu Alan "sprite" 1333 | * 1334 | * Released under the MIT license. 1335 | * 1336 | * More Info: 1337 | * http://ejohn.org/projects/javascript-diff-algorithm/ 1338 | * 1339 | * Usage: QUnit.diff(expected, actual) 1340 | * 1341 | * QUnit.diff("the quick brown fox jumped over", "the quick fox jumps over") == "the quick brown fox jumped jumps over" 1342 | */ 1343 | QUnit.diff = (function() { 1344 | function diff(o, n) { 1345 | var ns = new Object(); 1346 | var os = new Object(); 1347 | 1348 | for (var i = 0; i < n.length; i++) { 1349 | if (ns[n[i]] == null) ns[n[i]] = { 1350 | rows: new Array(), 1351 | o: null 1352 | }; 1353 | ns[n[i]].rows.push(i); 1354 | } 1355 | 1356 | for (var i = 0; i < o.length; i++) { 1357 | if (os[o[i]] == null) os[o[i]] = { 1358 | rows: new Array(), 1359 | n: null 1360 | }; 1361 | os[o[i]].rows.push(i); 1362 | } 1363 | 1364 | for (var i in ns) { 1365 | if (ns[i].rows.length == 1 && typeof(os[i]) != "undefined" && os[i].rows.length == 1) { 1366 | n[ns[i].rows[0]] = { 1367 | text: n[ns[i].rows[0]], 1368 | row: os[i].rows[0] 1369 | }; 1370 | o[os[i].rows[0]] = { 1371 | text: o[os[i].rows[0]], 1372 | row: ns[i].rows[0] 1373 | }; 1374 | } 1375 | } 1376 | 1377 | for (var i = 0; i < n.length - 1; i++) { 1378 | if (n[i].text != null && n[i + 1].text == null && n[i].row + 1 < o.length && o[n[i].row + 1].text == null && n[i + 1] == o[n[i].row + 1]) { 1379 | n[i + 1] = { 1380 | text: n[i + 1], 1381 | row: n[i].row + 1 1382 | }; 1383 | o[n[i].row + 1] = { 1384 | text: o[n[i].row + 1], 1385 | row: i + 1 1386 | }; 1387 | } 1388 | } 1389 | 1390 | for (var i = n.length - 1; i > 0; i--) { 1391 | if (n[i].text != null && n[i - 1].text == null && n[i].row > 0 && o[n[i].row - 1].text == null && n[i - 1] == o[n[i].row - 1]) { 1392 | n[i - 1] = { 1393 | text: n[i - 1], 1394 | row: n[i].row - 1 1395 | }; 1396 | o[n[i].row - 1] = { 1397 | text: o[n[i].row - 1], 1398 | row: i - 1 1399 | }; 1400 | } 1401 | } 1402 | 1403 | return { 1404 | o: o, 1405 | n: n 1406 | }; 1407 | } 1408 | 1409 | return function(o, n) { 1410 | o = o.replace(/\s+$/, ''); 1411 | n = n.replace(/\s+$/, ''); 1412 | var out = diff(o == "" ? [] : o.split(/\s+/), n == "" ? [] : n.split(/\s+/)); 1413 | 1414 | var str = ""; 1415 | 1416 | var oSpace = o.match(/\s+/g); 1417 | if (oSpace == null) { 1418 | oSpace = [" "]; 1419 | } else { 1420 | oSpace.push(" "); 1421 | } 1422 | var nSpace = n.match(/\s+/g); 1423 | if (nSpace == null) { 1424 | nSpace = [" "]; 1425 | } else { 1426 | nSpace.push(" "); 1427 | } 1428 | 1429 | if (out.n.length == 0) { 1430 | for (var i = 0; i < out.o.length; i++) { 1431 | str += '' + out.o[i] + oSpace[i] + ""; 1432 | } 1433 | } else { 1434 | if (out.n[0].text == null) { 1435 | for (n = 0; n < out.o.length && out.o[n].text == null; n++) { 1436 | str += '' + out.o[n] + oSpace[n] + ""; 1437 | } 1438 | } 1439 | 1440 | for (var i = 0; i < out.n.length; i++) { 1441 | if (out.n[i].text == null) { 1442 | str += '' + out.n[i] + nSpace[i] + ""; 1443 | } else { 1444 | var pre = ""; 1445 | 1446 | for (n = out.n[i].row + 1; n < out.o.length && out.o[n].text == null; n++) { 1447 | pre += '' + out.o[n] + oSpace[n] + ""; 1448 | } 1449 | str += " " + out.n[i].text + nSpace[i] + pre; 1450 | } 1451 | } 1452 | } 1453 | 1454 | return str; 1455 | }; 1456 | })(); 1457 | 1458 | })(this); -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 2 | var ELEM_SELECTOR = '#stopwatch'; 3 | 4 | 5 | module("Create & Destroy", { 6 | setup: function(){ 7 | $elem = $(ELEM_SELECTOR); 8 | }, 9 | teardown: function(){ 10 | $elem.removeData(); 11 | } 12 | }); 13 | 14 | test("Basic initialise", function() { 15 | var elem = $elem.stopwatch('init'); 16 | ok(elem instanceof Object); 17 | same(elem, $elem.stopwatch('init')); //init call on existing stopwatch 18 | }); 19 | 20 | test("Default data", function() { 21 | var data = $elem.stopwatch('init').data('stopwatch'); 22 | equals(data.updateInterval, 1000, 23 | 'Default update interval should be 1000, not ' + data.updateInterval); 24 | equals(data.startTime, 0, 25 | 'Default start time offset should be 0, not ' + data.startTime); 26 | equals(data.active, false); 27 | }); 28 | 29 | test("User provided data", function() { 30 | var user_data = {startTime: 1000, updateInterval: 2000}; 31 | var data = $elem.stopwatch('init', user_data).data('stopwatch'); 32 | equals(data.startTime, user_data.startTime, 33 | 'Start time offset should equal ' + user_data.startTime + ' not ' + data.startTime); 34 | equals(data.updateInterval, user_data.updateInterval, 35 | 'Update interval should equal ' + user_data.updateInterval + ' not ' + data.updateInterval); 36 | }); 37 | 38 | test("Destroy", function() { 39 | $elem.stopwatch('init').stopwatch('destroy'); 40 | ok( !$elem.data('stopwatch') ); 41 | }); 42 | 43 | 44 | 45 | module("Start & Stop", { 46 | setup: function(){ 47 | $elem = $(ELEM_SELECTOR); 48 | $elem.stopwatch('init'); 49 | }, 50 | teardown: function(){ 51 | $elem.stopwatch('destroy'); 52 | } 53 | }); 54 | 55 | test("Start", function(){ 56 | $elem.stopwatch('start'); 57 | equals($elem.data('stopwatch').active, true); 58 | }); 59 | 60 | test("Stop", function(){ 61 | $elem.stopwatch('stop'); 62 | equals($elem.data('stopwatch').active, false); 63 | }); 64 | 65 | test("Toggle", function(){ 66 | $elem.stopwatch('stop'); 67 | $elem.stopwatch('toggle'); 68 | equals($elem.data('stopwatch').active, true); 69 | $elem.stopwatch('toggle'); 70 | equals($elem.data('stopwatch').active, false); 71 | }); 72 | 73 | 74 | 75 | module("Standard Operation", { 76 | setup: function(){ 77 | $elem = $(ELEM_SELECTOR); 78 | $elem.stopwatch('init'); 79 | }, 80 | teardown: function(){ 81 | $elem.stopwatch('destroy'); 82 | } 83 | }); 84 | 85 | test("Render", function(){ 86 | $elem.stopwatch('render'); 87 | equals($elem.text(), '00:00:00'); 88 | }); 89 | 90 | test("Elapsed time", function(){ 91 | var data = $elem.data('stopwatch'); 92 | equals(data.elapsed, 0); 93 | $elem.stopwatch('start'); 94 | stop(); 95 | setTimeout(function(){ 96 | equals(data.elapsed, 2000); 97 | start(); 98 | }, 2500); 99 | }); 100 | 101 | test("Get time", function(){ 102 | var data = $elem.data('stopwatch'); 103 | equals($elem.stopwatch('getTime'), 0); 104 | equals(data.elapsed, $elem.stopwatch('getTime')); 105 | $elem.stopwatch('start'); 106 | stop(); 107 | setTimeout(function(){ 108 | equals($elem.stopwatch('getTime'), 2000); 109 | start(); 110 | }, 2500); 111 | }); 112 | 113 | 114 | 115 | module("Jintervals Formatting", { 116 | setup: function(){ 117 | $elem = $(ELEM_SELECTOR); 118 | }, 119 | teardown: function(){ 120 | $elem.stopwatch('destroy'); 121 | } 122 | }); 123 | 124 | test("jsinterval formatting", function() { 125 | var user_data = {startTime: 1000, format: '{MM}:{SS}'}; 126 | var data = $elem.stopwatch('init', user_data).data('stopwatch'); 127 | equals(data.formatter(data.elapsed, data), '00:01', 128 | 'Formatted output should be 00:01'); 129 | }); 130 | 131 | 132 | 133 | module("Reset", { 134 | setup: function(){ 135 | $elem = $(ELEM_SELECTOR); 136 | $elem.stopwatch('init'); 137 | }, 138 | teardown: function(){ 139 | $elem.removeData(); 140 | } 141 | }); 142 | 143 | test("from inactive", function(){ 144 | $elem.stopwatch('start'); 145 | stop(); 146 | setTimeout(function(){ 147 | $elem.stopwatch('stop'); //stop first, then reset 148 | $elem.stopwatch('reset'); 149 | equals($elem.data('stopwatch').elapsed, $elem.data('stopwatch').startTime); 150 | start(); 151 | }, 2000); 152 | }); 153 | 154 | test("from active", function(){ 155 | $elem.stopwatch('start'); 156 | stop(); 157 | setTimeout(function(){ 158 | $elem.stopwatch('reset'); //reset whilst active, then stop 159 | $elem.stopwatch('stop'); 160 | equals($elem.data('stopwatch').elapsed, $elem.data('stopwatch').startTime); 161 | start(); 162 | }, 2000); 163 | }); 164 | 165 | 166 | 167 | module("Non-Standard Operation", { 168 | setup: function(){ 169 | $elem = $(ELEM_SELECTOR); 170 | }, 171 | teardown: function(){ 172 | $elem.stopwatch('destroy'); 173 | } 174 | }); 175 | 176 | test("Custom update interval", function(){ 177 | // wait 2 secs, with update interval of 100 millis, elapsed should be 1900 178 | $elem.stopwatch('init', {updateInterval: 100}); 179 | var data = $elem.data('stopwatch'); 180 | equals(data.updateInterval, 100); 181 | $elem.stopwatch('start'); 182 | stop(); 183 | setTimeout(function(){ 184 | equals(data.elapsed, 1900); 185 | start(); 186 | }, 2000); 187 | }); 188 | --------------------------------------------------------------------------------