mirror of https://github.com/freeCodeCamp/devdocs
commit
18986c1814
@ -0,0 +1,10 @@
|
|||||||
|
.DS_Store
|
||||||
|
.bundle
|
||||||
|
*.pxm
|
||||||
|
*.sketch
|
||||||
|
tmp
|
||||||
|
public/assets
|
||||||
|
public/fonts
|
||||||
|
public/docs/**/*
|
||||||
|
!public/docs/docs.json
|
||||||
|
!public/docs/**/index.json
|
@ -0,0 +1,84 @@
|
|||||||
|
# Contributing to DevDocs
|
||||||
|
|
||||||
|
Wish to contribute? Great. Please review the following guidelines carefully and always search for existing issues before opening a new one. More time spent managing issues means less time spent improving the software.
|
||||||
|
|
||||||
|
_Note: DevDocs is my first open source project and one which I deeply care about. Please forgive my inexperience and the fact that I may push back on certain things in order to keep the project to my liking. Feedback and advice are always welcome._
|
||||||
|
|
||||||
|
**Table of Contents:**
|
||||||
|
|
||||||
|
1. [Reporting bugs](#reporting-bugs)
|
||||||
|
2. [Requesting new features](#requesting-new-features)
|
||||||
|
3. [Requesting new docs](#requesting-new-docs)
|
||||||
|
4. [Contributing code and features](#contributing-code-and-features)
|
||||||
|
5. [Contributing new docs](#contributing-new-docs)
|
||||||
|
6. [Other contributions](#other-contributions)
|
||||||
|
7. [Coding conventions](#coding-conventions)
|
||||||
|
8. [Questions?](#questions)
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
1. Always update to the most recent master release; the bug may already be fixed.
|
||||||
|
2. Search for existing issues; it's possible someone has already encountered this bug.
|
||||||
|
3. Try to isolate the problem and include steps to reproduce it.
|
||||||
|
4. Share as much information as possible (e.g. browser/OS environment, log output, stack trace, screenshots, etc.).
|
||||||
|
|
||||||
|
## Requesting new features
|
||||||
|
|
||||||
|
1. Search for similar feature requests; someone may have already requested it.
|
||||||
|
2. Make sure your feature fits DevDocs's [vision and stated goals](https://github.com/Thibaut/devdocs/blob/master/README.md#vision).
|
||||||
|
3. Provide a clear and detailed explanation of the feature and why it's important to add it.
|
||||||
|
|
||||||
|
For general feedback and ideas, please use the [mailing list](https://groups.google.com/d/forum/devdocs).
|
||||||
|
|
||||||
|
## Requesting new docs
|
||||||
|
|
||||||
|
Please do not open issues to request new documentations.
|
||||||
|
Use the [Trello board](https://trello.com/b/6BmTulfx/devdocs-documentation) where everyone can vote and contributors can get a feel for what's wished for.
|
||||||
|
|
||||||
|
## Contributing code and features
|
||||||
|
|
||||||
|
1. Search for existing issues; someone may already be working on a similar feature.
|
||||||
|
2. Before embarking on any significant pull request, please open an issue describing the changes you intend to make. Otherwise you risk spending a lot of time working on something that I may not want to merge. This also tells other contributors that you're working on the feature.
|
||||||
|
3. Follow the [coding conventions](#coding-conventions).
|
||||||
|
4. If you're modifying the Ruby code, include tests and ensure they pass.
|
||||||
|
5. Try to keep your pull request small and simple.
|
||||||
|
6. When it makes sense, squash your commits into a single commit.
|
||||||
|
7. Describe all your changes in the commit message and/or pull request.
|
||||||
|
|
||||||
|
## Contributing new docs
|
||||||
|
|
||||||
|
**Note:** there is currently no documentation on how to create a scraper/documentation. I'm working on it.
|
||||||
|
|
||||||
|
**Important:** in order to keep things fast and manageable, only the documentation of popular open source projects will be accepted into DevDocs. As more projects find their way in, the required level of popularity will gradually decrease. Additionally, the documentation's license must permit alteration, redistribution, and commercial use of the work. Software vendors that wish to add commercial software documentation to DevDocs may contact me privately.
|
||||||
|
|
||||||
|
**Please open an issue before adding any new documentation.**
|
||||||
|
|
||||||
|
In addition to the [guidelines for contributing code](#contributing-code-and-features), the following guidelines apply to pull requests that add a new documentation:
|
||||||
|
|
||||||
|
* Your documentation must come with a clean and official icon, in both 1x and 2x resolutions (16x16 and 32x32 pixels). This is important because icons are the only thing differentiating search results inside the app. If a project doesn't have an official icon, it won't be accepted into DevDocs—sorry.
|
||||||
|
* DevDocs favors quality over quantity. Your documentation should only include API/reference documents that most developers may wish to read semi-regularly. By reducing the number of entries you make it easier for people to find other, more relevant entries. _(Note: you're more than welcome to submit pull requests removing seldom-used entries from existing documentations.)_
|
||||||
|
* Try to remove as much content and HTML markup as possible, particularly content which isn't associated with any entries (e.g. introduction, changelog, etc.).
|
||||||
|
* Names must be as short as possible and unique across the documentation.
|
||||||
|
* The number of types (categories) must be less than 50.
|
||||||
|
* It's ok to leave the CSS up to me.
|
||||||
|
* Don't modify the icon sprite. I'll do it when merging your pull request.
|
||||||
|
* Once your documentation is accepted into DevDocs, you'll be expected to maintain it on a regular basis.
|
||||||
|
|
||||||
|
## Other contributions
|
||||||
|
|
||||||
|
Besides new docs and features, here are other ways you can contribute:
|
||||||
|
|
||||||
|
* **Improve words and sentences.** English isn't my first language so if you notice grammatical or usage errors, feel free to submit a pull request—it'll be much appreciated. (Note: American English is the preferred form)
|
||||||
|
* **Write documentation.** Although this task is mainly up to me, any documentation you can write that may help other developers understand and contribute to the code is highly appreciated.
|
||||||
|
* **Participate in the issue tracker.** Your opinion matters—feel free to add comments to existing issues. You're also welcome to participate to the [mailing list](https://groups.google.com/d/forum/devdocs).
|
||||||
|
|
||||||
|
## Coding conventions
|
||||||
|
|
||||||
|
* two spaces; no tabs
|
||||||
|
* no trailing whitespace; blank lines should have no spaces
|
||||||
|
* use the same coding style as the rest of the codebase
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have any questions, please feel free to ask on the [mailing list](https://groups.google.com/d/forum/devdocs).
|
||||||
|
|
@ -0,0 +1,14 @@
|
|||||||
|
Copyright 2013 Thibaut Courouble and other contributors
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
NOTE: DevDocs is considered a trademark. You may not use the name to
|
||||||
|
endorse or promote products derived from this software without
|
||||||
|
Thibaut Courouble's permission, except as may be necessary to
|
||||||
|
comply with the notice/attribution requirements.
|
||||||
|
|
||||||
|
ADDITIONALLY: it is expected that any documentation file generated
|
||||||
|
using this software must be attributed to DevDocs. Let's be fair
|
||||||
|
to all contributors by not stealing their hard work.
|
@ -0,0 +1,41 @@
|
|||||||
|
source 'https://rubygems.org'
|
||||||
|
ruby '2.0.0'
|
||||||
|
|
||||||
|
gem 'thor'
|
||||||
|
gem 'pry', '~> 0.9.12'
|
||||||
|
gem 'activesupport', '~> 4.0', require: false
|
||||||
|
gem 'yajl-ruby', require: false
|
||||||
|
|
||||||
|
group :app do
|
||||||
|
gem 'rack'
|
||||||
|
gem 'sinatra'
|
||||||
|
gem 'sinatra-contrib'
|
||||||
|
gem 'thin'
|
||||||
|
gem 'sprockets'
|
||||||
|
gem 'sprockets-helpers'
|
||||||
|
gem 'erubis'
|
||||||
|
gem 'browser'
|
||||||
|
gem 'sass'
|
||||||
|
gem 'coffee-script'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :production do
|
||||||
|
gem 'uglifier'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'better_errors'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :docs do
|
||||||
|
gem 'typhoeus'
|
||||||
|
gem 'nokogiri', '~> 1.6.0'
|
||||||
|
gem 'html-pipeline'
|
||||||
|
gem 'progress_bar'
|
||||||
|
gem 'unix_utils'
|
||||||
|
end
|
||||||
|
|
||||||
|
group :test do
|
||||||
|
gem 'minitest'
|
||||||
|
gem 'rr', require: false
|
||||||
|
end
|
@ -0,0 +1,131 @@
|
|||||||
|
GEM
|
||||||
|
remote: https://rubygems.org/
|
||||||
|
specs:
|
||||||
|
activesupport (4.0.0)
|
||||||
|
i18n (~> 0.6, >= 0.6.4)
|
||||||
|
minitest (~> 4.2)
|
||||||
|
multi_json (~> 1.3)
|
||||||
|
thread_safe (~> 0.1)
|
||||||
|
tzinfo (~> 0.3.37)
|
||||||
|
atomic (1.1.14)
|
||||||
|
backports (3.3.5)
|
||||||
|
better_errors (1.0.1)
|
||||||
|
coderay (>= 1.0.0)
|
||||||
|
erubis (>= 2.6.6)
|
||||||
|
browser (0.2.1)
|
||||||
|
coderay (1.0.9)
|
||||||
|
coffee-script (2.2.0)
|
||||||
|
coffee-script-source
|
||||||
|
execjs
|
||||||
|
coffee-script-source (1.6.3)
|
||||||
|
daemons (1.1.9)
|
||||||
|
erubis (2.7.0)
|
||||||
|
escape_utils (0.3.2)
|
||||||
|
ethon (0.6.1)
|
||||||
|
ffi (>= 1.3.0)
|
||||||
|
mime-types (~> 1.18)
|
||||||
|
eventmachine (1.0.3)
|
||||||
|
execjs (2.0.2)
|
||||||
|
fattr (2.2.1)
|
||||||
|
ffi (1.9.0)
|
||||||
|
gemoji (1.4.0)
|
||||||
|
github-markdown (0.5.5)
|
||||||
|
highline (1.6.19)
|
||||||
|
hike (1.2.3)
|
||||||
|
html-pipeline (0.2.1)
|
||||||
|
escape_utils (~> 0.3)
|
||||||
|
gemoji (~> 1.0)
|
||||||
|
github-markdown (~> 0.5)
|
||||||
|
nokogiri (~> 1.4)
|
||||||
|
rinku (~> 1.7)
|
||||||
|
sanitize (~> 2.0)
|
||||||
|
i18n (0.6.5)
|
||||||
|
method_source (0.8.2)
|
||||||
|
mime-types (1.25)
|
||||||
|
mini_portile (0.5.1)
|
||||||
|
minitest (4.7.5)
|
||||||
|
multi_json (1.8.1)
|
||||||
|
nokogiri (1.6.0)
|
||||||
|
mini_portile (~> 0.5.0)
|
||||||
|
options (2.3.0)
|
||||||
|
fattr
|
||||||
|
progress_bar (1.0.0)
|
||||||
|
highline (~> 1.6.1)
|
||||||
|
options (~> 2.3.0)
|
||||||
|
pry (0.9.12.2)
|
||||||
|
coderay (~> 1.0.5)
|
||||||
|
method_source (~> 0.8)
|
||||||
|
slop (~> 3.4)
|
||||||
|
rack (1.5.2)
|
||||||
|
rack-protection (1.5.0)
|
||||||
|
rack
|
||||||
|
rack-test (0.6.2)
|
||||||
|
rack (>= 1.0)
|
||||||
|
rinku (1.7.3)
|
||||||
|
rr (1.1.2)
|
||||||
|
sanitize (2.0.6)
|
||||||
|
nokogiri (>= 1.4.4)
|
||||||
|
sass (3.2.12)
|
||||||
|
sinatra (1.4.3)
|
||||||
|
rack (~> 1.4)
|
||||||
|
rack-protection (~> 1.4)
|
||||||
|
tilt (~> 1.3, >= 1.3.4)
|
||||||
|
sinatra-contrib (1.4.1)
|
||||||
|
backports (>= 2.0)
|
||||||
|
multi_json
|
||||||
|
rack-protection
|
||||||
|
rack-test
|
||||||
|
sinatra (~> 1.4.0)
|
||||||
|
tilt (~> 1.3)
|
||||||
|
slop (3.4.6)
|
||||||
|
sprockets (2.10.0)
|
||||||
|
hike (~> 1.2)
|
||||||
|
multi_json (~> 1.0)
|
||||||
|
rack (~> 1.0)
|
||||||
|
tilt (~> 1.1, != 1.3.0)
|
||||||
|
sprockets-helpers (1.0.1)
|
||||||
|
sprockets (~> 2.0)
|
||||||
|
thin (1.6.0)
|
||||||
|
daemons (>= 1.0.9)
|
||||||
|
eventmachine (>= 1.0.0)
|
||||||
|
rack (>= 1.5.0)
|
||||||
|
thor (0.18.1)
|
||||||
|
thread_safe (0.1.3)
|
||||||
|
atomic
|
||||||
|
tilt (1.4.1)
|
||||||
|
typhoeus (0.6.5)
|
||||||
|
ethon (~> 0.6.1)
|
||||||
|
tzinfo (0.3.38)
|
||||||
|
uglifier (2.2.1)
|
||||||
|
execjs (>= 0.3.0)
|
||||||
|
multi_json (~> 1.0, >= 1.0.2)
|
||||||
|
unix_utils (0.0.15)
|
||||||
|
yajl-ruby (1.1.0)
|
||||||
|
|
||||||
|
PLATFORMS
|
||||||
|
ruby
|
||||||
|
|
||||||
|
DEPENDENCIES
|
||||||
|
activesupport (~> 4.0)
|
||||||
|
better_errors
|
||||||
|
browser
|
||||||
|
coffee-script
|
||||||
|
erubis
|
||||||
|
html-pipeline
|
||||||
|
minitest
|
||||||
|
nokogiri (~> 1.6.0)
|
||||||
|
progress_bar
|
||||||
|
pry (~> 0.9.12)
|
||||||
|
rack
|
||||||
|
rr
|
||||||
|
sass
|
||||||
|
sinatra
|
||||||
|
sinatra-contrib
|
||||||
|
sprockets
|
||||||
|
sprockets-helpers
|
||||||
|
thin
|
||||||
|
thor
|
||||||
|
typhoeus
|
||||||
|
uglifier
|
||||||
|
unix_utils
|
||||||
|
yajl-ruby
|
@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
@ -0,0 +1,136 @@
|
|||||||
|
# [DevDocs](http://devdocs.io) — Documentation Browser
|
||||||
|
|
||||||
|
DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
|
||||||
|
|
||||||
|
* Created by [Thibaut Courouble](http://thibaut.me)
|
||||||
|
* Sponsored by [MaxCDN](http://www.maxcdn.com)
|
||||||
|
|
||||||
|
Keep track of development and community news:
|
||||||
|
|
||||||
|
* Subscribe to the [newsletter](http://eepurl.com/HnLUz)
|
||||||
|
* Follow [@DevDocs](https://twitter.com/DevDocs) on Twitter
|
||||||
|
* Join the [mailing list](https://groups.google.com/d/forum/devdocs)
|
||||||
|
|
||||||
|
DevDocs is free and open source. If you use it and like it, please consider donating through [Gittip](https://www.gittip.com/Thibaut/) or [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4PTFAGT7K6QVG). Your support helps sustain the project and is highly appreciated.
|
||||||
|
|
||||||
|
**Table of Contents:** [Quick Start](#quick-start) · [Vision](#vision) · [App](#app) · [Scraper](#scraper) · [Commands](#available-commands) · [Contributing](#contributing) · [License](#copyright--license) · [Questions?](#questions)
|
||||||
|
|
||||||
|
**Note:** I'm in the process of writing more documentation. As DevDocs is quite big, it'll take time. Feel free to [contact me directly](mailto:thibaut@devdocs.io) in the meantime.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
Unless you wish to use DevDocs offline or contribute to the code, I recommend using the hosted version at [devdocs.io](http://devdocs.io). It's up-to-date and requires no setup.
|
||||||
|
|
||||||
|
DevDocs is made of two separate pieces of software: a Ruby scraper responsible for generating the documentation files and indexes, and a JavaScript front-end powered by on small Sinatra app.
|
||||||
|
|
||||||
|
DevDocs requires Ruby 2.0. Once you have it installed, run the following commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
gem install bundler
|
||||||
|
bundle install
|
||||||
|
thor docs:download --all
|
||||||
|
rackup
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, point your browser at [localhost:9292](http://localhost:9292) (the first request will take a few seconds to compile the assets). You're all set.
|
||||||
|
|
||||||
|
The `thor docs:download` command is used to download/update individual documentations (e.g. `thor docs:download html css`), or all at the same time (using the `--all` option). You can see the list of available documentations by running `thor docs:list`.
|
||||||
|
|
||||||
|
**Note:** there is currently no update mechanism other than using git to update the code and `thor docs:download` to download the latest version of the docs. To stay informed about new versions, be sure to subscribe to the [newsletter](http://eepurl.com/HnLUz).
|
||||||
|
|
||||||
|
## Vision
|
||||||
|
|
||||||
|
DevDocs aims to make reading and searching reference documentation fast, accessible and enjoyable, while aspiring to become the “one stop shop” for all open-source software documentations.
|
||||||
|
|
||||||
|
The app's main goals are to: keep boot and page load times as fast as possible; improve the quality, speed, and order of search results; maximize the use of caching and other performance optimizations; maintain a clean, readable user interface; support full keyboard navigation; reduce “context switch” by using a consistent typography and design across all documentations; reduce clutter by focusing on a specific category of content (API/reference) and indexing only the minimum useful to most developers.
|
||||||
|
|
||||||
|
**Note:** DevDocs is neither a programming guide nor a search engine. All content is pulled from third-party sources and the project does not intend to compete with full-text search engines. Its backbone is metadata: each piece of content must be identified by a unique, obvious and short string. Thus, tutorials, guides and other content that don't fit this requirement are outside the scope of the project.
|
||||||
|
|
||||||
|
## App
|
||||||
|
|
||||||
|
The web app is all JavaScript, written in [CoffeeScript](http://coffeescript.org), and powered by a small [Sinatra](http://www.sinatrarb.com)/[Sprockets](https://github.com/sstephenson/sprockets) application. It relies on files generated by the [scraper](#scraper).
|
||||||
|
|
||||||
|
Many of the code's design decisions were driven by the fact that the app uses XHR to load content directly into the main frame. This includes stripping the original documents of most of their HTML markup (e.g. scripts and stylesheets) to avoid polluting the main frame, and prefixing all CSS class names with an underscore to prevent conflicts.
|
||||||
|
|
||||||
|
Another driving factor is the requirement for speed. This is partially solved by maximizing caching (both `applicationCache`, which comes with its own set of constraints, and `localStorage` are used to their full extent), as well as by allowing the user to pick his/her own set of documentations. On the other hand, the search algorithm is currently not very sophisticated because it needs to be fast even searching through 100k entries.
|
||||||
|
|
||||||
|
DevDocs being a developer tool, the browser requirements are high:
|
||||||
|
|
||||||
|
1. On the desktop:
|
||||||
|
* Recent version of Chrome
|
||||||
|
* Recent version of Firefox
|
||||||
|
* Safari 5.1+
|
||||||
|
* Opera 12.1+
|
||||||
|
* Interner Explorer 10+
|
||||||
|
2. On mobile:
|
||||||
|
* iOS 6+
|
||||||
|
* Android 4.1+
|
||||||
|
* Windows Phone 8+
|
||||||
|
|
||||||
|
This allows the code to take advantage of the latest DOM and HTML5 APIs and make developing DevDocs a lot more fun!
|
||||||
|
|
||||||
|
## Scraper
|
||||||
|
|
||||||
|
The scraper is responsible for generating the documentation and index files (metadata) used by the [app](#app). It's written in Ruby under the `Docs` module.
|
||||||
|
|
||||||
|
There are currently two kinds of scrapers: `UrlScraper` which downloads files via HTTP and `FileScraper` which reads them from the local filesystem. They both make copies of HTML documents, recursively following links that match a given set of rules and applying all sorts of modifications along the way, in addition to building an index of the files and their metadata. Documents are parsed using [Nokogiri](http://nokogiri.org).
|
||||||
|
|
||||||
|
Modifications made to each document include:
|
||||||
|
* removing stuff, such as the document structure (`<html>`, `<head>`, etc.), comments, empty nodes, etc.
|
||||||
|
* fixing links (e.g. to remove duplicates)
|
||||||
|
* replacing all external (not copied) URLs with their fully qualified counterpart
|
||||||
|
* replacing all internal (copied) URLs with their unqualified and relative counterpart
|
||||||
|
* adding stuff, such as a title and link to the original document
|
||||||
|
|
||||||
|
These modifications are applied through a set of filters, with each scraper also applying custom filters specific to the documentation. Each document is also passed through a filter whose task is to figure out its metadata, namely its _name_ and _type_ (category).
|
||||||
|
|
||||||
|
The end result is a set of normalized HTML partials and a JSON index file. Because the index files are loaded separately by the [app](#app) following the user's preferences, the code also creates a JSON manifest file containing information about the documentations currently available on the system (such as their name, version, update date, etc.).
|
||||||
|
|
||||||
|
## Available Commands
|
||||||
|
|
||||||
|
The command-line interface uses [Thor](http://whatisthor.com). To see all commands and options, run `thor list` from the project's root.
|
||||||
|
|
||||||
|
```
|
||||||
|
# Server
|
||||||
|
rackup # Start the server (ctrl+c to stop)
|
||||||
|
rackup --help # List server options
|
||||||
|
|
||||||
|
# Docs
|
||||||
|
thor docs:list # List available documentations
|
||||||
|
thor docs:download # Download one or more documentations
|
||||||
|
thor docs:manifest # Create the manifest file used by the app
|
||||||
|
thor docs:generate # Generate/scrape a documentation
|
||||||
|
thor docs:page # Generate/scrape a documentation page
|
||||||
|
thor docs:package # Package a documentation for use with docs:download
|
||||||
|
|
||||||
|
# Console
|
||||||
|
thor console # Start a REPL
|
||||||
|
thor console:docs # Start a REPL in the "Docs" module
|
||||||
|
Note: tests can be run quickly from within the console using the "test" command. Run "help test"
|
||||||
|
for usage instructions.
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
thor test:all # Run all tests
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
thor assets:compile # Compile assets (not required in development mode)
|
||||||
|
thor assets:clean # Clean old assets
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are welcome. Please read the [contributing guidelines](https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## Copyright / License
|
||||||
|
|
||||||
|
Copyright 2013 Thibaut Courouble and [other contributors](https://github.com/Thibaut/devdocs/graphs/contributors)
|
||||||
|
|
||||||
|
This software is licensed under the terms of the Mozilla Public License v2.0. See the [COPYRIGHT](https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT) and [LICENSE](https://github.com/Thibaut/devdocs/blob/master/LICENSE) files.
|
||||||
|
|
||||||
|
**Note:** I consider _DevDocs_ to be a trademark. You may not use the name to endorse or promote products derived from this software without my permission, except as may be necessary to comply with the notice/attribution requirements.
|
||||||
|
|
||||||
|
**Additionally**, I wish that any documentation file generated using this software be attributed to DevDocs. Let's be fair to all contributors by not stealing their hard work.
|
||||||
|
|
||||||
|
## Questions?
|
||||||
|
|
||||||
|
If you have any questions, please feel free to ask on the [mailing list](https://groups.google.com/d/forum/devdocs).
|
@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env rake
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'thor'
|
||||||
|
|
||||||
|
$LOAD_PATH.unshift 'lib'
|
||||||
|
|
||||||
|
namespace :assets do
|
||||||
|
desc 'Compile all assets'
|
||||||
|
task :precompile do
|
||||||
|
load 'tasks/assets.thor'
|
||||||
|
AssetsCLI.new.compile
|
||||||
|
end
|
||||||
|
end
|
After Width: | Height: | Size: 9.1 KiB |
After Width: | Height: | Size: 23 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 7.4 KiB |
@ -0,0 +1,156 @@
|
|||||||
|
@app =
|
||||||
|
$: $
|
||||||
|
collections: {}
|
||||||
|
models: {}
|
||||||
|
templates: {}
|
||||||
|
views: {}
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
return unless @browserCheck()
|
||||||
|
@initErrorTracking()
|
||||||
|
@showLoading()
|
||||||
|
|
||||||
|
@store = new Store
|
||||||
|
@appCache = new app.AppCache if app.AppCache.isEnabled()
|
||||||
|
@settings = new app.Settings
|
||||||
|
|
||||||
|
@docs = new app.collections.Docs
|
||||||
|
@disabledDocs = new app.collections.Docs
|
||||||
|
@entries = new app.collections.Entries
|
||||||
|
|
||||||
|
@router = new app.Router
|
||||||
|
@shortcuts = new app.Shortcuts
|
||||||
|
@document = new app.views.Document
|
||||||
|
@mobile = new app.views.Mobile if @isMobile()
|
||||||
|
|
||||||
|
if @DOC
|
||||||
|
@bootOne()
|
||||||
|
else if @DOCS
|
||||||
|
@bootAll()
|
||||||
|
else
|
||||||
|
@onBootError()
|
||||||
|
return
|
||||||
|
|
||||||
|
browserCheck: ->
|
||||||
|
return true if @isSupportedBrowser()
|
||||||
|
@hideLoading()
|
||||||
|
document.body.innerHTML = app.templates.unsupportedBrowser
|
||||||
|
false
|
||||||
|
|
||||||
|
initErrorTracking: ->
|
||||||
|
# Show a warning message and don't track errors when the app is loaded
|
||||||
|
# from a domain other than our own, because things are likely to break.
|
||||||
|
# (e.g. cross-domain requests)
|
||||||
|
if @isInvalidLocation()
|
||||||
|
new app.views.Notif 'InvalidLocation'
|
||||||
|
else
|
||||||
|
Raven.config(@config.sentry_dsn).install() if @config.sentry_dsn
|
||||||
|
@previousErrorHandler = onerror
|
||||||
|
window.onerror = @onWindowError.bind(@)
|
||||||
|
return
|
||||||
|
|
||||||
|
bootOne: ->
|
||||||
|
@doc = new app.models.Doc @DOC
|
||||||
|
@docs.reset [@doc]
|
||||||
|
@doc.load @start.bind(@), @onBootError.bind(@), readCache: true
|
||||||
|
new app.views.Notice 'singleDoc', @doc
|
||||||
|
delete @DOC
|
||||||
|
return
|
||||||
|
|
||||||
|
bootAll: ->
|
||||||
|
docs = @settings.getDocs()
|
||||||
|
for doc in @DOCS
|
||||||
|
(if docs.indexOf(doc.slug) >= 0 then @docs else @disabledDocs).add(doc)
|
||||||
|
@docs.load @start.bind(@), @onBootError.bind(@), readCache: true, writeCache: true
|
||||||
|
delete @DOCS
|
||||||
|
return
|
||||||
|
|
||||||
|
start: ->
|
||||||
|
@entries.add doc.entries.all() for doc in @docs.all()
|
||||||
|
@trigger 'ready'
|
||||||
|
@router.start()
|
||||||
|
@hideLoading()
|
||||||
|
new app.views.News() unless @doc
|
||||||
|
@removeEvent 'ready bootError'
|
||||||
|
return
|
||||||
|
|
||||||
|
reload: ->
|
||||||
|
@docs.clearCache()
|
||||||
|
@disabledDocs.clearCache()
|
||||||
|
if @appCache then @appCache.reload() else window.location = '/'
|
||||||
|
return
|
||||||
|
|
||||||
|
reset: ->
|
||||||
|
@store.clear()
|
||||||
|
@settings.reset()
|
||||||
|
@appCache?.update()
|
||||||
|
window.location = '/'
|
||||||
|
return
|
||||||
|
|
||||||
|
showLoading: ->
|
||||||
|
document.body.classList.add '_loading'
|
||||||
|
return
|
||||||
|
|
||||||
|
hideLoading: ->
|
||||||
|
document.body.classList.remove '_booting'
|
||||||
|
document.body.classList.remove '_loading'
|
||||||
|
return
|
||||||
|
|
||||||
|
indexHost: ->
|
||||||
|
@config[if @appCache and @settings.hasDocs() then 'index_path' else 'docs_host']
|
||||||
|
|
||||||
|
onBootError: (args...) ->
|
||||||
|
@trigger 'bootError'
|
||||||
|
@hideLoading()
|
||||||
|
return
|
||||||
|
|
||||||
|
onWindowError: (args...) ->
|
||||||
|
if @isInjectionError args...
|
||||||
|
@onInjectionError()
|
||||||
|
else if @isAppError args...
|
||||||
|
@previousErrorHandler? args...
|
||||||
|
@hideLoading()
|
||||||
|
@errorNotif or= new app.views.Notif 'Error'
|
||||||
|
@errorNotif.show()
|
||||||
|
return
|
||||||
|
|
||||||
|
onInjectionError: ->
|
||||||
|
unless @injectionError
|
||||||
|
@injectionError = true
|
||||||
|
alert """
|
||||||
|
JavaScript code has been injected in the page which prevents DevDocs from running correctly.
|
||||||
|
Please check your browser extensions/addons. """
|
||||||
|
Raven.captureMessage 'injection error'
|
||||||
|
return
|
||||||
|
|
||||||
|
isInjectionError: ->
|
||||||
|
# Some browser extensions expect the entire web to use jQuery.
|
||||||
|
# I gave up trying to fight back.
|
||||||
|
window.$ isnt app.$
|
||||||
|
|
||||||
|
isAppError: (error, file) ->
|
||||||
|
# Ignore errors from external scripts.
|
||||||
|
file and file.indexOf('devdocs') isnt -1 and file.indexOf('.js') is file.length - 3
|
||||||
|
|
||||||
|
isSupportedBrowser: ->
|
||||||
|
try
|
||||||
|
return true if Function::bind and
|
||||||
|
history.pushState and
|
||||||
|
window.matchMedia and
|
||||||
|
document.body.classList and
|
||||||
|
document.body.insertAdjacentHTML and
|
||||||
|
document.createEvent('CustomEvent').defaultPrevented is false and
|
||||||
|
getComputedStyle(document.querySelector('._header')).backgroundImage isnt 'none'
|
||||||
|
catch
|
||||||
|
|
||||||
|
isMobile: ->
|
||||||
|
# Need to sniff the user agent because some Android and Windows Phone devices don't take
|
||||||
|
# resolution (dpi) into account when reporting device width/height.
|
||||||
|
@_isMobile ?= (matchMedia('(max-device-width: 767px), (max-device-height: 767px)').matches) or
|
||||||
|
(navigator.userAgent.indexOf('Android') isnt -1 and navigator.userAgent.indexOf('Mobile') isnt -1) or
|
||||||
|
(navigator.userAgent.indexOf('IEMobile') isnt -1)
|
||||||
|
|
||||||
|
isInvalidLocation: ->
|
||||||
|
@config.env is 'production' and location.host.indexOf(app.config.production_host) isnt 0
|
||||||
|
|
||||||
|
$.extend app, Events
|
@ -0,0 +1,42 @@
|
|||||||
|
class app.AppCache
|
||||||
|
$.extend @prototype, Events
|
||||||
|
|
||||||
|
@isEnabled: ->
|
||||||
|
try
|
||||||
|
applicationCache and applicationCache.status isnt applicationCache.UNCACHED
|
||||||
|
catch
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@cache = applicationCache
|
||||||
|
@onUpdateReady() if @cache.status is @cache.UPDATEREADY
|
||||||
|
|
||||||
|
$.on @cache, 'progress', @onProgress
|
||||||
|
$.on @cache, 'updateready', @onUpdateReady
|
||||||
|
|
||||||
|
@lastCheck = Date.now()
|
||||||
|
$.on window, 'focus', @checkForUpdate
|
||||||
|
|
||||||
|
update: ->
|
||||||
|
try @cache.update() catch
|
||||||
|
return
|
||||||
|
|
||||||
|
reload: ->
|
||||||
|
@reloading = true
|
||||||
|
$.on @cache, 'updateready noupdate error', -> window.location = '/'
|
||||||
|
@update()
|
||||||
|
return
|
||||||
|
|
||||||
|
checkForUpdate: =>
|
||||||
|
if Date.now() - @lastCheck > 86400e3
|
||||||
|
@lastCheck = Date.now()
|
||||||
|
@update()
|
||||||
|
return
|
||||||
|
|
||||||
|
onProgress: (event) =>
|
||||||
|
@trigger 'progress', event
|
||||||
|
return
|
||||||
|
|
||||||
|
onUpdateReady: =>
|
||||||
|
new app.views.Notif 'UpdateReady' unless @reloading
|
||||||
|
@trigger 'updateready'
|
||||||
|
return
|
@ -0,0 +1,10 @@
|
|||||||
|
app.config =
|
||||||
|
default_docs: ['css', 'dom', 'dom_events', 'html', 'http', 'javascript']
|
||||||
|
docs_host: '<%= App.docs_host %>'
|
||||||
|
env: '<%= App.environment %>'
|
||||||
|
history_cache_size: 10
|
||||||
|
index_path: '/<%= App.docs_prefix %>'
|
||||||
|
max_results: 50
|
||||||
|
production_host: 'devdocs.io'
|
||||||
|
search_param: 'q'
|
||||||
|
sentry_dsn: '<%= App.sentry_dsn %>'
|
@ -0,0 +1,114 @@
|
|||||||
|
class app.Router
|
||||||
|
$.extend @prototype, Events
|
||||||
|
|
||||||
|
@routes: [
|
||||||
|
['*', 'before' ]
|
||||||
|
['/', 'root' ]
|
||||||
|
['/about', 'about' ]
|
||||||
|
['/news', 'news' ]
|
||||||
|
['/help', 'help' ]
|
||||||
|
['/:doc-:type/', 'type' ]
|
||||||
|
['/:doc/', 'doc' ]
|
||||||
|
['/:doc/:path(*)', 'entry' ]
|
||||||
|
['*', 'notFound']
|
||||||
|
]
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
for [path, method] in @constructor.routes
|
||||||
|
page path, @[method].bind(@)
|
||||||
|
@setInitialPath()
|
||||||
|
|
||||||
|
start: ->
|
||||||
|
page.start()
|
||||||
|
return
|
||||||
|
|
||||||
|
show: (path) ->
|
||||||
|
page.show(path)
|
||||||
|
return
|
||||||
|
|
||||||
|
triggerRoute: (name) ->
|
||||||
|
@trigger name, @context
|
||||||
|
@trigger 'after', name, @context
|
||||||
|
return
|
||||||
|
|
||||||
|
before: (context, next) ->
|
||||||
|
@context = context
|
||||||
|
@trigger 'before', context
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
doc: (context, next) ->
|
||||||
|
if doc = app.docs.findBy('slug', context.params.doc) or app.disabledDocs.findBy('slug', context.params.doc)
|
||||||
|
context.doc = doc
|
||||||
|
context.entry = doc.indexEntry()
|
||||||
|
@triggerRoute 'entry'
|
||||||
|
else
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
type: (context, next) ->
|
||||||
|
doc = app.docs.findBy 'slug', context.params.doc
|
||||||
|
|
||||||
|
if type = doc?.types.findBy 'slug', context.params.type
|
||||||
|
context.doc = doc
|
||||||
|
context.type = type
|
||||||
|
@triggerRoute 'type'
|
||||||
|
else
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
entry: (context, next) ->
|
||||||
|
doc = app.docs.findBy 'slug', context.params.doc
|
||||||
|
|
||||||
|
if entry = doc?.findEntryByPathAndHash(context.params.path, context.hash)
|
||||||
|
context.doc = doc
|
||||||
|
context.entry = entry
|
||||||
|
@triggerRoute 'entry'
|
||||||
|
else
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
root: ->
|
||||||
|
@triggerRoute 'root'
|
||||||
|
return
|
||||||
|
|
||||||
|
about: (context) ->
|
||||||
|
context.page = 'about'
|
||||||
|
@triggerRoute 'page'
|
||||||
|
return
|
||||||
|
|
||||||
|
news: (context) ->
|
||||||
|
context.page = 'news'
|
||||||
|
@triggerRoute 'page'
|
||||||
|
return
|
||||||
|
|
||||||
|
help: (context) ->
|
||||||
|
context.page = 'help'
|
||||||
|
@triggerRoute 'page'
|
||||||
|
return
|
||||||
|
|
||||||
|
notFound: (context) ->
|
||||||
|
@triggerRoute 'notFound'
|
||||||
|
return
|
||||||
|
|
||||||
|
isRoot: ->
|
||||||
|
location.pathname is '/'
|
||||||
|
|
||||||
|
setInitialPath: ->
|
||||||
|
# Remove superfluous forward slashes at the beginning of the path
|
||||||
|
if (path = location.pathname.replace /^\/{2,}/g, '/') isnt location.pathname
|
||||||
|
page.replace path + location.search + location.hash, null, true
|
||||||
|
|
||||||
|
# When the path is "/#/path", replace it with "/path"
|
||||||
|
if @isRoot() and path = @getInitialPath()
|
||||||
|
page.replace path + location.search, null, true
|
||||||
|
return
|
||||||
|
|
||||||
|
getInitialPath: ->
|
||||||
|
try
|
||||||
|
(new RegExp "#/(.+)").exec(decodeURIComponent location.hash)?[1]
|
||||||
|
catch
|
||||||
|
|
||||||
|
replaceHash: (hash) ->
|
||||||
|
page.replace location.pathname + location.search + (hash or ''), null, true
|
||||||
|
return
|
@ -0,0 +1,221 @@
|
|||||||
|
class app.Searcher
|
||||||
|
$.extend @prototype, Events
|
||||||
|
|
||||||
|
CHUNK_SIZE = 10000
|
||||||
|
SEPARATOR = '.'
|
||||||
|
|
||||||
|
DEFAULTS =
|
||||||
|
max_results: app.config.max_results
|
||||||
|
fuzzy_min_length: 3
|
||||||
|
|
||||||
|
constructor: (options = {}) ->
|
||||||
|
@options = $.extend {}, DEFAULTS, options
|
||||||
|
|
||||||
|
find: (data, attr, query) ->
|
||||||
|
@kill()
|
||||||
|
|
||||||
|
@data = data
|
||||||
|
@attr = attr
|
||||||
|
@query = query
|
||||||
|
@setup()
|
||||||
|
|
||||||
|
if @isValid() then @match() else @end()
|
||||||
|
return
|
||||||
|
|
||||||
|
setup: ->
|
||||||
|
@query = @normalizeQuery @query
|
||||||
|
@queryLength = @query.length
|
||||||
|
@dataLength = @data.length
|
||||||
|
@matchers = ['exactMatch']
|
||||||
|
@totalResults = 0
|
||||||
|
@setupFuzzy()
|
||||||
|
return
|
||||||
|
|
||||||
|
setupFuzzy: ->
|
||||||
|
if @queryLength >= @options.fuzzy_min_length
|
||||||
|
@fuzzyRegexp = @queryToFuzzyRegexp @query
|
||||||
|
@matchers.push 'fuzzyMatch'
|
||||||
|
return
|
||||||
|
|
||||||
|
isValid: ->
|
||||||
|
@queryLength > 0
|
||||||
|
|
||||||
|
end: ->
|
||||||
|
@triggerResults [] unless @totalResults
|
||||||
|
@free()
|
||||||
|
return
|
||||||
|
|
||||||
|
kill: ->
|
||||||
|
if @timeout
|
||||||
|
clearTimeout @timeout
|
||||||
|
@free()
|
||||||
|
return
|
||||||
|
|
||||||
|
free: ->
|
||||||
|
@data = @attr = @query = @queryLength = @dataLength =
|
||||||
|
@fuzzyRegexp = @matchers = @totalResults = @scoreMap =
|
||||||
|
@cursor = @matcher = @timeout = null
|
||||||
|
return
|
||||||
|
|
||||||
|
match: =>
|
||||||
|
if not @foundEnough() and @matcher = @matchers.shift()
|
||||||
|
@setupMatcher()
|
||||||
|
@matchChunks()
|
||||||
|
else
|
||||||
|
@end()
|
||||||
|
return
|
||||||
|
|
||||||
|
setupMatcher: ->
|
||||||
|
@cursor = 0
|
||||||
|
@scoreMap = new Array(101)
|
||||||
|
return
|
||||||
|
|
||||||
|
matchChunks: =>
|
||||||
|
@matchChunk()
|
||||||
|
|
||||||
|
if @cursor is @dataLength or @scoredEnough()
|
||||||
|
@delay @match
|
||||||
|
@sendResults()
|
||||||
|
else
|
||||||
|
@delay @matchChunks
|
||||||
|
return
|
||||||
|
|
||||||
|
matchChunk: ->
|
||||||
|
for [0...@chunkSize()]
|
||||||
|
if score = @[@matcher](@data[@cursor][@attr])
|
||||||
|
@addResult @data[@cursor], score
|
||||||
|
@cursor++
|
||||||
|
return
|
||||||
|
|
||||||
|
chunkSize: ->
|
||||||
|
if @cursor + CHUNK_SIZE > @dataLength
|
||||||
|
@dataLength % CHUNK_SIZE
|
||||||
|
else
|
||||||
|
CHUNK_SIZE
|
||||||
|
|
||||||
|
scoredEnough: ->
|
||||||
|
@scoreMap[100]?.length >= @options.max_results
|
||||||
|
|
||||||
|
foundEnough: ->
|
||||||
|
@totalResults >= @options.max_results
|
||||||
|
|
||||||
|
addResult: (object, score) ->
|
||||||
|
(@scoreMap[Math.round(score)] or= []).push(object)
|
||||||
|
@totalResults++
|
||||||
|
return
|
||||||
|
|
||||||
|
getResults: ->
|
||||||
|
results = []
|
||||||
|
for objects in @scoreMap by -1 when objects
|
||||||
|
results.push.apply results, objects
|
||||||
|
results[0...@options.max_results]
|
||||||
|
|
||||||
|
sendResults: ->
|
||||||
|
results = @getResults()
|
||||||
|
@triggerResults results if results.length
|
||||||
|
return
|
||||||
|
|
||||||
|
triggerResults: (results) ->
|
||||||
|
@trigger 'results', results
|
||||||
|
return
|
||||||
|
|
||||||
|
delay: (fn) ->
|
||||||
|
@timeout = setTimeout(fn, 1)
|
||||||
|
|
||||||
|
normalizeQuery: (string) ->
|
||||||
|
string.replace(/\s/g, '').toLowerCase()
|
||||||
|
|
||||||
|
queryToFuzzyRegexp: (string) ->
|
||||||
|
chars = string.split ''
|
||||||
|
chars[i] = $.escapeRegexp(char) for char, i in chars
|
||||||
|
new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/
|
||||||
|
|
||||||
|
#
|
||||||
|
# Match functions
|
||||||
|
#
|
||||||
|
|
||||||
|
index = # position of the query in the string being matched
|
||||||
|
match = # regexp match data
|
||||||
|
score = # score for the current match
|
||||||
|
separators = # counter
|
||||||
|
i = null # cursor
|
||||||
|
|
||||||
|
exactMatch: (value) ->
|
||||||
|
index = value.indexOf @query
|
||||||
|
return unless index >= 0
|
||||||
|
|
||||||
|
# Remove one point for each unmatched character.
|
||||||
|
score = 100 - (value.length - @queryLength)
|
||||||
|
|
||||||
|
if index > 0
|
||||||
|
# If the character preceding the query is a dot, assign the same score
|
||||||
|
# as if the query was found at the beginning of the string, minus one.
|
||||||
|
if value[index - 1] is SEPARATOR
|
||||||
|
score += index - 1
|
||||||
|
# Don't match a single-character query unless it's found at the beginning
|
||||||
|
# of the string or is preceded by a dot.
|
||||||
|
else if @queryLength is 1
|
||||||
|
return
|
||||||
|
# (1) Remove one point for each unmatched character up to the nearest
|
||||||
|
# preceding dot or the beginning of the string.
|
||||||
|
# (2) Remove one point for each unmatched character following the query.
|
||||||
|
else
|
||||||
|
i = index - 2
|
||||||
|
i-- while i >= 0 and value[i] isnt SEPARATOR
|
||||||
|
score -= (index - i) + # (1)
|
||||||
|
(value.length - @queryLength - index) # (2)
|
||||||
|
|
||||||
|
# Remove one point for each dot preceding the query, except for the one
|
||||||
|
# immediately before the query.
|
||||||
|
separators = 0
|
||||||
|
i = index - 2
|
||||||
|
while i >= 0
|
||||||
|
separators++ if value[i] is SEPARATOR
|
||||||
|
i--
|
||||||
|
score -= separators
|
||||||
|
|
||||||
|
# Remove five points for each dot following the query.
|
||||||
|
separators = 0
|
||||||
|
i = value.length - @queryLength - index - 1
|
||||||
|
while i >= 0
|
||||||
|
separators++ if value[index + @queryLength + i] is SEPARATOR
|
||||||
|
i--
|
||||||
|
score -= separators * 5
|
||||||
|
|
||||||
|
Math.max 1, score
|
||||||
|
|
||||||
|
fuzzyMatch: (value) ->
|
||||||
|
return if value.length <= @queryLength or value.indexOf(@query) >= 0
|
||||||
|
return unless match = @fuzzyRegexp.exec(value)
|
||||||
|
|
||||||
|
# When the match is at the beginning of the string or preceded by a dot.
|
||||||
|
if match.index is 0 or value[match.index - 1] is SEPARATOR
|
||||||
|
Math.max 66, 100 - match[0].length
|
||||||
|
# When the match is at the end of the string.
|
||||||
|
else if match.index + match[0].length is value.length
|
||||||
|
Math.max 33, 67 - match[0].length
|
||||||
|
# When the match is in the middle of the string.
|
||||||
|
else
|
||||||
|
Math.max 1, 34 - match[0].length
|
||||||
|
|
||||||
|
class app.SynchronousSearcher extends app.Searcher
|
||||||
|
match: =>
|
||||||
|
if @matcher
|
||||||
|
@allResults or= []
|
||||||
|
@allResults.push.apply @allResults, @getResults()
|
||||||
|
super
|
||||||
|
|
||||||
|
free: ->
|
||||||
|
@allResults = null
|
||||||
|
super
|
||||||
|
|
||||||
|
end: ->
|
||||||
|
@sendResults true
|
||||||
|
super
|
||||||
|
|
||||||
|
sendResults: (end) ->
|
||||||
|
if end and @allResults?.length
|
||||||
|
@triggerResults @allResults
|
||||||
|
|
||||||
|
delay: (fn) ->
|
||||||
|
fn()
|
@ -0,0 +1,25 @@
|
|||||||
|
class app.Settings
|
||||||
|
hasDocs: ->
|
||||||
|
try
|
||||||
|
!!Cookies.get 'docs'
|
||||||
|
catch
|
||||||
|
|
||||||
|
getDocs: ->
|
||||||
|
try
|
||||||
|
Cookies.get('docs')?.split('/') or app.config.default_docs
|
||||||
|
catch
|
||||||
|
app.config.default_docs
|
||||||
|
|
||||||
|
setDocs: (docs) ->
|
||||||
|
try
|
||||||
|
Cookies.set 'docs', docs.join('/'),
|
||||||
|
path: '/'
|
||||||
|
expires: 1e8
|
||||||
|
catch
|
||||||
|
return
|
||||||
|
|
||||||
|
reset: ->
|
||||||
|
try
|
||||||
|
Cookies.expire 'docs'
|
||||||
|
catch
|
||||||
|
return
|
@ -0,0 +1,108 @@
|
|||||||
|
class app.Shortcuts
|
||||||
|
$.extend @prototype, Events
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@start()
|
||||||
|
|
||||||
|
start: ->
|
||||||
|
$.on document, 'keydown', @onKeydown
|
||||||
|
$.on document, 'keypress', @onKeypress
|
||||||
|
return
|
||||||
|
|
||||||
|
stop: ->
|
||||||
|
$.off document, 'keydown', @onKeydown
|
||||||
|
$.off document, 'keypress', @onKeypress
|
||||||
|
return
|
||||||
|
|
||||||
|
onKeydown: (event) =>
|
||||||
|
result = if event.ctrlKey or event.metaKey
|
||||||
|
@handleKeydownSuperEvent event unless event.altKey or event.shiftKey
|
||||||
|
else if event.shiftKey
|
||||||
|
@handleKeydownShiftEvent event unless event.altKey
|
||||||
|
else if event.altKey
|
||||||
|
@handleKeydownAltEvent event
|
||||||
|
else
|
||||||
|
@handleKeydownEvent event
|
||||||
|
|
||||||
|
event.preventDefault() if result is false
|
||||||
|
return
|
||||||
|
|
||||||
|
onKeypress: (event) =>
|
||||||
|
unless event.ctrlKey or event.metaKey
|
||||||
|
result = @handleKeypressEvent event
|
||||||
|
event.preventDefault() if result is false
|
||||||
|
return
|
||||||
|
|
||||||
|
handleKeydownEvent: (event) ->
|
||||||
|
if not event.target.form and 65 <= event.which <= 90
|
||||||
|
@trigger 'typing'
|
||||||
|
return
|
||||||
|
|
||||||
|
switch event.which
|
||||||
|
when 8
|
||||||
|
@trigger 'typing' unless event.target.form
|
||||||
|
when 13
|
||||||
|
@trigger 'enter'
|
||||||
|
when 27
|
||||||
|
@trigger 'escape'
|
||||||
|
when 32
|
||||||
|
@trigger 'pageDown'
|
||||||
|
false
|
||||||
|
when 33
|
||||||
|
@trigger 'pageUp'
|
||||||
|
when 34
|
||||||
|
@trigger 'pageDown'
|
||||||
|
when 35
|
||||||
|
@trigger 'end'
|
||||||
|
when 36
|
||||||
|
@trigger 'home'
|
||||||
|
when 37
|
||||||
|
@trigger 'left' unless event.target.value
|
||||||
|
when 38
|
||||||
|
@trigger 'up'
|
||||||
|
false
|
||||||
|
when 39
|
||||||
|
@trigger 'right' unless event.target.value
|
||||||
|
when 40
|
||||||
|
@trigger 'down'
|
||||||
|
false
|
||||||
|
|
||||||
|
handleKeydownSuperEvent: (event) ->
|
||||||
|
switch event.which
|
||||||
|
when 13
|
||||||
|
@trigger 'superEnter'
|
||||||
|
when 37
|
||||||
|
@trigger 'superLeft'
|
||||||
|
false
|
||||||
|
when 38
|
||||||
|
@trigger 'home'
|
||||||
|
false
|
||||||
|
when 39
|
||||||
|
@trigger 'superRight'
|
||||||
|
false
|
||||||
|
when 40
|
||||||
|
@trigger 'end'
|
||||||
|
false
|
||||||
|
|
||||||
|
handleKeydownShiftEvent: (event) ->
|
||||||
|
if not event.target.form and 65 <= event.which <= 90
|
||||||
|
@trigger 'typing'
|
||||||
|
return
|
||||||
|
|
||||||
|
if event.which is 32
|
||||||
|
@trigger 'pageUp'
|
||||||
|
false
|
||||||
|
|
||||||
|
handleKeydownAltEvent: (event) ->
|
||||||
|
switch event.which
|
||||||
|
when 38
|
||||||
|
@trigger 'altUp'
|
||||||
|
false
|
||||||
|
when 40
|
||||||
|
@trigger 'altDown'
|
||||||
|
false
|
||||||
|
|
||||||
|
handleKeypressEvent: (event) ->
|
||||||
|
if event.which is 63 and not event.target.value
|
||||||
|
@trigger 'help'
|
||||||
|
false
|
@ -0,0 +1,25 @@
|
|||||||
|
#= require_tree ./vendor
|
||||||
|
|
||||||
|
#= require lib/license
|
||||||
|
#= require_tree ./lib
|
||||||
|
|
||||||
|
#= require app/app
|
||||||
|
#= require app/config
|
||||||
|
#= require_tree ./app
|
||||||
|
|
||||||
|
#= require collections/collection
|
||||||
|
#= require_tree ./collections
|
||||||
|
|
||||||
|
#= require models/model
|
||||||
|
#= require_tree ./models
|
||||||
|
|
||||||
|
#= require views/view
|
||||||
|
#= require_tree ./views
|
||||||
|
|
||||||
|
#= require_tree ./templates
|
||||||
|
|
||||||
|
init = ->
|
||||||
|
document.removeEventListener 'DOMContentLoaded', init
|
||||||
|
app.init()
|
||||||
|
|
||||||
|
document.addEventListener 'DOMContentLoaded', init, false
|
@ -0,0 +1,43 @@
|
|||||||
|
class app.Collection
|
||||||
|
constructor: (objects = []) ->
|
||||||
|
@reset objects
|
||||||
|
|
||||||
|
model: ->
|
||||||
|
app.models[@constructor.model]
|
||||||
|
|
||||||
|
reset: (objects = []) ->
|
||||||
|
@models = []
|
||||||
|
@add object for object in objects
|
||||||
|
return
|
||||||
|
|
||||||
|
add: (object) ->
|
||||||
|
if object instanceof app.Model
|
||||||
|
@models.push object
|
||||||
|
else if object instanceof Array
|
||||||
|
@add obj for obj in object
|
||||||
|
else if object instanceof app.Collection
|
||||||
|
@models.push object.all()...
|
||||||
|
else
|
||||||
|
@models.push new (@model())(object)
|
||||||
|
return
|
||||||
|
|
||||||
|
size: ->
|
||||||
|
@models.length
|
||||||
|
|
||||||
|
isEmpty: ->
|
||||||
|
@models.length is 0
|
||||||
|
|
||||||
|
each: (fn) ->
|
||||||
|
fn(model) for model in @models
|
||||||
|
return
|
||||||
|
|
||||||
|
all: ->
|
||||||
|
@models
|
||||||
|
|
||||||
|
findBy: (attr, value) ->
|
||||||
|
for model in @models
|
||||||
|
return model if model[attr] is value
|
||||||
|
return
|
||||||
|
|
||||||
|
findAllBy: (attr, value) ->
|
||||||
|
model for model in @models when model[attr] is value
|
@ -0,0 +1,30 @@
|
|||||||
|
class app.collections.Docs extends app.Collection
|
||||||
|
@model: 'Doc'
|
||||||
|
|
||||||
|
# Load models concurrently.
|
||||||
|
# It's not pretty but I didn't want to import a promise library only for this.
|
||||||
|
CONCURRENCY = 3
|
||||||
|
load: (onComplete, onError, options) ->
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
next = =>
|
||||||
|
if i < @models.length
|
||||||
|
@models[i].load(next, fail, options)
|
||||||
|
else if i is @models.length + CONCURRENCY - 1
|
||||||
|
onComplete()
|
||||||
|
i++
|
||||||
|
return
|
||||||
|
|
||||||
|
fail = (args...) ->
|
||||||
|
if onError
|
||||||
|
onError(args...)
|
||||||
|
onError = null
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
next() for [0...CONCURRENCY]
|
||||||
|
return
|
||||||
|
|
||||||
|
clearCache: ->
|
||||||
|
doc.clearCache() for doc in @models
|
||||||
|
return
|
@ -0,0 +1,2 @@
|
|||||||
|
class app.collections.Entries extends app.Collection
|
||||||
|
@model: 'Entry'
|
@ -0,0 +1,2 @@
|
|||||||
|
class app.collections.Types extends app.Collection
|
||||||
|
@model: 'Type'
|
@ -0,0 +1,81 @@
|
|||||||
|
#
|
||||||
|
# App
|
||||||
|
#
|
||||||
|
|
||||||
|
_init = app.init
|
||||||
|
app.init = ->
|
||||||
|
console.time 'Init'
|
||||||
|
_init.call(app)
|
||||||
|
console.timeEnd 'Init'
|
||||||
|
console.time 'Load'
|
||||||
|
|
||||||
|
_start = app.start
|
||||||
|
app.start = ->
|
||||||
|
_start.call(app, arguments...)
|
||||||
|
console.timeEnd 'Load'
|
||||||
|
|
||||||
|
_super = app.Searcher
|
||||||
|
_proto = app.Searcher.prototype
|
||||||
|
|
||||||
|
#
|
||||||
|
# Searcher
|
||||||
|
#
|
||||||
|
|
||||||
|
app.Searcher = ->
|
||||||
|
_super.apply @, arguments
|
||||||
|
|
||||||
|
_setup = @setup.bind(@)
|
||||||
|
@setup = ->
|
||||||
|
console.groupCollapsed "Search: #{@query}"
|
||||||
|
console.time 'Total'
|
||||||
|
_setup()
|
||||||
|
|
||||||
|
_match = @match.bind(@)
|
||||||
|
@match = =>
|
||||||
|
if @matcher
|
||||||
|
console.timeEnd @matcher
|
||||||
|
if @matcher is 'exactMatch'
|
||||||
|
for entries, score in @scoreMap by -1 when entries
|
||||||
|
console.log '' + score + ': ' + entries.map((entry) -> entry.text).join("\n ")
|
||||||
|
_match()
|
||||||
|
|
||||||
|
_setupMatcher = @setupMatcher.bind(@)
|
||||||
|
@setupMatcher = ->
|
||||||
|
console.time @matcher
|
||||||
|
_setupMatcher()
|
||||||
|
|
||||||
|
_end = @end.bind(@)
|
||||||
|
@end = ->
|
||||||
|
console.log "Results: #{@totalResults}"
|
||||||
|
console.groupEnd()
|
||||||
|
console.timeEnd 'Total'
|
||||||
|
_end()
|
||||||
|
|
||||||
|
_kill = @kill.bind(@)
|
||||||
|
@kill = ->
|
||||||
|
if @timeout
|
||||||
|
console.timeEnd @matcher if @matcher
|
||||||
|
console.groupEnd()
|
||||||
|
console.timeEnd 'Total'
|
||||||
|
console.warn 'Killed'
|
||||||
|
_kill()
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
_proto.constructor = app.Searcher
|
||||||
|
app.Searcher.prototype = _proto
|
||||||
|
|
||||||
|
#
|
||||||
|
# View tree
|
||||||
|
#
|
||||||
|
|
||||||
|
@viewTree = (view = app.document, level = 0) ->
|
||||||
|
console.log "%c #{Array(level + 1).join(' ')}#{view.constructor.name}: #{view.activated}",
|
||||||
|
'color:' + (view.activated and 'green' or 'red')
|
||||||
|
|
||||||
|
for key, value of view when key isnt 'view' and value
|
||||||
|
if typeof value is 'object' and value.setupElement
|
||||||
|
@viewTree(value, level + 1)
|
||||||
|
else if value.constructor.toString().match(/Object\(\)/)
|
||||||
|
@viewTree(v, level + 1) for k, v of value when value and typeof value is 'object' and value.setupElement
|
||||||
|
return
|
@ -0,0 +1,2 @@
|
|||||||
|
//= depend_on docs.json
|
||||||
|
app.DOCS = <%= File.read App.docs_manifest_path %>;
|
@ -0,0 +1,122 @@
|
|||||||
|
MIME_TYPES =
|
||||||
|
json: 'application/json'
|
||||||
|
html: 'text/html'
|
||||||
|
|
||||||
|
@ajax = (options) ->
|
||||||
|
applyDefaults(options)
|
||||||
|
serializeData(options)
|
||||||
|
|
||||||
|
xhr = new XMLHttpRequest()
|
||||||
|
xhr.open(options.type, options.url, options.async)
|
||||||
|
|
||||||
|
applyCallbacks(xhr, options)
|
||||||
|
applyHeaders(xhr, options)
|
||||||
|
|
||||||
|
xhr.send(options.data)
|
||||||
|
|
||||||
|
if options.async
|
||||||
|
abort: abort.bind(undefined, xhr)
|
||||||
|
else
|
||||||
|
parseResponse(xhr, options)
|
||||||
|
|
||||||
|
ajax.defaults =
|
||||||
|
async: true
|
||||||
|
dataType: 'json'
|
||||||
|
timeout: 30000
|
||||||
|
type: 'GET'
|
||||||
|
# contentType
|
||||||
|
# context
|
||||||
|
# data
|
||||||
|
# error
|
||||||
|
# headers
|
||||||
|
# success
|
||||||
|
# url
|
||||||
|
|
||||||
|
applyDefaults = (options) ->
|
||||||
|
for key of ajax.defaults
|
||||||
|
options[key] ?= ajax.defaults[key]
|
||||||
|
return
|
||||||
|
|
||||||
|
serializeData = (options) ->
|
||||||
|
return unless options.data
|
||||||
|
|
||||||
|
if options.type is 'GET'
|
||||||
|
options.url += '?' + serializeParams(options.data)
|
||||||
|
options.data = null
|
||||||
|
else
|
||||||
|
options.data = serializeParams(options.data)
|
||||||
|
return
|
||||||
|
|
||||||
|
serializeParams = (params) ->
|
||||||
|
("#{encodeURIComponent key}=#{encodeURIComponent value}" for key, value of params).join '&'
|
||||||
|
|
||||||
|
applyCallbacks = (xhr, options) ->
|
||||||
|
return unless options.async
|
||||||
|
|
||||||
|
xhr.timer = setTimeout onTimeout.bind(undefined, xhr, options), options.timeout
|
||||||
|
xhr.onreadystatechange = ->
|
||||||
|
if xhr.readyState is 4
|
||||||
|
clearTimeout(xhr.timer)
|
||||||
|
onComplete(xhr, options)
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
applyHeaders = (xhr, options) ->
|
||||||
|
options.headers or= {}
|
||||||
|
|
||||||
|
if options.contentType
|
||||||
|
options.headers['Content-Type'] = options.contentType
|
||||||
|
|
||||||
|
if not options.headers['Content-Type'] and options.data and options.type isnt 'GET'
|
||||||
|
options.headers['Content-Type'] = 'application/x-www-form-urlencoded'
|
||||||
|
|
||||||
|
if options.dataType
|
||||||
|
options.headers['Accept'] = MIME_TYPES[options.dataType] or options.dataType
|
||||||
|
|
||||||
|
if isSameOrigin(options.url)
|
||||||
|
options.headers['X-Requested-With'] = 'XMLHttpRequest'
|
||||||
|
|
||||||
|
for key, value of options.headers
|
||||||
|
xhr.setRequestHeader(key, value)
|
||||||
|
return
|
||||||
|
|
||||||
|
onComplete = (xhr, options) ->
|
||||||
|
if 200 <= xhr.status < 300
|
||||||
|
if (response = parseResponse(xhr, options))?
|
||||||
|
onSuccess response, xhr, options
|
||||||
|
else
|
||||||
|
onError 'invalid', xhr, options
|
||||||
|
else
|
||||||
|
onError 'error', xhr, options
|
||||||
|
return
|
||||||
|
|
||||||
|
onSuccess = (response, xhr, options) ->
|
||||||
|
options.success?.call options.context, response, xhr, options
|
||||||
|
return
|
||||||
|
|
||||||
|
onError = (type, xhr, options) ->
|
||||||
|
options.error?.call options.context, type, xhr, options
|
||||||
|
return
|
||||||
|
|
||||||
|
onTimeout = (xhr, options) ->
|
||||||
|
xhr.abort()
|
||||||
|
onError 'timeout', xhr, options
|
||||||
|
return
|
||||||
|
|
||||||
|
abort = (xhr) ->
|
||||||
|
clearTimeout(xhr.timer)
|
||||||
|
xhr.onreadystatechange = null
|
||||||
|
xhr.abort()
|
||||||
|
return
|
||||||
|
|
||||||
|
isSameOrigin = (url) ->
|
||||||
|
url.indexOf('http') isnt 0 or url.indexOf(location.origin) is 0
|
||||||
|
|
||||||
|
parseResponse = (xhr, options) ->
|
||||||
|
if options.dataType is 'json'
|
||||||
|
parseJSON(xhr.responseText)
|
||||||
|
else
|
||||||
|
xhr.responseText
|
||||||
|
|
||||||
|
parseJSON = (json) ->
|
||||||
|
try JSON.parse(json) catch
|
@ -0,0 +1,26 @@
|
|||||||
|
@Events =
|
||||||
|
on: (event, callback) ->
|
||||||
|
if event.indexOf(' ') >= 0
|
||||||
|
@on name, callback for name in event.split(' ')
|
||||||
|
else
|
||||||
|
((@_callbacks ?= {})[event] ?= []).push callback
|
||||||
|
@
|
||||||
|
|
||||||
|
off: (event, callback) ->
|
||||||
|
if event.indexOf(' ') >= 0
|
||||||
|
@off name, callback for name in event.split(' ')
|
||||||
|
else if (callbacks = @_callbacks?[event]) and (index = callbacks.indexOf callback) >= 0
|
||||||
|
callbacks.splice index, 1
|
||||||
|
delete @_callbacks[event] unless callbacks.length
|
||||||
|
@
|
||||||
|
|
||||||
|
trigger: (event, args...) ->
|
||||||
|
if callbacks = @_callbacks?[event]
|
||||||
|
callback? args... for callback in callbacks.slice(0)
|
||||||
|
@trigger 'all', event, args... unless event is 'all'
|
||||||
|
@
|
||||||
|
|
||||||
|
removeEvent: (event) ->
|
||||||
|
if @_callbacks?
|
||||||
|
delete @_callbacks[name] for name in event.split(' ')
|
||||||
|
@
|
@ -0,0 +1,7 @@
|
|||||||
|
###
|
||||||
|
* Copyright 2013 Thibaut Courouble and other contributors
|
||||||
|
*
|
||||||
|
* This source code is licensed under the terms of the Mozilla
|
||||||
|
* Public License, v. 2.0, a copy of which may be obtained at:
|
||||||
|
* http://mozilla.org/MPL/2.0/
|
||||||
|
###
|
@ -0,0 +1,161 @@
|
|||||||
|
###
|
||||||
|
* Based on github.com/visionmedia/page.js
|
||||||
|
* Licensed under the MIT license
|
||||||
|
* Copyright 2012 TJ Holowaychuk <tj@vision-media.ca>
|
||||||
|
###
|
||||||
|
|
||||||
|
running = false
|
||||||
|
currentState = null
|
||||||
|
callbacks = []
|
||||||
|
|
||||||
|
@page = (value, fn) ->
|
||||||
|
if typeof value is 'function'
|
||||||
|
page '*', value
|
||||||
|
else if typeof fn is 'function'
|
||||||
|
route = new Route(value)
|
||||||
|
callbacks.push route.middleware(fn)
|
||||||
|
else if typeof value is 'string'
|
||||||
|
page.show(value, fn)
|
||||||
|
else
|
||||||
|
page.start(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
page.start = (options = {}) ->
|
||||||
|
unless running
|
||||||
|
running = true
|
||||||
|
addEventListener 'popstate', onpopstate
|
||||||
|
addEventListener 'click', onclick
|
||||||
|
page.replace currentPath(), null, null, true
|
||||||
|
return
|
||||||
|
|
||||||
|
page.stop = ->
|
||||||
|
if running
|
||||||
|
running = false
|
||||||
|
removeEventListener 'click', onclick
|
||||||
|
removeEventListener 'popstate', onpopstate
|
||||||
|
return
|
||||||
|
|
||||||
|
page.show = (path, state) ->
|
||||||
|
return if path is currentState?.path
|
||||||
|
context = new Context(path, state)
|
||||||
|
currentState = context.state
|
||||||
|
page.dispatch(context)
|
||||||
|
context.pushState()
|
||||||
|
context
|
||||||
|
|
||||||
|
page.replace = (path, state, skipDispatch, init) ->
|
||||||
|
context = new Context(path, state or currentState)
|
||||||
|
context.init = init
|
||||||
|
currentState = context.state
|
||||||
|
page.dispatch(context) unless skipDispatch
|
||||||
|
context.replaceState()
|
||||||
|
context
|
||||||
|
|
||||||
|
page.dispatch = (context) ->
|
||||||
|
i = 0
|
||||||
|
next = ->
|
||||||
|
fn(context, next) if fn = callbacks[i++]
|
||||||
|
return
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
currentPath = ->
|
||||||
|
location.pathname + location.search + location.hash
|
||||||
|
|
||||||
|
class Context
|
||||||
|
@initialPath: currentPath()
|
||||||
|
@sessionId: Date.now()
|
||||||
|
@stateId: 0
|
||||||
|
|
||||||
|
@isInitialPopState: (state) ->
|
||||||
|
state.path is @initialPath and @stateId is 1
|
||||||
|
|
||||||
|
@isSameSession: (state) ->
|
||||||
|
state.sessionId is @sessionId
|
||||||
|
|
||||||
|
constructor: (@path = '/', @state = {}) ->
|
||||||
|
@pathname = @path.replace /(?:\?([^#]*))?(?:#(.*))?$/, (_, query, hash) =>
|
||||||
|
@query = query
|
||||||
|
@hash = hash
|
||||||
|
''
|
||||||
|
|
||||||
|
@state.id ?= @constructor.stateId++
|
||||||
|
@state.sessionId ?= @constructor.sessionId
|
||||||
|
@state.path = @path
|
||||||
|
|
||||||
|
pushState: ->
|
||||||
|
history.pushState @state, '', @path
|
||||||
|
return
|
||||||
|
|
||||||
|
replaceState: ->
|
||||||
|
history.replaceState @state, '', @path
|
||||||
|
return
|
||||||
|
|
||||||
|
class Route
|
||||||
|
constructor: (@path, options = {}) ->
|
||||||
|
@keys = []
|
||||||
|
@regexp = pathtoRegexp @path, @keys
|
||||||
|
|
||||||
|
middleware: (fn) ->
|
||||||
|
(context, next) =>
|
||||||
|
if @match context.pathname, params = []
|
||||||
|
context.params = params
|
||||||
|
fn(context, next)
|
||||||
|
else
|
||||||
|
next()
|
||||||
|
return
|
||||||
|
|
||||||
|
match: (path, params) ->
|
||||||
|
return unless matchData = @regexp.exec(path)
|
||||||
|
|
||||||
|
for value, i in matchData[1..]
|
||||||
|
value = decodeURIComponent value if typeof value is 'string'
|
||||||
|
if key = @keys[i]
|
||||||
|
params[key.name] = value
|
||||||
|
else
|
||||||
|
params.push value
|
||||||
|
true
|
||||||
|
|
||||||
|
pathtoRegexp = (path, keys) ->
|
||||||
|
return path if path instanceof RegExp
|
||||||
|
|
||||||
|
path = "(#{path.join '|'})" if path instanceof Array
|
||||||
|
path = path
|
||||||
|
.replace(/\/\(/g, '(?:/')
|
||||||
|
.replace /(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?/g, (_, slash = '', format = '', key, capture, optional) ->
|
||||||
|
keys.push name: key, optional: !!optional
|
||||||
|
str = if optional then '' else slash
|
||||||
|
str += '(?:'
|
||||||
|
str += slash if optional
|
||||||
|
str += format
|
||||||
|
str += capture or if format then '([^/.]+?)' else '([^/]+?)'
|
||||||
|
str += ')'
|
||||||
|
str += optional if optional
|
||||||
|
str
|
||||||
|
.replace(/([\/.])/g, '\\$1')
|
||||||
|
.replace(/\*/g, '(.*)')
|
||||||
|
|
||||||
|
new RegExp "^#{path}$"
|
||||||
|
|
||||||
|
onpopstate = (event) ->
|
||||||
|
return if not event.state or Context.isInitialPopState(event.state)
|
||||||
|
|
||||||
|
if Context.isSameSession(event.state)
|
||||||
|
page.replace(event.state.path, event.state)
|
||||||
|
else
|
||||||
|
location.reload()
|
||||||
|
return
|
||||||
|
|
||||||
|
onclick = (event) ->
|
||||||
|
return if event.which isnt 1 or event.metaKey or event.ctrlKey or event.shiftKey or event.defaultPrevented
|
||||||
|
|
||||||
|
link = event.target
|
||||||
|
link = link.parentElement while link and link.tagName isnt 'A'
|
||||||
|
|
||||||
|
if link and not link.target and isSameOrigin(link.href)
|
||||||
|
event.preventDefault()
|
||||||
|
page.show link.pathname + link.search + link.hash
|
||||||
|
return
|
||||||
|
|
||||||
|
isSameOrigin = (url) ->
|
||||||
|
url.indexOf("#{location.protocol}//#{location.hostname}") is 0
|
@ -0,0 +1,23 @@
|
|||||||
|
class @Store
|
||||||
|
get: (key) ->
|
||||||
|
try
|
||||||
|
JSON.parse localStorage.getItem(key)
|
||||||
|
catch
|
||||||
|
|
||||||
|
set: (key, value) ->
|
||||||
|
try
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
true
|
||||||
|
catch
|
||||||
|
|
||||||
|
del: (key) ->
|
||||||
|
try
|
||||||
|
localStorage.removeItem(key)
|
||||||
|
true
|
||||||
|
catch
|
||||||
|
|
||||||
|
clear: ->
|
||||||
|
try
|
||||||
|
localStorage.clear()
|
||||||
|
true
|
||||||
|
catch
|
@ -0,0 +1,284 @@
|
|||||||
|
#
|
||||||
|
# Traversing
|
||||||
|
#
|
||||||
|
|
||||||
|
@$ = (selector, el = document) ->
|
||||||
|
try el.querySelector(selector) catch
|
||||||
|
|
||||||
|
@$$ = (selector, el = document) ->
|
||||||
|
try el.querySelectorAll(selector) catch
|
||||||
|
|
||||||
|
$.id = (id) ->
|
||||||
|
document.getElementById(id)
|
||||||
|
|
||||||
|
$.hasChild = (parent, el) ->
|
||||||
|
loop
|
||||||
|
return true if el is parent
|
||||||
|
return if el is document.body
|
||||||
|
el = el.parentElement
|
||||||
|
|
||||||
|
$.closestLink = (el, parent = document.body) ->
|
||||||
|
loop
|
||||||
|
return el if el.tagName is 'A'
|
||||||
|
return if el is parent
|
||||||
|
el = el.parentElement
|
||||||
|
|
||||||
|
#
|
||||||
|
# Events
|
||||||
|
#
|
||||||
|
|
||||||
|
$.on = (el, event, callback) ->
|
||||||
|
if event.indexOf(' ') >= 0
|
||||||
|
$.on el, name, callback for name in event.split(' ')
|
||||||
|
else
|
||||||
|
el.addEventListener(event, callback)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.off = (el, event, callback) ->
|
||||||
|
if event.indexOf(' ') >= 0
|
||||||
|
$.off el, name, callback for name in event.split(' ')
|
||||||
|
else
|
||||||
|
el.removeEventListener(event, callback)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.trigger = (el, type, canBubble = true, cancelable = true) ->
|
||||||
|
event = document.createEvent 'Event'
|
||||||
|
event.initEvent(type, canBubble, cancelable)
|
||||||
|
el.dispatchEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.click = (el) ->
|
||||||
|
event = document.createEvent 'MouseEvent'
|
||||||
|
event.initMouseEvent 'click', true, true, window, null, 0, 0, 0, 0, false, false, false, false, 0, null
|
||||||
|
el.dispatchEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.stopEvent = (event) ->
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
event.stopImmediatePropagation()
|
||||||
|
return
|
||||||
|
|
||||||
|
#
|
||||||
|
# Manipulation
|
||||||
|
#
|
||||||
|
|
||||||
|
buildFragment = (value) ->
|
||||||
|
fragment = document.createDocumentFragment()
|
||||||
|
|
||||||
|
if $.isCollection(value)
|
||||||
|
fragment.appendChild(child) for child in $.makeArray(value)
|
||||||
|
else
|
||||||
|
fragment.innerHTML = value
|
||||||
|
|
||||||
|
fragment
|
||||||
|
|
||||||
|
$.append = (el, value) ->
|
||||||
|
if typeof value is 'string'
|
||||||
|
el.insertAdjacentHTML 'beforeend', value
|
||||||
|
else
|
||||||
|
value = buildFragment(value) if $.isCollection(value)
|
||||||
|
el.appendChild(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.prepend = (el, value) ->
|
||||||
|
if not el.firstChild
|
||||||
|
$.append(value)
|
||||||
|
else if typeof value is 'string'
|
||||||
|
el.insertAdjacentHTML 'afterbegin', value
|
||||||
|
else
|
||||||
|
value = buildFragment(value) if $.isCollection(value)
|
||||||
|
el.insertBefore(value, el.firstChild)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.before = (el, value) ->
|
||||||
|
if typeof value is 'string' or $.isCollection(value)
|
||||||
|
value = buildFragment(value)
|
||||||
|
|
||||||
|
el.parentElement.insertBefore(value, el)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.after = (el, value) ->
|
||||||
|
if typeof value is 'string' or $.isCollection(value)
|
||||||
|
value = buildFragment(value)
|
||||||
|
|
||||||
|
if el.nextSibling
|
||||||
|
el.parentElement.insertBefore(value, el.nextSibling)
|
||||||
|
else
|
||||||
|
el.parentElement.appendChild(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.remove = (value) ->
|
||||||
|
if $.isCollection(value)
|
||||||
|
el.parentElement.removeChild(el) for el in $.makeArray(value)
|
||||||
|
else
|
||||||
|
value.parentElement.removeChild(value)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.empty = (el) ->
|
||||||
|
el.removeChild(el.firstChild) while el.firstChild
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calls the function while the element is off the DOM to avoid triggering
|
||||||
|
# unecessary reflows and repaints.
|
||||||
|
$.batchUpdate = (el, fn) ->
|
||||||
|
parent = el.parentNode
|
||||||
|
sibling = el.nextSibling
|
||||||
|
parent.removeChild(el)
|
||||||
|
|
||||||
|
fn(el)
|
||||||
|
|
||||||
|
if (sibling)
|
||||||
|
parent.insertBefore(el, sibling)
|
||||||
|
else
|
||||||
|
parent.appendChild(el)
|
||||||
|
return
|
||||||
|
|
||||||
|
#
|
||||||
|
# Offset
|
||||||
|
#
|
||||||
|
|
||||||
|
$.rect = (el) ->
|
||||||
|
el.getBoundingClientRect()
|
||||||
|
|
||||||
|
$.offset = (el, container = document.body) ->
|
||||||
|
top = 0
|
||||||
|
left = 0
|
||||||
|
|
||||||
|
while el and el isnt container
|
||||||
|
top += el.offsetTop
|
||||||
|
left += el.offsetLeft
|
||||||
|
el = el.offsetParent
|
||||||
|
|
||||||
|
top: top
|
||||||
|
left: left
|
||||||
|
|
||||||
|
$.scrollParent = (el) ->
|
||||||
|
while el = el.parentElement
|
||||||
|
break if el.scrollTop > 0
|
||||||
|
break if getComputedStyle(el).overflowY in ['auto', 'scroll']
|
||||||
|
el
|
||||||
|
|
||||||
|
$.scrollTo = (el, parent, position = 'center', options = {}) ->
|
||||||
|
return unless el
|
||||||
|
|
||||||
|
parent ?= $.scrollParent(el)
|
||||||
|
return unless parent
|
||||||
|
|
||||||
|
parentHeight = parent.clientHeight
|
||||||
|
return unless parent.scrollHeight > parentHeight
|
||||||
|
|
||||||
|
top = $.offset(el, parent).top
|
||||||
|
|
||||||
|
switch position
|
||||||
|
when 'top'
|
||||||
|
parent.scrollTop = top - (options.margin or 20)
|
||||||
|
when 'center'
|
||||||
|
parent.scrollTop = top - Math.round(parentHeight / 2 - el.offsetHeight / 2)
|
||||||
|
when 'continuous'
|
||||||
|
scrollTop = parent.scrollTop
|
||||||
|
height = el.offsetHeight
|
||||||
|
|
||||||
|
# If the target element is above the visible portion of its scrollable
|
||||||
|
# ancestor, move it near the top with a gap = options.topGap * target's height.
|
||||||
|
if top <= scrollTop + height * (options.topGap or 1)
|
||||||
|
parent.scrollTop = top - height * (options.topGap or 1)
|
||||||
|
# If the target element is below the visible portion of its scrollable
|
||||||
|
# ancestor, move it near the bottom with a gap = options.bottomGap * target's height.
|
||||||
|
else if top >= scrollTop + parentHeight - height * ((options.bottomGap or 1) + 1)
|
||||||
|
parent.scrollTop = top - parentHeight + height * ((options.bottomGap or 1) + 1)
|
||||||
|
return
|
||||||
|
|
||||||
|
$.scrollToWithImageLock = (el, parent, args...) ->
|
||||||
|
parent ?= $.scrollParent(el)
|
||||||
|
return unless parent
|
||||||
|
|
||||||
|
$.scrollTo el, parent, args...
|
||||||
|
|
||||||
|
# Lock the scroll position on the target element for up to 3 seconds while
|
||||||
|
# nearby images are loaded and rendered.
|
||||||
|
for image in parent.getElementsByTagName('img') when not image.complete
|
||||||
|
do ->
|
||||||
|
onLoad = (event) ->
|
||||||
|
clearTimeout(timeout)
|
||||||
|
unbind(event.target)
|
||||||
|
$.scrollTo el, parent, args...
|
||||||
|
|
||||||
|
unbind = (target) ->
|
||||||
|
$.off target, 'load', onLoad
|
||||||
|
|
||||||
|
$.on image, 'load', onLoad
|
||||||
|
timeout = setTimeout unbind.bind(null, image), 3000
|
||||||
|
return
|
||||||
|
|
||||||
|
# Calls the function while locking the element's position relative to the window.
|
||||||
|
$.lockScroll = (el, fn) ->
|
||||||
|
if parent = $.scrollParent(el)
|
||||||
|
top = $.rect(el).top
|
||||||
|
top -= $.rect(parent).top unless parent in [document.body, document.documentElement]
|
||||||
|
fn()
|
||||||
|
parent.scrollTop = $.offset(el, parent).top - top
|
||||||
|
else
|
||||||
|
fn()
|
||||||
|
return
|
||||||
|
|
||||||
|
#
|
||||||
|
# Utilities
|
||||||
|
#
|
||||||
|
|
||||||
|
$.extend = (target, objects...) ->
|
||||||
|
for object in objects when object
|
||||||
|
for key, value of object
|
||||||
|
target[key] = value
|
||||||
|
target
|
||||||
|
|
||||||
|
$.makeArray = (object) ->
|
||||||
|
if Array.isArray(object)
|
||||||
|
object
|
||||||
|
else
|
||||||
|
Array::slice.apply(object)
|
||||||
|
|
||||||
|
# Returns true if the object is an array or a collection of DOM elements.
|
||||||
|
$.isCollection = (object) ->
|
||||||
|
Array.isArray(object) or typeof object?.item is 'function'
|
||||||
|
|
||||||
|
ESCAPE_HTML_MAP =
|
||||||
|
'&': '&'
|
||||||
|
'<': '<'
|
||||||
|
'>': '>'
|
||||||
|
'"': '"'
|
||||||
|
"'": '''
|
||||||
|
'/': '/'
|
||||||
|
|
||||||
|
ESCAPE_HTML_REGEXP = /[&<>"'\/]/g
|
||||||
|
|
||||||
|
$.escape = (string) ->
|
||||||
|
string.replace ESCAPE_HTML_REGEXP, (match) -> ESCAPE_HTML_MAP[match]
|
||||||
|
|
||||||
|
ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g
|
||||||
|
|
||||||
|
$.escapeRegexp = (string) ->
|
||||||
|
string.replace ESCAPE_REGEXP, "\\$1"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Miscellaneous
|
||||||
|
#
|
||||||
|
|
||||||
|
$.noop = ->
|
||||||
|
|
||||||
|
$.popup = (value) ->
|
||||||
|
open value.href or value, '_blank'
|
||||||
|
return
|
||||||
|
|
||||||
|
$.isTouchScreen = ->
|
||||||
|
typeof ontouchstart isnt 'undefined'
|
||||||
|
|
||||||
|
HIGHLIGHT_DEFAULTS =
|
||||||
|
className: 'highlight'
|
||||||
|
delay: 1000
|
||||||
|
|
||||||
|
$.highlight = (el, options = {}) ->
|
||||||
|
options = $.extend {}, HIGHLIGHT_DEFAULTS, options
|
||||||
|
el.classList.add(options.className)
|
||||||
|
setTimeout (-> el.classList.remove(options.className)), options.delay
|
||||||
|
return
|
@ -0,0 +1,85 @@
|
|||||||
|
class app.models.Doc extends app.Model
|
||||||
|
# Attributes: name, slug, type, version, index_path, mtime
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@reset @
|
||||||
|
|
||||||
|
reset: (data) ->
|
||||||
|
@resetEntries data.entries
|
||||||
|
@resetTypes data.types
|
||||||
|
return
|
||||||
|
|
||||||
|
resetEntries: (entries) ->
|
||||||
|
@entries = new app.collections.Entries(entries)
|
||||||
|
@entries.each (entry) => entry.doc = @
|
||||||
|
return
|
||||||
|
|
||||||
|
resetTypes: (types) ->
|
||||||
|
@types = new app.collections.Types(types)
|
||||||
|
@types.each (type) => type.doc = @
|
||||||
|
return
|
||||||
|
|
||||||
|
fullPath: (path = '') ->
|
||||||
|
path = "/#{path}" unless path[0] is '/'
|
||||||
|
"/#{@slug}#{path}"
|
||||||
|
|
||||||
|
fileUrl: (path) ->
|
||||||
|
"#{app.config.docs_host}#{@fullPath(path)}"
|
||||||
|
|
||||||
|
indexUrl: ->
|
||||||
|
"#{app.indexHost()}/#{@index_path}"
|
||||||
|
|
||||||
|
indexEntry: ->
|
||||||
|
new app.models.Entry
|
||||||
|
doc: @
|
||||||
|
name: @name
|
||||||
|
path: 'index'
|
||||||
|
|
||||||
|
findEntryByPathAndHash: (path, hash) ->
|
||||||
|
if hash and entry = @entries.findBy 'path', "#{path}##{hash}"
|
||||||
|
entry
|
||||||
|
else if path is 'index'
|
||||||
|
@indexEntry()
|
||||||
|
else
|
||||||
|
@entries.findBy 'path', path
|
||||||
|
|
||||||
|
load: (onSuccess, onError, options = {}) ->
|
||||||
|
return if options.readCache and @_loadFromCache(onSuccess)
|
||||||
|
|
||||||
|
callback = (data) =>
|
||||||
|
@reset data
|
||||||
|
onSuccess()
|
||||||
|
@_setCache data if options.writeCache
|
||||||
|
|
||||||
|
ajax
|
||||||
|
url: @indexUrl()
|
||||||
|
success: callback
|
||||||
|
error: onError
|
||||||
|
|
||||||
|
clearCache: ->
|
||||||
|
app.store.del @slug
|
||||||
|
return
|
||||||
|
|
||||||
|
_loadFromCache: (onSuccess) ->
|
||||||
|
return unless data = @_getCache()
|
||||||
|
|
||||||
|
callback = =>
|
||||||
|
@reset data
|
||||||
|
onSuccess()
|
||||||
|
|
||||||
|
setTimeout callback, 0
|
||||||
|
true
|
||||||
|
|
||||||
|
_getCache: ->
|
||||||
|
return unless data = app.store.get @slug
|
||||||
|
|
||||||
|
if data[0] is @mtime
|
||||||
|
return data[1]
|
||||||
|
else
|
||||||
|
@clearCache()
|
||||||
|
return
|
||||||
|
|
||||||
|
_setCache: (data) ->
|
||||||
|
app.store.set @slug, [@mtime, data]
|
||||||
|
return
|
@ -0,0 +1,46 @@
|
|||||||
|
class app.models.Entry extends app.Model
|
||||||
|
# Attributes: name, type, path
|
||||||
|
|
||||||
|
SEPARATORS_REGEXP = /\ |#|::/g
|
||||||
|
PARANTHESES_REGEXP = /\(.*?\)$/
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
super
|
||||||
|
@text = @searchValue()
|
||||||
|
|
||||||
|
searchValue: ->
|
||||||
|
@name
|
||||||
|
.toLowerCase()
|
||||||
|
.replace('...', ' ')
|
||||||
|
.replace(' event', '')
|
||||||
|
.replace(SEPARATORS_REGEXP, '.')
|
||||||
|
.replace(/\.+/g, '.')
|
||||||
|
.replace(PARANTHESES_REGEXP, '')
|
||||||
|
.trim()
|
||||||
|
|
||||||
|
fullPath: ->
|
||||||
|
@doc.fullPath if @isIndex() then '' else @path
|
||||||
|
|
||||||
|
filePath: ->
|
||||||
|
@doc.fullPath @_filePath()
|
||||||
|
|
||||||
|
fileUrl: ->
|
||||||
|
@doc.fileUrl @_filePath()
|
||||||
|
|
||||||
|
_filePath: ->
|
||||||
|
result = @path.replace /#.*/, ''
|
||||||
|
result += '.html' unless result[-5..-1] is '.html'
|
||||||
|
result
|
||||||
|
|
||||||
|
isIndex: ->
|
||||||
|
@path is 'index'
|
||||||
|
|
||||||
|
getType: ->
|
||||||
|
@doc.types.findBy 'name', @type
|
||||||
|
|
||||||
|
loadFile: (onSuccess, onError) ->
|
||||||
|
ajax
|
||||||
|
url: @fileUrl()
|
||||||
|
dataType: 'html'
|
||||||
|
success: onSuccess
|
||||||
|
error: onError
|
@ -0,0 +1,3 @@
|
|||||||
|
class app.Model
|
||||||
|
constructor: (attributes) ->
|
||||||
|
@[key] = value for key, value of attributes
|
@ -0,0 +1,8 @@
|
|||||||
|
class app.models.Type extends app.Model
|
||||||
|
# Attributes: name, slug, count
|
||||||
|
|
||||||
|
fullPath: ->
|
||||||
|
"/#{@doc.slug}-#{@slug}/"
|
||||||
|
|
||||||
|
entries: ->
|
||||||
|
@doc.entries.findAllBy 'type', @name
|
@ -0,0 +1,11 @@
|
|||||||
|
app.templates.render = (name, value, args...) ->
|
||||||
|
template = app.templates[name]
|
||||||
|
|
||||||
|
if Array.isArray(value)
|
||||||
|
result = ''
|
||||||
|
result += template(val, args...) for val in value
|
||||||
|
result
|
||||||
|
else if typeof template is 'function'
|
||||||
|
template(value, args...)
|
||||||
|
else
|
||||||
|
template
|
@ -0,0 +1,44 @@
|
|||||||
|
error = (title, text = '', links = '') ->
|
||||||
|
text = """<p class="_error-text">#{text}</p>""" if text
|
||||||
|
links = """<p class="_error-links">#{links}</p>""" if links
|
||||||
|
"""<div class="_error"><h1 class="_error-title">#{title}</h1>#{text}#{links}</div>"""
|
||||||
|
|
||||||
|
back = '<a href="javascript:history.back()" class="_error-link">Go back</a>'
|
||||||
|
|
||||||
|
app.templates.notFoundPage = ->
|
||||||
|
error """ Oops, that page doesn't exist. """,
|
||||||
|
""" It may be missing from the source documentation or this could be a bug. """,
|
||||||
|
back
|
||||||
|
|
||||||
|
app.templates.pageLoadError = ->
|
||||||
|
error """ Oops, that page failed to load. """,
|
||||||
|
""" It may be missing from the server or you could be offline.<br>
|
||||||
|
If you keep seeing this, you're likely behind a proxy or firewall which blocks cross-domain requests. """,
|
||||||
|
""" #{back} · <a href="#" class="_error-link" data-retry>Retry</a> """
|
||||||
|
|
||||||
|
app.templates.bootError = ->
|
||||||
|
error """ Oops, the app failed to load. """,
|
||||||
|
""" Check your Internet connection and try <a href="javascript:location.reload()">reloading</a>.<br>
|
||||||
|
If you keep seeing this, you're likely behind a proxy or firewall that blocks cross-domain requests. """
|
||||||
|
|
||||||
|
app.templates.unsupportedBrowser = """
|
||||||
|
<div class="_fail">
|
||||||
|
<h1 class="_fail-title">Your browser is unsupported, sorry.</h1>
|
||||||
|
<p class="_fail-text">DevDocs is an API documentation browser which supports the following browsers:
|
||||||
|
<ul class="_fail-list">
|
||||||
|
<li>Recent version of Chrome
|
||||||
|
<li>Recent version of Firefox
|
||||||
|
<li>Safari 5.1+
|
||||||
|
<li>Opera 12.1+
|
||||||
|
<li>Internet Explorer 10+
|
||||||
|
<li>iOS 6+
|
||||||
|
<li>Android 4.1+
|
||||||
|
<li>Windows Phone 8+
|
||||||
|
</ul>
|
||||||
|
<p class="_fail-text">
|
||||||
|
If you're unable to upgrade, I apologize.
|
||||||
|
I decided to prioritize speed and new features over support for older browsers.
|
||||||
|
<p class="_fail-text">
|
||||||
|
— Thibaut <a href="https://twitter.com/DevDocs" class="_fail-link">@DevDocs</a>
|
||||||
|
</div>
|
||||||
|
"""
|
@ -0,0 +1,9 @@
|
|||||||
|
notice = (text) -> """<p class="_notice-text">#{text}</p>"""
|
||||||
|
|
||||||
|
app.templates.singleDocNotice = (doc) ->
|
||||||
|
notice """ You're currently browsing the #{doc.name} documentation. To browse all docs, go to
|
||||||
|
<a href="http://#{app.config.production_host}" target="_top">#{app.config.production_host}</a>. """
|
||||||
|
|
||||||
|
app.templates.disabledDocNotice = ->
|
||||||
|
notice """ <strong>This documentation is currently disabled.</strong>
|
||||||
|
To enable it, click <a class="_notice-link" data-pick-docs>Select documentation</a>. """
|
@ -0,0 +1,23 @@
|
|||||||
|
notif = (title, html) ->
|
||||||
|
html = html.replace /<a/g, '<a class="_notif-link"'
|
||||||
|
"""<h5 class="_notif-title">#{title}</h5>#{html}<div class="_notif-close"></div>"""
|
||||||
|
|
||||||
|
textNotif = (title, message) ->
|
||||||
|
notif title, """<p class="_notif-text">#{message}"""
|
||||||
|
|
||||||
|
app.templates.notifUpdateReady = ->
|
||||||
|
textNotif """ DevDocs has been updated. """,
|
||||||
|
""" <a href="javascript:location='/'">Reload the page</a> to use the new version. """
|
||||||
|
|
||||||
|
app.templates.notifError = ->
|
||||||
|
textNotif """ Oops, an error occured. """,
|
||||||
|
""" Try <a href="javascript:app.reload()">reloading</a>, and if the problem persists,
|
||||||
|
<a href="javascript:app.reset()">resetting the app</a>.<br>
|
||||||
|
I track these errors automatically but feel free to contact me. """
|
||||||
|
|
||||||
|
app.templates.notifInvalidLocation = ->
|
||||||
|
textNotif """ DevDocs must be loaded from #{app.config.production_host} """,
|
||||||
|
""" Otherwise things are likely to break. """
|
||||||
|
|
||||||
|
app.templates.notifNews = (news) ->
|
||||||
|
notif 'Changelog', app.templates.newsList(news)
|
@ -0,0 +1,145 @@
|
|||||||
|
app.templates.aboutPage = -> """
|
||||||
|
<div class="_toc">
|
||||||
|
<h3 class="_toc-title">Table of Contents</h3>
|
||||||
|
<ul class="_toc-list">
|
||||||
|
<li><a href="#credits">Credits</a>
|
||||||
|
<li><a href="#thanks">Special Thanks</a>
|
||||||
|
<li><a href="#faq">FAQ</a>
|
||||||
|
<li><a href="#copyright">Copyright</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="_lined-heading">API Documentation Browser</h1>
|
||||||
|
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
|
||||||
|
<ul>
|
||||||
|
<li>Created and maintained by <a href="http://thibaut.me">Thibaut Courouble</a>
|
||||||
|
<li>Proudly sponsored by <a href="http://www.maxcdn.com">MaxCDN</a>—content delivery that developers love
|
||||||
|
<li>Free and <a href="https://github.com/Thibaut/devdocs">open source</a>
|
||||||
|
<iframe class="_github-btn" src="http://ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
|
||||||
|
</ul>
|
||||||
|
<p>To keep up-to-date with the latest development and community news:
|
||||||
|
<ul>
|
||||||
|
<li>Subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a>
|
||||||
|
<li>Follow <a href="https://twitter.com/DevDocs">@DevDocs</a> on Twitter
|
||||||
|
<li>Join the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>
|
||||||
|
</ul>
|
||||||
|
<p class="_note _note-green">If you use and like DevDocs, please consider donating through
|
||||||
|
<a href="https://www.gittip.com/Thibaut/">Gittip</a> or
|
||||||
|
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=4PTFAGT7K6QVG">PayPal</a>.<br>
|
||||||
|
Your support helps sustain the project and is highly appreciated.
|
||||||
|
|
||||||
|
<h2 class="_lined-heading" id="credits">Credits</h2>
|
||||||
|
<table class="_credits">
|
||||||
|
<tr>
|
||||||
|
<th>Documentation
|
||||||
|
<th>Copyright
|
||||||
|
<th>License
|
||||||
|
#{("<tr><td>#{c[0]}<td>© #{c[1]}<td><a href=\"#{c[3]}\">#{c[2]}</a>" for c in credits).join('')}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h2 class="_lined-heading" id="thanks">Special Thanks</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://www.heroku.com">Heroku</a> and <a href="http://newrelic.com">New Relic</a> for providing awesome free service
|
||||||
|
<li>Daniel Bruce for the <a href="http://www.entypo.com">Entypo</a> pictograms
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2 class="_lined-heading" id="faq">Questions & Answsers</h2>
|
||||||
|
<dl>
|
||||||
|
<dt>Does it work offline?
|
||||||
|
<dd>Yes! DevDocs is open source. You can run <a href="https://github.com/Thibaut/devdocs">the code</a> locally on your computer.<br>
|
||||||
|
An offline version that requires no setup is planned for the future.
|
||||||
|
<dt>Where can I suggest new docs and features?
|
||||||
|
<dd>You can suggest and vote for new docs on the <a href="https://trello.com/b/6BmTulfx/devdocs-documentation">Trello board</a>.<br>
|
||||||
|
If you have a specific feature request, add it to the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>.<br>
|
||||||
|
Otherwise use the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>.
|
||||||
|
<dt>Where can I report bugs?
|
||||||
|
<dd>In the <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a>. Thanks!
|
||||||
|
</dl>
|
||||||
|
<p>For anything else, feel free to email me at <a href="mailto:thibaut@devdocs.io">thibaut@devdocs.io</a>.
|
||||||
|
|
||||||
|
<h2 class="_lined-heading" id="copyright">Copyright and License</h2>
|
||||||
|
<p class="_note">
|
||||||
|
<strong>Copyright 2013 Thibaut Courouble and <a href="https://github.com/Thibaut/devdocs/graphs/contributors">other contributors</a></strong><br>
|
||||||
|
This software is licensed under the terms of the Mozilla Public License v2.0.<br>
|
||||||
|
You may obtain a copy of the source code at <a href="https://github.com/Thibaut/devdocs">github.com/Thibaut/devdocs</a>.<br>
|
||||||
|
For more information, see the <a href="https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a>
|
||||||
|
and <a href="https://github.com/Thibaut/devdocs/blob/master/LICENSE">LICENSE</a> files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
credits = [
|
||||||
|
[ 'Angular.js',
|
||||||
|
'2010-2013 Google, Inc.',
|
||||||
|
'CC BY',
|
||||||
|
'http://creativecommons.org/licenses/by/3.0/'
|
||||||
|
], [
|
||||||
|
'Backbone.js',
|
||||||
|
'2010-2013 Jeremy Ashkenas, DocumentCloud',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/jashkenas/backbone/master/LICENSE'
|
||||||
|
], [
|
||||||
|
'CoffeeScript',
|
||||||
|
'2009-2013 Jeremy Ashkenas',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/jashkenas/coffee-script/master/LICENSE'
|
||||||
|
], [
|
||||||
|
'CSS<br>DOM<br>HTML<br>JavaScript',
|
||||||
|
'2005-2013 Mozilla Developer Network and individual contributors',
|
||||||
|
'CC BY-SA',
|
||||||
|
'http://creativecommons.org/licenses/by-sa/2.5/'
|
||||||
|
], [
|
||||||
|
'Ember.js',
|
||||||
|
'2013 Yehuda Katz, Tom Dale and Ember.js contributors',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/emberjs/ember.js/master/LICENSE'
|
||||||
|
], [
|
||||||
|
'HTTP',
|
||||||
|
'1999 The Internet Society',
|
||||||
|
'Custom',
|
||||||
|
'http://www.w3.org/Protocols/rfc2616/rfc2616-sec21.html#sec21'
|
||||||
|
], [
|
||||||
|
'jQuery',
|
||||||
|
'2009 Packt Publishing<br>© 2013 jQuery Foundation',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/jquery/api.jquery.com/master/LICENSE-MIT.txt'
|
||||||
|
], [
|
||||||
|
'jQuery Mobile',
|
||||||
|
'2013 jQuery Foundation',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/jquery/api.jquerymobile.com/master/LICENSE-MIT.txt'
|
||||||
|
], [
|
||||||
|
'jQuery UI',
|
||||||
|
'2013 jQuery Foundation',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/jquery/api.jqueryui.com/master/LICENSE-MIT.txt'
|
||||||
|
], [
|
||||||
|
'Less',
|
||||||
|
'2009-2013 Alexis Sellier & The Core Less Team',
|
||||||
|
'Apache v2',
|
||||||
|
'https://raw.github.com/less/less.js/master/LICENSE'
|
||||||
|
], [
|
||||||
|
'Lo-Dash',
|
||||||
|
'2009-2013 The Dojo Foundation',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/lodash/lodash/master/LICENSE.txt'
|
||||||
|
], [
|
||||||
|
'Node.js',
|
||||||
|
'Joyent, Inc. and other Node contributors',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/joyent/node/master/LICENSE'
|
||||||
|
], [
|
||||||
|
'PHP',
|
||||||
|
'1997-2013 The PHP Documentation Group',
|
||||||
|
'CC BY',
|
||||||
|
'http://creativecommons.org/licenses/by/3.0/'
|
||||||
|
], [
|
||||||
|
'Sass',
|
||||||
|
'2006-2013 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/nex3/sass/master/MIT-LICENSE'
|
||||||
|
], [
|
||||||
|
'Underscore.js',
|
||||||
|
'2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors',
|
||||||
|
'MIT',
|
||||||
|
'https://raw.github.com/jashkenas/underscore/master/LICENSE'
|
||||||
|
]
|
||||||
|
]
|
@ -0,0 +1,89 @@
|
|||||||
|
ctrlKey = if navigator.userAgent.indexOf 'Mac OS X' then 'cmd' else 'ctrl'
|
||||||
|
|
||||||
|
app.templates.helpPage = """
|
||||||
|
<div class="_toc">
|
||||||
|
<h3 class="_toc-title">Table of Contents</h3>
|
||||||
|
<ul class="_toc-list">
|
||||||
|
<li><a href="#search">Search</a>
|
||||||
|
<li><a href="#shortcuts">Keyboard Shortcuts</a>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="_lined-heading" id="search">Search</h2>
|
||||||
|
<p>
|
||||||
|
The search is case-insensitive, ignores spaces, and supports fuzzy matching (for queries longer than two characters).
|
||||||
|
For example, searching <code class="_label">bgcp</code> brings up <code class="_label">background-clip</code>.
|
||||||
|
<dl>
|
||||||
|
<dt id="doc_search">Searching a specific documentation
|
||||||
|
<dd>
|
||||||
|
You can scope the search to a specific documentation by typing its name (or an abbreviation),
|
||||||
|
and pressing <code class="_label">Tab</code> (<code class="_label">Space</code> on mobile devices).
|
||||||
|
For example, to search the JavaScript documentation, enter <code class="_label">javascript</code>
|
||||||
|
or <code class="_label">js</code>, then <code class="_label">Tab</code>.<br>
|
||||||
|
To clear the current scope, empty the search field and hit <code class="_label">Backspace</code>.
|
||||||
|
<dt id="url_search">Prefilling the search field
|
||||||
|
<dd>
|
||||||
|
The search field can be prefilled from the URL by visiting <a href="/#q=keyword" target="_top">devdocs.io/#q=keyword</a>.
|
||||||
|
Characters after <code class="_label">#q=</code> will be used as search string.<br>
|
||||||
|
To search a specific documentation, add its name and a space before the keyword:
|
||||||
|
<a href="/#q=js%20date" target="_top">devdocs.io/#q=js date</a>.
|
||||||
|
<dt id="browser_search">Searching using the address bar
|
||||||
|
<dd>
|
||||||
|
DevDocs supports OpenSearch, meaning it can easily be installed as a search engine on most web browsers.
|
||||||
|
<ul>
|
||||||
|
<li>On Chrome, the setup is done automatically. Simply press <code class="_label">Tab</code> when devdocs.io is autocompleted
|
||||||
|
in the omnibox (to set a custom keyword, click <em>Manage search engines…</em> in Chrome's settings).
|
||||||
|
<li>On Firefox, open the search engine list (icon in the search bar) and select <em>Add "DevDocs Search"</em>.
|
||||||
|
DevDocs is now available in the search bar. You can also search from the location bar by following
|
||||||
|
<a href="https://support.mozilla.org/en-US/kb/search-bar-easily-choose-your-search-engine#w_keywords">these instructions</a>.
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<h2 class="_lined-heading" id="shortcuts">Keyboard Shortcuts</h2>
|
||||||
|
<h3 class="_shortcuts-title">Selection</h3>
|
||||||
|
<dl class="_shortcuts-dl">
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">↓</code>
|
||||||
|
<code class="_shortcut-code">↑</code>
|
||||||
|
<dd class="_shortcuts-dd">Move selection
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">→</code>
|
||||||
|
<code class="_shortcut-code">←</code>
|
||||||
|
<dd class="_shortcuts-dd">Show/hide sub-list
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">enter</code>
|
||||||
|
<dd class="_shortcuts-dd">Open selection
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">#{ctrlKey} + enter</code>
|
||||||
|
<dd class="_shortcuts-dd">Open selection in a new tab
|
||||||
|
</dl>
|
||||||
|
<h3 class="_shortcuts-title">Navigation</h3>
|
||||||
|
<dl class="_shortcuts-dl">
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">#{ctrlKey} + ←</code>
|
||||||
|
<code class="_shortcut-code">#{ctrlKey} + →</code>
|
||||||
|
<dd class="_shortcuts-dd">Go back/forward
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">alt + ↓</code>
|
||||||
|
<code class="_shortcut-code">alt + ↑</code>
|
||||||
|
<dd class="_shortcuts-dd">Scroll step by step
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">space</code>
|
||||||
|
<code class="_shortcut-code">shift + space</code>
|
||||||
|
<dd class="_shortcuts-dd">Scroll screen by screen
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">#{ctrlKey} + ↑</code>
|
||||||
|
<code class="_shortcut-code">#{ctrlKey} + ↓</code>
|
||||||
|
<dd class="_shortcuts-dd">Scroll to the top/bottom
|
||||||
|
</dl>
|
||||||
|
<h3 class="_shortcuts-title">Misc</h3>
|
||||||
|
<dl class="_shortcuts-dl">
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">escape</code>
|
||||||
|
<dd class="_shortcuts-dd">Reset
|
||||||
|
<dt class="_shortcuts-dt">
|
||||||
|
<code class="_shortcut-code">?</code>
|
||||||
|
<dd class="_shortcuts-dd">Show this page
|
||||||
|
</dl>
|
||||||
|
<p class="_note">
|
||||||
|
<strong>Tip:</strong> If the cursor is no longer in the search field, just press backspace or
|
||||||
|
continue to type and it will refocus the search field and start showing new results. """
|
@ -0,0 +1,114 @@
|
|||||||
|
app.templates.newsPage = ->
|
||||||
|
""" <h1 class="_lined-heading">Changelog</h1>
|
||||||
|
<p class="_note">For the latest news and updates,
|
||||||
|
subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a>
|
||||||
|
or follow <a href="https://twitter.com/DevDocs">@DevDocs</a>.
|
||||||
|
<div class="_news">#{app.templates.newsList app.news}</div> """
|
||||||
|
|
||||||
|
app.templates.newsList = (news) ->
|
||||||
|
result = ''
|
||||||
|
result += newsItem new Date(value[0]), value[1..] for value in news
|
||||||
|
result
|
||||||
|
|
||||||
|
MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']
|
||||||
|
|
||||||
|
newsItem = (date, news) ->
|
||||||
|
date = """<span class="_news-date">#{MONTHS[date.getUTCMonth()]} #{date.getUTCDate()}</span>"""
|
||||||
|
result = ''
|
||||||
|
|
||||||
|
for text, i in news
|
||||||
|
text = text.split "\n"
|
||||||
|
title = """<span class="_news-title">#{text.shift()}</span>"""
|
||||||
|
result += """<div class="_news-row">#{if i is 0 then date else ''} #{title} #{text.join '<br>'}</div>"""
|
||||||
|
|
||||||
|
result
|
||||||
|
|
||||||
|
app.news = [
|
||||||
|
[ 1382572800000, # October 24, 2013
|
||||||
|
""" DevDocs is now <a href="https://github.com/Thibaut/devdocs">open source</a>. """
|
||||||
|
], [
|
||||||
|
1381276800000, # October 9, 2013
|
||||||
|
""" DevDocs is now available as a <a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>. """
|
||||||
|
], [ 1379808000000, # September 22, 2013
|
||||||
|
""" New <a href="/php/">PHP</a> documentation """
|
||||||
|
], [
|
||||||
|
1378425600000, # September 6, 2013
|
||||||
|
""" New <a href="/lodash/">Lo-Dash</a> documentation """,
|
||||||
|
""" On mobile devices you can now search a specific documentation by typing its name and <code class="_label">Space</code>. """
|
||||||
|
], [
|
||||||
|
1377993600000, # September 1, 2013
|
||||||
|
""" New <a href="/jqueryui/">jQuery UI</a> and <a href="/jquerymobile/">jQuery Mobile</a> documentations """
|
||||||
|
], [
|
||||||
|
1377648000000, # August 28, 2013
|
||||||
|
""" New smartphone interface
|
||||||
|
Tested on iOS 6+ and Android 4.1+ """
|
||||||
|
], [
|
||||||
|
1377388800000, # August 25, 2013
|
||||||
|
""" New <a href="/ember/">Ember.js</a> documentation """
|
||||||
|
], [
|
||||||
|
1376784000000, # August 18, 2013
|
||||||
|
""" New <a href="/coffeescript/">CoffeeScript</a> documentation """,
|
||||||
|
""" URL search now automatically opens the first result. """
|
||||||
|
], [
|
||||||
|
1376352000000, # August 13, 2013
|
||||||
|
""" New <a href="/angular/">Angular.js</a> documentation """
|
||||||
|
], [
|
||||||
|
1376179200000, # August 11, 2013
|
||||||
|
""" New <a href="/sass/">Sass</a> and <a href="/less/">Less</a> documentations """
|
||||||
|
], [
|
||||||
|
1375660800000, # August 5, 2013
|
||||||
|
""" New <a href="/node/">Node.js</a> documentation """
|
||||||
|
], [
|
||||||
|
1375488000000, # August 3, 2013
|
||||||
|
""" Added support for OpenSearch """
|
||||||
|
], [
|
||||||
|
1375142400000, # July 30, 2013
|
||||||
|
""" New <a href="/backbone/">Backbone.js</a> documentation """
|
||||||
|
], [
|
||||||
|
1374883200000, # July 27, 2013
|
||||||
|
""" You can now customize the list of documentations.
|
||||||
|
New docs will be hidden by default, but you'll see a notification when there are new releases. """,
|
||||||
|
""" New <a href="/http/">HTTP</a> documentation """
|
||||||
|
], [
|
||||||
|
1373846400000, # July 15, 2013
|
||||||
|
""" URL search now works with single documentations: <a href="/#q=js%20sort">devdocs.io/#q=js sort</a> """
|
||||||
|
], [
|
||||||
|
1373673600000, # July 13, 2013
|
||||||
|
""" Added syntax highlighting """,
|
||||||
|
""" Added documentation versions """
|
||||||
|
], [
|
||||||
|
1373500800000, # July 11, 2013
|
||||||
|
""" New <a href="/underscore/">Underscore.js</a> documentation """,
|
||||||
|
""" Improved compatibility with tablets
|
||||||
|
A mobile version is planned as soon as other high priority features have been implemented. """
|
||||||
|
], [
|
||||||
|
1373414400000, # July 10, 2013
|
||||||
|
""" You can now search specific documentations.
|
||||||
|
Simply type the documentation's name and press <code class="_label">Tab</code>.
|
||||||
|
The name is fuzzy matched so you can use abbreviations like <code>js</code> for <code>JavaScript</code>. """
|
||||||
|
], [
|
||||||
|
1373241600000, # July 8, 2013
|
||||||
|
""" Improved search with fuzzy matching and better results
|
||||||
|
For example, searching <code>jqmka</code> now returns <code>jQuery.makeArray()</code>. """,
|
||||||
|
""" DevDocs finally has an icon. """,
|
||||||
|
""" <code class="_label">space</code> has replaced <code class="_label">alt + space</code> for scrolling down. """
|
||||||
|
], [
|
||||||
|
1373068800000, # July 6, 2013
|
||||||
|
""" New <a href="/dom/">DOM</a> and <a href="/dom_events/">DOM Events</a> documentations
|
||||||
|
DevDocs now includes almost all reference documents available on the Mozilla Developer Network.
|
||||||
|
Big thank you to Mozilla and all the people that contributed to MDN. """,
|
||||||
|
""" Implemented URL search: <a href="/#q=sort">devdocs.io/#q=sort</a> """
|
||||||
|
], [
|
||||||
|
1372723200000, # July 2, 2013
|
||||||
|
""" New <a href="/javascript/">JavaScript</a> documentation """
|
||||||
|
], [
|
||||||
|
1372377600000, # June 28, 2013
|
||||||
|
""" DevDocs made the front page of Hacker News!
|
||||||
|
Hi everyone — thanks for trying DevDocs.
|
||||||
|
Please bear with me while I fix bugs and scramble to add more docs.
|
||||||
|
This is only v1. There's a lot more to come. """
|
||||||
|
], [
|
||||||
|
1371513600000, # June 18, 2013
|
||||||
|
""" Initial release """
|
||||||
|
]
|
||||||
|
]
|
@ -0,0 +1,77 @@
|
|||||||
|
app.templates.splash = """
|
||||||
|
<div class="_splash-title">DevDocs</div>
|
||||||
|
<a href="http://www.maxcdn.com" class="_splash-maxcdn">Sponsored by<span class="_maxcdn-logo-bw"> MaxCDN</span></a>
|
||||||
|
"""
|
||||||
|
|
||||||
|
<% if App.development? %>
|
||||||
|
app.templates.intro = """
|
||||||
|
<div class="_intro"><div class="_intro-message">
|
||||||
|
<a class="_intro-hide" data-hide-intro>Stop showing this message</a>
|
||||||
|
<h2 class="_intro-title">Hi there!</h2>
|
||||||
|
<p>Thanks for downloading DevDocs. Here are a few things you should know:
|
||||||
|
<ol class="_intro-list">
|
||||||
|
<li>Your local version of DevDocs will not self-update. Unless you're offline or modifying the code,
|
||||||
|
I recommend using the hosted version at <a href="http://devdocs.io">devdocs.io</a>.
|
||||||
|
<li>Run <code class="_label">thor docs:list</code> to see all available documentations.
|
||||||
|
<li>Run <code class="_label">thor docs:download --all</code> to download/update all documentations.
|
||||||
|
<li>To be notified about new versions, don't forget to subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a>.
|
||||||
|
<li>The <a href="https://github.com/Thibaut/devdocs/issues">issue tracker</a> is the preferred channel for bug reports and
|
||||||
|
feature requests. For everything else, use the <a href="https://groups.google.com/d/forum/devdocs">mailing list</a>.
|
||||||
|
<li>Contributions are welcome. See the <a href="https://github.com/Thibaut/devdocs/blob/master/CONTRIBUTING.md">guidelines</a>.
|
||||||
|
<li>DevDocs is licensed under the terms of the Mozilla Public License v2.0. For more information,
|
||||||
|
see the <a href="https://github.com/Thibaut/devdocs/blob/master/COPYRIGHT">COPYRIGHT</a> and
|
||||||
|
<a href="https://github.com/Thibaut/devdocs/blob/master/LICENSE">LICENSE</a> files.
|
||||||
|
</ol>
|
||||||
|
<a href="http://www.maxcdn.com" class="_intro-maxcdn">Sponsored by<span class="_maxcdn-logo"> MaxCDN</span></a>
|
||||||
|
<p>That's all. Happy coding!
|
||||||
|
</div></div>
|
||||||
|
"""
|
||||||
|
<% else %>
|
||||||
|
app.templates.intro = """
|
||||||
|
<div class="_intro"><div class="_intro-message">
|
||||||
|
<a class="_intro-hide" data-hide-intro>Stop showing this message</a>
|
||||||
|
<h2 class="_intro-title">Welcome!</h2>
|
||||||
|
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
|
||||||
|
Here's what you should know before you start:
|
||||||
|
<ol class="_intro-list">
|
||||||
|
<li>To pick your docs, click <a class="_intro-link" data-pick-docs>Select documentation</a> in the bottom left corner
|
||||||
|
<li>You don't have to use your mouse — see the list of <a href="/help#shortcuts">keyboard shortcuts</a>
|
||||||
|
<li>The search supports fuzzy matching (e.g. "bgcp" brings up "background-clip")
|
||||||
|
<li>To search a specific documentation, type its name (or an abbreviation), then Tab
|
||||||
|
<li>You can search using your browser's address bar — <a href="/help#browser_search">learn how</a>
|
||||||
|
<li>DevDocs works on mobile and is available as a <a href="https://chrome.google.com/webstore/detail/devdocs/mnfehgbmkapmjnhcnbodoamcioleeooe">Chrome web app</a>
|
||||||
|
<li>For the latest news, subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a> or follow <a href="https://twitter.com/DevDocs">@DevDocs</a>
|
||||||
|
<li>DevDocs is free and <a href="https://github.com/Thibaut/devdocs">open source</a>
|
||||||
|
<iframe class="_github-btn" src="http://ghbtns.com/github-btn.html?user=Thibaut&repo=devdocs&type=watch&count=true" allowtransparency="true" frameborder="0" scrolling="0" width="100" height="20"></iframe>
|
||||||
|
</ol>
|
||||||
|
<a href="http://www.maxcdn.com" class="_intro-maxcdn">Sponsored by<span class="_maxcdn-logo"> MaxCDN</span></a>
|
||||||
|
<p>That's all. Happy coding!
|
||||||
|
</div></div>
|
||||||
|
"""
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
app.templates.mobileNav = """
|
||||||
|
<nav class="_mobile-nav">
|
||||||
|
<a href="/about" class="_mobile-nav-link">About</a>
|
||||||
|
<a href="/news" class="_mobile-nav-link">News</a>
|
||||||
|
<a href="/help" class="_mobile-nav-link">Help</a>
|
||||||
|
</nav>
|
||||||
|
"""
|
||||||
|
|
||||||
|
app.templates.mobileIntro = """
|
||||||
|
<div class="_mobile-intro">
|
||||||
|
<h2 class="_intro-title">Welcome!</h2>
|
||||||
|
<p>DevDocs combines multiple API documentations in a fast, organized, and searchable interface.
|
||||||
|
Here's what you should know before you start:
|
||||||
|
<ol class="_intro-list">
|
||||||
|
<li>To pick your docs, click <a data-pick-docs>Select documentation</a> at the bottom of the menu
|
||||||
|
<li>The search supports fuzzy matching (e.g. "bgcp" matches "background-clip")
|
||||||
|
<li>To search a specific documentation, type its name (or an abbreviation), then Space
|
||||||
|
<li>For the latest news, subscribe to the <a href="http://eepurl.com/HnLUz">newsletter</a> or follow <a href="https://twitter.com/DevDocs">@DevDocs</a>
|
||||||
|
<li>DevDocs is <a href="https://github.com/Thibaut/devdocs">open source</a>
|
||||||
|
</ol>
|
||||||
|
<p>That's all. Happy coding!
|
||||||
|
<p class="_intro-maxcdn">Sponsored by <a href="http://www.maxcdn.com" class="_intro-maxcdn-logo">MaxCDN</a></p>
|
||||||
|
<a class="_intro-hide" data-hide-intro>Stop showing this message</a>
|
||||||
|
</div>
|
||||||
|
"""
|
@ -0,0 +1,6 @@
|
|||||||
|
app.templates.typePage = (type) ->
|
||||||
|
""" <h1>#{type.doc.name} / #{type.name}</h1>
|
||||||
|
<ul class="_entry-list">#{app.templates.render 'typePageEntry', type.entries()}</ul> """
|
||||||
|
|
||||||
|
app.templates.typePageEntry = (entry) ->
|
||||||
|
"""<li><a href="#{entry.fullPath()}">#{$.escape entry.name}</a></li>"""
|
@ -0,0 +1,40 @@
|
|||||||
|
templates = app.templates
|
||||||
|
|
||||||
|
templates.sidebarDoc = (doc, options = {}) ->
|
||||||
|
link = """<a href="#{doc.fullPath()}" class="_list-item _icon-#{doc.slug} """
|
||||||
|
link += if options.disabled then '_list-disabled' else '_list-dir'
|
||||||
|
link += """" data-slug="#{doc.slug}">"""
|
||||||
|
link += """<span class="_list-arrow"></span>""" unless options.disabled
|
||||||
|
link += """<span class="_list-count">#{doc.version}</span>""" if doc.version
|
||||||
|
link + "#{doc.name}</a>"
|
||||||
|
|
||||||
|
templates.sidebarType = (type) ->
|
||||||
|
"""<a href="#{type.fullPath()}" class="_list-item _list-dir" data-slug="#{type.slug}"><span class="_list-arrow"></span><span class="_list-count">#{type.count}</span>#{type.name}</a>"""
|
||||||
|
|
||||||
|
templates.sidebarEntry = (entry) ->
|
||||||
|
name = $.escape(entry.name)
|
||||||
|
"""<a href="#{entry.fullPath()}" class="_list-item" title="#{name}">#{name}</a>"""
|
||||||
|
|
||||||
|
templates.sidebarResult = (entry) ->
|
||||||
|
name = $.escape(entry.name)
|
||||||
|
"""<a href="#{entry.fullPath()}" class="_list-item _list-result _icon-#{entry.doc.slug}" title="#{entry.doc.name}: #{name}">#{name}</a>"""
|
||||||
|
|
||||||
|
templates.sidebarPageLink = (count) ->
|
||||||
|
"""<span class="_list-item _list-pagelink">Show more… (#{count})</span>"""
|
||||||
|
|
||||||
|
templates.sidebarLabel = (doc, options = {}) ->
|
||||||
|
label = """<label class="_list-item _list-label _icon-#{doc.slug}"""
|
||||||
|
label += ' _list-label-off' unless options.checked
|
||||||
|
label += """"><input type="checkbox" name="#{doc.slug}" class="_list-checkbox" """
|
||||||
|
label += 'checked' if options.checked
|
||||||
|
label + ">#{doc.name}</label>"
|
||||||
|
|
||||||
|
templates.sidebarVote = '<a href="https://trello.com/b/6BmTulfx/devdocs-documentation" class="_list-link" target="_blank">Vote for new documentation</a>'
|
||||||
|
|
||||||
|
sidebarFooter = (html) -> """<div class="_sidebar-footer">#{html}</div>"""
|
||||||
|
|
||||||
|
templates.sidebarSettings = ->
|
||||||
|
sidebarFooter """<a class="_sidebar-footer-link _sidebar-footer-edit" data-pick-docs>Select documentation</a>"""
|
||||||
|
|
||||||
|
templates.sidebarSave = ->
|
||||||
|
sidebarFooter """<a class="_sidebar-footer-link _sidebar-footer-save">Save</a>"""
|
@ -0,0 +1,141 @@
|
|||||||
|
/*!
|
||||||
|
* Cookies.js - 0.3.1
|
||||||
|
* Wednesday, April 24 2013 @ 2:28 AM EST
|
||||||
|
*
|
||||||
|
* Copyright (c) 2013, Scott Hamper
|
||||||
|
* Licensed under the MIT license,
|
||||||
|
* http://www.opensource.org/licenses/MIT
|
||||||
|
*/
|
||||||
|
(function (undefined) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var Cookies = function (key, value, options) {
|
||||||
|
return arguments.length === 1 ?
|
||||||
|
Cookies.get(key) : Cookies.set(key, value, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Allows for setter injection in unit tests
|
||||||
|
Cookies._document = document;
|
||||||
|
Cookies._navigator = navigator;
|
||||||
|
|
||||||
|
Cookies.defaults = {
|
||||||
|
path: '/'
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies.get = function (key) {
|
||||||
|
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
|
||||||
|
Cookies._renewCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cookies._cache[key];
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies.set = function (key, value, options) {
|
||||||
|
options = Cookies._getExtendedOptions(options);
|
||||||
|
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
|
||||||
|
|
||||||
|
Cookies._document.cookie = Cookies._generateCookieString(key, value, options);
|
||||||
|
|
||||||
|
return Cookies;
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies.expire = function (key, options) {
|
||||||
|
return Cookies.set(key, undefined, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._getExtendedOptions = function (options) {
|
||||||
|
return {
|
||||||
|
path: options && options.path || Cookies.defaults.path,
|
||||||
|
domain: options && options.domain || Cookies.defaults.domain,
|
||||||
|
expires: options && options.expires || Cookies.defaults.expires,
|
||||||
|
secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._isValidDate = function (date) {
|
||||||
|
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._getExpiresDate = function (expires, now) {
|
||||||
|
now = now || new Date();
|
||||||
|
switch (typeof expires) {
|
||||||
|
case 'number': expires = new Date(now.getTime() + expires * 1000); break;
|
||||||
|
case 'string': expires = new Date(expires); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expires && !Cookies._isValidDate(expires)) {
|
||||||
|
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
|
||||||
|
}
|
||||||
|
|
||||||
|
return expires;
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._generateCookieString = function (key, value, options) {
|
||||||
|
key = encodeURIComponent(key);
|
||||||
|
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var cookieString = key + '=' + value;
|
||||||
|
cookieString += options.path ? ';path=' + options.path : '';
|
||||||
|
cookieString += options.domain ? ';domain=' + options.domain : '';
|
||||||
|
cookieString += options.expires ? ';expires=' + options.expires.toGMTString() : '';
|
||||||
|
cookieString += options.secure ? ';secure' : '';
|
||||||
|
|
||||||
|
return cookieString;
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._getCookieObjectFromString = function (documentCookie) {
|
||||||
|
var cookieObject = {};
|
||||||
|
var cookiesArray = documentCookie ? documentCookie.split('; ') : [];
|
||||||
|
|
||||||
|
for (var i = 0; i < cookiesArray.length; i++) {
|
||||||
|
var cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
|
||||||
|
|
||||||
|
if (cookieObject[cookieKvp.key] === undefined) {
|
||||||
|
cookieObject[cookieKvp.key] = cookieKvp.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cookieObject;
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._getKeyValuePairFromCookieString = function (cookieString) {
|
||||||
|
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
|
||||||
|
var separatorIndex = cookieString.indexOf('=');
|
||||||
|
|
||||||
|
// IE omits the "=" when the cookie value is an empty string
|
||||||
|
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: decodeURIComponent(cookieString.substr(0, separatorIndex)),
|
||||||
|
value: decodeURIComponent(cookieString.substr(separatorIndex + 1))
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._renewCache = function () {
|
||||||
|
Cookies._cache = Cookies._getCookieObjectFromString(Cookies._document.cookie);
|
||||||
|
Cookies._cachedDocumentCookie = Cookies._document.cookie;
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies._areEnabled = function () {
|
||||||
|
return Cookies._navigator.cookieEnabled ||
|
||||||
|
Cookies.set('cookies.js', 1).get('cookies.js') === '1';
|
||||||
|
};
|
||||||
|
|
||||||
|
Cookies.enabled = Cookies._areEnabled();
|
||||||
|
|
||||||
|
// AMD support
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
define(function () { return Cookies; });
|
||||||
|
// CommonJS and Node.js module support.
|
||||||
|
} else if (typeof exports !== 'undefined') {
|
||||||
|
// Support Node.js specific `module.exports` (which can be a function)
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
exports = module.exports = Cookies;
|
||||||
|
}
|
||||||
|
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function)
|
||||||
|
exports.Cookies = Cookies;
|
||||||
|
} else {
|
||||||
|
window.Cookies = Cookies;
|
||||||
|
}
|
||||||
|
})();
|
@ -0,0 +1,740 @@
|
|||||||
|
/**
|
||||||
|
* @preserve FastClick: polyfill to remove click delays on browsers with touch UIs.
|
||||||
|
*
|
||||||
|
* @version 0.6.9
|
||||||
|
* @codingstandard ftlabs-jsv2
|
||||||
|
* @copyright The Financial Times Limited [All Rights Reserved]
|
||||||
|
* @license MIT License (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*jslint browser:true, node:true*/
|
||||||
|
/*global define, Event, Node*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate fast-clicking listeners on the specificed layer.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @param {Element} layer The layer to listen on
|
||||||
|
*/
|
||||||
|
function FastClick(layer) {
|
||||||
|
'use strict';
|
||||||
|
var oldOnClick, self = this;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether a click is currently being tracked.
|
||||||
|
*
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
this.trackingClick = false;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp for when when click tracking started.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
this.trackingClickStart = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The element being tracked for a click.
|
||||||
|
*
|
||||||
|
* @type EventTarget
|
||||||
|
*/
|
||||||
|
this.targetElement = null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* X-coordinate of touch start event.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
this.touchStartX = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Y-coordinate of touch start event.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
this.touchStartY = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID of the last touch, retrieved from Touch.identifier.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
this.lastTouchIdentifier = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Touchmove boundary, beyond which a click will be cancelled.
|
||||||
|
*
|
||||||
|
* @type number
|
||||||
|
*/
|
||||||
|
this.touchBoundary = 10;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The FastClick layer.
|
||||||
|
*
|
||||||
|
* @type Element
|
||||||
|
*/
|
||||||
|
this.layer = layer;
|
||||||
|
|
||||||
|
if (!layer || !layer.nodeType) {
|
||||||
|
throw new TypeError('Layer must be a document node');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @type function() */
|
||||||
|
this.onClick = function() { return FastClick.prototype.onClick.apply(self, arguments); };
|
||||||
|
|
||||||
|
/** @type function() */
|
||||||
|
this.onMouse = function() { return FastClick.prototype.onMouse.apply(self, arguments); };
|
||||||
|
|
||||||
|
/** @type function() */
|
||||||
|
this.onTouchStart = function() { return FastClick.prototype.onTouchStart.apply(self, arguments); };
|
||||||
|
|
||||||
|
/** @type function() */
|
||||||
|
this.onTouchEnd = function() { return FastClick.prototype.onTouchEnd.apply(self, arguments); };
|
||||||
|
|
||||||
|
/** @type function() */
|
||||||
|
this.onTouchCancel = function() { return FastClick.prototype.onTouchCancel.apply(self, arguments); };
|
||||||
|
|
||||||
|
if (FastClick.notNeeded(layer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event handlers as required
|
||||||
|
if (this.deviceIsAndroid) {
|
||||||
|
layer.addEventListener('mouseover', this.onMouse, true);
|
||||||
|
layer.addEventListener('mousedown', this.onMouse, true);
|
||||||
|
layer.addEventListener('mouseup', this.onMouse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.addEventListener('click', this.onClick, true);
|
||||||
|
layer.addEventListener('touchstart', this.onTouchStart, false);
|
||||||
|
layer.addEventListener('touchend', this.onTouchEnd, false);
|
||||||
|
layer.addEventListener('touchcancel', this.onTouchCancel, false);
|
||||||
|
|
||||||
|
// Hack is required for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
|
||||||
|
// which is how FastClick normally stops click events bubbling to callbacks registered on the FastClick
|
||||||
|
// layer when they are cancelled.
|
||||||
|
if (!Event.prototype.stopImmediatePropagation) {
|
||||||
|
layer.removeEventListener = function(type, callback, capture) {
|
||||||
|
var rmv = Node.prototype.removeEventListener;
|
||||||
|
if (type === 'click') {
|
||||||
|
rmv.call(layer, type, callback.hijacked || callback, capture);
|
||||||
|
} else {
|
||||||
|
rmv.call(layer, type, callback, capture);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
layer.addEventListener = function(type, callback, capture) {
|
||||||
|
var adv = Node.prototype.addEventListener;
|
||||||
|
if (type === 'click') {
|
||||||
|
adv.call(layer, type, callback.hijacked || (callback.hijacked = function(event) {
|
||||||
|
if (!event.propagationStopped) {
|
||||||
|
callback(event);
|
||||||
|
}
|
||||||
|
}), capture);
|
||||||
|
} else {
|
||||||
|
adv.call(layer, type, callback, capture);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a handler is already declared in the element's onclick attribute, it will be fired before
|
||||||
|
// FastClick's onClick handler. Fix this by pulling out the user-defined handler function and
|
||||||
|
// adding it as listener.
|
||||||
|
if (typeof layer.onclick === 'function') {
|
||||||
|
|
||||||
|
// Android browser on at least 3.2 requires a new reference to the function in layer.onclick
|
||||||
|
// - the old one won't work if passed to addEventListener directly.
|
||||||
|
oldOnClick = layer.onclick;
|
||||||
|
layer.addEventListener('click', function(event) {
|
||||||
|
oldOnClick(event);
|
||||||
|
}, false);
|
||||||
|
layer.onclick = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android requires exceptions.
|
||||||
|
*
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
FastClick.prototype.deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS requires exceptions.
|
||||||
|
*
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
FastClick.prototype.deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS 4 requires an exception for select elements.
|
||||||
|
*
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
FastClick.prototype.deviceIsIOS4 = FastClick.prototype.deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* iOS 6.0(+?) requires the target element to be manually derived
|
||||||
|
*
|
||||||
|
* @type boolean
|
||||||
|
*/
|
||||||
|
FastClick.prototype.deviceIsIOSWithBadTarget = FastClick.prototype.deviceIsIOS && (/OS ([6-9]|\d{2})_\d/).test(navigator.userAgent);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a given element requires a native click.
|
||||||
|
*
|
||||||
|
* @param {EventTarget|Element} target Target DOM element
|
||||||
|
* @returns {boolean} Returns true if the element needs a native click
|
||||||
|
*/
|
||||||
|
FastClick.prototype.needsClick = function(target) {
|
||||||
|
'use strict';
|
||||||
|
switch (target.nodeName.toLowerCase()) {
|
||||||
|
|
||||||
|
// Don't send a synthetic click to disabled inputs (issue #62)
|
||||||
|
case 'button':
|
||||||
|
case 'select':
|
||||||
|
case 'textarea':
|
||||||
|
if (target.disabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'input':
|
||||||
|
|
||||||
|
// File inputs need real clicks on iOS 6 due to a browser bug (issue #68)
|
||||||
|
if ((this.deviceIsIOS && target.type === 'file') || target.disabled) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case 'label':
|
||||||
|
case 'video':
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (/\bneedsclick\b/).test(target.className);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether a given element requires a call to focus to simulate click into element.
|
||||||
|
*
|
||||||
|
* @param {EventTarget|Element} target Target DOM element
|
||||||
|
* @returns {boolean} Returns true if the element requires a call to focus to simulate native click.
|
||||||
|
*/
|
||||||
|
FastClick.prototype.needsFocus = function(target) {
|
||||||
|
'use strict';
|
||||||
|
switch (target.nodeName.toLowerCase()) {
|
||||||
|
case 'textarea':
|
||||||
|
case 'select':
|
||||||
|
return true;
|
||||||
|
case 'input':
|
||||||
|
switch (target.type) {
|
||||||
|
case 'button':
|
||||||
|
case 'checkbox':
|
||||||
|
case 'file':
|
||||||
|
case 'image':
|
||||||
|
case 'radio':
|
||||||
|
case 'submit':
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No point in attempting to focus disabled inputs
|
||||||
|
return !target.disabled && !target.readOnly;
|
||||||
|
default:
|
||||||
|
return (/\bneedsfocus\b/).test(target.className);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a click event to the specified element.
|
||||||
|
*
|
||||||
|
* @param {EventTarget|Element} targetElement
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
FastClick.prototype.sendClick = function(targetElement, event) {
|
||||||
|
'use strict';
|
||||||
|
var clickEvent, touch;
|
||||||
|
|
||||||
|
// On some Android devices activeElement needs to be blurred otherwise the synthetic click will have no effect (#24)
|
||||||
|
if (document.activeElement && document.activeElement !== targetElement) {
|
||||||
|
document.activeElement.blur();
|
||||||
|
}
|
||||||
|
|
||||||
|
touch = event.changedTouches[0];
|
||||||
|
|
||||||
|
// Synthesise a click event, with an extra attribute so it can be tracked
|
||||||
|
clickEvent = document.createEvent('MouseEvents');
|
||||||
|
clickEvent.initMouseEvent('click', true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null);
|
||||||
|
clickEvent.forwardedTouchEvent = true;
|
||||||
|
targetElement.dispatchEvent(clickEvent);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {EventTarget|Element} targetElement
|
||||||
|
*/
|
||||||
|
FastClick.prototype.focus = function(targetElement) {
|
||||||
|
'use strict';
|
||||||
|
var length;
|
||||||
|
|
||||||
|
if (this.deviceIsIOS && targetElement.setSelectionRange) {
|
||||||
|
length = targetElement.value.length;
|
||||||
|
targetElement.setSelectionRange(length, length);
|
||||||
|
} else {
|
||||||
|
targetElement.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the given target element is a child of a scrollable layer and if so, set a flag on it.
|
||||||
|
*
|
||||||
|
* @param {EventTarget|Element} targetElement
|
||||||
|
*/
|
||||||
|
FastClick.prototype.updateScrollParent = function(targetElement) {
|
||||||
|
'use strict';
|
||||||
|
var scrollParent, parentElement;
|
||||||
|
|
||||||
|
scrollParent = targetElement.fastClickScrollParent;
|
||||||
|
|
||||||
|
// Attempt to discover whether the target element is contained within a scrollable layer. Re-check if the
|
||||||
|
// target element was moved to another parent.
|
||||||
|
if (!scrollParent || !scrollParent.contains(targetElement)) {
|
||||||
|
parentElement = targetElement;
|
||||||
|
do {
|
||||||
|
if (parentElement.scrollHeight > parentElement.offsetHeight) {
|
||||||
|
scrollParent = parentElement;
|
||||||
|
targetElement.fastClickScrollParent = parentElement;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parentElement = parentElement.parentElement;
|
||||||
|
} while (parentElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always update the scroll top tracker if possible.
|
||||||
|
if (scrollParent) {
|
||||||
|
scrollParent.fastClickLastScrollTop = scrollParent.scrollTop;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {EventTarget} targetElement
|
||||||
|
* @returns {Element|EventTarget}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.getTargetElementFromEventTarget = function(eventTarget) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// On some older browsers (notably Safari on iOS 4.1 - see issue #56) the event target may be a text node.
|
||||||
|
if (eventTarget.nodeType === Node.TEXT_NODE) {
|
||||||
|
return eventTarget.parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventTarget;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On touch start, record the position and scroll offset.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.onTouchStart = function(event) {
|
||||||
|
'use strict';
|
||||||
|
var targetElement, touch, selection;
|
||||||
|
|
||||||
|
// Ignore multiple touches, otherwise pinch-to-zoom is prevented if both fingers are on the FastClick element (issue #111).
|
||||||
|
if (event.targetTouches.length > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetElement = this.getTargetElementFromEventTarget(event.target);
|
||||||
|
touch = event.targetTouches[0];
|
||||||
|
|
||||||
|
if (this.deviceIsIOS) {
|
||||||
|
|
||||||
|
// Only trusted events will deselect text on iOS (issue #49)
|
||||||
|
selection = window.getSelection();
|
||||||
|
if (selection.rangeCount && !selection.isCollapsed) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.deviceIsIOS4) {
|
||||||
|
|
||||||
|
// Weird things happen on iOS when an alert or confirm dialog is opened from a click event callback (issue #23):
|
||||||
|
// when the user next taps anywhere else on the page, new touchstart and touchend events are dispatched
|
||||||
|
// with the same identifier as the touch event that previously triggered the click that triggered the alert.
|
||||||
|
// Sadly, there is an issue on iOS 4 that causes some normal touch events to have the same identifier as an
|
||||||
|
// immediately preceeding touch event (issue #52), so this fix is unavailable on that platform.
|
||||||
|
if (touch.identifier === this.lastTouchIdentifier) {
|
||||||
|
event.preventDefault();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastTouchIdentifier = touch.identifier;
|
||||||
|
|
||||||
|
// If the target element is a child of a scrollable layer (using -webkit-overflow-scrolling: touch) and:
|
||||||
|
// 1) the user does a fling scroll on the scrollable layer
|
||||||
|
// 2) the user stops the fling scroll with another tap
|
||||||
|
// then the event.target of the last 'touchend' event will be the element that was under the user's finger
|
||||||
|
// when the fling scroll was started, causing FastClick to send a click event to that layer - unless a check
|
||||||
|
// is made to ensure that a parent layer was not scrolled before sending a synthetic click (issue #42).
|
||||||
|
this.updateScrollParent(targetElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.trackingClick = true;
|
||||||
|
this.trackingClickStart = event.timeStamp;
|
||||||
|
this.targetElement = targetElement;
|
||||||
|
|
||||||
|
this.touchStartX = touch.pageX;
|
||||||
|
this.touchStartY = touch.pageY;
|
||||||
|
|
||||||
|
// Prevent phantom clicks on fast double-tap (issue #36)
|
||||||
|
if ((event.timeStamp - this.lastClickTime) < 200) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based on a touchmove event object, check whether the touch has moved past a boundary since it started.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.touchHasMoved = function(event) {
|
||||||
|
'use strict';
|
||||||
|
var touch = event.changedTouches[0], boundary = this.touchBoundary;
|
||||||
|
|
||||||
|
if (Math.abs(touch.pageX - this.touchStartX) > boundary || Math.abs(touch.pageY - this.touchStartY) > boundary) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to find the labelled control for the given label element.
|
||||||
|
*
|
||||||
|
* @param {EventTarget|HTMLLabelElement} labelElement
|
||||||
|
* @returns {Element|null}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.findControl = function(labelElement) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Fast path for newer browsers supporting the HTML5 control attribute
|
||||||
|
if (labelElement.control !== undefined) {
|
||||||
|
return labelElement.control;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All browsers under test that support touch events also support the HTML5 htmlFor attribute
|
||||||
|
if (labelElement.htmlFor) {
|
||||||
|
return document.getElementById(labelElement.htmlFor);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no for attribute exists, attempt to retrieve the first labellable descendant element
|
||||||
|
// the list of which is defined here: http://www.w3.org/TR/html5/forms.html#category-label
|
||||||
|
return labelElement.querySelector('button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On touch end, determine whether to send a click event at once.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.onTouchEnd = function(event) {
|
||||||
|
'use strict';
|
||||||
|
var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement;
|
||||||
|
|
||||||
|
// If the touch has moved, cancel the click tracking
|
||||||
|
if (this.touchHasMoved(event)) {
|
||||||
|
this.trackingClick = false;
|
||||||
|
this.targetElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.trackingClick) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent phantom clicks on fast double-tap (issue #36)
|
||||||
|
if ((event.timeStamp - this.lastClickTime) < 200) {
|
||||||
|
this.cancelNextClick = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastClickTime = event.timeStamp;
|
||||||
|
|
||||||
|
trackingClickStart = this.trackingClickStart;
|
||||||
|
this.trackingClick = false;
|
||||||
|
this.trackingClickStart = 0;
|
||||||
|
|
||||||
|
// On some iOS devices, the targetElement supplied with the event is invalid if the layer
|
||||||
|
// is performing a transition or scroll, and has to be re-detected manually. Note that
|
||||||
|
// for this to function correctly, it must be called *after* the event target is checked!
|
||||||
|
// See issue #57; also filed as rdar://13048589 .
|
||||||
|
if (this.deviceIsIOSWithBadTarget) {
|
||||||
|
touch = event.changedTouches[0];
|
||||||
|
|
||||||
|
// In certain cases arguments of elementFromPoint can be negative, so prevent setting targetElement to null
|
||||||
|
targetElement = document.elementFromPoint(touch.pageX - window.pageXOffset, touch.pageY - window.pageYOffset) || targetElement;
|
||||||
|
targetElement.fastClickScrollParent = this.targetElement.fastClickScrollParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetTagName = targetElement.tagName.toLowerCase();
|
||||||
|
if (targetTagName === 'label') {
|
||||||
|
forElement = this.findControl(targetElement);
|
||||||
|
if (forElement) {
|
||||||
|
this.focus(targetElement);
|
||||||
|
if (this.deviceIsAndroid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
targetElement = forElement;
|
||||||
|
}
|
||||||
|
} else if (this.needsFocus(targetElement)) {
|
||||||
|
|
||||||
|
// Case 1: If the touch started a while ago (best guess is 100ms based on tests for issue #36) then focus will be triggered anyway. Return early and unset the target element reference so that the subsequent click will be allowed through.
|
||||||
|
// Case 2: Without this exception for input elements tapped when the document is contained in an iframe, then any inputted text won't be visible even though the value attribute is updated as the user types (issue #37).
|
||||||
|
if ((event.timeStamp - trackingClickStart) > 100 || (this.deviceIsIOS && window.top !== window && targetTagName === 'input')) {
|
||||||
|
this.targetElement = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.focus(targetElement);
|
||||||
|
|
||||||
|
// Select elements need the event to go through on iOS 4, otherwise the selector menu won't open.
|
||||||
|
if (!this.deviceIsIOS4 || targetTagName !== 'select') {
|
||||||
|
this.targetElement = null;
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.deviceIsIOS && !this.deviceIsIOS4) {
|
||||||
|
|
||||||
|
// Don't send a synthetic click event if the target element is contained within a parent layer that was scrolled
|
||||||
|
// and this tap is being used to stop the scrolling (usually initiated by a fling - issue #42).
|
||||||
|
scrollParent = targetElement.fastClickScrollParent;
|
||||||
|
if (scrollParent && scrollParent.fastClickLastScrollTop !== scrollParent.scrollTop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent the actual click from going though - unless the target node is marked as requiring
|
||||||
|
// real clicks or if it is in the whitelist in which case only non-programmatic clicks are permitted.
|
||||||
|
if (!this.needsClick(targetElement)) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.sendClick(targetElement, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On touch cancel, stop tracking the click.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.onTouchCancel = function() {
|
||||||
|
'use strict';
|
||||||
|
this.trackingClick = false;
|
||||||
|
this.targetElement = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine mouse events which should be permitted.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.onMouse = function(event) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// If a target element was never set (because a touch event was never fired) allow the event
|
||||||
|
if (!this.targetElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.forwardedTouchEvent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Programmatically generated events targeting a specific element should be permitted
|
||||||
|
if (!event.cancelable) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Derive and check the target element to see whether the mouse event needs to be permitted;
|
||||||
|
// unless explicitly enabled, prevent non-touch click events from triggering actions,
|
||||||
|
// to prevent ghost/doubleclicks.
|
||||||
|
if (!this.needsClick(this.targetElement) || this.cancelNextClick) {
|
||||||
|
|
||||||
|
// Prevent any user-added listeners declared on FastClick element from being fired.
|
||||||
|
if (event.stopImmediatePropagation) {
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Part of the hack for browsers that don't support Event#stopImmediatePropagation (e.g. Android 2)
|
||||||
|
event.propagationStopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel the event
|
||||||
|
event.stopPropagation();
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the mouse event is permitted, return true for the action to go through.
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On actual clicks, determine whether this is a touch-generated click, a click action occurring
|
||||||
|
* naturally after a delay after a touch (which needs to be cancelled to avoid duplication), or
|
||||||
|
* an actual click which should be permitted.
|
||||||
|
*
|
||||||
|
* @param {Event} event
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.onClick = function(event) {
|
||||||
|
'use strict';
|
||||||
|
var permitted;
|
||||||
|
|
||||||
|
// It's possible for another FastClick-like library delivered with third-party code to fire a click event before FastClick does (issue #44). In that case, set the click-tracking flag back to false and return early. This will cause onTouchEnd to return early.
|
||||||
|
if (this.trackingClick) {
|
||||||
|
this.targetElement = null;
|
||||||
|
this.trackingClick = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Very odd behaviour on iOS (issue #18): if a submit element is present inside a form and the user hits enter in the iOS simulator or clicks the Go button on the pop-up OS keyboard the a kind of 'fake' click event will be triggered with the submit-type input element as the target.
|
||||||
|
if (event.target.type === 'submit' && event.detail === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
permitted = this.onMouse(event);
|
||||||
|
|
||||||
|
// Only unset targetElement if the click is not permitted. This will ensure that the check for !targetElement in onMouse fails and the browser's click doesn't go through.
|
||||||
|
if (!permitted) {
|
||||||
|
this.targetElement = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If clicks are permitted, return true for the action to go through.
|
||||||
|
return permitted;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all FastClick's event listeners.
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
FastClick.prototype.destroy = function() {
|
||||||
|
'use strict';
|
||||||
|
var layer = this.layer;
|
||||||
|
|
||||||
|
if (this.deviceIsAndroid) {
|
||||||
|
layer.removeEventListener('mouseover', this.onMouse, true);
|
||||||
|
layer.removeEventListener('mousedown', this.onMouse, true);
|
||||||
|
layer.removeEventListener('mouseup', this.onMouse, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.removeEventListener('click', this.onClick, true);
|
||||||
|
layer.removeEventListener('touchstart', this.onTouchStart, false);
|
||||||
|
layer.removeEventListener('touchend', this.onTouchEnd, false);
|
||||||
|
layer.removeEventListener('touchcancel', this.onTouchCancel, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether FastClick is needed.
|
||||||
|
*
|
||||||
|
* @param {Element} layer The layer to listen on
|
||||||
|
*/
|
||||||
|
FastClick.notNeeded = function(layer) {
|
||||||
|
'use strict';
|
||||||
|
var metaViewport;
|
||||||
|
|
||||||
|
// Devices that don't support touch don't need FastClick
|
||||||
|
if (typeof window.ontouchstart === 'undefined') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((/Chrome\/[0-9]+/).test(navigator.userAgent)) {
|
||||||
|
|
||||||
|
// Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89)
|
||||||
|
if (FastClick.prototype.deviceIsAndroid) {
|
||||||
|
metaViewport = document.querySelector('meta[name=viewport]');
|
||||||
|
if (metaViewport && metaViewport.content.indexOf('user-scalable=no') !== -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chrome desktop doesn't need FastClick (issue #15)
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IE10 with -ms-touch-action: none, which disables double-tap-to-zoom (issue #97)
|
||||||
|
if (layer.style.msTouchAction === 'none') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method for creating a FastClick object
|
||||||
|
*
|
||||||
|
* @param {Element} layer The layer to listen on
|
||||||
|
*/
|
||||||
|
FastClick.attach = function(layer) {
|
||||||
|
'use strict';
|
||||||
|
return new FastClick(layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
if (typeof define !== 'undefined' && define.amd) {
|
||||||
|
|
||||||
|
// AMD. Register as an anonymous module.
|
||||||
|
define(function() {
|
||||||
|
'use strict';
|
||||||
|
return FastClick;
|
||||||
|
});
|
||||||
|
} else if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = FastClick.attach;
|
||||||
|
module.exports.FastClick = FastClick;
|
||||||
|
} else {
|
||||||
|
window.FastClick = FastClick;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,154 @@
|
|||||||
|
class app.views.Content extends app.View
|
||||||
|
@el: '._content'
|
||||||
|
@loadingClass: '_content-loading'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
altUp: 'scrollStepUp'
|
||||||
|
altDown: 'scrollStepDown'
|
||||||
|
pageUp: 'scrollPageUp'
|
||||||
|
pageDown: 'scrollPageDown'
|
||||||
|
home: 'scrollToTop'
|
||||||
|
end: 'scrollToBottom'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
before: 'beforeRoute'
|
||||||
|
after: 'afterRoute'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@scrollEl = if app.isMobile() then document.body else @el
|
||||||
|
@scrollMap = {}
|
||||||
|
@scrollStack = []
|
||||||
|
|
||||||
|
@rootPage = new app.views.RootPage
|
||||||
|
@staticPage = new app.views.StaticPage
|
||||||
|
@typePage = new app.views.TypePage
|
||||||
|
@entryPage = new app.views.EntryPage
|
||||||
|
|
||||||
|
@entryPage
|
||||||
|
.on('loading', @onEntryLoading)
|
||||||
|
.on('loaded', @onEntryLoaded)
|
||||||
|
|
||||||
|
app
|
||||||
|
.on('ready', @onReady)
|
||||||
|
.on('bootError', @onBootError)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
show: (view) ->
|
||||||
|
unless view is @view
|
||||||
|
@view?.deactivate()
|
||||||
|
@html @view = view
|
||||||
|
@view.activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
showLoading: ->
|
||||||
|
@addClass @constructor.loadingClass
|
||||||
|
return
|
||||||
|
|
||||||
|
hideLoading: ->
|
||||||
|
@removeClass @constructor.loadingClass
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollTo: (value) ->
|
||||||
|
@scrollEl.scrollTop = value or 0
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollBy: (n) ->
|
||||||
|
@scrollEl.scrollTop += n
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollToTop: =>
|
||||||
|
@scrollTo 0
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollToBottom: =>
|
||||||
|
@scrollTo @scrollEl.scrollHeight
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollStepUp: =>
|
||||||
|
@scrollBy -50
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollStepDown: =>
|
||||||
|
@scrollBy 50
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollPageUp: =>
|
||||||
|
@scrollBy 80 - @scrollEl.clientHeight
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollPageDown: =>
|
||||||
|
@scrollBy @scrollEl.clientHeight - 80
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollToTarget: ->
|
||||||
|
if @routeCtx.hash and el = $.id @routeCtx.hash
|
||||||
|
$.scrollToWithImageLock el, @scrollEl, 'top',
|
||||||
|
margin: 20 + if @scrollEl is @el then 0 else $.offset(@el).top
|
||||||
|
$.highlight el, className: '_highlight'
|
||||||
|
else
|
||||||
|
@scrollTo @scrollMap[@routeCtx.state.id]
|
||||||
|
return
|
||||||
|
|
||||||
|
onReady: =>
|
||||||
|
@hideLoading()
|
||||||
|
return
|
||||||
|
|
||||||
|
onBootError: =>
|
||||||
|
@hideLoading()
|
||||||
|
@html @tmpl('bootError')
|
||||||
|
return
|
||||||
|
|
||||||
|
onEntryLoading: =>
|
||||||
|
@showLoading()
|
||||||
|
return
|
||||||
|
|
||||||
|
onEntryLoaded: =>
|
||||||
|
@hideLoading()
|
||||||
|
@scrollToTarget()
|
||||||
|
return
|
||||||
|
|
||||||
|
beforeRoute: (context) =>
|
||||||
|
@cacheScrollPosition()
|
||||||
|
@routeCtx = context
|
||||||
|
@delay @scrollToTarget
|
||||||
|
return
|
||||||
|
|
||||||
|
cacheScrollPosition: ->
|
||||||
|
return if not @routeCtx or @routeCtx.hash
|
||||||
|
|
||||||
|
unless @scrollMap[@routeCtx.state.id]?
|
||||||
|
@scrollStack.push @routeCtx.state.id
|
||||||
|
while @scrollStack.length > app.config.history_cache_size
|
||||||
|
delete @scrollMap[@scrollStack.shift()]
|
||||||
|
|
||||||
|
@scrollMap[@routeCtx.state.id] = @scrollEl.scrollTop
|
||||||
|
return
|
||||||
|
|
||||||
|
afterRoute: (route, context) =>
|
||||||
|
switch route
|
||||||
|
when 'root'
|
||||||
|
@show @rootPage
|
||||||
|
when 'entry'
|
||||||
|
@show @entryPage
|
||||||
|
when 'type'
|
||||||
|
@show @typePage
|
||||||
|
else
|
||||||
|
@show @staticPage
|
||||||
|
|
||||||
|
@view.onRoute(context)
|
||||||
|
app.document.setTitle @view.getTitle?()
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
link = $.closestLink event.target, @el
|
||||||
|
if link and @isExternalUrl link.getAttribute('href')
|
||||||
|
$.stopEvent(event)
|
||||||
|
$.popup(link)
|
||||||
|
return
|
||||||
|
|
||||||
|
isExternalUrl: (url) ->
|
||||||
|
url?[0..3] is 'http'
|
@ -0,0 +1,108 @@
|
|||||||
|
class app.views.EntryPage extends app.View
|
||||||
|
@className: '_page'
|
||||||
|
@loadingClass: '_page-loading'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
before: 'beforeRoute'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@cacheMap = {}
|
||||||
|
@cacheStack = []
|
||||||
|
return
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
@empty()
|
||||||
|
@entry = null
|
||||||
|
return
|
||||||
|
|
||||||
|
loading: ->
|
||||||
|
@empty()
|
||||||
|
@trigger 'loading'
|
||||||
|
return
|
||||||
|
|
||||||
|
render: (content = '') ->
|
||||||
|
@empty()
|
||||||
|
|
||||||
|
@subview = new (@subViewClass()) @el, @entry
|
||||||
|
@subview.render(content)
|
||||||
|
|
||||||
|
if app.disabledDocs.findBy 'slug', @entry.doc.slug
|
||||||
|
@hiddenView = new app.views.HiddenPage @el, @entry
|
||||||
|
|
||||||
|
@trigger 'loaded'
|
||||||
|
return
|
||||||
|
|
||||||
|
empty: ->
|
||||||
|
@subview?.deactivate()
|
||||||
|
@subview = null
|
||||||
|
|
||||||
|
@hiddenView?.deactivate()
|
||||||
|
@hiddenView = null
|
||||||
|
|
||||||
|
@resetClass()
|
||||||
|
super
|
||||||
|
return
|
||||||
|
|
||||||
|
subViewClass: ->
|
||||||
|
docType = @entry.doc.type
|
||||||
|
app.views["#{docType[0].toUpperCase()}#{docType[1..]}Page"] or app.views.BasePage
|
||||||
|
|
||||||
|
getTitle: ->
|
||||||
|
@entry.doc.name + if @entry.isIndex() then '' else "/#{@entry.name}"
|
||||||
|
|
||||||
|
beforeRoute: =>
|
||||||
|
@abort()
|
||||||
|
@cache()
|
||||||
|
return
|
||||||
|
|
||||||
|
onRoute: (context) ->
|
||||||
|
isSameFile = context.entry.filePath() is @entry?.filePath()
|
||||||
|
@entry = context.entry
|
||||||
|
@restore() or @load() unless isSameFile
|
||||||
|
return
|
||||||
|
|
||||||
|
load: ->
|
||||||
|
@loading()
|
||||||
|
@xhr = @entry.loadFile @onSuccess, @onError
|
||||||
|
return
|
||||||
|
|
||||||
|
abort: ->
|
||||||
|
if @xhr
|
||||||
|
@xhr.abort()
|
||||||
|
@xhr = null
|
||||||
|
return
|
||||||
|
|
||||||
|
onSuccess: (response) =>
|
||||||
|
@xhr = null
|
||||||
|
@render response
|
||||||
|
return
|
||||||
|
|
||||||
|
onError: =>
|
||||||
|
@xhr = null
|
||||||
|
@render @tmpl('pageLoadError')
|
||||||
|
return
|
||||||
|
|
||||||
|
cache: ->
|
||||||
|
return if not @entry or @cacheMap[path = @entry.filePath()]
|
||||||
|
|
||||||
|
@cacheMap[path] = @el.innerHTML
|
||||||
|
@cacheStack.push(path)
|
||||||
|
|
||||||
|
while @cacheStack.length > app.config.history_cache_size
|
||||||
|
delete @cacheMap[@cacheStack.shift()]
|
||||||
|
return
|
||||||
|
|
||||||
|
restore: ->
|
||||||
|
if @cacheMap[path = @entry.filePath()]
|
||||||
|
@render @cacheMap[path]
|
||||||
|
true
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target.hasAttribute 'data-retry'
|
||||||
|
$.stopEvent(event)
|
||||||
|
@load()
|
||||||
|
return
|
@ -0,0 +1,34 @@
|
|||||||
|
class app.views.RootPage extends app.View
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@setHidden false unless @isHidden() # reserve space in local storage
|
||||||
|
@render()
|
||||||
|
return
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@empty()
|
||||||
|
@append @tmpl('mobileNav') if app.isMobile()
|
||||||
|
@append @tmpl if @isHidden() then 'splash' else if app.isMobile() then 'mobileIntro' else 'intro'
|
||||||
|
return
|
||||||
|
|
||||||
|
hideIntro: ->
|
||||||
|
@setHidden true
|
||||||
|
@render()
|
||||||
|
return
|
||||||
|
|
||||||
|
setHidden: (value) ->
|
||||||
|
app.store.set 'hideIntro', value
|
||||||
|
return
|
||||||
|
|
||||||
|
isHidden: ->
|
||||||
|
app.doc or app.store.get 'hideIntro'
|
||||||
|
|
||||||
|
onRoute: ->
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target.hasAttribute 'data-hide-intro'
|
||||||
|
$.stopEvent(event)
|
||||||
|
@hideIntro()
|
||||||
|
return
|
@ -0,0 +1,26 @@
|
|||||||
|
class app.views.StaticPage extends app.View
|
||||||
|
@className: '_static'
|
||||||
|
|
||||||
|
@titles:
|
||||||
|
about: 'About'
|
||||||
|
news: 'News'
|
||||||
|
help: 'Help'
|
||||||
|
notFound: '404'
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
@empty()
|
||||||
|
@page = null
|
||||||
|
return
|
||||||
|
|
||||||
|
render: (page) ->
|
||||||
|
@page = page
|
||||||
|
@html @tmpl("#{@page}Page")
|
||||||
|
return
|
||||||
|
|
||||||
|
getTitle: ->
|
||||||
|
@constructor.titles[@page]
|
||||||
|
|
||||||
|
onRoute: (context) ->
|
||||||
|
@render context.page or 'notFound'
|
||||||
|
return
|
@ -0,0 +1,20 @@
|
|||||||
|
class app.views.TypePage extends app.View
|
||||||
|
@className: '_page'
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
@empty()
|
||||||
|
@type = null
|
||||||
|
return
|
||||||
|
|
||||||
|
render: (@type) ->
|
||||||
|
@type = type
|
||||||
|
@html @tmpl('typePage', @type)
|
||||||
|
return
|
||||||
|
|
||||||
|
getTitle: ->
|
||||||
|
"#{@type.doc.name}/#{@type.name}"
|
||||||
|
|
||||||
|
onRoute: (context) ->
|
||||||
|
@render context.type
|
||||||
|
return
|
@ -0,0 +1,32 @@
|
|||||||
|
class app.views.Document extends app.View
|
||||||
|
@el: document
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
help: 'onHelp'
|
||||||
|
escape: 'onEscape'
|
||||||
|
superLeft: 'onBack'
|
||||||
|
superRight: 'onForward'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@addSubview @nav = new app.views.Nav,
|
||||||
|
@addSubview @sidebar = new app.views.Sidebar
|
||||||
|
@addSubview @content = new app.views.Content
|
||||||
|
|
||||||
|
@setTitle()
|
||||||
|
@activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
setTitle: (title) ->
|
||||||
|
@el.title = if title then "DevDocs/#{title}" else 'DevDocs'
|
||||||
|
|
||||||
|
onHelp: ->
|
||||||
|
app.router.show '/help#shortcuts'
|
||||||
|
|
||||||
|
onEscape: ->
|
||||||
|
if app.doc then window.location = '/' else app.router.show '/'
|
||||||
|
|
||||||
|
onBack: ->
|
||||||
|
history.back()
|
||||||
|
|
||||||
|
onForward: ->
|
||||||
|
history.forward()
|
@ -0,0 +1,74 @@
|
|||||||
|
class app.views.Mobile extends app.View
|
||||||
|
@className: '_mobile'
|
||||||
|
|
||||||
|
@elements:
|
||||||
|
body: 'body'
|
||||||
|
content: '._container'
|
||||||
|
sidebar: '._sidebar'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
after: 'afterRoute'
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@el = document.documentElement
|
||||||
|
super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
FastClick.attach @body
|
||||||
|
app.shortcuts.stop()
|
||||||
|
|
||||||
|
$.on @body, 'click', @onClick
|
||||||
|
$.on $('._home-link'), 'click', @onClickHome
|
||||||
|
$.on $('._menu-link'), 'click', @onClickMenu
|
||||||
|
$.on $('._search'), 'touchend', @onTapSearch
|
||||||
|
|
||||||
|
app.document.sidebar.search
|
||||||
|
.on('searching', @showSidebar)
|
||||||
|
.on('clear', @hideSidebar)
|
||||||
|
|
||||||
|
@activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
showSidebar: =>
|
||||||
|
return if @isSidebarShown()
|
||||||
|
@contentTop = @body.scrollTop
|
||||||
|
@content.style.display = 'none'
|
||||||
|
@sidebar.style.display = 'block'
|
||||||
|
|
||||||
|
if selection = @findByClass app.views.ListSelect.activeClass
|
||||||
|
$.scrollTo selection, @body, 'center'
|
||||||
|
else
|
||||||
|
@body.scrollTop = @findByClass(app.views.ListFold.activeClass) and @sidebarTop or 0
|
||||||
|
return
|
||||||
|
|
||||||
|
hideSidebar: =>
|
||||||
|
return unless @isSidebarShown()
|
||||||
|
@sidebarTop = @body.scrollTop
|
||||||
|
@sidebar.style.display = 'none'
|
||||||
|
@content.style.display = 'block'
|
||||||
|
@body.scrollTop = @contentTop or 0
|
||||||
|
return
|
||||||
|
|
||||||
|
isSidebarShown: ->
|
||||||
|
@sidebar.style.display isnt 'none'
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target.hasAttribute 'data-pick-docs'
|
||||||
|
@showSidebar()
|
||||||
|
return
|
||||||
|
|
||||||
|
onClickHome: =>
|
||||||
|
app.shortcuts.trigger 'escape'
|
||||||
|
@hideSidebar()
|
||||||
|
return
|
||||||
|
|
||||||
|
onClickMenu: =>
|
||||||
|
if @isSidebarShown() then @hideSidebar() else @showSidebar()
|
||||||
|
return
|
||||||
|
|
||||||
|
onTapSearch: =>
|
||||||
|
@body.scrollTop = 0
|
||||||
|
|
||||||
|
afterRoute: =>
|
||||||
|
@hideSidebar()
|
||||||
|
return
|
@ -0,0 +1,24 @@
|
|||||||
|
class app.views.Nav extends app.View
|
||||||
|
@el: '._nav'
|
||||||
|
@activeClass: '_nav-current'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
after: 'afterRoute'
|
||||||
|
|
||||||
|
select: (href) ->
|
||||||
|
@deselect()
|
||||||
|
if @current = @find "a[href='#{href}']"
|
||||||
|
@current.classList.add @constructor.activeClass
|
||||||
|
return
|
||||||
|
|
||||||
|
deselect: ->
|
||||||
|
if @current
|
||||||
|
@current.classList.remove @constructor.activeClass
|
||||||
|
@current = null
|
||||||
|
return
|
||||||
|
|
||||||
|
afterRoute: (route, context) =>
|
||||||
|
if route is 'page'
|
||||||
|
@select context.pathname
|
||||||
|
else
|
||||||
|
@deselect()
|
@ -0,0 +1,109 @@
|
|||||||
|
class app.views.ListFocus extends app.View
|
||||||
|
@activeClass: 'focus'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
up: 'onUp'
|
||||||
|
down: 'onDown'
|
||||||
|
left: 'onLeft'
|
||||||
|
enter: 'onEnter'
|
||||||
|
superEnter: 'onSuperEnter'
|
||||||
|
escape: 'blur'
|
||||||
|
|
||||||
|
constructor: (@el) -> super
|
||||||
|
|
||||||
|
focus: (el) ->
|
||||||
|
if el and not el.classList.contains @constructor.activeClass
|
||||||
|
@blur()
|
||||||
|
el.classList.add @constructor.activeClass
|
||||||
|
$.trigger el, 'focus'
|
||||||
|
return
|
||||||
|
|
||||||
|
blur: =>
|
||||||
|
if cursor = @getCursor()
|
||||||
|
cursor.classList.remove @constructor.activeClass
|
||||||
|
$.trigger cursor, 'blur'
|
||||||
|
return
|
||||||
|
|
||||||
|
getCursor: ->
|
||||||
|
@findByClass(@constructor.activeClass) or @findByClass(app.views.ListSelect.activeClass)
|
||||||
|
|
||||||
|
findNext: (cursor) ->
|
||||||
|
if next = cursor.nextSibling
|
||||||
|
if next.tagName is 'A'
|
||||||
|
next
|
||||||
|
else if next.tagName is 'SPAN' # pagination link
|
||||||
|
$.click(next)
|
||||||
|
@findNext cursor
|
||||||
|
else # sub-list
|
||||||
|
@findFirst(next) or @findNext(next)
|
||||||
|
else if cursor.parentElement isnt @el
|
||||||
|
@findNext cursor.parentElement
|
||||||
|
|
||||||
|
findFirst: (cursor) ->
|
||||||
|
return unless first = cursor.firstChild
|
||||||
|
|
||||||
|
if first.tagName is 'A'
|
||||||
|
first
|
||||||
|
else if first.tagName is 'SPAN' # pagination link
|
||||||
|
$.click(first)
|
||||||
|
@findFirst cursor
|
||||||
|
|
||||||
|
findPrev: (cursor) ->
|
||||||
|
if prev = cursor.previousSibling
|
||||||
|
if prev.tagName is 'A'
|
||||||
|
prev
|
||||||
|
else if prev.tagName is 'SPAN' # pagination link
|
||||||
|
$.click(prev)
|
||||||
|
@findPrev cursor
|
||||||
|
else # sub-list
|
||||||
|
@findLast(prev) or @findPrev(prev)
|
||||||
|
else if cursor.parentElement isnt @el
|
||||||
|
@findPrev cursor.parentElement
|
||||||
|
|
||||||
|
findLast: (cursor) ->
|
||||||
|
return unless last = cursor.lastChild
|
||||||
|
|
||||||
|
if last.tagName is 'A'
|
||||||
|
last
|
||||||
|
else if last.tagName is 'SPAN' # pagination link
|
||||||
|
@findPrev last
|
||||||
|
else # sub-list
|
||||||
|
@findLast last
|
||||||
|
|
||||||
|
onDown: =>
|
||||||
|
if cursor = @getCursor()
|
||||||
|
@focus @findNext(cursor)
|
||||||
|
else
|
||||||
|
@focus @findByTag('a')
|
||||||
|
return
|
||||||
|
|
||||||
|
onUp: =>
|
||||||
|
if cursor = @getCursor()
|
||||||
|
@focus @findPrev(cursor)
|
||||||
|
else
|
||||||
|
@focus @findLastByTag('a')
|
||||||
|
return
|
||||||
|
|
||||||
|
onLeft: =>
|
||||||
|
cursor = @getCursor()
|
||||||
|
if cursor and not cursor.classList.contains(app.views.ListFold.activeClass) and cursor.parentElement isnt @el
|
||||||
|
@focus cursor.parentElement.previousSibling
|
||||||
|
return
|
||||||
|
|
||||||
|
onEnter: =>
|
||||||
|
if cursor = @getCursor()
|
||||||
|
$.click(cursor)
|
||||||
|
return
|
||||||
|
|
||||||
|
onSuperEnter: =>
|
||||||
|
if cursor = @getCursor()
|
||||||
|
$.popup(cursor)
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target.tagName is 'A'
|
||||||
|
@focus event.target
|
||||||
|
return
|
@ -0,0 +1,64 @@
|
|||||||
|
class app.views.ListFold extends app.View
|
||||||
|
@targetClass: '_list-dir'
|
||||||
|
@handleClass: '_list-arrow'
|
||||||
|
@activeClass: 'open'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
left: 'onLeft'
|
||||||
|
right: 'onRight'
|
||||||
|
escape: 'reset'
|
||||||
|
|
||||||
|
constructor: (@el) -> super
|
||||||
|
|
||||||
|
open: (el) ->
|
||||||
|
if el and not el.classList.contains @constructor.activeClass
|
||||||
|
el.classList.add @constructor.activeClass
|
||||||
|
$.trigger el, 'open'
|
||||||
|
return
|
||||||
|
|
||||||
|
close: (el) ->
|
||||||
|
if el and el.classList.contains @constructor.activeClass
|
||||||
|
el.classList.remove @constructor.activeClass
|
||||||
|
$.trigger el, 'close'
|
||||||
|
return
|
||||||
|
|
||||||
|
toggle: (el) ->
|
||||||
|
if el.classList.contains @constructor.activeClass
|
||||||
|
@close el
|
||||||
|
else
|
||||||
|
@open el
|
||||||
|
return
|
||||||
|
|
||||||
|
reset: =>
|
||||||
|
while el = @findByClass @constructor.activeClass
|
||||||
|
@close el
|
||||||
|
return
|
||||||
|
|
||||||
|
getCursor: ->
|
||||||
|
@findByClass(app.views.ListFocus.activeClass) or @findByClass(app.views.ListSelect.activeClass)
|
||||||
|
|
||||||
|
onLeft: =>
|
||||||
|
cursor = @getCursor()
|
||||||
|
if cursor?.classList.contains @constructor.activeClass
|
||||||
|
@close cursor
|
||||||
|
return
|
||||||
|
|
||||||
|
onRight: =>
|
||||||
|
cursor = @getCursor()
|
||||||
|
if cursor?.classList.contains @constructor.targetClass
|
||||||
|
@open cursor
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
return unless event.pageY # ignore fabricated clicks
|
||||||
|
el = event.target
|
||||||
|
|
||||||
|
if el.classList.contains @constructor.handleClass
|
||||||
|
$.stopEvent(event)
|
||||||
|
@toggle el.parentElement
|
||||||
|
else if el.classList.contains @constructor.targetClass
|
||||||
|
@open el
|
||||||
|
return
|
@ -0,0 +1,41 @@
|
|||||||
|
class app.views.ListSelect extends app.View
|
||||||
|
@activeClass: 'active'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
constructor: (@el) -> super
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
@deselect() if super
|
||||||
|
return
|
||||||
|
|
||||||
|
select: (el) ->
|
||||||
|
@deselect()
|
||||||
|
if el
|
||||||
|
el.classList.add @constructor.activeClass
|
||||||
|
$.trigger el, 'select'
|
||||||
|
return
|
||||||
|
|
||||||
|
deselect: ->
|
||||||
|
if selection = @getSelection()
|
||||||
|
selection.classList.remove @constructor.activeClass
|
||||||
|
$.trigger selection, 'deselect'
|
||||||
|
return
|
||||||
|
|
||||||
|
selectByHref: (href) ->
|
||||||
|
unless @getSelection()?.getAttribute('href') is href
|
||||||
|
@select @find("a[href='#{href}']")
|
||||||
|
return
|
||||||
|
|
||||||
|
selectCurrent: ->
|
||||||
|
@selectByHref location.pathname + location.hash
|
||||||
|
return
|
||||||
|
|
||||||
|
getSelection: ->
|
||||||
|
@findByClass @constructor.activeClass
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target.tagName is 'A'
|
||||||
|
@select event.target
|
||||||
|
return
|
@ -0,0 +1,89 @@
|
|||||||
|
class app.views.PaginatedList extends app.View
|
||||||
|
PER_PAGE = app.config.max_results
|
||||||
|
|
||||||
|
constructor: (@data) ->
|
||||||
|
(@constructor.events or= {}).click ?= 'onClick'
|
||||||
|
super
|
||||||
|
|
||||||
|
renderPaginated: ->
|
||||||
|
@page = 0
|
||||||
|
|
||||||
|
if @totalPages() > 1
|
||||||
|
@paginateNext()
|
||||||
|
else
|
||||||
|
@html @renderAll()
|
||||||
|
return
|
||||||
|
|
||||||
|
# render: (dataSlice) -> implemented by subclass
|
||||||
|
|
||||||
|
renderAll: ->
|
||||||
|
@render @data
|
||||||
|
|
||||||
|
renderPage: (page) ->
|
||||||
|
@render @data[((page - 1) * PER_PAGE)...(page * PER_PAGE)]
|
||||||
|
|
||||||
|
renderPageLink: (count) ->
|
||||||
|
@tmpl 'sidebarPageLink', count
|
||||||
|
|
||||||
|
renderPrevLink: (page) ->
|
||||||
|
@renderPageLink (page - 1) * PER_PAGE
|
||||||
|
|
||||||
|
renderNextLink: (page) ->
|
||||||
|
@renderPageLink @data.length - page * PER_PAGE
|
||||||
|
|
||||||
|
totalPages: ->
|
||||||
|
Math.ceil @data.length / PER_PAGE
|
||||||
|
|
||||||
|
paginate: (link) ->
|
||||||
|
$.lockScroll link.nextSibling or link.previousSibling, =>
|
||||||
|
$.batchUpdate @el, =>
|
||||||
|
if link.nextSibling then @paginatePrev link else @paginateNext link
|
||||||
|
return
|
||||||
|
return
|
||||||
|
return
|
||||||
|
|
||||||
|
paginateNext: ->
|
||||||
|
@remove @el.lastChild if @el.lastChild # remove link
|
||||||
|
@hideTopPage() if @page >= 2 # keep previous page into view
|
||||||
|
@page++
|
||||||
|
@append @renderPage(@page)
|
||||||
|
@append @renderNextLink(@page) if @page < @totalPages()
|
||||||
|
return
|
||||||
|
|
||||||
|
paginatePrev: ->
|
||||||
|
@remove @el.firstChild # remove link
|
||||||
|
@hideBottomPage()
|
||||||
|
@page--
|
||||||
|
@prepend @renderPage(@page - 1) # previous page is offset by one
|
||||||
|
@prepend @renderPrevLink(@page - 1) if @page >= 3
|
||||||
|
return
|
||||||
|
|
||||||
|
paginateTo: (object) ->
|
||||||
|
index = @data.indexOf(object)
|
||||||
|
if index >= PER_PAGE
|
||||||
|
@paginateNext() for [0...Math.floor(index / PER_PAGE)]
|
||||||
|
return
|
||||||
|
|
||||||
|
hideTopPage: ->
|
||||||
|
n = if @page <= 2
|
||||||
|
PER_PAGE
|
||||||
|
else
|
||||||
|
PER_PAGE + 1 # remove link
|
||||||
|
@remove @el.firstChild for [0...n]
|
||||||
|
@prepend @renderPrevLink(@page)
|
||||||
|
return
|
||||||
|
|
||||||
|
hideBottomPage: ->
|
||||||
|
n = if @page is @totalPages()
|
||||||
|
@data.length % PER_PAGE or PER_PAGE
|
||||||
|
else
|
||||||
|
PER_PAGE + 1 # remove link
|
||||||
|
@remove @el.lastChild for [0...n]
|
||||||
|
@append @renderNextLink(@page - 1)
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target.tagName is 'SPAN' # link
|
||||||
|
$.stopEvent(event)
|
||||||
|
@paginate event.target
|
||||||
|
return
|
@ -0,0 +1,34 @@
|
|||||||
|
#= require views/misc/notif
|
||||||
|
|
||||||
|
class app.views.News extends app.views.Notif
|
||||||
|
@className += ' _notif-news'
|
||||||
|
|
||||||
|
@defautOptions:
|
||||||
|
autoHide: null
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@unreadNews = @getUnreadNews()
|
||||||
|
@show() if @unreadNews.length
|
||||||
|
@markAllAsRead()
|
||||||
|
return
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html app.templates.notifNews(@unreadNews)
|
||||||
|
return
|
||||||
|
|
||||||
|
getUnreadNews: ->
|
||||||
|
return [] unless time = @getLastReadTime()
|
||||||
|
|
||||||
|
for news in app.news
|
||||||
|
break if news[0] <= time
|
||||||
|
news
|
||||||
|
|
||||||
|
getLastNewsTime: ->
|
||||||
|
app.news[0][0]
|
||||||
|
|
||||||
|
getLastReadTime: ->
|
||||||
|
app.store.get 'news'
|
||||||
|
|
||||||
|
markAllAsRead: ->
|
||||||
|
app.store.set 'news', @getLastNewsTime()
|
||||||
|
return
|
@ -0,0 +1,25 @@
|
|||||||
|
class app.views.Notice extends app.View
|
||||||
|
@className: '_notice'
|
||||||
|
|
||||||
|
constructor: (@type, @args...) -> super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
activate: ->
|
||||||
|
@show() if super
|
||||||
|
return
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
@hide() if super
|
||||||
|
return
|
||||||
|
|
||||||
|
show: ->
|
||||||
|
@html @tmpl("#{@type}Notice", @args...)
|
||||||
|
@prependTo $('._app')
|
||||||
|
return
|
||||||
|
|
||||||
|
hide: ->
|
||||||
|
$.remove @el
|
||||||
|
return
|
@ -0,0 +1,54 @@
|
|||||||
|
class app.views.Notif extends app.View
|
||||||
|
@className: '_notif'
|
||||||
|
@activeClass: '_in'
|
||||||
|
|
||||||
|
@defautOptions:
|
||||||
|
autoHide: 15000
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
constructor: (@type, @options = {}) ->
|
||||||
|
@options = $.extend {}, @constructor.defautOptions, @options
|
||||||
|
super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@show()
|
||||||
|
return
|
||||||
|
|
||||||
|
show: ->
|
||||||
|
if @timeout
|
||||||
|
clearTimeout @timeout
|
||||||
|
@timeout = @delay @hide, @options.autoHide
|
||||||
|
else
|
||||||
|
@render()
|
||||||
|
@position()
|
||||||
|
@activate()
|
||||||
|
@appendTo document.body
|
||||||
|
@el.offsetWidth # force reflow
|
||||||
|
@addClass @constructor.activeClass
|
||||||
|
@timeout = @delay @hide, @options.autoHide if @options.autoHide
|
||||||
|
return
|
||||||
|
|
||||||
|
hide: ->
|
||||||
|
clearTimeout @timeout
|
||||||
|
@timeout = null
|
||||||
|
@detach()
|
||||||
|
return
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html @tmpl("notif#{@type}")
|
||||||
|
return
|
||||||
|
|
||||||
|
position: ->
|
||||||
|
notifications = $$ ".#{@constructor.className}"
|
||||||
|
if notifications.length
|
||||||
|
lastNotif = notifications[notifications.length - 1]
|
||||||
|
@el.style.top = lastNotif.offsetTop + lastNotif.offsetHeight + 16 + 'px'
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
unless event.target.tagName is 'A'
|
||||||
|
$.stopEvent(event)
|
||||||
|
@hide()
|
||||||
|
return
|
@ -0,0 +1,6 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.AngularPage extends app.views.BasePage
|
||||||
|
afterRender: ->
|
||||||
|
@highlightCode @findAllByClass('prettyprint'), 'javascript'
|
||||||
|
return
|
@ -0,0 +1,17 @@
|
|||||||
|
class app.views.BasePage extends app.View
|
||||||
|
constructor: (@el, @entry) -> super
|
||||||
|
|
||||||
|
render: (content) ->
|
||||||
|
@addClass "_#{@entry.doc.type}" unless @constructor.className
|
||||||
|
@html content
|
||||||
|
@activate()
|
||||||
|
@delay @afterRender if @afterRender
|
||||||
|
return
|
||||||
|
|
||||||
|
highlightCode: (el, language) ->
|
||||||
|
if $.isCollection(el)
|
||||||
|
@highlightCode e, language for e in el
|
||||||
|
else if el
|
||||||
|
el.classList.add "language-#{language}"
|
||||||
|
Prism.highlightElement(el)
|
||||||
|
return
|
@ -0,0 +1,7 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.CoffeescriptPage extends app.views.BasePage
|
||||||
|
afterRender: ->
|
||||||
|
@highlightCode @findAll('.code > pre:first-child'), 'coffeescript'
|
||||||
|
@highlightCode @findAll('.code > pre:last-child'), 'javascript'
|
||||||
|
return
|
@ -0,0 +1,11 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.EmberPage extends app.views.BasePage
|
||||||
|
afterRender: ->
|
||||||
|
for el in @findAllByTag 'pre'
|
||||||
|
language = if el.classList.contains 'javascript'
|
||||||
|
'javascript'
|
||||||
|
else if el.classList.contains 'html'
|
||||||
|
'markup'
|
||||||
|
@highlightCode el, language if language
|
||||||
|
return
|
@ -0,0 +1,16 @@
|
|||||||
|
class app.views.HiddenPage extends app.View
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
constructor: (@el, @entry) -> super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@addSubview @notice = new app.views.Notice 'disabledDoc'
|
||||||
|
@activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if link = $.closestLink(event.target, @el)
|
||||||
|
$.stopEvent(event)
|
||||||
|
$.popup(link)
|
||||||
|
return
|
@ -0,0 +1,61 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.JqueryPage extends app.views.BasePage
|
||||||
|
@demoClassName: '_jquery-demo'
|
||||||
|
|
||||||
|
afterRender: ->
|
||||||
|
# Prevent jQuery Mobile's demo iframes from scrolling the page
|
||||||
|
for iframe in @findAllByTag 'iframe'
|
||||||
|
iframe.style.display = 'none'
|
||||||
|
$.on iframe, 'load', @onIframeLoaded
|
||||||
|
|
||||||
|
@runExamples()
|
||||||
|
|
||||||
|
for el in @findAllByClass 'syntaxhighlighter'
|
||||||
|
language = if el.classList.contains('javascript') then 'javascript' else 'markup'
|
||||||
|
@highlightCode el, language
|
||||||
|
return
|
||||||
|
|
||||||
|
onIframeLoaded: (event) =>
|
||||||
|
event.target.style.display = ''
|
||||||
|
$.off event.target, 'load', @onIframeLoaded
|
||||||
|
return
|
||||||
|
|
||||||
|
runExamples: ->
|
||||||
|
for el in @findAllByClass 'entry-example'
|
||||||
|
try @runExample el catch
|
||||||
|
return
|
||||||
|
|
||||||
|
runExample: (el) ->
|
||||||
|
source = el.getElementsByClassName('syntaxhighlighter')[0]
|
||||||
|
return unless source and source.innerHTML.indexOf('!doctype') isnt -1
|
||||||
|
|
||||||
|
unless iframe = el.getElementsByClassName(@constructor.demoClassName)[0]
|
||||||
|
iframe = document.createElement 'iframe'
|
||||||
|
iframe.className = @constructor.demoClassName
|
||||||
|
iframe.width = '100%'
|
||||||
|
iframe.height = 200
|
||||||
|
el.appendChild(iframe)
|
||||||
|
|
||||||
|
doc = iframe.contentDocument
|
||||||
|
doc.write @fixIframeSource(source.textContent)
|
||||||
|
doc.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
fixIframeSource: (source) ->
|
||||||
|
source = source.replace '"/resources/', '"http://api.jquery.com/resources/' # attr(), keydown()
|
||||||
|
source.replace '</head>', """
|
||||||
|
<style>
|
||||||
|
html, body { border: 0; margin: 0; padding: 0; }
|
||||||
|
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
$.ajaxPrefilter(function(opt, opt2, xhr) {
|
||||||
|
if (opt.url.indexOf('http') !== 0) {
|
||||||
|
xhr.abort();
|
||||||
|
document.body.innerHTML = "<p><strong>This demo cannot run inside DevDocs.</strong></p>";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
"""
|
@ -0,0 +1,4 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
#= require views/pages/underscore
|
||||||
|
|
||||||
|
app.views.LodashPage = app.views.UnderscorePage
|
@ -0,0 +1,15 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.MdnPage extends app.views.BasePage
|
||||||
|
@className: '_mdn'
|
||||||
|
|
||||||
|
LANGUAGE_REGEXP = /brush: ?(\w+)/
|
||||||
|
|
||||||
|
afterRender: ->
|
||||||
|
for el in @findAll 'pre[class^="brush"]'
|
||||||
|
language = el.className.match(LANGUAGE_REGEXP)[1]
|
||||||
|
.replace('html', 'markup')
|
||||||
|
.replace('js', 'javascript')
|
||||||
|
el.className = ''
|
||||||
|
@highlightCode el, language
|
||||||
|
return
|
@ -0,0 +1,6 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.NodePage extends app.views.BasePage
|
||||||
|
afterRender: ->
|
||||||
|
@highlightCode @findAll('pre > code'), 'javascript'
|
||||||
|
return
|
@ -0,0 +1,6 @@
|
|||||||
|
#= require views/pages/base
|
||||||
|
|
||||||
|
class app.views.UnderscorePage extends app.views.BasePage
|
||||||
|
afterRender: ->
|
||||||
|
@highlightCode @findAllByTag('pre'), 'javascript'
|
||||||
|
return
|
@ -0,0 +1,113 @@
|
|||||||
|
class app.views.Search extends app.View
|
||||||
|
SEARCH_PARAM = app.config.search_param
|
||||||
|
|
||||||
|
@el: '._search'
|
||||||
|
@activeClass: '_search-active'
|
||||||
|
|
||||||
|
@elements:
|
||||||
|
input: '._search-input'
|
||||||
|
resetLink: '._search-clear'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
input: 'onInput'
|
||||||
|
click: 'onClick'
|
||||||
|
submit: 'onSubmit'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
typing: 'autoFocus'
|
||||||
|
escape: 'reset'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
root: 'onRoot'
|
||||||
|
after: 'autoFocus'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@addSubview @scope = new app.views.SearchScope @el
|
||||||
|
|
||||||
|
@searcher = new app.Searcher
|
||||||
|
@searcher.on 'results', @onResults
|
||||||
|
|
||||||
|
app.on 'ready', @onReady
|
||||||
|
$.on window, 'hashchange', @searchUrl
|
||||||
|
$.on window, 'focus', @autoFocus
|
||||||
|
return
|
||||||
|
|
||||||
|
focus: ->
|
||||||
|
@input.focus() unless document.activeElement is @input
|
||||||
|
return
|
||||||
|
|
||||||
|
autoFocus: =>
|
||||||
|
@focus() unless $.isTouchScreen()
|
||||||
|
return
|
||||||
|
|
||||||
|
reset: =>
|
||||||
|
@el.reset()
|
||||||
|
@onInput()
|
||||||
|
@autoFocus()
|
||||||
|
return
|
||||||
|
|
||||||
|
onReady: =>
|
||||||
|
@value = ''
|
||||||
|
@delay @onInput
|
||||||
|
return
|
||||||
|
|
||||||
|
onInput: =>
|
||||||
|
return if not @value? or # ignore events pre-"ready"
|
||||||
|
@value is @input.value
|
||||||
|
@value = @input.value
|
||||||
|
|
||||||
|
if @value.length
|
||||||
|
@search()
|
||||||
|
else
|
||||||
|
@clear()
|
||||||
|
return
|
||||||
|
|
||||||
|
search: (url = false) ->
|
||||||
|
@addClass @constructor.activeClass
|
||||||
|
@trigger 'searching'
|
||||||
|
|
||||||
|
@flags = urlSearch: url, initialResults: true
|
||||||
|
@searcher.find @scope.getScope().entries.all(), 'text', @value
|
||||||
|
return
|
||||||
|
|
||||||
|
searchUrl: =>
|
||||||
|
return unless app.router.isRoot()
|
||||||
|
@scope.searchUrl()
|
||||||
|
|
||||||
|
return unless value = @extractHashValue()
|
||||||
|
@input.value = @value = value
|
||||||
|
@search true
|
||||||
|
true
|
||||||
|
|
||||||
|
clear: ->
|
||||||
|
@removeClass @constructor.activeClass
|
||||||
|
@trigger 'clear'
|
||||||
|
|
||||||
|
onResults: (results) =>
|
||||||
|
@trigger 'results', results, @flags
|
||||||
|
@flags.initialResults = false
|
||||||
|
return
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target is @resetLink
|
||||||
|
$.stopEvent(event)
|
||||||
|
@reset()
|
||||||
|
@focus()
|
||||||
|
return
|
||||||
|
|
||||||
|
onSubmit: (event) ->
|
||||||
|
$.stopEvent(event)
|
||||||
|
return
|
||||||
|
|
||||||
|
onRoot: (context) =>
|
||||||
|
@reset() unless context.init
|
||||||
|
@delay @searchUrl if context.hash
|
||||||
|
return
|
||||||
|
|
||||||
|
extractHashValue: ->
|
||||||
|
if (value = @getHashValue())?
|
||||||
|
app.router.replaceHash()
|
||||||
|
value
|
||||||
|
|
||||||
|
getHashValue: ->
|
||||||
|
try (new RegExp "##{SEARCH_PARAM}=(.*)").exec(decodeURIComponent location.hash)?[1] catch
|
@ -0,0 +1,84 @@
|
|||||||
|
class app.views.SearchScope extends app.View
|
||||||
|
SEARCH_PARAM = app.config.search_param
|
||||||
|
|
||||||
|
@elements:
|
||||||
|
input: '._search-input'
|
||||||
|
tag: '._search-tag'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
keydown: 'onKeydown'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
escape: 'reset'
|
||||||
|
|
||||||
|
constructor: (@el) -> super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@placeholder = @input.getAttribute 'placeholder'
|
||||||
|
|
||||||
|
@searcher = new app.SynchronousSearcher
|
||||||
|
fuzzy_min_length: 2
|
||||||
|
max_results: 1
|
||||||
|
@searcher.on 'results', @onResults
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
getScope: ->
|
||||||
|
@doc or app
|
||||||
|
|
||||||
|
search: (value) ->
|
||||||
|
unless @doc
|
||||||
|
@searcher.find app.docs.all(), 'slug', value
|
||||||
|
return
|
||||||
|
|
||||||
|
searchUrl: ->
|
||||||
|
if value = @extractHashValue()
|
||||||
|
@search value
|
||||||
|
return
|
||||||
|
|
||||||
|
onResults: (results) =>
|
||||||
|
if results.length
|
||||||
|
@selectDoc results[0]
|
||||||
|
return
|
||||||
|
|
||||||
|
selectDoc: (doc) ->
|
||||||
|
@doc = doc
|
||||||
|
|
||||||
|
@tag.textContent = doc.name
|
||||||
|
@tag.style.display = 'block'
|
||||||
|
|
||||||
|
@input.removeAttribute 'placeholder'
|
||||||
|
@input.value = @input.value[@input.selectionStart..]
|
||||||
|
@input.style.paddingLeft = @tag.offsetWidth + 6 + 'px'
|
||||||
|
$.trigger @input, 'input'
|
||||||
|
|
||||||
|
reset: =>
|
||||||
|
@doc = null
|
||||||
|
|
||||||
|
@tag.textContent = ''
|
||||||
|
@tag.style.display = 'none'
|
||||||
|
|
||||||
|
@input.setAttribute 'placeholder', @placeholder
|
||||||
|
@input.style.paddingLeft = ''
|
||||||
|
|
||||||
|
onKeydown: (event) =>
|
||||||
|
return if event.ctrlKey or event.metaKey or event.altKey or event.shiftKey
|
||||||
|
|
||||||
|
if event.which is 8 # backspace
|
||||||
|
if @doc and not @input.value
|
||||||
|
$.stopEvent(event)
|
||||||
|
@reset()
|
||||||
|
else if event.which is 9 or # tab
|
||||||
|
event.which is 32 and (app.isMobile() or $.isTouchScreen()) # space
|
||||||
|
$.stopEvent(event)
|
||||||
|
@search @input.value[0...@input.selectionStart]
|
||||||
|
return
|
||||||
|
|
||||||
|
extractHashValue: ->
|
||||||
|
if value = @getHashValue()
|
||||||
|
newHash = decodeURIComponent(location.hash).replace "##{SEARCH_PARAM}=#{value} ", "##{SEARCH_PARAM}="
|
||||||
|
app.router.replaceHash(newHash)
|
||||||
|
value
|
||||||
|
|
||||||
|
getHashValue: ->
|
||||||
|
try (new RegExp "^##{SEARCH_PARAM}=(.+?) .").exec(decodeURIComponent location.hash)?[1] catch
|
@ -0,0 +1,91 @@
|
|||||||
|
class app.views.DocList extends app.View
|
||||||
|
@className: '_list'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
open: 'onOpen'
|
||||||
|
close: 'onClose'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
after: 'afterRoute'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@lists = {}
|
||||||
|
|
||||||
|
@addSubview @listSelect = new app.views.ListSelect @el
|
||||||
|
@addSubview @listFocus = new app.views.ListFocus @el unless $.isTouchScreen()
|
||||||
|
@addSubview @listFold = new app.views.ListFold @el
|
||||||
|
|
||||||
|
app.on 'ready', @render
|
||||||
|
return
|
||||||
|
|
||||||
|
activate: ->
|
||||||
|
if super
|
||||||
|
list.activate() for slug, list of @lists
|
||||||
|
@listSelect.selectCurrent()
|
||||||
|
return
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
list.deactivate() for slug, list of @lists
|
||||||
|
return
|
||||||
|
|
||||||
|
render: =>
|
||||||
|
@html @tmpl('sidebarDoc', app.docs.all())
|
||||||
|
unless app.doc or app.settings.hasDocs()
|
||||||
|
@append @tmpl('sidebarDoc', app.disabledDocs.all(), disabled: true)
|
||||||
|
return
|
||||||
|
|
||||||
|
onOpen: (event) =>
|
||||||
|
$.stopEvent(event)
|
||||||
|
doc = app.docs.findBy 'slug', event.target.getAttribute('data-slug')
|
||||||
|
|
||||||
|
if doc and not @lists[doc.slug]
|
||||||
|
@lists[doc.slug] = if doc.types.isEmpty()
|
||||||
|
new app.views.EntryList doc.entries.all()
|
||||||
|
else
|
||||||
|
new app.views.TypeList doc
|
||||||
|
$.after event.target, @lists[doc.slug].el
|
||||||
|
return
|
||||||
|
|
||||||
|
onClose: (event) =>
|
||||||
|
$.stopEvent(event)
|
||||||
|
doc = app.docs.findBy 'slug', event.target.getAttribute('data-slug')
|
||||||
|
|
||||||
|
if doc and @lists[doc.slug]
|
||||||
|
@lists[doc.slug].detach()
|
||||||
|
delete @lists[doc.slug]
|
||||||
|
return
|
||||||
|
|
||||||
|
revealType: (type) ->
|
||||||
|
@openDoc type.doc
|
||||||
|
return
|
||||||
|
|
||||||
|
revealEntry: (entry) ->
|
||||||
|
@openDoc entry.doc
|
||||||
|
@openType entry.getType() if entry.type
|
||||||
|
@lists[entry.doc.slug]?.revealEntry(entry)
|
||||||
|
return
|
||||||
|
|
||||||
|
openDoc: (doc) ->
|
||||||
|
@listFold.open @find("[data-slug='#{doc.slug}']")
|
||||||
|
return
|
||||||
|
|
||||||
|
openType: (type) ->
|
||||||
|
@listFold.open @lists[type.doc.slug].find("[data-slug='#{type.slug}']")
|
||||||
|
return
|
||||||
|
|
||||||
|
afterRoute: (route, context) =>
|
||||||
|
if context.init
|
||||||
|
switch route
|
||||||
|
when 'type' then @revealType context.type
|
||||||
|
when 'entry' then @revealEntry context.entry
|
||||||
|
|
||||||
|
if route in ['type', 'entry']
|
||||||
|
@listSelect.selectByHref (context.type or context.entry).fullPath()
|
||||||
|
else
|
||||||
|
@listSelect.deselect()
|
||||||
|
|
||||||
|
if context.init
|
||||||
|
$.scrollTo @listSelect.getSelection()
|
||||||
|
|
||||||
|
return
|
@ -0,0 +1,69 @@
|
|||||||
|
class app.views.DocPicker extends app.View
|
||||||
|
@className: '_list'
|
||||||
|
|
||||||
|
@elements:
|
||||||
|
saveLink: '._sidebar-footer-save'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
click: 'onClick'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
enter: 'onEnter'
|
||||||
|
|
||||||
|
activate: ->
|
||||||
|
if super
|
||||||
|
@render()
|
||||||
|
app.appCache?.on 'progress', @onAppCacheProgress
|
||||||
|
return
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
@empty()
|
||||||
|
app.appCache?.off 'progress', @onAppCacheProgress
|
||||||
|
return
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html @tmpl('sidebarLabel', app.docs.all(), checked: true) +
|
||||||
|
@tmpl('sidebarLabel', app.disabledDocs.all()) +
|
||||||
|
@tmpl('sidebarVote') +
|
||||||
|
@tmpl('sidebarSave')
|
||||||
|
|
||||||
|
@refreshElements()
|
||||||
|
|
||||||
|
@delay -> # trigger animation
|
||||||
|
@el.offsetWidth
|
||||||
|
@addClass '_in'
|
||||||
|
return
|
||||||
|
|
||||||
|
empty: ->
|
||||||
|
@resetClass()
|
||||||
|
super
|
||||||
|
return
|
||||||
|
|
||||||
|
save: ->
|
||||||
|
unless @saving
|
||||||
|
@saving = true
|
||||||
|
app.settings.setDocs @getSelectedDocs()
|
||||||
|
@saveLink.textContent = if app.appCache then 'Downloading…' else 'Saving…'
|
||||||
|
app.reload()
|
||||||
|
return
|
||||||
|
|
||||||
|
getSelectedDocs: ->
|
||||||
|
for input in @findAllByTag 'input' when input?.checked
|
||||||
|
input.name
|
||||||
|
|
||||||
|
onClick: (event) =>
|
||||||
|
if event.target is @saveLink
|
||||||
|
$.stopEvent(event)
|
||||||
|
@save()
|
||||||
|
return
|
||||||
|
|
||||||
|
onEnter: =>
|
||||||
|
@save()
|
||||||
|
return
|
||||||
|
|
||||||
|
onAppCacheProgress: (event) =>
|
||||||
|
if event.lengthComputable
|
||||||
|
percentage = Math.round event.loaded * 100 / event.total
|
||||||
|
@saveLink.textContent = "Downloading… (#{percentage}%)"
|
||||||
|
return
|
@ -0,0 +1,19 @@
|
|||||||
|
#= require views/list/paginated_list
|
||||||
|
|
||||||
|
class app.views.EntryList extends app.views.PaginatedList
|
||||||
|
@tagName: 'div'
|
||||||
|
@className: '_list _list-sub'
|
||||||
|
|
||||||
|
constructor: (@entries) -> super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@renderPaginated()
|
||||||
|
@activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
render: (entries) ->
|
||||||
|
@tmpl 'sidebarEntry', entries
|
||||||
|
|
||||||
|
revealEntry: (entry) ->
|
||||||
|
@paginateTo entry
|
||||||
|
return
|
@ -0,0 +1,48 @@
|
|||||||
|
class app.views.Results extends app.View
|
||||||
|
@className: '_list'
|
||||||
|
|
||||||
|
@routes:
|
||||||
|
after: 'afterRoute'
|
||||||
|
|
||||||
|
constructor: (@search) -> super
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
@empty()
|
||||||
|
return
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@addSubview @listSelect = new app.views.ListSelect @el
|
||||||
|
@addSubview @listFocus = new app.views.ListFocus @el unless $.isTouchScreen()
|
||||||
|
|
||||||
|
@search
|
||||||
|
.on('results', @onResults)
|
||||||
|
.on('clear', @onClear)
|
||||||
|
return
|
||||||
|
|
||||||
|
onResults: (entries, flags) =>
|
||||||
|
@empty() if flags.initialResults
|
||||||
|
@append @tmpl('sidebarResult', entries)
|
||||||
|
|
||||||
|
if flags.initialResults
|
||||||
|
if flags.urlSearch then @openFirst() else @focusFirst()
|
||||||
|
return
|
||||||
|
|
||||||
|
onClear: =>
|
||||||
|
@empty()
|
||||||
|
return
|
||||||
|
|
||||||
|
focusFirst: ->
|
||||||
|
@listFocus?.focus @el.firstChild
|
||||||
|
return
|
||||||
|
|
||||||
|
openFirst: ->
|
||||||
|
@el.firstChild?.click()
|
||||||
|
return
|
||||||
|
|
||||||
|
afterRoute: (route, context) =>
|
||||||
|
if route is 'entry'
|
||||||
|
@listSelect.selectByHref context.entry.fullPath()
|
||||||
|
else
|
||||||
|
@listSelect.deselect()
|
||||||
|
return
|
@ -0,0 +1,79 @@
|
|||||||
|
class app.views.Sidebar extends app.View
|
||||||
|
@el: '._sidebar'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
focus: 'onFocus'
|
||||||
|
|
||||||
|
@shortcuts:
|
||||||
|
escape: 'onEscape'
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@addSubview @search = new app.views.Search
|
||||||
|
|
||||||
|
@search
|
||||||
|
.on('searching', @showResults)
|
||||||
|
.on('clear', @showDocList)
|
||||||
|
|
||||||
|
@results = new app.views.Results @search
|
||||||
|
@docList = new app.views.DocList
|
||||||
|
@docPicker = new app.views.DocPicker unless app.doc
|
||||||
|
|
||||||
|
app.on 'ready', @showDocList
|
||||||
|
$.on document, 'click', @onGlobalClick if @docPicker
|
||||||
|
return
|
||||||
|
|
||||||
|
show: (view) ->
|
||||||
|
unless @view is view
|
||||||
|
@saveScrollPosition()
|
||||||
|
@view?.deactivate()
|
||||||
|
@html @view = view
|
||||||
|
@append @tmpl('sidebarSettings') if @view is @docList and @docPicker
|
||||||
|
@view.activate()
|
||||||
|
@restoreScrollPosition()
|
||||||
|
return
|
||||||
|
|
||||||
|
showDocList: =>
|
||||||
|
@show @docList
|
||||||
|
return
|
||||||
|
|
||||||
|
showDocPicker: =>
|
||||||
|
@show @docPicker
|
||||||
|
return
|
||||||
|
|
||||||
|
showResults: =>
|
||||||
|
@show @results
|
||||||
|
return
|
||||||
|
|
||||||
|
saveScrollPosition: ->
|
||||||
|
if @view is @docList
|
||||||
|
@scrollTop = @el.scrollTop
|
||||||
|
return
|
||||||
|
|
||||||
|
restoreScrollPosition: ->
|
||||||
|
if @view is @docList and @scrollTop
|
||||||
|
@el.scrollTop = @scrollTop
|
||||||
|
@scrollTop = null
|
||||||
|
else
|
||||||
|
@scrollToTop()
|
||||||
|
return
|
||||||
|
|
||||||
|
scrollToTop: ->
|
||||||
|
@el.scrollTop = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
onFocus: (event) =>
|
||||||
|
$.scrollTo event.target, @el, 'continuous', bottomGap: 2
|
||||||
|
return
|
||||||
|
|
||||||
|
onEscape: =>
|
||||||
|
@showDocList()
|
||||||
|
@scrollToTop()
|
||||||
|
return
|
||||||
|
|
||||||
|
onGlobalClick: (event) =>
|
||||||
|
if event.target.hasAttribute? 'data-pick-docs'
|
||||||
|
$.stopEvent(event)
|
||||||
|
@showDocPicker()
|
||||||
|
else if @view is @docPicker
|
||||||
|
@showDocList() unless $.hasChild @el, event.target
|
||||||
|
return
|
@ -0,0 +1,51 @@
|
|||||||
|
class app.views.TypeList extends app.View
|
||||||
|
@tagName: 'div'
|
||||||
|
@className: '_list _list-sub'
|
||||||
|
|
||||||
|
@events:
|
||||||
|
open: 'onOpen'
|
||||||
|
close: 'onClose'
|
||||||
|
|
||||||
|
constructor: (@doc) -> super
|
||||||
|
|
||||||
|
init: ->
|
||||||
|
@lists = {}
|
||||||
|
@render()
|
||||||
|
@activate()
|
||||||
|
return
|
||||||
|
|
||||||
|
activate: ->
|
||||||
|
if super
|
||||||
|
list.activate() for slug, list of @lists
|
||||||
|
return
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
if super
|
||||||
|
list.deactivate() for slug, list of @lists
|
||||||
|
return
|
||||||
|
|
||||||
|
render: ->
|
||||||
|
@html @tmpl('sidebarType', @doc.types.all())
|
||||||
|
|
||||||
|
onOpen: (event) =>
|
||||||
|
$.stopEvent(event)
|
||||||
|
type = @doc.types.findBy 'slug', event.target.getAttribute('data-slug')
|
||||||
|
|
||||||
|
if type and not @lists[type.slug]
|
||||||
|
@lists[type.slug] = new app.views.EntryList(type.entries())
|
||||||
|
$.after event.target, @lists[type.slug].el
|
||||||
|
return
|
||||||
|
|
||||||
|
onClose: (event) =>
|
||||||
|
$.stopEvent(event)
|
||||||
|
type = @doc.types.findBy 'slug', event.target.getAttribute('data-slug')
|
||||||
|
|
||||||
|
if type and @lists[type.slug]
|
||||||
|
@lists[type.slug].detach()
|
||||||
|
delete @lists[type.slug]
|
||||||
|
return
|
||||||
|
|
||||||
|
revealEntry: (entry) ->
|
||||||
|
if entry.type
|
||||||
|
@lists[entry.getType().slug]?.revealEntry(entry)
|
||||||
|
return
|
@ -0,0 +1,161 @@
|
|||||||
|
class app.View
|
||||||
|
$.extend @prototype, Events
|
||||||
|
|
||||||
|
constructor: ->
|
||||||
|
@setupElement()
|
||||||
|
@originalClassName = @el.className if @el.className
|
||||||
|
@resetClass() if @constructor.className
|
||||||
|
@refreshElements()
|
||||||
|
@init?()
|
||||||
|
@refreshElements()
|
||||||
|
|
||||||
|
setupElement: ->
|
||||||
|
@el ?= if typeof @constructor.el is 'string'
|
||||||
|
$ @constructor.el
|
||||||
|
else if @constructor.el
|
||||||
|
@constructor.el
|
||||||
|
else
|
||||||
|
document.createElement @constructor.tagName or 'div'
|
||||||
|
return
|
||||||
|
|
||||||
|
refreshElements: ->
|
||||||
|
if @constructor.elements
|
||||||
|
@[name] = @find selector for name, selector of @constructor.elements
|
||||||
|
return
|
||||||
|
|
||||||
|
addClass: (name) ->
|
||||||
|
@el.classList.add(name)
|
||||||
|
return
|
||||||
|
|
||||||
|
removeClass: (name) ->
|
||||||
|
@el.classList.remove(name)
|
||||||
|
return
|
||||||
|
|
||||||
|
resetClass: ->
|
||||||
|
@el.className = @originalClassName or ''
|
||||||
|
if @constructor.className
|
||||||
|
@addClass name for name in @constructor.className.split ' '
|
||||||
|
return
|
||||||
|
|
||||||
|
find: (selector) ->
|
||||||
|
$ selector, @el
|
||||||
|
|
||||||
|
findAll: (selector) ->
|
||||||
|
$$ selector, @el
|
||||||
|
|
||||||
|
findByClass: (name) ->
|
||||||
|
@findAllByClass(name)[0]
|
||||||
|
|
||||||
|
findLastByClass: (name) ->
|
||||||
|
all = @findAllByClass(name)[0]
|
||||||
|
all[all.length - 1]
|
||||||
|
|
||||||
|
findAllByClass: (name) ->
|
||||||
|
@el.getElementsByClassName(name)
|
||||||
|
|
||||||
|
findByTag: (tag) ->
|
||||||
|
@findAllByTag(tag)[0]
|
||||||
|
|
||||||
|
findLastByTag: (tag) ->
|
||||||
|
all = @findAllByTag(tag)
|
||||||
|
all[all.length - 1]
|
||||||
|
|
||||||
|
findAllByTag: (tag) ->
|
||||||
|
@el.getElementsByTagName(tag)
|
||||||
|
|
||||||
|
append: (value) ->
|
||||||
|
$.append @el, value.el or value
|
||||||
|
return
|
||||||
|
|
||||||
|
appendTo: (value) ->
|
||||||
|
$.append value.el or value, @el
|
||||||
|
return
|
||||||
|
|
||||||
|
prepend: (value) ->
|
||||||
|
$.prepend @el, value.el or value
|
||||||
|
return
|
||||||
|
|
||||||
|
prependTo: (value) ->
|
||||||
|
$.prepend value.el or value, @el
|
||||||
|
return
|
||||||
|
|
||||||
|
before: (value) ->
|
||||||
|
$.before @el, value.el or value
|
||||||
|
return
|
||||||
|
|
||||||
|
after: (value) ->
|
||||||
|
$.after @el, value.el or value
|
||||||
|
return
|
||||||
|
|
||||||
|
remove: (value) ->
|
||||||
|
$.remove value.el or value
|
||||||
|
return
|
||||||
|
|
||||||
|
empty: ->
|
||||||
|
$.empty @el
|
||||||
|
@refreshElements()
|
||||||
|
return
|
||||||
|
|
||||||
|
html: (value) ->
|
||||||
|
@empty()
|
||||||
|
@append value
|
||||||
|
return
|
||||||
|
|
||||||
|
tmpl: (args...) ->
|
||||||
|
app.templates.render(args...)
|
||||||
|
|
||||||
|
delay: (fn, args...) ->
|
||||||
|
delay = if typeof args[args.length - 1] is 'number' then args.pop() else 0
|
||||||
|
setTimeout fn.bind(@, args...), delay
|
||||||
|
|
||||||
|
onDOM: (event, callback) ->
|
||||||
|
$.on @el, event, callback
|
||||||
|
return
|
||||||
|
|
||||||
|
offDOM: (event, callback) ->
|
||||||
|
$.off @el, event, callback
|
||||||
|
return
|
||||||
|
|
||||||
|
bindEvents: ->
|
||||||
|
if @constructor.events
|
||||||
|
@onDOM name, @[method] for name, method of @constructor.events
|
||||||
|
|
||||||
|
if @constructor.routes
|
||||||
|
app.router.on name, @[method] for name, method of @constructor.routes
|
||||||
|
|
||||||
|
if @constructor.shortcuts
|
||||||
|
app.shortcuts.on name, @[method] for name, method of @constructor.shortcuts
|
||||||
|
return
|
||||||
|
|
||||||
|
unbindEvents: ->
|
||||||
|
if @constructor.events
|
||||||
|
@offDOM name, @[method] for name, method of @constructor.events
|
||||||
|
|
||||||
|
if @constructor.routes
|
||||||
|
app.router.off name, @[method] for name, method of @constructor.routes
|
||||||
|
|
||||||
|
if @constructor.shortcuts
|
||||||
|
app.shortcuts.off name, @[method] for name, method of @constructor.shortcuts
|
||||||
|
return
|
||||||
|
|
||||||
|
addSubview: (view) ->
|
||||||
|
(@subviews or= []).push(view)
|
||||||
|
|
||||||
|
activate: ->
|
||||||
|
return if @activated
|
||||||
|
@bindEvents()
|
||||||
|
view.activate() for view in @subviews if @subviews
|
||||||
|
@activated = true
|
||||||
|
true
|
||||||
|
|
||||||
|
deactivate: ->
|
||||||
|
return unless @activated
|
||||||
|
@unbindEvents()
|
||||||
|
view.deactivate() for view in @subviews if @subviews
|
||||||
|
@activated = false
|
||||||
|
true
|
||||||
|
|
||||||
|
detach: ->
|
||||||
|
@deactivate()
|
||||||
|
$.remove @el
|
||||||
|
return
|
@ -0,0 +1,41 @@
|
|||||||
|
//= depend_on icons.png
|
||||||
|
//= depend_on icons@2x.png
|
||||||
|
|
||||||
|
//= require vendor/open-sans
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Copyright 2013 Thibaut Courouble and other contributors
|
||||||
|
*
|
||||||
|
* This source code is licensed under the terms of the Mozilla
|
||||||
|
* Public License, v. 2.0, a copy of which may be obtained at:
|
||||||
|
* http://mozilla.org/MPL/2.0/
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import 'global/variables',
|
||||||
|
'global/icons',
|
||||||
|
'global/classes',
|
||||||
|
'global/base';
|
||||||
|
|
||||||
|
@import 'components/app',
|
||||||
|
'components/header',
|
||||||
|
'components/sidebar',
|
||||||
|
'components/content',
|
||||||
|
'components/page',
|
||||||
|
'components/fail',
|
||||||
|
'components/notice',
|
||||||
|
'components/notif',
|
||||||
|
'components/prism',
|
||||||
|
'components/mobile';
|
||||||
|
|
||||||
|
@import 'pages/angular',
|
||||||
|
'pages/coffeescript',
|
||||||
|
'pages/ember',
|
||||||
|
'pages/jquery',
|
||||||
|
'pages/less',
|
||||||
|
'pages/lodash',
|
||||||
|
'pages/mdn',
|
||||||
|
'pages/node',
|
||||||
|
'pages/php',
|
||||||
|
'pages/rfc',
|
||||||
|
'pages/underscore',
|
||||||
|
'pages/yard';
|
@ -0,0 +1,34 @@
|
|||||||
|
._app {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
height: 100%;
|
||||||
|
padding-top: $headerHeight;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-transition: opacity .2s;
|
||||||
|
transition: opacity .2s;
|
||||||
|
@extend %border-box;
|
||||||
|
|
||||||
|
._booting > & { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._booting {
|
||||||
|
opacity: 0;
|
||||||
|
-webkit-transition: opacity .1s .3s;
|
||||||
|
transition: opacity .1s .3s;
|
||||||
|
|
||||||
|
&._loading { opacity: 1; }
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: 'Loading…';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
line-height: 1;
|
||||||
|
margin-top: -.75em;
|
||||||
|
font-size: 4rem;
|
||||||
|
color: #ccc;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: -.125rem;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,325 @@
|
|||||||
|
//
|
||||||
|
// Content
|
||||||
|
//
|
||||||
|
|
||||||
|
._container {
|
||||||
|
position: relative;
|
||||||
|
z-index: $contentZ;
|
||||||
|
height: 100%;
|
||||||
|
margin-left: $sidebarWidth;
|
||||||
|
border-top: 1px solid #b4b7bf;
|
||||||
|
box-shadow: inset 0 1px rgba(black, .04), // top inner shadow
|
||||||
|
inset 1px 0 #f4f4f4; // left inner shadow
|
||||||
|
pointer-events: none;
|
||||||
|
@extend %border-box;
|
||||||
|
|
||||||
|
@media #{$mediumScreen} { margin-left: $sidebarMediumWidth; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._content {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 1.25rem 1.5rem 0;
|
||||||
|
font-size: .875rem;
|
||||||
|
pointer-events: auto;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
@extend %border-box;
|
||||||
|
|
||||||
|
-webkit-padding-start: .75rem;
|
||||||
|
@media (-moz-overlay-scrollbars) { padding-left: .75rem; }
|
||||||
|
@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { margin-left: 0; }
|
||||||
|
|
||||||
|
&:after { // padding bottom
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._content-loading:before {
|
||||||
|
color: #e6e6e6;
|
||||||
|
@extend ._booting:before;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Splash screen
|
||||||
|
//
|
||||||
|
|
||||||
|
._splash-title {
|
||||||
|
color: #ddd;
|
||||||
|
cursor: default;
|
||||||
|
@extend ._booting:before, %user-select-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
._splash-maxcdn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1.25rem;
|
||||||
|
left: 50%;
|
||||||
|
width: 16rem;
|
||||||
|
margin-left: -8rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
color: #bbb;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:hover { color: $linkColor; }
|
||||||
|
> ._maxcdn-logo-bw { opacity: .2; }
|
||||||
|
&:hover > ._maxcdn-logo-bw { opacity: .5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Intro
|
||||||
|
//
|
||||||
|
|
||||||
|
._intro { text-align: center; }
|
||||||
|
|
||||||
|
._intro-message {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
max-width: 37rem;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
text-align: left;
|
||||||
|
@extend %note, %note-green;
|
||||||
|
}
|
||||||
|
|
||||||
|
._intro-hide {
|
||||||
|
float: right;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
._intro-title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._intro-list {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding-left: 2.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._intro-link { cursor: pointer; }
|
||||||
|
|
||||||
|
._intro-maxcdn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
color: $textColorLight;
|
||||||
|
|
||||||
|
&:hover { color: $linkColor; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Error
|
||||||
|
//
|
||||||
|
|
||||||
|
._error {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 2rem;
|
||||||
|
line-height: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
._error-title {
|
||||||
|
margin: -5.5rem 0 .5rem;
|
||||||
|
line-height: 2;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._error-text {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
color: $textColorLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
._error-links {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
._error-link { padding: 0 .5rem; }
|
||||||
|
|
||||||
|
//
|
||||||
|
// Heading
|
||||||
|
//
|
||||||
|
|
||||||
|
._lined-heading,
|
||||||
|
%lined-heading {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
overflow-wrap: normal;
|
||||||
|
word-wrap: normal;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
line-height: 0;
|
||||||
|
margin-left: 1rem;
|
||||||
|
background: #dde3e8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Table of contents
|
||||||
|
//
|
||||||
|
|
||||||
|
._toc {
|
||||||
|
float: right;
|
||||||
|
max-width: 15em;
|
||||||
|
margin: .25rem 0 1.5rem 1.5rem;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
@extend %box;
|
||||||
|
|
||||||
|
+ ._lined-heading { margin-top: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._toc-title {
|
||||||
|
margin: 0 0 .75em;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
._toc-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 1em 0 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Static page
|
||||||
|
//
|
||||||
|
|
||||||
|
._static {
|
||||||
|
padding-bottom: 2em;
|
||||||
|
|
||||||
|
> ._lined-heading:first-child { margin-top: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Credits table
|
||||||
|
//
|
||||||
|
|
||||||
|
._credits {
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
td:first-child, td:last-child { white-space: nowrap; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// News
|
||||||
|
//
|
||||||
|
|
||||||
|
._content {
|
||||||
|
._news-row {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 10em;
|
||||||
|
font-size: .8125rem;
|
||||||
|
color: $textColorLight;
|
||||||
|
|
||||||
|
+ ._news-row { margin-top: 1em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._news-title {
|
||||||
|
display: block;
|
||||||
|
font-size: .875rem;
|
||||||
|
color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
._news-date {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Keyboard shortcuts
|
||||||
|
//
|
||||||
|
|
||||||
|
._shortcuts-title {
|
||||||
|
width: 16rem;
|
||||||
|
max-width: 40%;
|
||||||
|
margin: 2rem 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
._shortcuts-dl { margin: 1rem 0; }
|
||||||
|
|
||||||
|
._shortcuts-dt {
|
||||||
|
float: left;
|
||||||
|
clear: left;
|
||||||
|
margin: 0;
|
||||||
|
width: 16rem;
|
||||||
|
max-width: 40%;
|
||||||
|
font-weight: normal;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
._shortcuts-dd {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 .75rem;
|
||||||
|
padding: 1px 0 1px .75rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
._shortcut-code {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0 .5em;
|
||||||
|
@extend %label;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Utilities
|
||||||
|
//
|
||||||
|
|
||||||
|
._note { @extend %note; }
|
||||||
|
._note-green { @extend %note-green; }
|
||||||
|
._label { @extend %label; }
|
||||||
|
._highlight { background: #fffdcd !important; }
|
||||||
|
|
||||||
|
._github-btn {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-top;
|
||||||
|
margin-left: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
%maxcdn-logo {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 6.25rem;
|
||||||
|
margin-left: .375rem;
|
||||||
|
overflow: hidden;
|
||||||
|
text-indent: -20rem;
|
||||||
|
background-position: center center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 6.25rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._maxcdn-logo {
|
||||||
|
background-image: image-url('maxcdn.png');
|
||||||
|
@extend %maxcdn-logo;
|
||||||
|
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
|
||||||
|
background-image: image-url('maxcdn@2x.png');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._maxcdn-logo-bw {
|
||||||
|
background-image: image-url('maxcdn-bw.png');
|
||||||
|
@extend %maxcdn-logo;
|
||||||
|
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 1.5), (min-resolution: 144dpi) {
|
||||||
|
background-image: image-url('maxcdn-bw@2x.png');
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
._fail {
|
||||||
|
position: relative;
|
||||||
|
top: 1.5rem;
|
||||||
|
width: 24rem;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background: #eaefef;
|
||||||
|
border-radius: 5px;
|
||||||
|
@extend %border-box;
|
||||||
|
|
||||||
|
&:after { // margin
|
||||||
|
content: '';
|
||||||
|
position: relative;
|
||||||
|
top: 3rem;
|
||||||
|
float: left;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._fail-title {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
._fail-text, ._fail-list {
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._fail-text:last-child { margin: 0; }
|
||||||
|
|
||||||
|
._fail-link { float: right; }
|
@ -0,0 +1,157 @@
|
|||||||
|
//
|
||||||
|
// Header
|
||||||
|
//
|
||||||
|
|
||||||
|
._header {
|
||||||
|
position: absolute;
|
||||||
|
z-index: $headerZ;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: $headerHeight;
|
||||||
|
line-height: $headerHeight;
|
||||||
|
text-shadow: 0 1px rgba(white, .5);
|
||||||
|
background: -webkit-linear-gradient(top, #f6f6f8, #e4e4e6);
|
||||||
|
background: linear-gradient(to bottom, #f6f6f8, #e4e4e6);
|
||||||
|
box-shadow: inset 0 1px rgba(white, .8), // top inner glow
|
||||||
|
inset 0 -1px rgba(white, .3); // bottom inner glow
|
||||||
|
@extend %user-select-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Navigation menu
|
||||||
|
//
|
||||||
|
|
||||||
|
._nav {
|
||||||
|
float: right;
|
||||||
|
margin-right: .5rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
color: lighten($textColor, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
._nav-link,
|
||||||
|
._nav-link:hover {
|
||||||
|
float: left;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: solid transparent;
|
||||||
|
border-width: 0 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
._nav-current,
|
||||||
|
._nav-current:hover {
|
||||||
|
color: lighten($textColor, 8%);
|
||||||
|
background: -webkit-linear-gradient(top, #e1e1e4, #f2f2f5);
|
||||||
|
background: linear-gradient(to bottom, #e1e1e4, #f2f2f5);
|
||||||
|
border-color: rgba(black, .15);
|
||||||
|
box-shadow: inset 0 1px rgba(black, .07), // top border
|
||||||
|
inset 0 1px 2px rgba(black, .1), // top inner shadow
|
||||||
|
1px 0 rgba(white, .2), // right glow
|
||||||
|
-1px 0 rgba(white, .2); // left glow
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Logo
|
||||||
|
//
|
||||||
|
|
||||||
|
._logo {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
height: $headerHeight;
|
||||||
|
margin: 0;
|
||||||
|
line-height: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
|
||||||
|
&:before, &:after { // left border
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: -1px;
|
||||||
|
width: 1px;
|
||||||
|
background: -webkit-linear-gradient(bottom, #b4b7bf, rgba(#b4b7bf, 0) 80%);
|
||||||
|
background: linear-gradient(to top, #b4b7bf, rgba(#b4b7bf, 0) 80%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after { // left glow
|
||||||
|
left: -2px;
|
||||||
|
background: -webkit-linear-gradient(bottom, rgba(white, .4), rgba(white, 0) 80%);
|
||||||
|
background: linear-gradient(to top, rgba(white, .4), rgba(white, 0) 80%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Search form
|
||||||
|
//
|
||||||
|
|
||||||
|
._search {
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
width: $sidebarWidth;
|
||||||
|
height: 100%;
|
||||||
|
padding: .625rem;
|
||||||
|
border-right: 1px solid transparent;
|
||||||
|
@extend %border-box;
|
||||||
|
|
||||||
|
@media #{$mediumScreen} { width: $sidebarMediumWidth; }
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
left: 1rem;
|
||||||
|
opacity: .5;
|
||||||
|
@extend %icon, %icon-search;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._search-input {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 .75rem 1px 1.625rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #b2b5bb #babbc5 #bebfc6;
|
||||||
|
border-radius: 1rem;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(black, .1), // top inner shadow
|
||||||
|
0 1px rgba(white, .3); // bottom glow
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 0;
|
||||||
|
border-color: #35b5f4 #35b5f4 #30aeee;
|
||||||
|
box-shadow: inset 0 0 1px rgba(#35b5f4, .5), // inner glow
|
||||||
|
0 0 2px rgba(#35b5f4, .8); // outer glow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._search-clear {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: .5em;
|
||||||
|
right: .5em;
|
||||||
|
padding: .5em;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: .3;
|
||||||
|
|
||||||
|
&:hover { opacity: .5; }
|
||||||
|
&:before { @extend %icon, %icon-clear; }
|
||||||
|
._search-active > & { display: block; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._search-tag {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: .875rem;
|
||||||
|
left: .875rem;
|
||||||
|
margin: -1px 0 0 -1px;
|
||||||
|
padding: 0 .625rem;
|
||||||
|
line-height: 1.25rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
background: #dfeafe;
|
||||||
|
border: 1px solid #98aed8;
|
||||||
|
border-radius: .75rem;
|
||||||
|
box-shadow: inset 0 1px rgba(white, .2), // top inner glow
|
||||||
|
0 1px rgba(black, .05); // bottom shadow
|
||||||
|
}
|
@ -0,0 +1,201 @@
|
|||||||
|
//
|
||||||
|
// Mobile overrides
|
||||||
|
//
|
||||||
|
|
||||||
|
._mobile {
|
||||||
|
font-size: 100%;
|
||||||
|
|
||||||
|
// Layout
|
||||||
|
|
||||||
|
body { -ms-overflow-style: -ms-autohiding-scrollbar; }
|
||||||
|
|
||||||
|
._app, ._container, ._content { overflow: visible; }
|
||||||
|
|
||||||
|
._container {
|
||||||
|
margin: 0;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
._content {
|
||||||
|
position: static;
|
||||||
|
height: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: .75rem 1rem 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._booting:before, ._content-loading:before {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header
|
||||||
|
|
||||||
|
._header {
|
||||||
|
position: fixed;
|
||||||
|
z-index: $contentZ + 1;
|
||||||
|
border-bottom: 1px solid #b4b7bf;
|
||||||
|
box-shadow: 0 1px rgba(black, .03);
|
||||||
|
}
|
||||||
|
|
||||||
|
._logo, ._nav { display: none; }
|
||||||
|
._home-link, ._menu-link { display: block; }
|
||||||
|
|
||||||
|
._search {
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
border-right: 0;
|
||||||
|
|
||||||
|
&:before { left: .5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._search-clear { padding-right: 0; }
|
||||||
|
._search-tag { left: .325rem; }
|
||||||
|
|
||||||
|
// Sidebar
|
||||||
|
|
||||||
|
._sidebar {
|
||||||
|
position: static;
|
||||||
|
min-height: 100%;
|
||||||
|
overflow: visible;
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
> ._list { padding-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._list, ._sidebar-footer {
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-item { border-right-width: 0; }
|
||||||
|
|
||||||
|
._list-link { display: none; }
|
||||||
|
|
||||||
|
._sidebar-footer {
|
||||||
|
position: static;
|
||||||
|
margin-top: .5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:before { content: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._sidebar-footer-save {
|
||||||
|
margin-top: 1rem;
|
||||||
|
border-bottom: 1px solid #bac6d7;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notice
|
||||||
|
|
||||||
|
._notice {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
padding: 0 .5rem;
|
||||||
|
|
||||||
|
&:before { content: none; }
|
||||||
|
~ ._sidebar { padding-bottom: 4rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._notice-text { font-size: .75em; }
|
||||||
|
|
||||||
|
// Notification
|
||||||
|
|
||||||
|
._notif { position: fixed; }
|
||||||
|
|
||||||
|
// Table of contents
|
||||||
|
|
||||||
|
._toc {
|
||||||
|
float: none;
|
||||||
|
max-width: none;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Splash
|
||||||
|
|
||||||
|
._splash-title { margin-top: -.5em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Fix viewport on Windows Phone
|
||||||
|
//
|
||||||
|
|
||||||
|
@-ms-viewport { width: device-width; }
|
||||||
|
@media (orientation: portrait) and (min-device-width: 720px) and (max-device-width: 768px),
|
||||||
|
(orientation: landscape) and (device-width: 1280px) and (max-device-height: 768px) {
|
||||||
|
@-ms-viewport { width: 50%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Header buttons
|
||||||
|
//
|
||||||
|
|
||||||
|
%mobile-link {
|
||||||
|
display: none;
|
||||||
|
position: relative;
|
||||||
|
float: left;
|
||||||
|
width: 2.5rem;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
margin: -.5rem 0 0 -.5rem;
|
||||||
|
@extend %icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._home-link {
|
||||||
|
@extend %mobile-link;
|
||||||
|
|
||||||
|
&:before { @extend %icon-home; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._menu-link {
|
||||||
|
float: right;
|
||||||
|
@extend %mobile-link;
|
||||||
|
|
||||||
|
&:before { @extend %icon-menu; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Navigation menu
|
||||||
|
//
|
||||||
|
|
||||||
|
._mobile-nav {
|
||||||
|
margin: .25rem 0 1.25rem;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 2.8;
|
||||||
|
overflow: hidden;
|
||||||
|
@extend %box;
|
||||||
|
}
|
||||||
|
|
||||||
|
._mobile-nav-link {
|
||||||
|
float: left;
|
||||||
|
width: 33%;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
&:nth-child(2n) { width: 34%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Intro
|
||||||
|
//
|
||||||
|
|
||||||
|
._mobile-intro {
|
||||||
|
> ._intro-list { padding-left: 1.5rem; }
|
||||||
|
|
||||||
|
> ._intro-hide,
|
||||||
|
> ._intro-maxcdn {
|
||||||
|
position: static;
|
||||||
|
float: none;
|
||||||
|
display: block;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
._notice {
|
||||||
|
position: absolute;
|
||||||
|
z-index: $noticeZ;
|
||||||
|
bottom: 0;
|
||||||
|
left: $sidebarWidth;
|
||||||
|
right: 0;
|
||||||
|
height: 2.5rem;
|
||||||
|
padding: 0 1.25rem;
|
||||||
|
text-shadow: 0 1px rgba(white, .5);
|
||||||
|
background: #faf9e2;
|
||||||
|
box-shadow: inset 0 1px #dddaaa, // top border
|
||||||
|
inset 0 2px rgba(white, .7), // top inner glow
|
||||||
|
inset 1px 0 rgba(black, .03); // left inner shadow
|
||||||
|
|
||||||
|
@media #{$mediumScreen} { left: $sidebarMediumWidth; }
|
||||||
|
|
||||||
|
~ ._container { padding-bottom: 2.5rem; }
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 1.5rem;
|
||||||
|
right: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background-image: -webkit-linear-gradient(top, rgba(white, 0), rgba(white, .95));
|
||||||
|
background-image: linear-gradient(to bottom, rgba(white, 0), rgba(white, .95));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._notice-text {
|
||||||
|
display: table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin: 0;
|
||||||
|
height: 2.5rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
._notice-link { cursor: pointer; }
|
@ -0,0 +1,97 @@
|
|||||||
|
._notif {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
width: 25rem;
|
||||||
|
max-width: 90%;
|
||||||
|
padding: .75rem 1rem;
|
||||||
|
font-size: .75rem;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px 1px rgba(black, .4);
|
||||||
|
background: -webkit-linear-gradient(top, rgba(#3a3a3a, .9), rgba(#202020, .9));
|
||||||
|
background: linear-gradient(to bottom, rgba(#3a3a3a, .9), rgba(#202020, .9));
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 1px solid black;
|
||||||
|
border-radius: .25rem;
|
||||||
|
box-shadow: inset 0 1px rgba(white, .1), // top inner glow
|
||||||
|
inset 0 0 0 1px rgba(white, .1), // inner glow
|
||||||
|
0 1px 3px rgba(black, .5); // drop shadow
|
||||||
|
transition: opacity .2s;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: default;
|
||||||
|
@extend %border-box, %user-select-none;
|
||||||
|
|
||||||
|
&._in { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._notif-title {
|
||||||
|
margin: 0 0 .375rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
._notif-text { margin-bottom: 0; }
|
||||||
|
|
||||||
|
._notif-link,
|
||||||
|
._notif-link:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
._notif-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: .625rem;
|
||||||
|
opacity: .9;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:before { @extend %icon, %icon-close-white; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._notif-news {
|
||||||
|
width: 20rem;
|
||||||
|
max-height: 85%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
> ._notif-title {
|
||||||
|
margin: -.125rem 0 1em;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
> ._news-row {
|
||||||
|
line-height: 1.125rem;
|
||||||
|
font-size: .6875rem;
|
||||||
|
color: #bbb;
|
||||||
|
|
||||||
|
+ ._news-row { margin-top: .75rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._news-title {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: .25rem;
|
||||||
|
font-size: .75rem;
|
||||||
|
font-weight: normal;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
._news-date {
|
||||||
|
float: right;
|
||||||
|
margin-left: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: baseline;
|
||||||
|
line-height: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
text-shadow: inherit;
|
||||||
|
background: none;
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
//
|
||||||
|
// Page
|
||||||
|
//
|
||||||
|
|
||||||
|
._page {
|
||||||
|
> h1 { @extend ._lined-heading; }
|
||||||
|
> h1:first-child { margin-top: 0; }
|
||||||
|
|
||||||
|
a[href^="http:"], a[href^="https:"] { @extend %external-link; }
|
||||||
|
|
||||||
|
a:not([href]) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
display: block;
|
||||||
|
padding: 1px;
|
||||||
|
border: 1px dotted #ddd;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Attribution box
|
||||||
|
//
|
||||||
|
|
||||||
|
._attribution {
|
||||||
|
clear: both;
|
||||||
|
margin: 2rem 0 1.5rem;
|
||||||
|
font-size: .75rem;
|
||||||
|
color: $textColorLight;
|
||||||
|
text-align: center;
|
||||||
|
-webkit-font-smoothing: subpixel-antialiased;
|
||||||
|
|
||||||
|
& + & { margin-top: 1.5rem; }
|
||||||
|
& + & > ._attribution-link { display: none; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._attribution-p {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0;
|
||||||
|
padding: .25rem .75rem;
|
||||||
|
text-shadow: 0 1px rgba(white, .3);
|
||||||
|
background: #f2f2f2;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
._attribution-link { @extend %internal-link; }
|
||||||
|
|
||||||
|
//
|
||||||
|
// Entry list
|
||||||
|
//
|
||||||
|
|
||||||
|
._entry-list {
|
||||||
|
padding-left: 1em;
|
||||||
|
list-style: none;
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
.token.comment,
|
||||||
|
.token.prolog,
|
||||||
|
.token.doctype,
|
||||||
|
.token.cdata,
|
||||||
|
.token.punctuation {
|
||||||
|
color: $textColorLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
.namespace {
|
||||||
|
opacity: .7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.property,
|
||||||
|
.token.tag,
|
||||||
|
.token.boolean,
|
||||||
|
.token.number {
|
||||||
|
color: #905;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.selector,
|
||||||
|
.token.attr-name,
|
||||||
|
.token.string {
|
||||||
|
color: #5e8e01;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.operator,
|
||||||
|
.token.entity,
|
||||||
|
.token.url,
|
||||||
|
.language-css .token.string,
|
||||||
|
.style .token.string {
|
||||||
|
color: #a67f59;
|
||||||
|
background: hsla(0, 0%, 100%, .5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.atrule,
|
||||||
|
.token.attr-value,
|
||||||
|
.token.keyword {
|
||||||
|
color: #0070a3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.regex,
|
||||||
|
.token.important {
|
||||||
|
color: #e90;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.important {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token.entity {
|
||||||
|
cursor: help;
|
||||||
|
}
|
@ -0,0 +1,285 @@
|
|||||||
|
//
|
||||||
|
// Sidebar
|
||||||
|
//
|
||||||
|
|
||||||
|
._sidebar {
|
||||||
|
position: absolute;
|
||||||
|
z-index: $sidebarZ;
|
||||||
|
top: $headerHeight;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: scroll;
|
||||||
|
text-shadow: 0 1px rgba(white, .3);
|
||||||
|
background: #e5eaf4;
|
||||||
|
box-shadow: inset 0 1px #b4b7bf, // top border
|
||||||
|
inset 0 2px rgba(black, .03); // top inner shadow
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
-ms-overflow-style: none; // IE 10 doesn't support pointer-events
|
||||||
|
@extend %user-select-none;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar { width: 10px; }
|
||||||
|
&::-webkit-scrollbar-button { display: none; }
|
||||||
|
&::-webkit-scrollbar-track { background: white; }
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
min-height: 1rem;
|
||||||
|
background: #ccc;
|
||||||
|
background-clip: padding-box;
|
||||||
|
border: 3px solid white;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #999;
|
||||||
|
border-width: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// List
|
||||||
|
//
|
||||||
|
|
||||||
|
._list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
width: $sidebarWidth;
|
||||||
|
|
||||||
|
@media #{$mediumScreen} { width: $sidebarMediumWidth; }
|
||||||
|
|
||||||
|
._sidebar > & {
|
||||||
|
min-height: 100%;
|
||||||
|
padding-bottom: 3.5rem;
|
||||||
|
box-shadow: inset -1px 0 #bbc1cc, // right border
|
||||||
|
inset -2px 0 rgba(white, .2); // right inner glow
|
||||||
|
@extend %border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-item {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 0 .75rem;
|
||||||
|
line-height: 1.75rem;
|
||||||
|
font-size: .875rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
border: solid transparent;
|
||||||
|
border-width: 1px 1px 1px 0;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
&, &:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.focus,
|
||||||
|
&.focus:hover,
|
||||||
|
&.active,
|
||||||
|
&.active:hover {
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 1px rgba(black, .2);
|
||||||
|
background: -webkit-linear-gradient(top, #bfc6dd, #949eb8);
|
||||||
|
background: linear-gradient(to bottom, #bfc6dd, #949eb8);
|
||||||
|
border-color: #96a1c6 #8e99b7 #7f87a4;
|
||||||
|
box-shadow: inset 0 1px rgba(white, .15), // top inner glow
|
||||||
|
inset 0 0 0 1px rgba(white, .08), // inner glow
|
||||||
|
0 1px rgba(black, .05); // drop shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active,
|
||||||
|
&.active:hover {
|
||||||
|
background: -webkit-linear-gradient(top, #65b2fb, #3492e9);
|
||||||
|
background: linear-gradient(to bottom, #65b2fb, #3492e9);
|
||||||
|
border-color: #318ce4 #2b82db #2878c7;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
float: left;
|
||||||
|
margin: .375rem .625rem 0 0;
|
||||||
|
@extend %icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-count {
|
||||||
|
float: right;
|
||||||
|
font-size: .75rem;
|
||||||
|
color: $textColorLighter;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
.focus > &,
|
||||||
|
.active > & {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// List hierarchy
|
||||||
|
//
|
||||||
|
|
||||||
|
._list-dir,
|
||||||
|
%_list-dir {
|
||||||
|
line-height: 2rem;
|
||||||
|
padding-left: 2.25rem;
|
||||||
|
|
||||||
|
&:before { margin-top: .5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-disabled {
|
||||||
|
@extend %_list-dir;
|
||||||
|
|
||||||
|
&, &:hover { color: $textColorLight; }
|
||||||
|
&:before { opacity: .7; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-arrow {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: .25rem;
|
||||||
|
padding: .5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: .45;
|
||||||
|
|
||||||
|
&:hover { opacity: .7; }
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
@extend %icon, %icon-dir;
|
||||||
|
|
||||||
|
.open > & {
|
||||||
|
-webkit-transform: rotate(90deg);
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-sub {
|
||||||
|
> ._list-item { padding-left: 2.75rem; }
|
||||||
|
> ._list-item:before { content: none; }
|
||||||
|
> ._list-dir { line-height: 1.75rem; }
|
||||||
|
|
||||||
|
._list-arrow {
|
||||||
|
left: 1rem;
|
||||||
|
padding: .375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// List pagination
|
||||||
|
//
|
||||||
|
|
||||||
|
._list-pagelink {
|
||||||
|
color: $linkColor;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $linkColorHover;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// List picker
|
||||||
|
//
|
||||||
|
|
||||||
|
._list-checkbox {
|
||||||
|
position: absolute;
|
||||||
|
top: .5rem;
|
||||||
|
right: -1rem;
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
transition: .2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-label {
|
||||||
|
transition: .2s;
|
||||||
|
@extend %_list-dir;
|
||||||
|
|
||||||
|
._in > & { padding-left: .75rem; }
|
||||||
|
._in > & > ._list-checkbox { right: .5rem }
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-label-off {
|
||||||
|
opacity: 0;
|
||||||
|
padding-left: .75rem;
|
||||||
|
|
||||||
|
._in > & { opacity: 1; }
|
||||||
|
> ._list-checkbox { right: .5rem; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._list-link {
|
||||||
|
display: block;
|
||||||
|
margin-top: .75rem;
|
||||||
|
font-size: .8125rem;
|
||||||
|
text-align: center;
|
||||||
|
@extend %external-link;
|
||||||
|
|
||||||
|
&:after { visibility: hidden; }
|
||||||
|
&:hover:after { visibility: visible; }
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Footer
|
||||||
|
//
|
||||||
|
|
||||||
|
._sidebar-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: $sidebarWidth;
|
||||||
|
background: #e5eaf4;
|
||||||
|
box-shadow: inset -1px 0 #bbc1cc, // right border
|
||||||
|
inset -2px 0 rgba(white, .2); // right inner glow
|
||||||
|
|
||||||
|
@media #{$mediumScreen} { width: $sidebarMediumWidth; }
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 1px;
|
||||||
|
height: 1em;
|
||||||
|
background-image: -webkit-linear-gradient(top, rgba(#e5eaf4, 0), rgba(#e5eaf4, .95));
|
||||||
|
background-image: linear-gradient(to bottom, rgba(#e5eaf4, 0), rgba(#e5eaf4, .95));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._sidebar-footer-link {
|
||||||
|
position: relative;
|
||||||
|
display: block;
|
||||||
|
height: 2.5rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
padding: .75rem;
|
||||||
|
font-size: .875em;
|
||||||
|
cursor: pointer;
|
||||||
|
@extend %border-box;
|
||||||
|
|
||||||
|
&, &:hover {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
float: left;
|
||||||
|
margin-right: .625rem;
|
||||||
|
@extend %icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
._sidebar-footer-edit {
|
||||||
|
&:before { @extend %icon-settings; }
|
||||||
|
}
|
||||||
|
|
||||||
|
._sidebar-footer-save {
|
||||||
|
margin-right: 1px;
|
||||||
|
font-weight: bold;
|
||||||
|
background-image: -webkit-linear-gradient(top, #fbfbfe, #f5f5f8 50%, #eeeef1 51%, #e8e8ec);
|
||||||
|
background-image: linear-gradient(to bottom, #fbfbfe, #f5f5f8 50%, #eeeef1 51%, #e8e8ec);
|
||||||
|
box-shadow: inset 0 1px white, // top inner glow
|
||||||
|
inset 0 0 0 1px rgba(white, .2), // inner glow
|
||||||
|
0 -1px #c4cfde; // top border
|
||||||
|
|
||||||
|
&:before { @extend %icon-check; }
|
||||||
|
}
|
@ -0,0 +1,183 @@
|
|||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
|
font-size: 100%;
|
||||||
|
background: white;
|
||||||
|
|
||||||
|
@media #{$mediumScreen} { font-size: 93.75%; }
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
font: normal 1em/1.7 $baseFont;
|
||||||
|
color: $textColor;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
-webkit-tap-highlight-color: rgba(black, 0);
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $linkColor;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $linkColorHover;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus { outline: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin: 1.5em 0 1em;
|
||||||
|
line-height: 1.3;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 { font-size: 1.5em; }
|
||||||
|
h2 { font-size: 1.375em; }
|
||||||
|
h3 { font-size: 1.25em; }
|
||||||
|
h4 { font-size: 1.125em; }
|
||||||
|
h5 { font-size: 1em; }
|
||||||
|
h6 { font-size: .9375em; }
|
||||||
|
|
||||||
|
p { margin: 0 0 1em; }
|
||||||
|
p:last-child { margin-bottom: 0; }
|
||||||
|
|
||||||
|
b, strong { font-weight: bold; }
|
||||||
|
|
||||||
|
small { font-size: .9em; }
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
padding: 0 0 0 2em;
|
||||||
|
list-style: disc outside;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul ul { list-style-type: circle; }
|
||||||
|
ol { list-style-type: decimal; }
|
||||||
|
ol ol { list-style-type: lower-alpha; }
|
||||||
|
ol ol ol { list-style-type: lower-roman; }
|
||||||
|
|
||||||
|
li + li { margin-top: .25em; }
|
||||||
|
li > ul, li > ol, dd > ul, dd > ol { margin: .5em 0; }
|
||||||
|
|
||||||
|
dl { margin: 1.5em 0; }
|
||||||
|
dt { font-weight: bold; }
|
||||||
|
dd {
|
||||||
|
margin: .375em;
|
||||||
|
padding-left: 1em;
|
||||||
|
|
||||||
|
+ dt { margin-top: 1em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
dfn, var { font-style: normal; }
|
||||||
|
|
||||||
|
abbr, acronym, dfn {
|
||||||
|
cursor: help;
|
||||||
|
border-bottom: 1px dotted $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code, samp {
|
||||||
|
font-family: $monoFont;
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
color: $textColor;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
-moz-tab-size: 2;
|
||||||
|
-o-tab-size: 2;
|
||||||
|
tab-size: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
padding: .375rem .75rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow: auto;
|
||||||
|
font-size: .9em;
|
||||||
|
@extend %box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a > code { color: inherit; }
|
||||||
|
|
||||||
|
table {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
background: none;
|
||||||
|
border: 1px solid #d5d5d5;
|
||||||
|
border-collapse: separate;
|
||||||
|
border-spacing: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: 0 1px 1px rgba(black, .04);
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: .3em .7em;
|
||||||
|
padding-bottom: -webkit-calc(.3em + 1px);
|
||||||
|
padding-bottom: calc(.3em + 1px);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid #d5d5d5;
|
||||||
|
border-radius: 0;
|
||||||
|
@extend %heading-box;
|
||||||
|
|
||||||
|
&:empty {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ th { border-left: 1px solid rgba(black, .12); }
|
||||||
|
+ td { border-left: 1px solid #d5d5d5; }
|
||||||
|
|
||||||
|
tr:first-child > &:first-child { border-top-left-radius: 3px; }
|
||||||
|
tr:first-child > &:last-child { border-top-right-radius: 3px; }
|
||||||
|
tr:last-child > &:first-child { border-bottom-left-radius: 3px; }
|
||||||
|
thead > tr:last-child > &:first-child { border-bottom-left-radius: 0; }
|
||||||
|
tr:last-child > & { border-bottom-width: 0; }
|
||||||
|
thead > tr:last-child > & { border-bottom-width: 1px; }
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
|
||||||
|
+ td { border-left: 1px solid #e5e5e5; }
|
||||||
|
tr:last-child > & { border-bottom: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin: 0;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
line-height: normal;
|
||||||
|
@extend %border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="search"] { -webkit-appearance: textfield; }
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-ms-clear { display: none; }
|
||||||
|
|
||||||
|
::-moz-focus-inner {
|
||||||
|
padding: 0 !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder { color: #aaa; }
|
||||||
|
::-moz-placeholder { color: #aaa; opacity: 1; }
|
||||||
|
:-ms-input-placeholder { color: #aaa; }
|
@ -0,0 +1,143 @@
|
|||||||
|
//
|
||||||
|
// Utilities
|
||||||
|
//
|
||||||
|
|
||||||
|
%border-box {
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
%user-select-none {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Boxes
|
||||||
|
//
|
||||||
|
|
||||||
|
%box {
|
||||||
|
text-shadow: 0 1px rgba(white, .3);
|
||||||
|
background: #f8f8f8;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #d5d5d5 #d5d5d5 #d1d1d1;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 1px rgba(white, .3), // top inner glow
|
||||||
|
0 1px 1px rgba(black, .04); // drop shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
%heading-box {
|
||||||
|
text-shadow: 0 1px rgba(white, .3);
|
||||||
|
background: -webkit-linear-gradient(top, #f7f7fa, #f0f0f2);
|
||||||
|
background: linear-gradient(to bottom, #f7f7fa, #f0f0f2);
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #d5d5d5 #d5d5d5 #d1d1d1;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 1px rgba(white, .3), // top inner glow
|
||||||
|
inset 0 0 0 1px rgba(white, .2), // inner glow
|
||||||
|
0 1px 1px rgba(black, .04); // drop shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
%block-heading {
|
||||||
|
line-height: 1.25rem;
|
||||||
|
margin: 2em 0 1em;
|
||||||
|
padding: .5em .75em;
|
||||||
|
font-size: 1rem;
|
||||||
|
overflow: hidden;
|
||||||
|
@extend %heading-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Notes
|
||||||
|
//
|
||||||
|
|
||||||
|
%note {
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
padding: .5rem .875rem;
|
||||||
|
text-shadow: 0 1px rgba(white, .3);
|
||||||
|
background: #faf9e2;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #dddaaa #dddaaa #d7d7a9;
|
||||||
|
border-radius: 3px;
|
||||||
|
box-shadow: inset 0 1px rgba(white, .2), // top inner glow
|
||||||
|
0 1px 1px rgba(black, .04); // drop shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
%note-green {
|
||||||
|
background: #f1faeb;
|
||||||
|
border-color: #b3dba8 #b3dba8 #aed7a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
%note-blue {
|
||||||
|
background: #f2f2ff;
|
||||||
|
border-color: #c6cde9 #c6cde9 #c3cce7;
|
||||||
|
}
|
||||||
|
|
||||||
|
%note-gold {
|
||||||
|
background: #fff0aa;
|
||||||
|
border-color: #ddce81 #ddce81 #d9ca7f;
|
||||||
|
}
|
||||||
|
|
||||||
|
%note-red {
|
||||||
|
background: #fed5d3;
|
||||||
|
border-color: #dc7874 #dc7874 #d47474;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Labels
|
||||||
|
//
|
||||||
|
|
||||||
|
%label {
|
||||||
|
margin: 0 1px;
|
||||||
|
padding: 0 .3em 1px;
|
||||||
|
text-shadow: 0 1px rgba(white, .3);
|
||||||
|
background: #f2f2f2;
|
||||||
|
border: 1px solid;
|
||||||
|
border-color: #d2d2d2 #d2d2d2 #ccc;
|
||||||
|
border-radius: 2px;
|
||||||
|
box-shadow: 0 1px rgba(black, .04);
|
||||||
|
}
|
||||||
|
|
||||||
|
%block-label {
|
||||||
|
display: block;
|
||||||
|
margin: 2em 0 1em;
|
||||||
|
padding-left: .5em;
|
||||||
|
padding-right: .5em;
|
||||||
|
line-height: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
@extend %label;
|
||||||
|
}
|
||||||
|
|
||||||
|
%label-blue {
|
||||||
|
background: #d5daff;
|
||||||
|
border-color: #a3a4d4;
|
||||||
|
}
|
||||||
|
|
||||||
|
%label-yellow {
|
||||||
|
background: #ffdfb2;
|
||||||
|
border-color: #c2a16f;
|
||||||
|
}
|
||||||
|
|
||||||
|
%label-red {
|
||||||
|
background: #fed5d3;
|
||||||
|
border-color: #dc7874;
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// External links
|
||||||
|
//
|
||||||
|
|
||||||
|
%external-link {
|
||||||
|
&:after {
|
||||||
|
display: inline-block;
|
||||||
|
width: .5rem;
|
||||||
|
height: .4375rem;
|
||||||
|
margin: .125rem 0 0 .0625rem;
|
||||||
|
vertical-align: top;
|
||||||
|
@extend %icon, %icon-link;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
%internal-link:after { content: none !important; }
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue