commit 7ff29265785f5fe89bd2d622d219d235b8e2d735 Author: mntmn Date: Fri Apr 7 01:29:05 2017 +0200 initial commit. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..216f193 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +public/stylesheets/* +javascripts/maps +javascripts/spacedeck.js + diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 0000000..af47a5d --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,62 @@ +var gulp = require('gulp'); +var sass = require('gulp-sass'); +var concat = require('gulp-concat'); +var server = require('gulp-express'); +var nodemon = require('gulp-nodemon'); +var revReplace = require("gulp-rev-replace"); +var clean = require('gulp-clean'); + +var child_process = require('child_process'); +var path = require('path'); +var uglify = require('gulp-uglify'); +var fingerprint = require('gulp-fingerprint'); +var rev = require('gulp-rev'); + +var RevAll = require('gulp-rev-all'); + +gulp.task('rev', () => { + var revAll = new RevAll(); + return gulp.src(['public/**']) + .pipe(gulp.dest('build/assets')) + .pipe(revAll.revision()) + .pipe(gulp.dest('build/assets')) + .pipe(revAll.manifestFile()) + .pipe(gulp.dest('build/assets')); +}); + +gulp.task("all", ["styles", "uglify", "rev", "copylocales"], function(){ + var manifest = gulp.src("./build/assets/rev-manifest.json"); + return gulp.src("./views/**/*") + .pipe(revReplace({manifest: manifest})) + .pipe(gulp.dest("./build/views")); +}); + +gulp.task('copylocales', function(){ + return gulp.src('./locales/*.js').pipe(gulp.dest('./build/locales')); +}); + +gulp.task('clean', function () { + return gulp.src('./build').pipe(clean()); +}); + +gulp.task('styles', function() { + gulp.src('styles/**/*.scss') + .pipe(sass({ + errLogToConsole: true + })) + .pipe(gulp.dest('./public/stylesheets/')) + .pipe(concat('style.css')); +}); + +gulp.task('uglify', () => { + child_process.exec('sed -n \'s/.*script minify src="\\(.*\\)".*/.\\/public\\/\\1/p\' views/spacedeck.html', + function (error, stdout, stderr) { + var scripts = stdout.split('\n').map(function(p){return path.normalize(p)}).filter(function(p){return p!='.'}); + console.log("scripts: ",scripts); + + gulp.src(scripts) + .pipe(uglify({output:{beautify:true}})) + .pipe(concat('spacedeck.js')) + .pipe(gulp.dest('public/javascripts')); + }); +}); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2def0e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03431ae --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Spacedeck Open + +This is the free and open source version of Spacedeck, a web based, real time, collaborative whiteboard application with rich media support. Spacedeck was developed in 6 major releases during Autumn 2011 until the end of 2016 and was originally a commercial SaaS. The developers were Lukas F. Hartmann (mntmn) and Martin Güther (magegu). All icons and large parts of the CSS were designed by Thomas Helbig (dergraph). + +As we plan to retire the subscription based service at spacedeck.com in late 2017, we decided to open-source Spacedeck to allow educational and other organizations who currently rely on Spacedeck to migrate to a self-hosted version. + +Data migration features will be added soon. + +We appreciate filed issues, pull requests and general discussion. + +# Features + +- Create virtual whiteboards called "Spaces" with virtually unlimited size +- Drag & drop images, videos and audio from your computer or the web +- Write and format text with full control over fonts, colors and style +- Draw, annotate and highlight with included graphical shapes +- Turn your Space into a zooming presentation +- Collaborate and chat in realtime with teammates, students or friends +- Share Spaces on the web or via email +- Export your work as printable PDF or ZIP + +# Requirements, Installation + +Spacedeck uses the following major building blocks: + +- Node.js 4.x (Backend / API) +- MongoDB 3.x (Datastore) +- Redis 3.x (Datastore for realtime channels) +- Vue.js (Frontend) + +It also has some binary dependencies for media conversion and PDF export: + +- imagemagick + +Currently, media files are stored in Amazon S3, so you need an Amazon AWS account and have the ```AWS_ACCESS_KEY_ID``` and ```AWS_SECRET_ACCESS_KEY``` environment variables defined. For sending emails, Amazon SES is required. + +To install Spacedeck, you need node.js 4.x and a running MongoDB instance. Then, to install all node dependencies, run + + npm install + +To rebuild the frontend CSS styles (you need to do this at least once): + + gulp styles + +# Run + + export NODE_ENV=development + npm start + +# License + +Spacedeck Open is released under the GNU Affero General Public License Version 3 (GNU AGPLv3). + + Spacedeck Open - Web-based Collaborative Whiteboard For Rich Media + Copyright (C) 2011-2017 Lukas F. Hartmann, Martin Güther, Thomas Helbig + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . diff --git a/app.js b/app.js new file mode 100644 index 0000000..24b4fb5 --- /dev/null +++ b/app.js @@ -0,0 +1,173 @@ +"use strict"; + +require('./models/schema'); +require("log-timestamp"); + +const config = require('config'); +const redis = require('./helpers/redis'); +const websockets = require('./helpers/websockets'); + +const http = require('http'); +const path = require('path'); + +const _ = require('underscore'); +const favicon = require('serve-favicon'); +const logger = require('morgan'); +const cookieParser = require('cookie-parser'); +const bodyParser = require('body-parser'); +const mongoose = require('mongoose'); +const swig = require('swig'); +const i18n = require('i18n-2'); +const helmet = require('helmet'); + +const express = require('express'); +const app = express(); + +const isProduction = app.get('env') === 'production'; + +console.log("Booting Spacedeck Open… (environment: " + app.get('env') + ")"); + +app.use(logger(isProduction ? 'combined' : 'dev')); + +i18n.expressBind(app, { + locales: ["en", "de", "fr"], + defaultLocale: "en", + cookieName: "spacedeck_locale", + devMode: (app.get('env') == 'development') +}); + +swig.setDefaults({ + varControls: ["[[", "]]"] // otherwise it's not compatible with vue.js +}); + +swig.setFilter('cdn', function(input, idx) { + return input; +}); + +app.engine('html', swig.renderFile); +app.set('view engine', 'html'); + +if (app.get('env') != 'development') { + app.set('views', path.join(__dirname, 'build', 'views')); + app.use(favicon(path.join(__dirname, 'build', 'assets', 'images', 'favicon.png'))); + app.use(express.static(path.join(__dirname, 'build', 'assets'))); +} else { + app.set('views', path.join(__dirname, 'views')); + app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.png'))); + app.use(express.static(path.join(__dirname, 'public'))); +} + +app.use(bodyParser.json({ + limit: '50mb' +})); + +app.use(bodyParser.urlencoded({ + extended: false, + limit: '50mb' +})); + +app.use(cookieParser()); +app.use(helmet.noCache()) +app.use(helmet.frameguard()) +app.use(helmet.xssFilter()) +app.use(helmet.hsts({ + maxAge: 7776000000, + includeSubdomains: true +})) +app.disable('x-powered-by'); +app.use(helmet.noSniff()) + +// CUSTOM MIDDLEWARES + +app.use(require("./middlewares/templates")); +app.use(require("./middlewares/error_helpers")); +app.use(require("./middlewares/setuser")); +app.use(require("./middlewares/subdomain")); +app.use(require("./middlewares/cors")); +app.use(require("./middlewares/i18n")); +app.use("/api", require("./middlewares/api_helpers")); +app.use('/api/spaces/:id', require("./middlewares/space_helpers")); +app.use('/api/spaces/:id/artifacts/:artifact_id', require("./middlewares/artifact_helpers")); +app.use('/api/teams', require("./middlewares/team_helpers")); + +// REAL ROUTES + +app.use('/api/users', require('./routes/api/users')); +app.use('/api/memberships', require('./routes/api/memberships')); + +const spaceRouter = require('./routes/api/spaces'); +app.use('/api/spaces', spaceRouter); + +spaceRouter.use('/:id/artifacts', require('./routes/api/space_artifacts')); +spaceRouter.use('/:id/memberships', require('./routes/api/space_memberships')); +spaceRouter.use('/:id/messages', require('./routes/api/space_messages')); +spaceRouter.use('/:id/digest', require('./routes/api/space_digest')); +spaceRouter.use('/:id', require('./routes/api/space_exports')); + +app.use('/api/sessions', require('./routes/api/sessions')); +app.use('/api/teams', require('./routes/api/teams')); +app.use('/api/webgrabber', require('./routes/api/webgrabber')); +app.use('/', require('./routes/root')); + +// catch 404 and forward to error handler +app.use(require('./middlewares/404')); +if (app.get('env') == 'development') { + app.set('view cache', false); + swig.setDefaults({cache: false}); +} else { + app.use(require('./middlewares/500')); +} + +module.exports = app; + +// CONNECT TO DATABASE +const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || 'localhost'; +mongoose.connect('mongodb://' + mongoHost + '/spacedeck'); + +// START WEBSERVER +const port = 9000; + +const server = http.Server(app).listen(port, () => { + + if ("send" in process) { + process.send('online'); + } + +}).on('listening', () => { + + const host = server.address().address; + const port = server.address().port; + console.log('Spacedeck Open listening at http://%s:%s', host, port); + +}).on('error', (error) => { + + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +}); + +//WEBSOCKETS & WORKER +websockets.startWebsockets(server); +redis.connectRedis(); + +process.on('message', (message) => { + console.log("Process message:", message); + if (message === 'shutdown') { + console.log("Exiting spacedeck."); + process.exit(0); + } +}); diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..a1d9973 --- /dev/null +++ b/bin/www @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +var app = require('../app'); +var http = require('http'); +var server = http.createServer(app); diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..445fa23 --- /dev/null +++ b/config/default.json @@ -0,0 +1,9 @@ +{ + "endpoint": "http://localhost:9000", + "storage_bucket": "my_spacedeck_s3_bucket", + "storage_cdn": "xyz.cloudfront.net", + "google_access" : "", + "google_secret" : "", + "admin_pass": "very_secret_admin_password", + "phantom_api_secret": "very_secret_phantom_password" +} diff --git a/helpers/artifact_converter.js b/helpers/artifact_converter.js new file mode 100644 index 0000000..3605a90 --- /dev/null +++ b/helpers/artifact_converter.js @@ -0,0 +1,615 @@ +'use strict'; + +const exec = require('child_process'); +const gm = require('gm'); +const async = require('async'); +const fs = require('fs'); +const Models = require('../models/schema'); +const uploader = require('../helpers/uploader'); +const path = require('path'); + +const fileExtensionMap = { + ".amr" : "audio/AMR", + ".ogg" : "audio/ogg", + ".aac" : "audio/aac", + ".mp3" : "audio/mpeg", + ".mpg" : "video/mpeg", + ".3ga" : "audio/3ga", + ".mp4" : "video/mp4", + ".wav" : "audio/wav", + ".mov" : "video/quicktime", + ".doc" : "application/msword", + ".dot" : "application/msword", + ".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".dotx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + ".docm" : "application/vnd.ms-word.document.macroEnabled.12", + ".dotm" : "application/vnd.ms-word.template.macroEnabled.12", + ".xls" : "application/vnd.ms-excel", + ".xlt" : "application/vnd.ms-excel", + ".xla" : "application/vnd.ms-excel", + ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xltx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + ".xlsm" : "application/vnd.ms-excel.sheet.macroEnabled.12", + ".xltm" : "application/vnd.ms-excel.template.macroEnabled.12", + ".xlam" : "application/vnd.ms-excel.addin.macroEnabled.12", + ".xlsb" : "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + ".ppt" : "application/vnd.ms-powerpoint", + ".pot" : "application/vnd.ms-powerpoint", + ".pps" : "application/vnd.ms-powerpoint", + ".ppa" : "application/vnd.ms-powerpoint", + ".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".potx" : "application/vnd.openxmlformats-officedocument.presentationml.template", + ".ppsx" : "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + ".ppam" : "application/vnd.ms-powerpoint.addin.macroEnabled.12", + ".pptm" : "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + ".potm" : "application/vnd.ms-powerpoint.template.macroEnabled.12", + ".ppsm" : "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + ".key" : "application/x-iwork-keynote-sffkey", + ".pages" : "application/x-iwork-pages-sffpages", + ".numbers" : "application/x-iwork-numbers-sffnumbers", + ".ttf" : "application/x-font-ttf" +}; + +const convertableImageTypes = [ + "image/png", + "image/jpeg", + "application/pdf", + "image/jpg", + "image/gif", + "image/tiff", + "image/vnd.adobe.photoshop"]; + +const convertableVideoTypes = [ + "video/quicktime", + "video/3gpp", + "video/mpeg", + "video/mp4", + "video/ogg"]; + +const convertableAudioTypes = [ + "application/ogg", + "audio/AMR", + "audio/3ga", + "audio/wav", + "audio/3gpp", + "audio/x-wav", + "audio/aiff", + "audio/x-aiff", + "audio/ogg", + "audio/mp4", + "audio/x-m4a", + "audio/mpeg", + "audio/mp3", + "audio/x-hx-aac-adts", + "audio/aac"]; + + +function getDuration(localFilePath, callback){ + exec.execFile("ffprobe", ["-show_format", "-of", "json", localFilePath], function(error, stdout, stderr) { + var test = JSON.parse(stdout); + callback(parseFloat(test.format.duration)); + }); +} + +function createWaveform(fileName, localFilePath, callback){ + var filePathImage = localFilePath + "-" + (new Date().getTime()) + ".png"; + + getDuration(localFilePath, function(duration){ + var totalTime = duration || 1.0; + var pixelsPerSecond = 256.0; + do { + var targetWidth = parseInt(pixelsPerSecond*totalTime, 10); + if (targetWidth>2048) pixelsPerSecond/=2.0; + } while (targetWidth>2048 && pixelsPerSecond>1); + + exec.execFile("audiowaveform", + [ + "-w", + ""+targetWidth, + "--pixels-per-second", + ""+parseInt(pixelsPerSecond), + "--background-color", "ffffff00", + "--border-color", "ffffff", + "--waveform-color", "3498db", + "--no-axis-labels", + "-i", localFilePath, "-o", filePathImage + ], + {}, function(error, stdout, stderr) { + if(!error) { + callback(null, filePathImage); + } else { + console.log("error:", stdout, stderr); + callback(error, null); + } + }); + }); +} + +function convertVideo(fileName, filePath, codec, callback, progress_callback) { + var ext = path.extname(fileName); + var presetMime = fileExtensionMap[ext]; + + var newExt = codec == "mp4" ? "mp4" : "ogv"; + var convertedPath = filePath + "." + newExt; + + console.log("converting", filePath, "to", convertedPath, "progress_cb:",progress_callback); + + var convertArgs = (codec == "mp4") ? [ + "-i", filePath, + "-threads", "4", + "-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value + "-b:v", "2000k", + "-acodec", "libvo_aacenc", + "-b:a", "96k", + "-vcodec", "libx264", + "-y", convertedPath ] + : [ + "-i", filePath, + "-threads", "4", + "-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value + "-b:v", "2000k", + "-acodec", "libvorbis", + "-b:a", "96k", + "-vcodec", "libtheora", + "-y", convertedPath]; + + var ff = exec.spawn('ffmpeg', convertArgs, { + stdio: [ + 'pipe', // use parents stdin for child + 'pipe', // pipe child's stdout to parent + 'pipe' + ] + }); + + ff.stdout.on('data', function (data) { + console.log('[ffmpeg-video] stdout: ' + data); + }); + + ff.stderr.on('data', function (data) { + console.log('[ffmpeg-video] stderr: ' + data); + if (progress_callback) { + progress_callback(data); + } + }); + + ff.on('close', function (code) { + console.log('[ffmpeg-video] child process exited with code ' + code); + if (!code) { + console.log("converted", filePath, "to", convertedPath); + callback(null, convertedPath); + } else { + callback(code, null); + } + }); +} + +function convertAudio(fileName, filePath, codec, callback) { + var ext = path.extname(fileName); + var presetMime = fileExtensionMap[ext]; + + var newExt = codec == "mp3" ? "mp3" : "ogg"; + var convertedPath = filePath + "." + newExt; + + console.log("converting audio", filePath, "to", convertedPath); + + var convertArgs = (ext == ".aac") ? [ "-i", filePath, "-y", convertedPath ] + : [ "-i", filePath, + "-b:a", "128k", + "-y", convertedPath]; + + exec.execFile("ffmpeg", convertArgs , {}, function(error, stdout, stderr) { + if(!error){ + console.log("converted", filePath, "to", convertedPath); + callback(null, convertedPath); + }else{ + console.log(error,stdout, stderr); + callback(error, null); + } + }); +} + +function createThumbnailForVideo(fileName, filePath, callback) { + var filePathImage = filePath + ".jpg"; + exec.execFile("ffmpeg", ["-y", "-i", filePath, "-ss", "00:00:01.00", "-vcodec", "mjpeg", "-vframes", "1", "-f", "image2", filePathImage], {}, function(error, stdout, stderr){ + if(!error){ + callback(null, filePathImage); + }else{ + console.log("error:", stdout, stderr); + callback(error, null); + } + }); +} + +function getMime(fileName, filePath, callback) { + var ext = path.extname(fileName); + var presetMime = fileExtensionMap[ext]; + + if (presetMime) { + callback(null, presetMime); + } else { + exec.execFile("file", ["-b","--mime-type", filePath], {}, function(error, stdout, stderr) { + console.log("file stdout: ",stdout); + if (stderr === '' && error == null) { + //filter special chars from commandline + var mime = stdout.replace(/[^a-zA-Z0-9\/\-]/g,''); + callback(null, mime); + } else { + console.log("getMime file error: ", error); + callback(error, null); + } + }); + } +} + +function resizeAndUpload(a, size, max, fileName, localFilePath, callback) { + if (max>320 || size.width > max || size.height > max) { + var resizedFileName = max + "_"+fileName; + var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + resizedFileName; + var localResizedFilePath = "/tmp/"+resizedFileName; + gm(localFilePath).resize(max, max).autoOrient().write(localResizedFilePath, function (err) { + if(!err) { + uploader.uploadFile(s3Key, "image/jpeg", localResizedFilePath, function(err, url) { + if (err) callback(err); + else{ + console.log(localResizedFilePath); + fs.unlink(localResizedFilePath, function (err) { + if (err) { + console.error(err); + callback(null, url); + } + else callback(null, url); + }); + } + }); + } else { + console.error(err); + callback(err); + } + }); + } else { + callback(null, ""); + } +} + + +var resizeAndUploadImage = function(a, mime, size, fileName, fileNameOrg, imageFilePath, originalFilePath, payloadCallback) { + async.parallel({ + small: function(callback){ + resizeAndUpload(a, size, 320, fileName, imageFilePath, callback); + }, + medium: function(callback){ + resizeAndUpload(a, size, 800, fileName, imageFilePath, callback); + }, + big: function(callback){ + resizeAndUpload(a, size, 1920, fileName, imageFilePath, callback); + }, + original: function(callback){ + var s3Key = "s"+ a.space_id.toString() + "/a" + a._id + "/" + fileNameOrg; + uploader.uploadFile(s3Key, mime, originalFilePath, function(err, url){ + callback(null, url); + }); + } + }, function(err, results) { + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(originalFilePath); + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = results.small; + a.payload_thumbnail_medium_uri = results.medium; + a.payload_thumbnail_big_uri = results.big; + a.payload_uri = results.original; + + var factor = 320/size.width; + var newBoardSpecs = a.board; + newBoardSpecs.w = Math.round(size.width*factor); + newBoardSpecs.h = Math.round(size.height*factor); + a.board = newBoardSpecs; + + a.updated_at = new Date(); + a.save(function(err) { + if(err) payloadCallback(err, null); + else { + fs.unlink(originalFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + originalFilePath); + payloadCallback(null, a); + } + }); + } + }); + }); +}; + +module.exports = { + convert: function(a, fileName, localFilePath, payloadCallback, progress_callback) { + getMime(fileName, localFilePath, function(err, mime){ + console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mime:", mime); + + if (!err) { + if (convertableImageTypes.indexOf(mime) > -1) { + + gm(localFilePath).size(function (err, size) { + console.log("[convert] gm:", err, size); + + if (!err) { + if(mime == "application/pdf") { + var firstImagePath = localFilePath + ".jpeg"; + exec.execFile("gs", ["-sDEVICE=jpeg","-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-dFirstPage=1", "-dLastPage=1", "-sOutputFile=" + firstImagePath, "-r90", "-f", localFilePath], {}, function(error, stdout, stderr) { + if(error === null) { + resizeAndUploadImage(a, mime, size, fileName + ".jpeg", fileName, firstImagePath, localFilePath, function(err, a) { + fs.unlink(firstImagePath, function (err) { + payloadCallback(err, a); + }); + }); + } else { + payloadCallback(error, null); + } + }); + + } else if(mime == "image/gif") { + //gifs are buggy after convertion, so we should not convert them + + var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName; + + uploader.uploadFile(s3Key, "image/gif", localFilePath, function(err, url) { + if(err)callback(err); + else{ + console.log(localFilePath); + var stats = fs.statSync(localFilePath); + + a.state = "idle"; + a.mime = mime; + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = url; + a.payload_thumbnail_medium_uri = url; + a.payload_thumbnail_big_uri = url; + a.payload_uri = url; + + var factor = 320/size.width; + var newBoardSpecs = a.board; + newBoardSpecs.w = Math.round(size.width*factor); + newBoardSpecs.h = Math.round(size.height*factor); + a.board = newBoardSpecs; + + a.updated_at = new Date(); + a.save(function(err){ + if(err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + localFilePath); + payloadCallback(null, a); + } + }); + } + }); + } + }); + + } else { + resizeAndUploadImage(a, mime, size, fileName, fileName, localFilePath, localFilePath, payloadCallback); + } + } else payloadCallback(err); + }); + + } else if (convertableVideoTypes.indexOf(mime) > -1) { + async.parallel({ + thumbnail: function(callback) { + createThumbnailForVideo(fileName, localFilePath, function(err, created){ + console.log("thumbnail created: ", err, created); + if(err) callback(err); + else{ + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".jpg" ; + uploader.uploadFile(keyName, "image/jpeg", created, function(err, url){ + if (err) callback(err); + else callback(null, url); + }); + } + }); + }, + ogg: function(callback) { + if (mime == "video/ogg") { + callback(null, "org"); + } else { + convertVideo(fileName, localFilePath, "ogg", function(err, file) { + if(err) callback(err); + else { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogv" ; + uploader.uploadFile(keyName, "video/ogg", file, function(err, url){ + if (err) callback(err); + else callback(null, url); + }); + } + }, progress_callback); + } + }, + mp4: function(callback) { + if (mime == "video/mp4") { + callback(null, "org"); + } else { + convertVideo(fileName, localFilePath, "mp4", function(err, file) { + if (err) callback(err); + else { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp4"; + uploader.uploadFile(keyName, "video/mp4" ,file, function(err, url) { + if (err) callback(err); + else callback(null, url); + }); + } + }, progress_callback); + } + }, + original: function(callback){ + uploader.uploadFile(fileName, mime, localFilePath, function(err, url){ + callback(null, url); + }); + } + }, function(err, results){ + console.log(err, results); + + if (err) payloadCallback(err, a); + else { + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(localFilePath); + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = results.thumbnail; + a.payload_thumbnail_medium_uri = results.thumbnail; + a.payload_thumbnail_big_uri = results.thumbnail; + a.payload_uri = results.original; + + if (mime == "video/mp4") { + a.payload_alternatives = [ + { + mime: "video/ogg", + payload_uri: results.ogg + } + ]; + } else { + a.payload_alternatives = [ + { + mime: "video/mp4", + payload_uri: results.mp4 + } + ]; + } + + a.updated_at = new Date(); + a.save(function(err) { + if (err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + localFilePath); + payloadCallback(null, a); + } + }); + } + }); + } + }); + + } else if (convertableAudioTypes.indexOf(mime) > -1) { + + async.parallel({ + ogg: function(callback) { + convertAudio(fileName, localFilePath, "ogg", function(err, file) { + if(err) callback(err); + else { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogg" ; + uploader.uploadFile(keyName, "audio/ogg", file, function(err, url){ + if (err) callback(err); + else callback(null, url); + }); + } + }); + }, + mp3_waveform: function(callback) { + convertAudio(fileName, localFilePath, "mp3", function(err, file) { + if(err) callback(err); + else { + + createWaveform(fileName, file, function(err, filePath){ + + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + "-" + (new Date().getTime()) + ".png"; + uploader.uploadFile(keyName, "image/png", filePath, function(err, pngUrl){ + + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp3" ; + uploader.uploadFile(keyName, "audio/mp3", file, function(err, mp3Url){ + if (err) callback(err); + else callback(null, {waveform: pngUrl, mp3: mp3Url}); + }); + + }); + }); + } + }); + }, + original: function(callback) { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName; + uploader.uploadFile(keyName, mime, localFilePath, function(err, url){ + callback(null, url); + }); + } + }, function(err, results) { + console.log(err, results); + + if (err) payloadCallback(err, a); + else { + + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(localFilePath); + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = results.mp3_waveform.waveform; + a.payload_thumbnail_medium_uri = results.mp3_waveform.waveform; + a.payload_thumbnail_big_uri = results.mp3_waveform.waveform; + a.payload_uri = results.original; + a.payload_alternatives = [ + {payload_uri:results.ogg, mime:"audio/ogg"}, + {payload_uri:results.mp3_waveform.mp3, mime:"audio/mpeg"} + ]; + + a.updated_at = new Date(); + a.save(function(err){ + if(err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + localFilePath); + payloadCallback(null, a); + } + }); + } + }); + } + }); + + + } else { + console.log("mime not matched for conversion, storing file"); + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName; + uploader.uploadFile(keyName, mime, localFilePath, function(err, url) { + + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(localFilePath); + a.payload_size = stats["size"]; + a.payload_uri = url; + + a.updated_at = new Date(); + a.save(function(err) { + if(err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + payloadCallback(null, a); + }); + } + }); + }); + } + } else { + //there was an error getting mime + payloadCallback(err); + } + }); + } +}; + + diff --git a/helpers/mailer.js b/helpers/mailer.js new file mode 100644 index 0000000..8da189d --- /dev/null +++ b/helpers/mailer.js @@ -0,0 +1,61 @@ +'use strict'; + +var swig = require('swig'); +var AWS = require('aws-sdk'); + +module.exports = { + sendMail: (to_email, subject, body, options) => { + + if (!options) { + options = {}; + } + + // FIXME + const teamname = options.teamname || "My Open Spacedeck" + const from = teamname + ' '; + + let reply_to = [from]; + if (options.reply_to) { + reply_to = [options.reply_to]; + } + + let plaintext = body; + if (options.action && options.action.link) { + plaintext+="\n"+options.action.link+"\n\n"; + } + + const htmlText = swig.renderFile('./views/emails/action.html', { + text: body.replace(/(?:\n)/g, '
'), + options: options + }); + + if (process.env.NODE_ENV === 'development') { + console.log("Email: to " + to_email + " in production.\nreply_to: " + reply_to + "\nsubject: " + subject + "\nbody: \n" + htmlText + "\n\n plaintext:\n" + plaintext); + } else { + AWS.config.update({region: 'eu-west-1'}); + var ses = new AWS.SES(); + + ses.sendEmail( { + Source: from, + Destination: { ToAddresses: [to_email] }, + ReplyToAddresses: reply_to, + Message: { + Subject: { + Data: subject + }, + Body: { + Text: { + Data: plaintext, + }, + Html: { + Data: htmlText + } + } + } + }, function(err, data) { + if(err) console.log('Email not sent:', err); + else console.log("Email sent."); + }); + } + } +}; diff --git a/helpers/phantom.js b/helpers/phantom.js new file mode 100644 index 0000000..c195a52 --- /dev/null +++ b/helpers/phantom.js @@ -0,0 +1,64 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); +var phantom = require('node-phantom-simple'); + +module.exports = { + // type = "pdf" or "png" + takeScreenshot: function(space,type,on_success,on_error) { + var spaceId = space._id; + var space_url = config.get("endpoint")+"/api/spaces/"+spaceId+"/html"; + + var export_path = "/tmp/"+spaceId+"."+type; + + var timeout = 5000; + if (type=="pdf") timeout = 30000; + + space_url += "?api_token="+config.get("phantom_api_secret"); + + console.log("[space-screenshot] url: "+space_url); + console.log("[space-screenshot] export_path: "+export_path); + + var on_success_called = false; + + var on_exit = function(exit_code) { + if (exit_code>0) { + console.log("phantom abnormal exit for url "+space_url); + if (!on_success_called && on_error) { + on_error(); + } + } + }; + + phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) { + return browser.createPage(function (err, page) { + console.log("page created, opening ",space_url); + + if (type=="pdf") { + var psz = { + width: space.advanced.width+"px", + height: space.advanced.height+"px" + }; + page.set('paperSize', psz); + } + + page.set('settings.resourceTimeout',timeout); + page.set('settings.javascriptEnabled',false); + + return page.open(space_url, function (err,status) { + page.render(export_path, function() { + on_success_called = true; + if (on_success) { + on_success(export_path); + } + page.close(); + browser.exit(); + }); + }); + }); + }, { + onExit: on_exit + }); + } +}; diff --git a/helpers/redis.js b/helpers/redis.js new file mode 100644 index 0000000..90b440a --- /dev/null +++ b/helpers/redis.js @@ -0,0 +1,61 @@ +'use strict'; + +const RedisConnection = require('ioredis'); +const websockets = require('./websockets'); + +module.exports = { + connectRedis(){ + const redisHost = process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost'; + this.connection = new RedisConnection(6379, redisHost); + }, + sendMessage(action, model, attributes, channelId) { + const data = JSON.stringify({ + channel_id: channelId, + action: action, + model: model, + object: attributes + }); + this.connection.publish('updates', data); + }, + logIp(ip, cb) { + this.connection.incr("ip_"+ ip, (err, socketCounter) => { + cb(); + }); + }, + rateLimit(namespace, ip, cb) { + const key = "limit_"+ namespace + "_"+ ip; + const redis = this.connection; + + redis.get(key, (err, count)=> { + if (count) { + if(count < 150) { + redis.incr(key, (err, newCount) => { + if (newCount==150) { + // limit + } + cb(true); + }); + } else { + cb(false); + } + } else { + redis.set(key, 1, (err, count) => { + redis.expire(key, 1800, (err, expResult) => { + cb(true); + }); + }); + } + }); + }, + isOnlineInSpace(user, space, cb) { + this.connection.smembers("space_" + space._id.toString(), function(err, list) { + if (err) cb(err); + else { + var users = list.filter(function(item) { + return user._id.toString() === item; + }); + cb(null, (users.length > 0)); + } + }); + } +}; diff --git a/helpers/space-render.js b/helpers/space-render.js new file mode 100644 index 0000000..99164ef --- /dev/null +++ b/helpers/space-render.js @@ -0,0 +1,149 @@ +var fs = require('fs'); +var cheerio = require("cheerio"); + +var artifact_vector_render = require("../public/javascripts/vector-render.js"); + +global.render_vector_shape = artifact_vector_render.render_vector_shape; +global.render_vector_drawing = artifact_vector_render.render_vector_drawing; + +var artifact_view_model = require("../public/javascripts/spacedeck_board_artifacts.js").SpacedeckBoardArtifacts; + +var template = fs.readFileSync("views/partials/space-isolated.html"); + +var dom = cheerio.load(template); + +var compiled_js = ""; + +function emit(str,indent) { + var spaces=""; + for (var i=0; i0; i--) { + indent--; + emit("}",indent); + } + } +} + +function render_space_as_html(space, artifacts) { + if (!compiled_js.length) { + walk(dom("#space")[0],0); + //console.log("compiled template: \n"+compiled_js); + } + + // -------- + var mouse_state = "idle"; + var active_tool = "pointer"; + var active_space = space; + var active_space_artifacts = artifacts; + + var bounds_zoom = 1; + var bounds_margin_horiz = 0; + var bounds_margin_vert = 0; + var viewport_zoom = 1; + // -------- + + var editing_artifact_id = null; + var urls_to_links = function(html) { + return html; + } + + artifact_view_model.selected_artifacts_dict = {}; + + for (var i=0; i\n\n\n'+h+"\n\n"; + + return h; +} + +exports.render_space_as_html = render_space_as_html; + diff --git a/helpers/uploader.js b/helpers/uploader.js new file mode 100644 index 0000000..c314062 --- /dev/null +++ b/helpers/uploader.js @@ -0,0 +1,64 @@ +'use strict'; + +var AWS = require('aws-sdk'); +AWS.config.region = 'eu-central-1'; + +var fs = require('fs'); +var config = require('config'); + +module.exports = { + removeFile: (path, callback) => { + const s3 = new AWS.S3({ + region: 'eu-central-1' + }); + const bucket = config.get("storage_bucket"); + s3.deleteObject({ + Bucket: bucket, Key: path + }, (err, res) => { + if (err){ + console.error(err); + callback(err); + }else { + callback(null, res); + } + }); + }, + uploadFile: function(fileName, mime, localFilePath, callback) { + if (typeof(localFilePath)!="string") { + callback({error:"missing path"}, null); + return; + } + console.log("[s3] uploading", localFilePath, " to ", fileName); + + const bucket = config.get("storage_bucket"); + const fileStream = fs.createReadStream(localFilePath); + fileStream.on('error', function (err) { + if (err) { + console.error(err); + callback(err); + } + }); + fileStream.on('open', function () { + // FIXME + var s3 = new AWS.S3({ + region: 'eu-central-1' + }); + + s3.putObject({ + Bucket: bucket, + Key: fileName, + ContentType: mime, + Body: fileStream + }, function (err) { + if (err){ + console.error(err); + callback(err); + }else { + const url = "https://"+ config.get("storage_cdn") + "/" + fileName; + console.log("[s3]" + localFilePath + " to " + url); + callback(null, url); + } + }); + }); + } +}; diff --git a/helpers/websockets.js b/helpers/websockets.js new file mode 100644 index 0000000..26fcd1c --- /dev/null +++ b/helpers/websockets.js @@ -0,0 +1,291 @@ +'use strict'; +require('../models/schema'); + +const WebSocketServer = require('ws').Server; + +const Redis = require('ioredis'); +const async = require('async'); +const _ = require("underscore"); +const mongoose = require("mongoose"); +const crypto = require('crypto'); + +module.exports = { + startWebsockets: function(server){ + this.setupSubscription(); + this.state = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost'); + + if(!this.current_websockets){ + this.current_websockets = []; + } + + const wss = new WebSocketServer({ server:server, path: "/socket" }); + wss.on('connection', function(ws) { + + this.state.incr("socket_id", function(err, socketCounter) { + const socketId = "socket_" + socketCounter + "_" + crypto.randomBytes(64).toString('hex').substring(0,8); + const serverScope = this; + + ws.on('message', function(msgString){ + const socket = this; + + const msg = JSON.parse(msgString); + + if(msg.action == "auth"){ + + const token = msg.auth_token; + const editorName = msg.editor_name; + const editorAuth = msg.editor_auth; + const spaceId = msg.space_id; + + Space.findOne({"_id": spaceId}).populate('creator').exec((err, space) => { + if (space) { + const upgradeSocket = function() { + if (token) { + User.findBySessionToken(token, function(err, user) { + if (err) { + console.error(err, user); + } else { + if (user) { + serverScope.addUserInSpace(user._id, space, ws, function(err){ + serverScope.addLocalUser(user._id, ws); + console.log("[websockets] user " + user.email + " online in space " + space._id); + }); + } + } + }); + } else { + const anonymousUserId = space._id + "-" + editorName; + + if(space.access_mode == "private" && space.edit_hash != editorAuth){ + console.error("closing websocket: unauthed."); + ws.send(JSON.stringify({error: "auth_failed"})); + // ws.close(); + return; + } + + serverScope.addUserInSpace(anonymousUserId, space, ws, function(err){ + serverScope.addLocalUser(anonymousUserId, ws); + console.log("[websockets] anonymous user " + anonymousUserId + " online in space " + space._id); + }); + } + }; + + if (!ws.id) { + ws['id'] = socketId; + try { + ws.send(JSON.stringify({"action": "init", "channel_id": socketId})); + } catch (e) { + console.log("ws.send error: "+e); + } + } + + if (ws.space_id) { + serverScope.removeUserInSpace(ws.space_id, ws, function(err) { + upgradeSocket(); + }); + } else { + upgradeSocket(); + } + } else { + ws.send(JSON.stringify({error: "space not found"})); + ws.close(); + return; + } + }); + + } else if (msg.action == "cursor" || msg.action == "viewport" || msg.action=="media") { + msg.space_id = socket.space_id; + msg.from_socket_id = socket.id; + serverScope.state.publish('cursors', JSON.stringify(msg)); + } + }); + + ws.on('close', function(evt) { + console.log("websocket closed: ", ws.id, ws.space_id); + const spaceId = ws.space_id; + serverScope.removeUserInSpace(spaceId, ws, function(err) { + this.removeLocalUser(ws, function(err) { + }.bind(this)); + }.bind(this)); + }.bind(this)); + + ws.on('error', function(ws, err) { + console.error(err, res); + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + setupSubscription: function() { + this.cursorSubscriber = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost'); + this.cursorSubscriber.subscribe(['cursors', 'users', 'updates'], function (err, count) { + console.log("[redis] websockets to " + count + " topics." ); + }); + this.cursorSubscriber.on('message', function (channel, rawMessage) { + const msg = JSON.parse(rawMessage); + const spaceId = msg.space_id; + + const websockets = this.current_websockets; + + if(channel === "updates") { + for(let i=0;i { + console.log("old websocket removed"); + }); + } + } + } + } + } else { + console.error("userlist undefined for websocket"); + } + } else if(channel === "cursors") { + const socketId = msg.from_socket_id; + for (let i=0;i -1) { + this.removed_items = this.current_websockets.splice(idx, 1); + console.log("removed local socket, current online on this process: ", this.current_websockets.length); + } else { + console.log("websocket not found to remove"); + } + + this.state.del(ws.id, function(err, res) { + if (err) console.error(err, res); + else { + this.removeUserInSpace(ws.space_id, ws, (err) => { + console.log("removed user from space list"); + this.distributeUsers(ws.space_id); + }) + if(cb) + cb(err); + } + }.bind(this)); + }, + + addUserInSpace: function(username, space, ws, cb) { + console.log("[websockets] user "+username+" in "+space.access_mode +" space " + space._id + " with socket " + ws.id); + this.state.set(ws.id, username, function(err, res) { + if(err) console.error(err, res); + else { + this.state.sadd("space_" + space._id, ws.id, function(err, res) { + if(err) cb(err); + else { + ws['space_id'] = space._id.toString(); + + this.distributeUsers(ws.space_id); + if(cb) + cb(); + } + }.bind(this)); + } + }.bind(this)); + }, + removeUserInSpace: function(spaceId, ws, cb) { + this.state.srem("space_" + spaceId, ws.id, function(err, res) { + if (err) cb(err); + else { + console.log("[websockets] socket "+ ws.id + " went offline in space " + spaceId); + this.distributeUsers(spaceId); + ws['space_id'] = null; + + if (cb) + cb(); + } + }.bind(this)); + }, + + distributeUsers: function(spaceId) { + if(!spaceId) + return; + + this.state.smembers("space_" + spaceId, function(err, list) { + async.map(list, function(item, callback) { + this.state.get(item, function(err, userId) { + console.log(item, "->", userId); + callback(null, userId); + }); + }.bind(this), function(err, userIds) { + const uniqueUserIds = _.unique(userIds); + const validUserIds = _.filter(uniqueUserIds, function(uId) { + return mongoose.Types.ObjectId.isValid(uId); + }); + + const nonValidUserIds = _.filter(uniqueUserIds, function(uId) { + return (uId !== null && !mongoose.Types.ObjectId.isValid(uId)); + }); + + const anonymousUsers = _.map(nonValidUserIds, function(nonValidId) { + const realNickname = nonValidId.slice(nonValidId.indexOf("-")+1); + return {nickname: realNickname, email: null, avatar_thumbnail_uri: null }; + }); + + User.find({"_id" : { "$in" : validUserIds }}, { "nickname" : 1 , "email" : 1, "avatar_thumbnail_uri": 1 }, function(err, users) { + if (err) + console.error(err); + else { + const allUsers = users.concat(anonymousUsers); + const strUsers = JSON.stringify({users: allUsers, space_id: spaceId}); + this.state.publish("users", strUsers); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + } +}; diff --git a/locales/de.js b/locales/de.js new file mode 100644 index 0000000..05b2605 --- /dev/null +++ b/locales/de.js @@ -0,0 +1,321 @@ +{ + "lang": "de", + "ok": "OK", + "cancel": "Abbrechen", + "close": "Schließen", + "open": "Öffnen", + "folder": "Ordner", + "duplicate": "Duplizieren", + "save": "Speichern", + "saved": "Gespeichert", + "created": "Erstellt", + "delete": "Löschen", + "remove": "Entfernen", + "set": "Übernehmen", + "reset": "Zurücksetzen", + "thanks": "Danke", + "share": "Teilen", + "signup": "Registrieren", + "login": "Anmelden", + "logout": "Abmelden", + "email": "E-Mail-Adresse", + "password": "Passwort", + "width": "Breite", + "height": "Höhe", + "nick": "Benutzername", + "role": "Rolle", + "members": "Mitglieder", + "actions": "Aktionen", + "or": "oder", + "you": "du", + "via": "via", + "by": "von", + "new": "Neu", + "zero": "Null", + "page": "Seite", + "copy": "Kopie", + "home": "Übersicht", + "owner": "Besitzer", + "space": "Space", + "second": "Sekunde", + "not_found": "Nicht Gefunden.", + "untitled_space": "Unbenannter Space", + "untitled_folder": "Unbenannter Order", + "untitled": "Unbenannter", + "sure": "Bist du sicher?", + "specify": "Bitte spezifiziere", + "confirm": "Bitte bestätige", + "signup_google": "Mit Google anmelden", + "error_unknown_email": "Unbekannte Kombination von Email und Passwort. Oder versuche dich mit Google anzumelden.", + "error_password_confirmation": "Die beiden Passwörter stimmen nicht überein.", + "error_domain_blocked": "Diese Domain ist gesperrt.", + "error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.", + "support": "Spacedeck-Support", + "offline": "Verbindungsverlust. Mehr Infos hier.", + "error": "Entschuldigung, etwas ist schiefgegangen. Bitte kontaktiere support@spacedeck.com", + "welcome": "Willkommen", + "claim": "Dein digitales Whiteboard.", + "trynow": "Jetzt probieren.", + "about": "Über uns", + "terms": "AGBs", + "contact": "Kontakt", + "privacy": "Privatsphäre", + "post_adress": "Postadresse", + "phone": "Phone", + "business_address": "business_address", + "ceo": "Geschäftsführer", + "business_adress": "business_adress", + "title": "Titel", + "name": "Name", + "confirm_subject": "E-Mail Bestätigung für Spacedeck", + "confirm_body": "Danke, dass du dich bei Spacedeck angemeldet hast.\nBitte klicke auf den folgenden Link, um deine E-Mail Adresse zu bestätigen.\n ", + "confirm_action": "E-Mail Bestätigen", + "team_invite_membership_subject": "Einladung zu %s auf Spacedeck", + "team_invite_membership_body": "Du wurdest zu %s auf Spacedeck eingeladen. \n\nBitte klicke auf den folgenden Link, um die Einladung anzunehmen.\n", + "team_invite_user_body": "Du wurdest zu %s auf Spacedeck eingeladen. Dein temporäres Passwort ist \"%s\".\n Bitte klicke auf den folgenden Link, um die Einladung anzunehmen.", + "team_invite_admin_body": " %s wurde zu %s auf Spacedeck eingeladen. Das temporäres Passwort ist \"%s\".", + "team_invite_membership_action": "Annehmen", + "team_new_member_subject": "Neues Team Mitglied", + "team_new_member_body": "%s hat gerade seine Einladung zum Team %s angenommen.", + "space_invite_membership_subject": "Einladung von %s in Space %s", + "space_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.", + "space_invite_membership_action": "Annehmen", + "folder_invite_membership_subject": "Einladung von %s in Ordner %s", + "folder_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.", + "folder_invite_membership_action": "Accept", + "upgrade": "Upgrade", + "upgrade_now": "Jetzt Upgraden", + "create_space": "Space Erstellen", + "create_folder": "Ordner Erstellen", + "email_unconfirmed": "Email Unbestätigt", + "confirmation_sent": "Email Versandt", + "folder_filter": "Filter", + "sort_by": "Reihenfolge", + "last_modified": "Zuletzt Geändert", + "last_opened": "Zuletzt Geöffnet", + "edit_team": "Team Verwalten", + "edit_account": "Konto Bearbeiten", + "log_out": "Abmelden", + "no_spaces_yet": "Du hast noch keine Spaces erstellt.", + "new_folder_title": "Neuer Titel für Ordner", + "folder_settings": "Ordner-Einstellungen", + "upload_cover_image": "Ordnerbild Hochladen", + "spacedeck_pro_ad_folders": "Mit Spacedeck Pro kannst du beliebig viele Spaces in Ordnerstrukturen organisieren und für jeden Ordner Zugriffsrechte regeln. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_versions": "Mit Spacedeck Pro kannst du beliebig viele Versionen deines Spaces festhalten und später die Entwicklungsgeschichte nachvollziehen. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_pdf": "Mit Spacedeck Pro kannst du Spaces und sogar ganze Ordner als druckreife PDFs exportieren oder per Mail versenden. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_zip": "Mit Spacedeck Pro kannst du die Inhalte deiner Spaces jederzeit als ZIP-Paket exportieren. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_colors": "Spacedeck Pro enthält einen Profi-Farbmischer, mit dem du deine eigenen Farben mischen kannst.", + "profile_caption": "Profil", + "upload_avatar": "Profilbild Hochladen", + "uploading_avatar": "Profilbild wird hochgeladen…", + "avatar_dimensions": "Bestes Format: 200×200 Pixel.", + "profile_name": "Name", + "profile_email": "Email-Adresse", + "send_again": "Erneut Senden", + "confirmation_sent_long": "Email-Bestätigungslink versandt. Bitte überprüfe deine Mails.", + "confirmation_sent_another": "Wir haben eine weiteren Bestätigungslink versandt.", + "confirmation_sent_dialog_text": "Wir haben dir eine Email geschickt, die erklärt, wie das mit der Bestätigung läuft.", + "payment_caption": "Bezahlung", + "language_caption": "Sprache", + "notifications_caption": "Emails", + "notifications_option_chat": "Haltet mich über neue Kommentare auf dem Laufenden.", + "notifications_option_spaces": "Schickt mir täglich eine Zusammenfassung über Änderungen an meinen Spaces und Ordnern.", + "password_caption": "Passwort", + "current_password": "Altes Passwort", + "new_password": "Neues Passwort", + "verify_password": "Zur Sicherheit nochmal", + "change_password": "Passwort Ändern", + "reset_password": "Passwort Zurücksetzen", + "terminate_caption": "Kündigen", + "terminate_warning": "Wenn du kündigst, werden all deine Spaces, Ordner und Nachrichten und alle ihre Inhalte gelöscht.", + "terminate_warning2": "Das kann man nicht rückgängig machen.", + "terminate_reason": "Kündigungsgrund", + "terminate_reason_caption": "Wenn du uns mitteilst, was dich gestört hat, hilft uns das dabei ein besseres Produkt zu machen.", + "terminate_terminate": "Wirklich Kündigen", + "space_blank1": "Dies ist dein brandneuer, leerer Space!", + "space_blank2": "Wirf Dateien rein, paste Web-Links", + "space_blank3": "oder nutz die Werkzeuge da unten.", + "space_blank4": "Sei kreativ und tob dich aus!", + "draft": "Entwurf", + "publish": "Veröffentlichen", + "published": "Veröffentlicht", + "save_version": "Version Speichern", + "version_saved": "Version Gespeichert", + "post": "Abschicken", + "chat_invite_cta1": "Zusammen arbeiten macht Spaß!", + "chat_invite_cta2": "Warum ", + "chat_invite_cta3": "lädst du nicht ein paar Leute ein", + "chat_invite_cta4": "mit denen du dann zusammen arbeiten kannst?", + "chat_message_placeholder": "Schreib hier deine Nachricht…", + "view": "Ansicht", + "edit": "Bearb.", + "present": "Präse.", + "chat": "Chat", + "meta": "Teilen", + "tool_search": "Suche", + "tool_upload": "Upload", + "tool_text": "Text", + "tool_shape": "Grafik", + "tool_zones": "Zonen", + "tool_canvas": "Wand", + "search_media": "Medien im Web suchen…", + "type_here": "Hier was eingeben", + "text_formats": "Formate", + "format_p": "Absatz", + "format_bullets": "Bullet-Liste", + "format_numbers": "Nummerierte Liste", + "format_h1": "Überschrift 1", + "format_h2": "Überschrift 2", + "format_h3": "Überschrift 3", + "font_size": "Schriftgröße", + "line_height": "Zeilenhöhe", + "tool_align": "Bund", + "tool_styles": "Stil", + "tool_bullets": "Bullets", + "tool_numbers": "Zahlen", + "color_fill": "Füllung", + "color_stroke": "Strich", + "color_text": "Text", + "tool_type": "Typo", + "tool_box": "Box", + "tool_link": "Link", + "tool_layout": "Layout", + "tool_options": "Mehr", + "tool_stroke": "Strich", + "tool_delete": "Löschen", + "tool_lock": "Sperren", + "tool_copy": "Kopie", + "stack": "Anordnung", + "tool_circle": "Kreis", + "tool_hexagon": "Sechseck", + "tool_square": "Quadrat", + "tool_diamond": "Diamant", + "tool_bubble": "Blase", + "tool_cloud": "Wolke", + "tool_burst": "Burst", + "tool_star": "Stern", + "tool_heart": "Herz", + "tool_scribble": "Kritzeln", + "tool_line": "Linie", + "tool_arrow": "Pfeil", + "search_media_placeholder": "Online-Medien suchen…", + "add_zone": "Neue Zone", + "palette": "Palette", + "picker": "Mischen", + "background_image_caption": "Bild", + "background_color_caption": "Farbe", + "upload_background_caption": "Klicke hier, um ein Hintergrundbild hochzuladen.", + "upload_background": "Hintergrund Hochladen", + "access_caption": "Zugriff", + "versions_caption": "Versionen", + "info_caption": "Info", + "mode_private": "Privat: Nur Mitglieder können zugreifen", + "mode_public": "Öffentlich: Jede(r) mit Kenntnis des Links darf reinschauen", + "invite_collaborators": "Mitarbeiter Einladen", + "invitee_email_address": "Email-Adresse des neuen Mitglieds", + "optional_message": "Optionale Nachricht", + "revoke_access": "Zugriff Entfernen", + "invite": "Einladen", + "role_viewer": "Betrachter", + "role_editor": "Bearbeiter", + "role_admin": "Admin", + "new_space_title": "Neuer Titel für Space", + "logging_in": "logging_in", + "password_confirmation": "Passwort Wiederholung", + "confirm_again": "In deinem Postfach solltest du eine Bestätigungsmail finden. Bitte klicke auf den Link darin.", + "confirmed": "E-Mail Adresse wurde erfolgreich bestätigt. Danke!", + "password_check_inbox": "password_check_inbox", + "viewer": "Zuschauer", + "editor": "Bearbeiter", + "admin": "Admin", + "mobile": "Mobil", + "image": "Bild", + "tool_filter": "Filter", + "team": "Team", + "search": "Suche", + "search_no_results": "Keine Resultate", + "search_clear": "Zurücksetzen", + "rename": "Umbennen", + "login_google": "Mit Google anmelden", + "save_changes": "Änderungen speichern", + "what_is_your_name": "Willkommen bei %s ! Bitte wähle einen Benutzernamen.", + "landing_title": "Dein Online-Whiteboard.", + "landing_claim": "Mit Spacedeck kannst du multimedial auf virtuellen Whiteboards im Internet zusammenarbeiten: Kombiniere Texte, Fotos, Websites oder sogar Videos und Sounds. ", + "landing_example": "Spacedeck ist ideal, um Ideen zu visualisieren, in kreativen Teams Projekte zu überblicken oder um den Unterricht in Schulen und Universitäten interaktiv zu gestalten.", + "spaces": "Meine Spaces", + "access_editor_link": "Sofort-Mitmachen-Link", + "access_editor_link_desc": "Mit diesem Link kann man sogar ohne Spacedeck-Account sofort mitarbeiten. Praktisch!", + "access_editor_link_desc_slug": "Dieser Link beinhaltet den Namen vom Space.", + "access_anonymous_edit_blocking": "Anonyme Mitarbeiter dürfen keine Daten anderer anonymer Mitarbeiter ändern.", + "access_current_members": "Aktuelle Mitarbeiter", + "access_new_members": "Neue Mitarbeiter einladen", + "landing_customers": "Tausende Anwender weltweit vertrauen uns.", + "landing_features_title": "Schneller zum Ergebnis.", + "landing_features_text": "Spacedeck 5 hat eine brandneue Benutzeroberfläche, die das Arbeiten einfacher und intuitiver und macht - gleichzeitig aber auch mehr mächtige Werkzeuge bereitstellt.", + "landing_features_1": "Drag & Drop: Bilder-, Video- und Ton-Dateien direkt vom Desktop oder von anderen Webseiten in Spaces ziehen", + "landing_features_2": "Textnotizen mit allen Möglichkeiten bei Schriftart, Farbe und Stil", + "landing_features_3": "Zeichne und Markiere freihändig oder mit fertigen Formen", + "landing_features_4": "Verwandle dein Whiteboard in eine zoombare Präsentation", + "landing_features_5": "Arbeite in Echtzeit mit deinen Kollegen, Schülern oder Freunden zusammen", + "landing_features_6": "Teile deine Whiteboards per Link oder per E-Mail", + "landing_features_7": "Exportiere deine Arbeit als PDF- oder ZIP-Datei", + "landing_pricing": "Unfassbar günstig.", + "landing_pricing_lite": "Private Nutzung", + "landing_pricing_lite_text": "Basisvariante, ausreichend um multimedial zu arbeiten.", + "landing_pricing_pro_features_list": "
  • Unbegrenzte Spaces
  • Hierarchische Ordnerstruktur
  • PDF und ZIP Export
  • Keine Wasserzeichen
  • Eigene Hintergründe
  • Liste von Aktivitäten
  • 20 GB Speicherplatz
    • ", + "landing_pricing_pro": "€4,90/Anwender/Mo.
      oder €49,90/Anwender/Jahr", + "landing_pricing_pro_text": "Alle Features um professionell zu arbeiten.", + "welcome_subject": "Willkommen bei Spacedeck", + "welcome_body": "Danke, dass du dich bei Spacedeck angemeldet hast.
      Wir hoffen, du wirst viel Spaß mit Spacedeck haben.
      Vergiss nicht, dass du mit unbegrenzt vielen Kollegen und Freunden kostenlos zusammen arbeiten kannst. ", + "parent_folder": "Übergeordneter Ordner", + "access_no_members": "Noch keine Mitglieder", + "invite_emails": "E-Mail Adressen", + "created_by": "Erstellt von", + "last_updated": "Zuletzt aktualisiert", + "comments": "Kommentare", + "history_recently_updated": "Aktuelles", + "history_recently_empty": "Noch nichts passiert.", + "signing_up": "Registierung läuft", + "feedback_sent": "Danke für dein Feedback!", + "role_member": "Mitglied", + "space_message_subject": "Neue Nachricht im Space %s", + "space_message_body": "%s schrieb in %s: \n", + "password_reset_subject": "Neues Passwort für Spacedeck", + "password_reset_body": "Du möchtest das Passwort für deinen Spacedeck Account zurücksetzen?\nBitte klicke dafür auf den folgenden Link:", + "password_reset_action": "Jetzt Passwort neu setzen", + "pro_ad_history_headline": "Nach einem Upgrade zu Spacedeck Pro kannst du hier einen Überblick über alle aktuellen Aktivitäten in Spaces bekommen.", + "was_offline": "Die Verbindung wurde unterbrochen. Wir empfehlen, alle geänderten Objekte erneut zu selektieren, um sie zu speichern.", + "subscription_failed_user_subject": "Zahlung fehlgeschlagen", + "subscription_failed_user_body": "Unfortunately, we could not process your payment method. You can easly create a new payment method including PayPal in your account settings.", + "subscription_failed_team_subject": "Zahlung fehlgeschlagen", + "subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.", + "add": "hinzufügen", + "team_name": "Team-Name", + "subdomain": "Subdomain", + "invited": "eingeladen", + "team_adresses": "E-Mail Adressen", + "duplicate_destination": "In welchen Ordner möchtest du den Space duplizieren?", + "duplicate_confirm": "%s nach %s duplizieren?", + "duplicate_success": "%s wurde in %s dupliziert.", + "goto_space": "Gehe zu %s", + "goto_folder": "Gehe zu Ordner %s", + "stay_here": "Hier bleiben", + "sharing": "sharing", + "list": "Liste", + "download_space": "Space Herunterladen", + "duplicate_destination_folder": "Zielordner für Duplikat", + "type": "Typ", + "promote": "Befördern", + "demote": "Zurückstufen", + "Previous Zone": "Vorherige Zone", + "Next Zone": "Nächste Zone", + "profile_slug": "profile_slug", + "lock": "sperren", + "unlock": "entsperren", + "link": "verlinken", + "download": "download", + "more": "mehr", + "follow_present": "Folgen", + "mute_present": "Entfolgen", + "follow_present_help": "Wenn jemand den Space präsentiert, folgen die anderen Mitglieder automatisch der Präsentation. Mit diesem Knopf lässt sich das an- oder ausschalten." +} diff --git a/locales/en.js b/locales/en.js new file mode 100644 index 0000000..be30cf8 --- /dev/null +++ b/locales/en.js @@ -0,0 +1,325 @@ +{ + "ok": "OK", + "cancel": "Cancel", + "close": "Close", + "open": "Open", + "folder": "Folder", + "save": "Save", + "saved": "Saved", + "created": "created", + "duplicate": "Duplicate", + "delete": "Delete", + "remove": "Remove", + "set": "set", + "reset": "reset", + "thanks": "Thanks", + "share": "Share", + "signup": "Sign Up", + "login": "Log in", + "logout": "Log out", + "email": "Email Address", + "password": "Password", + "width": "Width", + "height": "Height", + "nick": "Name", + "role": "Role", + "members": "Members", + "actions": "Actions", + "or": "or", + "you": "you", + "via": "via", + "by": "by", + "zero": "Zero", + "page": "Page", + "new": "New", + "copy": "Copy", + "home": "Home", + "owner": "Owner", + "space": "Space", + "second": "Second", + "not_found": "Not Found.", + "untitled_space": "Untitled Space", + "untitled_folder": "Untitled Folder", + "untitled": "untitled", + "sure": "Are you sure?", + "specify": "Please Specify", + "confirm": "Please Confirm", + "signup_google": "Sign In with Google", + "error_unknown_email": "This email/password combination is unknown. Try login with Google.", + "error_password_confirmation": "The entered passwords don't match.", + "error_domain_blocked": "Your domain is blocked.", + "error_user_email_already_used": "This email address is already in use.", + "support": "Spacedeck Support", + "offline": "Offline. Click for more.", + "error": "Sorry, but something went wrong. Please contact support@spacedeck.com", + "welcome": "Welcome", + "claim": "Your digital Whiteboard.", + "trynow": "Try now.", + "about": "About us", + "terms": "Terms", + "contact": "Contact", + "privacy": "Privacy", + "business_adress": "Business Adress", + "post_adress": "Post Adress", + "phone": "Phone", + "ceo": "Managing Director", + "name": "Name", + "confirm_subject": "Spacedeck Email Confirmation", + "confirm_body": "Thank you for signing up at Spacedeck.\nPlease click on the following link to confirm your email address.\n", + "confirm_action": "Confirm Now", + "team_invite_membership_subject": "Team Invitation for %s", + "team_invite_membership_body": "You have been invited to %s on Spacedeck. Please click on the following link to accept the invitation.", + "team_invite_user_body": "You have been invited to %s on Spacedeck.\nYour temporary password is \"%s\".\nPlease click on the following link to accept the invitation.", + "team_invite_admin_body": "%s was invited for your team: %s. The temporary password is \"%s\".", + "team_invite_membership_acction": "Accept", + "team_new_member_subject": "New Team Member for %s signed up", + "team_new_member_body": "%s just joined Team %s on Spacedeck.", + "space_invite_membership_subject": "%s invited you to a Space %s ", + "space_invite_membership_body": "You have been invited by %s to join a Space %s on Spacedeck. Please click on the following link to accept the invitation.", + "space_invite_membership_action": "Accept", + "folder_invite_membership_subject": "Space", + "folder_invite_membership_body": "You have been invited to a Team on Spacedeck. Please click on the following link to accept the invitation.", + "folder_invite_membership_acction": "Accept", + "login_google": "Login With Google", + "save_changes": "Save Changes", + "upgrade": "Upgrade", + "upgrade_now": "Upgrade Now", + "create_space": "Create Space", + "create_folder": "Create Folder", + "email_unconfirmed": "Email Unconfirmed", + "confirmation_sent": "Email Sent", + "folder_filter": "Filter", + "sort_by": "Sort by", + "last_modified": "Last Modified", + "last_opened": "Last Opened", + "title": "Title", + "edit_team": "Edit Team", + "edit_account": "Edit Account", + "log_out": "Log Out", + "no_spaces_yet": "Welcome! You can create Spaces and Folders here using the buttons in the top left corner.", + "new_folder_title": "New title for folder", + "folder_settings": "Folder Settings", + "upload_cover_image": "Upload Cover Image", + "spacedeck_pro_ad_folders": "With Spacedeck Pro, you can organize an unlimited amount of Spaces in Folders and manage access controls for each Folder. Would you like to learn more about Pro features?", + "spacedeck_pro_ad_versions": "With Spacedeck Pro, you can save unlimited versions of each Space to track your progress or keep snapshots safe. Would you like to learn more about Pro features?", + "spacedeck_pro_ad_pdf": "With Spacedeck Pro, you can export your Spaces as crisp PDFs for archiving, mailing around, or printing. Do you want to learn more about Pro features?", + "spacedeck_pro_ad_zip": "With Spacedeck Pro, you can export the contents of a Space as a ZIP package. Do you want to learn more about Pro features?", + "spacedeck_pro_ad_colors": "With Spacedeck Pro, you can mix your own colors using a professional color picker.", + "profile_caption": "Profile", + "upload_avatar": "Upload Avatar", + "uploading_avatar": "Uploading Avatar…", + "avatar_dimensions": "Recommended dimensions: 200×200 pixels.", + "profile_name": "Name", + "profile_email": "Email Address", + "send_again": "Send Again", + "confirmation_sent_long": "Email confirmation link sent. Please check your inbox.", + "confirmation_sent_another": "Another confirmation link sent.", + "confirmation_sent_dialog_text": "We sent you an email explaining how to confirm your email address.", + "payment_caption": "Payment", + "language_caption": "Language", + "notifications_caption": "Notifications", + "notifications_option_chat": "Inform me via email about new comments", + "notifications_option_spaces": "Send me a daily digest of what happened in my Spaces and Folders", + "password_caption": "Password", + "current_password": "Current Password", + "new_password": "New Password", + "verify_password": "Verify Password", + "change_password": "Change Password", + "reset_password": "Reset Password", + "terminate_caption": "Delete Account", + "terminate_warning": "If you delete your account, all Spaces, Folders and Messages including all content you and other people created in your Spaces will be destroyed.", + "terminate_warning2": "This cannot be undone.", + "terminate_reason": "Message", + "terminate_reason_caption": "Help us improve by sharing your reasons for cancelling.", + "terminate_terminate": "Terminate", + "space_blank1": "Welcome to a fresh new Space!", + "space_blank2": "Drop files, paste links", + "space_blank3": "or use the tools below", + "space_blank4": "to fill this Space with content.", + "draft": "Draft", + "publish": "Publish", + "published": "Published", + "save_version": "Save Version", + "version_saved": "Version Saved", + "post": "Post Message", + "chat_invite_cta1": "Collaboration is fun!", + "chat_invite_cta2": "Why not ", + "chat_invite_cta3": "invite some people", + "chat_invite_cta4": "to work with you?", + "chat_message_placeholder": "Write your message…", + "view": "View", + "edit": "Edit", + "present": "Present", + "chat": "Chat", + "meta": "Meta", + "tool_search": "Search", + "tool_upload": "Upload", + "tool_text": "Text", + "tool_shape": "Shape", + "tool_zones": "Zones", + "tool_canvas": "Canvas", + "search_media": "Search media…", + "type_here": "Type here", + "text_formats": "Formats", + "format_p": "Paragraph", + "format_bullets": "Bullet List", + "format_numbers": "Numbered List", + "format_h1": "Headline 1", + "format_h2": "Headline 2", + "format_h3": "Headline 3", + "font_size": "Font Size", + "line_height": "Line Height", + "tool_align": "Align", + "tool_styles": "Styles", + "tool_bullets": "Bullets", + "tool_numbers": "Numbers", + "color_fill": "Fill", + "color_stroke": "Stroke", + "color_text": "Text", + "tool_type": "Type", + "tool_box": "Box", + "tool_link": "Link", + "tool_layout": "Layout", + "tool_options": "Options", + "tool_stroke": "Stroke", + "tool_delete": "Delete", + "tool_lock": "Lock", + "tool_copy": "Copy", + "stack": "Stack", + "tool_circle": "Circle", + "tool_hexagon": "Hexagon", + "tool_square": "Square", + "tool_diamond": "Diamond", + "tool_bubble": "Bubble", + "tool_cloud": "Cloud", + "tool_burst": "Burst", + "tool_star": "Star", + "tool_heart": "Heart", + "tool_scribble": "Scribble", + "tool_line": "Line", + "tool_arrow": "Arrow", + "search_media_placeholder": "Search web media…", + "add_zone": "New Zone", + "palette": "Palette", + "picker": "Picker", + "background_image_caption": "Image", + "background_color_caption": "Color", + "upload_background_caption": "Click to upload a background image", + "upload_background": "Upload Background", + "access_caption": "Access", + "versions_caption": "Versions", + "info_caption": "Info", + "mode_private": "Private: Only members can view or edit", + "mode_public": "Public: Anyone with the link can view", + "invite_collaborators": "Invite Collaborators", + "revoke_access": "Revoke Access", + "invite": "Send Invitations", + "invitee_email_address": "Email address of new member", + "optional_message": "Optional message", + "role_viewer": "Viewer", + "role_editor": "Editor", + "role_admin": "Admin", + "new_space_title": "New title for Space", + "team": "Team", + "search": "Search", + "search_no_results": "search_no_results", + "search_clear": "search_clear", + "rename": "Rename", + "mobile": "mobile", + "image": "image", + "tool_filter": "filter", + "canel": "canel", + "invite_membership_action": "invite_membership_action", + "viewer": "viewer", + "editor": "editor", + "admin": "admin", + "logging_in": "logging in", + "password_confirmation": "Password Confirmation", + "confirm_again": "We sent you an email explaining how to confirm your email address.", + "confirmed": "Your Account was confirmed successfully. Thank you.", + "signing_up": "Signing up", + "password_check_inbox": "Please check your inbox", + "new_space": "New Space", + "tool_more": "More", + "what_is_your_name": "Welcome to %s! Please choose a username.", + "lang": "en", + "landing_title": "Your Whiteboard on the Web.", + "landing_claim": "Spacedeck lets you easily combine all kinds of media on virtual whiteboards: Text notes, photos, web links, even videos and audio recordings. ", + "landing_example": "People use Spacedeck to organize their ideas, in teams to see whole projects at a glance, or in schools and universities for richer, connected learning experiences.", + "spaces": "My Spaces", + "access_editor_link": "Instant Edit Link", + "access_editor_link_desc": "Give this link to anyone who should be able to instantly edit this Space, no account required: ", + "access_editor_link_desc_slug": "This link contains the name of the space, too. ", + "access_anonymous_edit_blocking": "Anonymous Editors may only change their own items", + "access_current_members": "Current Members", + "access_new_members": "Invite New Members", + "access_no_members": "Members of this Space will show up here.", + "comments": "comments", + "landing_customers": "Trusted by Thousands.", + "landing_features_title": "A Breeze To Use.", + "landing_features_text": "The new Spacedeck 5 has a streamlined, beautiful user interface that makes your work easier and more fun than ever before – while giving you even more powerful features:", + "landing_features_1": "Drag & drop images, videos and audio from your computer or the web", + "landing_features_2": "Write and format text with full control over fonts, colors and style", + "landing_features_3": "Draw, annotate and highlight with included graphical shapes", + "landing_features_4": "Turn your board into a zooming presentation", + "landing_features_5": "Collaborate and chat in realtime with teammates, students or friends.", + "landing_features_6": "Share Spaces on the web or via email", + "landing_features_7": "Export your work as printable PDF or ZIP", + "landing_pricing": "Incredibly Affordable.", + "landing_pricing_lite": "Free/Personal Use", + "landing_pricing_lite_text": "The basic, well-rounded version for collecting pictures and keeping notes.", + "landing_pricing_pro_features_list": "
      • Unlimited Spaces
      • Folder Structures
      • PDF and ZIP Export
      • No Watermarks
      • Custom Backgrounds
      • Activity History
      • 20 GB Storage
        • ", + "landing_pricing_pro": "€4,90/User/Mo.
          or 49,90/User/Year", + "landing_pricing_pro_text": "Turbocharged with all the power you expect.", + "landing_pricing_pro_features": "Turbocharged with all the power you expect.", + "welcome_subject": "Welcome to Spacedeck", + "welcome_body": "Hello!\nThank you for signing up at Spacedeck.
          We hope you will enjoy working in Spaces.
          Remember, your account includes unlimited collaborators. Feel free to share your Spaces with friends and colleagues all over the world.", + "invite_emails": "Email addresses (Comma separated)", + "history_recently_updated": "Recently Updated", + "history_recently_empty": "Nothing has happened yet.", + "parent_folder": "parent_folder", + "created_by": "Created by", + "last_updated": "Last updated", + "feedback_sent": "Thanks for your feedback!", + "role_member": "Member", + "team_invite_membership_action": "Accept invitation", + "space_message_subject": "New Message in Space %s", + "space_message_body": "%s wrote in %s: \n", + "pro_ad_history_headline": "When you upgrade to Spacedeck Pro, you will see a history of recent updates across all your (shared) Spaces here.", + "password_reset_subject": "Reset Password for Spacedeck", + "password_reset_body": "You requested a reset of your Spacedeck password.\nPlease click on the following link to set a new password.", + "password_reset_action": "Reset Now", + "was_offline": "The connection to Spacedeck was interrupted. If you have unsaved work, please keep this browser tab open until the connection is re-established, then touch the unsaved objects again.", + "subscription_failed_user_subject": "Problem with your Spacedeck Payment", + "subscription_failed_user_body": "Unfortunately, we could not process your Payment-method. You can easly create a new payment method including PayPal in your account settings.", + "subscription_failed_team_subject": "Problem with your Spacedeck Payment", + "subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.", + "team_name": "Team Name", + "subdomain": "Subdomain", + "team_adresses": "Email adresses", + "add": "Add", + "invited": "invited", + "duplicate_destination": "Into which folder do you want to duplicate this Space?", + "duplicate_confirm": "Duplicate %s into %s?", + "duplicate_success": "%s was duplicated into %s.", + "goto_space": "Go to Space %s", + "goto_folder": "Go to Folder %s", + "stay_here": "Stay here", + "sharing": "Sharing", + "list": "Export List", + "link": "Link", + "download_space": "Download Space", + "type": "Type", + "download": "Download", + "Previous Zone": "Previous Zone", + "Next Zone": "Next Zone", + "promote": "Promote", + "demote": "Demote", + "more": "More", + "lock": "Lock", + "unlock": "Unlock", + "follow_present": "Follow", + "mute_present": "Unfollow", + "follow_present_help": "If someone else is presenting this Space, the other members automatically follow the presentation. Switch following on or off with this button.", + "export": "export" +} \ No newline at end of file diff --git a/locales/fr.js b/locales/fr.js new file mode 100644 index 0000000..99d5faf --- /dev/null +++ b/locales/fr.js @@ -0,0 +1,318 @@ +{ + "lang": "fr", + "ok": "OK", + "cancel": "Annuler", + "close": "Fermer", + "open": "Ouvrir", + "folder": "Dossier", + "save": "Enregistrer", + "saved": "Enregistrée", + "created": "établi", + "duplicate": "Dupliquer", + "delete": "Supprimer", + "remove": "Enlever", + "set": "Définir", + "reset": "Rédéfinir", + "thanks": "Merci", + "share": "Partager", + "signup": "S'inscrire", + "login": "Se connecter", + "logout": "Se déconnecter", + "email": "Adresse email", + "password": "Mot de passe", + "width": "Largeur", + "height": "Hauteur", + "nick": "Nom", + "role": "Rôle", + "members": "Membres", + "actions": "Actions", + "or": "ou", + "you": "vous", + "via": "par", + "by": "par", + "zero": "Zero", + "page": "Page", + "new": "Neuf", + "copy": "Copier", + "owner": "Possesseur", + "home": "Accueil", + "space": "Espace", + "second": "Seconde", + "not_found": "Pas trouvé.", + "untitled": "sans titre", + "untitled_space": "Espace sans titre", + "untitled_folder": "Dossier sans titre", + "sure": "Êtes-vous sûr?", + "specify": "Veuillez préciser:", + "confirm": "Veuillez confirmer", + "signup_google": "S'inscrire avec Google", + "error_unknown_email": "Combinaison inconnue de l'email et mot de passe. Ou essayer de signer avec Google.", + "error_password_confirmation": "Les deux mots de passe ne correspondent pas.", + "error_domain_blocked": "Ce domaine a été désactivé.", + "error_user_email_already_used": "Cette adresse email est déjà enregistré.", + "support": "Aide Spacedeck", + "offline": "Désolé , mais les serveurs Spacedeck ne peuvent pas être atteint pour le moment. Plus d' informations ici.", + "error": "Désolé, une erreur s'est produite. Veuillez contacter support@spacedeck.com", + "welcome": "Bienvenue", + "claim": "Le tableau blanc partagé pour tout le monde", + "trynow": "Essayez-le gratuitement", + "about": "de nous", + "terms": "termes", + "contact": "contact", + "privacy": "sphère privée", + "business_adress": "Siège social", + "post_adress": "Adresse courrier", + "phone": "téléphone", + "ceo": "Gestionnaire", + "name": "name", + "confirm_subject": "Confirmation de l'email Spacedeck", + "confirm_body": "Merci pour votre inscription à Spacedeck.\nSil vous plaît cliquez sur le lien suivant pour confirmer votre adresse e-mail.", + "confirm_action": "Confirmer", + "team_invite_membership_subject": "Team Invitation for %s", + "team_invite_membership_body": "You have been invited to %s on Spacedeck.\nPlease click on the following link to accept the invitation.", + "team_invite_user_body": "You have been invited to %s on Spacedeck.\nYour temporary password is \"%s\".\nPlease click on the following link to accept the invitation.", + "team_invite_admin_body": "%s was invited for your team: %s. The temporary password is \"%s\".", + "team_invite_membership_acction": "Accept", + "team_new_member_subject": "New Team Member", + "team_new_member_body": "%s just joined Team %s on Spacedeck.", + "invite_emails": "Entrer les adresses email (séparées pas des virgules)", + "optional_message": "Message personnel (facultatif)", + "space_invite_membership_subject": "Invitation Espace par %s: %s", + "space_invite_membership_body": "Vous avez été invité par %s à Espace \"%s\"", + "space_invite_membership_action": "Accepter L'invitation", + "folder_invite_membership_subject": "Space", + "folder_invite_membership_body": "You have been invited to a Team on Spacedeck. Please click on the following link to accept the invitation.", + "folder_invite_membership_acction": "Accept", + "login_google": "S'inscrire avec Google", + "save_changes": "Enregistrer", + "upgrade": "Upgrade", + "upgrade_now": "Mise à niveau", + "create_space": "Créer un espace", + "create_folder": "Créer un dossier", + "email_unconfirmed": "Email non confirmée", + "confirmation_sent": "L'email est envoyé.", + "folder_filter": "Filtre", + "sort_by": "Ordre", + "last_modified": "Dernière modification", + "last_opened": "Dernière ouverture", + "title": "Titre", + "edit_team": "Modifier l'équipe", + "edit_account": "Modifier le compte", + "log_out": "Déconnecter", + "no_spaces_yet": "Vous ne avez pas encore créé d'espaces.", + "new_folder_title": "Nouveau titre pour le dossier", + "folder_settings": "Paramètres du dossier", + "upload_cover_image": "Charger image de couverture", + "spacedeck_pro_ad_folders": "Avec Spacedeck Pro, vous pouvez organiser un nombre illimité de espaces dans les dossiers et gérer les contrôles d'accès pour chaque dossier. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_versions": "Avec Spacedeck Pro, vous pouvez enregistrer des versions illimitées de chaque espace pour suivre vos progrès ou de conserver des instantanés sécurité. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_pdf": "Avec Spacedeck Pro, vous pouvez exporter vos espaces et même des dossiers entiers belles PDF pour l'archivage, de diffusion, ou autour de l'impression. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_zip": "Avec Spacedeck Pro, vous pouvez exporter le contenu d'un espace comme un paquet ZIP. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_colors": "Avec Spacedeck Pro, vous pouvez mélanger vos propres couleurs en utilisant un sélecteur de couleur professionnelle.", + "profile_caption": "Profil", + "upload_avatar": "Télécharger l'image profil", + "uploading_avatar": "L'image de profil est téléchargée…", + "avatar_dimensions": "Format suggéré: 200×200 pixels.", + "profile_name": "Name", + "profile_email": "Email", + "send_again": "Renvoyer", + "confirmation_sent_long": "Lien de confirmation email envoyé. Se il vous plaît vérifier votre courrier.", + "confirmation_sent_another": "Nous avons envoyé un autre lien de confirmation.", + "confirmation_sent_dialog_text": "Nous vous avons envoyé un email expliquant comment confirmer votre adresse email.", + "payment_caption": "Paiement", + "language_caption": "Langue", + "notifications_caption": "Emails", + "notifications_option_chat": "Envoyez-moi les nouveaux commentaires par email.", + "notifications_option_spaces": "Envoyez-moi un résumé quotidien des modifications à mes espaces.", + "password_caption": "Mot de passe", + "current_password": "Ancien mot de passe", + "new_password": "Nouveau mot de passe", + "verify_password": "Répéter mot de passe", + "change_password": "Enregistrer", + "reset_password": "Mot de passe oublié?", + "terminate_caption": "Supprimer le compte", + "terminate_warning": "En supprimant votre compte, vos messages, espaces, dossiers et tout leur contenu seront effacés. Cette action ne peut être annulée.", + "terminate_warning2": "Cela ne peut pas être annulée.", + "terminate_reason": "Problèmes rencontrés", + "terminate_reason_caption": "Aidez-nous à améliorer le produit en précisant les raisons de la suppression de votre compte.", + "terminate_terminate": "Supprimer le compte définitivement?", + "space_blank1": "Ceci est votre nouvel espace.", + "space_blank2": "Déposez des fichiers, collez des liens web", + "space_blank3": "ou utilisez les outils.", + "space_blank4": "Soyez créatifs!", + "draft": "Conception", + "publish": "Publier", + "published": "Publié", + "save_version": "Enregistrer une version", + "version_saved": "Version enregistrée.", + "post": "Envoyer", + "chat_invite_cta1": "Travailler ensemble est amusant!", + "chat_invite_cta2": "Pourquoi ", + "chat_invite_cta3": "ne pas vous invitez quelques collaborateurs?", + "chat_invite_cta4": "", + "chat_message_placeholder": "Votre message ici…", + "view": "Vue", + "edit": "Éditer", + "present": "Prés.", + "chat": "Chat", + "meta": "Meta", + "tool_search": "Chercher", + "tool_upload": "Charger", + "tool_text": "Texte", + "tool_shape": "Dessin", + "tool_zones": "Zones", + "tool_canvas": "Mur", + "search_media": "Chercher le web pour les médias…", + "type_here": "Entrez quelque chose ici", + "text_formats": "Formats", + "format_p": "Paragraphe", + "format_bullets": "Liste à puces", + "format_numbers": "Liste numérotée", + "format_h1": "Titre 1", + "format_h2": "Titre 2", + "format_h3": "Titre 3", + "font_size": "Taille de la police", + "line_height": "Hauteur de ligne", + "tool_align": "Align", + "tool_styles": "Style", + "tool_bullets": "Puces", + "tool_numbers": "Numéros", + "color_fill": "Fond", + "color_stroke": "Ligne", + "color_text": "Text", + "tool_type": "Typo.", + "tool_box": "Box", + "tool_link": "Lien", + "tool_layout": "Layout", + "tool_options": "Plus", + "tool_stroke": "Ligne", + "tool_delete": "Effacer", + "tool_lock": "Bloquer", + "tool_copy": "Copie", + "stack": "Empiler", + "tool_circle": "Cercle", + "tool_hexagon": "Hexagone", + "tool_square": "Carré", + "tool_diamond": "Diamant", + "tool_bubble": "Bulle", + "tool_cloud": "Nuage", + "tool_burst": "Éclat", + "tool_star": "Étoile", + "tool_heart": "Cœur", + "tool_scribble": "Crayon", + "tool_line": "Ligne", + "tool_arrow": "Flèche", + "search_media_placeholder": "Chercher le web pour les médias…", + "add_zone": "Ajouter Zone", + "palette": "Palette", + "picker": "Mélange", + "background_image_caption": "Image", + "background_color_caption": "Couleur", + "upload_background_caption": "Cliquez ici pour télécharger une image de fond.", + "upload_background": "Télécharger", + "access_caption": "Accès", + "versions_caption": "Versions", + "info_caption": "Info", + "mode_private": "Privé", + "mode_public": "Public", + "invite_collaborators": "Inviter les collaborateurs", + "revoke_access": "Révoquer l'accès", + "invite": "Inviter", + "role_viewer": "Spectateur", + "role_editor": "Éditeur", + "role_admin": "Administrateur", + "new_space_title": "Nouveau titre pour l'espace", + "invitee_email_address": "Adresse e-mail de invitee", + "viewer": "Spectateur", + "editor": "Éditeur", + "admin": "Administrateur", + "mobile": "Mobile", + "image": "Image", + "tool_filter": "Filter", + "team": "Team", + "search": "Recherche", + "search_no_results": "Aucun résultat trouvé", + "search_clear": "Supprimer", + "rename": "Renommer", + "logging_in": "Connexion", + "password_confirmation": "Confirmation du mot de passe", + "confirm_again": "Veuillez consulter votre boîte pour confirmer votre email.", + "confirmed": "Adresse email confirmée avec succès. merci!", + "password_check_inbox": "password_check_inbox", + "what_is_your_name": "Bonjour! Choisir un nom d'utilisateur s'il vous plaît.", + "landing_title": "Le tableau blanc partagé pour tout le monde.", + "landing_claim": "Le tableau blanc partagé pour tout le monde.", + "landing_example": "Que vous soyez étudiant, enseignant ou chercheur: Avec Spacedeck il est facile pour vous de créer, de gérer et de partager des cours ou le travail en classe. Développez vos théories visuellement. Organisez des notes de recherche, web, images, audio et vidéo.", + "spaces": "Espaces", + "access_editor_link": "Lien instantané.", + "access_editor_link_desc": "Donnez ce lien à tous ceux que vous voulez inviter rapidement. Ils n’ont pas besoin créer un compte Spacedeck.", + "access_editor_link_desc_slug": "Y compris le nom du lien", + "access_anonymous_edit_blocking": "Ces invités ne peuvent modifier que les éléments qu’ils ont eux-même créé.", + "access_current_members": "Membres actuels", + "comments": "Commentaires", + "access_no_members": "No members yet. You can invite some below.", + "access_new_members": "Inviter de nouveaux membres.", + "landing_customers": "Approuvé par des milliers.", + "landing_features_title": "Un jeu d'enfant.", + "landing_features_text": "Le tout nouveau Spacedeck 5 vous permet de travailler bien plus facilement grâce à sa magnifique interface simplifiée.", + "landing_features_1": "Glissez & déposez images, vidéos et audios de votre ordinateur ou du web", + "landing_features_2": "Ecrivez directement sur l'espace et choisissez les polices de caractère, couleurs et styles", + "landing_features_3": "Dessinez, annotez et surlignez grâce aux formes graphiques intégrées", + "landing_features_4": "Transformez votre espace en une présentation dynamique", + "landing_features_5": "Collaborez et discutez en temps réel avec vos collègues, élèves et amis", + "landing_features_6": "Partagez vos espaces sur le web ou par email", + "landing_features_7": "Exportez votre espace en PDF pour l'imprimer", + "landing_pricing": "Incroyablement abordable.", + "landing_pricing_lite": "Usage personnel", + "landing_pricing_lite_text": "La version de base, bien arrondi pour recueillir des images et de garder des notes.", + "landing_pricing_pro_features_list": "
          • Unlimited Spaces
          • Exporter PDF, ZIP
          • No Watermarks
          • Image de fonds
          • Activity History
          • 20 Go de stockage
            • ", + "landing_pricing_pro": "€4,90/User/Mo.
              €49,90/User/Year", + "landing_pricing_pro_text": "Avec toute la puissance que vous attendez.", + "landing_pricing_pro_features": "Avec toute la puissance que vous attendez.", + "welcome_subject": "Bienvenue sur Spacedeck", + "welcome_body": "Merci pour votre inscription à Spacedeck.\nNous espérons que vous aurez plaisir à travailler dans les Espaces.
              Rappelez-vous que votre compte comprend un nombre illimité de collaborateurs.
              N''hésitez pas à partager vos espaces avec des amis et collègues du monde entier.", + "parent_folder": "Dossier origine", + "created_by": "Créé par", + "last_updated": "Mis à jour", + "history_recently_updated": "Nouvelles", + "history_recently_empty": "Rien ne se passe", + "signing_up": "Signing Up", + "feedback_sent": "Merci pour votre commentaire!", + "space_message_subject": "A posté sur %s", + "space_message_body": "%s a commenté dans %s:\n", + "role_member": "role_member", + "password_reset_subject": "Réinitialiser le Mot de passe pour Spacedeck", + "password_reset_body": "Salut!

              Vous avez demandé la réinitialisation de votre Mot de passe.
              Veuillez cliquer sur le lien suivant pour définir un nouveau Mot de passe.
              ", + "password_reset_action": "Définir un nouveau Mot de passe", + "was_offline": "The connection to Spacedeck was interrupted. If you have unsaved work, please keep this browser tab open until the connection is re-established, then touch the unsaved objects again.", + "subscription_failed_user_subject": "Problem with your Spacedeck Payment", + "subscription_failed_user_body": "Unfortunately, we could not process your Payment-method. You can easly create a new payment method including PayPal in your account settings.", + "subscription_failed_team_subject": "Problem with your Spacedeck Payment", + "subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.", + "pro_ad_history_headline": "Après une mise à niveau vous pouvez obtenir un aperçu de toutes les activités actuelles dans les espaces ici.", + "add": "ajouter", + "team_name": "Nom de l'équipe", + "subdomain": "sous-domaine", + "invited": "invité", + "team_adresses": "E-mail adresse", + "duplicate_destination": "Sélectionnez le dossier de destination", + "duplicate_confirm": "Dupliquer %s dans %s?", + "duplicate_success": "%s a été dupliqué dans %s.", + "goto_space": "Aller à l'espace %s", + "goto_folder": "Aller au dossier %s", + "stay_here": "Reste ici", + "download_space": "télécharger un espace", + "type": "Type", + "Previous Zone": "Zone précédent", + "Next Zone": "Zone suivante", + "list": "liste", + "promote": "promouvoir", + "demote": "rétrograder", + "lock": "bloquer", + "unlock": "déverrouillage", + "link": "link", + "download": "download", + "more": "plus", + "follow_present": "Suivre", + "mute_present": "Pas suivre", + "follow_present_help": "follow_present_help" +} \ No newline at end of file diff --git a/middlewares/404.js b/middlewares/404.js new file mode 100644 index 0000000..ac38434 --- /dev/null +++ b/middlewares/404.js @@ -0,0 +1,19 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + var err = new Error('Not Found'); + if (req.accepts('text/html')) { + res.status(404).render('not_found', { + title: 'Page Not Found.' + }); + } else if (req.accepts('application/json')) { + res.status(404).json({ + "error": "not_found" + }); + } else { + res.status(404).send("Not Found."); + } +} \ No newline at end of file diff --git a/middlewares/500.js b/middlewares/500.js new file mode 100644 index 0000000..db4079c --- /dev/null +++ b/middlewares/500.js @@ -0,0 +1,10 @@ +'use strict'; + +module.exports = (err, req, res, next) => { + console.error(err); + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); +} \ No newline at end of file diff --git a/middlewares/api_helpers.js b/middlewares/api_helpers.js new file mode 100644 index 0000000..e6c844a --- /dev/null +++ b/middlewares/api_helpers.js @@ -0,0 +1,55 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); +const redis = require('../helpers/redis'); + +var saveAction = (actionKey, object) => { + if (object.constructor.modelName == "Space") + return; + + let attr = { + action: actionKey, + space: object.space_id || object.space, + user: object.user_id || object.user, + editor_name: object.editor_name, + object: object.toJSON() + }; + + let action = new Action(attr); + action.save(function(err) { + if (err) + console.error("saved create action err:", err); + }); +}; + +module.exports = (req, res, next) => { + res.header("Cache-Control", "no-cache"); + + req['channelId'] = req.headers['x-spacedeck-channel']; + req['spacePassword'] = req.headers['x-spacedeck-spacepassword']; + req['spaceAuth'] = req.query['spaceAuth'] || req.headers['x-spacedeck-space-auth']; + + res['distributeCreate'] = function(model, object) { + if (!object) return; + redis.sendMessage("create", model, object.toJSON(), req.channelId); + this.status(201).json(object.toJSON()); + saveAction("create", object); + }; + + res['distributeUpdate'] = function(model, object) { + if (!object) return; + redis.sendMessage("update", model, object.toJSON(), req.channelId); + this.status(200).json(object.toJSON()); + saveAction("update", object); + }; + + res['distributeDelete'] = function(model, object) { + if (!object) return; + redis.sendMessage("delete", model, object.toJSON(), req.channelId); + this.sendStatus(204); + saveAction("delete", object); + }; + + next(); +} diff --git a/middlewares/artifact_helpers.js b/middlewares/artifact_helpers.js new file mode 100644 index 0000000..4df5d58 --- /dev/null +++ b/middlewares/artifact_helpers.js @@ -0,0 +1,22 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + var artifactId = req.params.artifact_id; + Artifact.findOne({ + "_id": artifactId + }, (err, artifact) => { + if (err) { + res.status(400).json(err); + } else { + if (artifact) { + req['artifact'] = artifact; + next(); + } else { + res.sendStatus(404); + } + } + }); +}; \ No newline at end of file diff --git a/middlewares/cors.js b/middlewares/cors.js new file mode 100644 index 0000000..09c4875 --- /dev/null +++ b/middlewares/cors.js @@ -0,0 +1,48 @@ +'use strict'; + +require('../models/schema'); +const config = require('config'); +const url = require('url'); + +function respond(origin, req, res, next) { + res.header('Access-Control-Allow-Origin', origin); + res.header('Access-Control-Allow-Credentials', true); + res.header('Access-Control-Max-Age', 60 * 60 * 24); + res.header('Access-Control-Expose-Headers', 'Accepts, Content-Type, X-Spacedeck-Space-Role, X-Spacedeck-Channel, X-Spacedeck-Spacepassword, X-Spacedeck-Auth, X-Spacedeck-Space-Auth'); + res.header('Access-Control-Allow-Headers', 'Accepts, Accept-Language, Accept-Encoding, Accept-Language, Content-Type, X-Spacedeck-Space-Auth, X-Spacedeck-Space-Role, X-Spacedeck-Channel, X-Spacedeck-Spacepassword, X-Spacedeck-Auth'); + res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE'); + + if (req.method == 'OPTIONS') { + res.sendStatus(204); + } else { + next(); + } +} + +module.exports = (req, res, next) => { + const origin = req.headers.origin; + + if (origin) { + const parsedUrl = url.parse(origin, true, true); + + // FIXME + if (parsedUrl.hostname == "cdn.spacedeck.com") { + res.header('Cache-Control', "max-age"); + res.header('Expires', "30d"); + res.removeHeader("Pragma"); + + respond(origin, req, res, next); + } else { + Team.getTeamForHost(parsedUrl.hostname, (err, team, subdomain) => { + if (team) { + respond(origin, req, res, next); + } else { + next(); + } + }); + } + + } else { + next(); + } +} diff --git a/middlewares/error_helpers.js b/middlewares/error_helpers.js new file mode 100644 index 0000000..030a5b5 --- /dev/null +++ b/middlewares/error_helpers.js @@ -0,0 +1,16 @@ +'use strict'; + +module.exports = (req, res, next) => { + res.bad_request = (msg) => { + if (req.accepts('text/html')) { + res.status(400).render('error', { + message: msg + }); + } else { + res.status(400).json({ + "error": msg + }); + } + } + next(); +} diff --git a/middlewares/i18n.js b/middlewares/i18n.js new file mode 100644 index 0000000..a28203b --- /dev/null +++ b/middlewares/i18n.js @@ -0,0 +1,17 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + req.i18n.setLocale(req.i18n.prefLocale); + + if (req.cookies.spacedeck_locale) { + req.i18n.setLocaleFromCookie(); + } + + if (req.user && req.user.preferences.language) { + req.i18n.setLocale(req.user.preferences.language); + } + next(); +} \ No newline at end of file diff --git a/middlewares/setuser.js b/middlewares/setuser.js new file mode 100644 index 0000000..349aebf --- /dev/null +++ b/middlewares/setuser.js @@ -0,0 +1,38 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + const token = req.cookies["sdsession"]; + if (token && token != "null" && token !== null) { + User.findOne({ + "sessions.token": token + }).populate('team').exec((err, user) => { + if (!user) { + // FIXME + var domain = "localhost"; + res.clearCookie('sdsession', { + domain: domain + }); + + if (req.accepts("text/html")) { + res.redirect("/"); + } else if (req.accepts('application/json')) { + res.status(403).json({ + "error": "token_not_found" + }); + } else { + res.redirect("/"); + } + + } else { + req["token"] = token; + req["user"] = user; + next(); + } + }); + } else { + next(); + } +} diff --git a/middlewares/space_helpers.js b/middlewares/space_helpers.js new file mode 100644 index 0000000..96a9357 --- /dev/null +++ b/middlewares/space_helpers.js @@ -0,0 +1,160 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + let spaceId = req.params.id; + + let finalizeReq = (space, role) => { + if (role === "none") { + res.status(403).json({ + "error": "access denied" + }); + } else { + req['space'] = space; + req['spaceRole'] = role; + res.header("x-spacedeck-space-role", req['spaceRole']); + next(); + } + }; + + var rolePerUser = (originalSpace, user, cb) => { + originalSpace.path = []; + + if (originalSpace._id.equals(req.user.home_folder_id) || (originalSpace.creator && originalSpace.creator._id.equals(req.user._id))) { + cb("admin"); + } else { + var findMembershipsForSpace = function(space, allMemberships, prevRole) { + Membership.find({ + "space": space._id + }, function(err, parentMemberships) { + var currentMemberships = parentMemberships.concat(allMemberships); + + if (space.parent_space_id) { + Space.findOne({ + "_id": space.parent_space_id + }, function(err, parentSpace) { + findMembershipsForSpace(parentSpace, currentMemberships, prevRole); + }); + } else { + // reached the top + + var role = prevRole; + space.memberships = currentMemberships; + + if(role == "none"){ + if(originalSpace.access_mode == "public") { + role = "viewer"; + } + } + + currentMemberships.forEach(function(m, i) { + if (m.user && m.user.equals(user._id)) { + role = m.role; + } + }); + + cb(role); + } + }); + }; + findMembershipsForSpace(originalSpace, [], "none"); + } + }; + + var finalizeAnonymousLogin = function(space, spaceAuth) { + var role = "none"; + + if (spaceAuth && (spaceAuth === space.edit_hash)) { + role = "editor"; + } else { + if (space.access_mode == "public") { + role = "viewer"; + } else { + role = "none"; + } + } + + if (req.user) { + rolePerUser(space, req.user, function(newRole) { + if (newRole == "admin" && (role == "editor" || role == "viewer")) { + finalizeReq(space, newRole); + } else if (newRole == "editor" && (role == "viewer")) { + finalizeReq(space, newRole); + } else { + finalizeReq(space, role); + } + }); + } else { + finalizeReq(space, role); + } + }; + + var userMapping = { + '_id': 1, + 'nickname': 1, + 'email': 1 + }; + + Space.findOne({ + "_id": spaceId + }).populate("creator", userMapping).exec(function(err, space) { + if (err) { + res.status(400).json(err); + } else { + if (space) { + + if (space.access_mode == "public") { + + if (space.password) { + if (req.spacePassword) { + if (req.spacePassword === space.password) { + finalizeAnonymousLogin(space, req["spaceAuth"]); + } else { + res.status(403).json({ + "error": "password_wrong" + }); + } + } else { + res.status(401).json({ + "error": "password_required" + }); + } + } else { + finalizeAnonymousLogin(space, req["spaceAuth"]); + } + + } else { + // special permission for screenshot/pdf export from backend + if (req.query['api_token'] && req.query['api_token'] == config.get('phantom_api_secret')) { + finalizeReq(space, "viewer"); + return; + } + + if (req.user) { + rolePerUser(space, req.user, function(role) { + if (role == "none") { + finalizeAnonymousLogin(space, req["spaceAuth"]); + } else { + finalizeReq(space, role); + } + }); + } else { + if (req.spaceAuth && space.edit_hash) { + finalizeAnonymousLogin(space, req["spaceAuth"]); + } else { + res.status(403).json({ + "error": "auth_required" + }); + } + } + } + } else { + res.status(404).json({ + "error": "space_not_found" + }); + } + } + }); +} diff --git a/middlewares/subdomain.js b/middlewares/subdomain.js new file mode 100644 index 0000000..2ce6a6a --- /dev/null +++ b/middlewares/subdomain.js @@ -0,0 +1,33 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + let host = req.headers.host; + Team.getTeamForHost(host, (err, team, subdomain) => { + if (subdomain) { + if (!err && team) { + req.subdomainTeam = team; + req.subdomain = subdomain; + next() + } else { + if (req.accepts('text/html')) { + res.status(404).render('not_found', { + title: 'Page Not Found.' + }); + } else if (req.accepts('application/json')) { + res.status(404).json({ + "error": "not_found" + }); + } else { + res.status(404).render('not_found', { + title: 'Page Not Found.' + }); + } + } + } else { + next(); + } + }); +} diff --git a/middlewares/team_helpers.js b/middlewares/team_helpers.js new file mode 100644 index 0000000..b072043 --- /dev/null +++ b/middlewares/team_helpers.js @@ -0,0 +1,23 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); + +module.exports = (req, res, next) => { + if (req.user) { + var isAdmin = req.user.team.admins.indexOf(req.user._id) >= 0; + var correctMethod = req.method == "GET" || (req.method == "DELETE" || req.method == "PUT" || req.method == "POST"); + + if (correctMethod && isAdmin) { + next(); + } else { + res.status(403, { + "error": "not authorized" + }); + } + } else { + res.status(403, { + "error": "not logged in" + }); + } +} \ No newline at end of file diff --git a/middlewares/templates.js b/middlewares/templates.js new file mode 100644 index 0000000..4aebb63 --- /dev/null +++ b/middlewares/templates.js @@ -0,0 +1,31 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); +var _ = require('underscore'); + +module.exports = (req, res, next) => { + res.oldRender = res.render; + res.render = function(template, params) { + + var team = req.subdomainTeam; + if (team) { + team = _.pick(team.toObject(), ['_id', 'name', 'subdomain', 'avatar_original_uri']); + } else { + team = null; + } + + const addParams = { + locale: req.i18n.locale, + config: config, + subdomain_team: team, + user: req.user, + csrf_token: "", + socket_auth: req.token + }; + + const all = _.extend(params, addParams); + res.oldRender(template, all); + }; + next(); +} \ No newline at end of file diff --git a/models/action.js b/models/action.js new file mode 100644 index 0000000..71a0da4 --- /dev/null +++ b/models/action.js @@ -0,0 +1,32 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.actionSchema = mongoose.Schema({ + space: { + type: Schema.Types.ObjectId, + ref: 'Space' + }, + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + editor_name: String, + action: String, + object: Schema.Types.Mixed, + created_at: { + type: Date, + default: Date.now + }, + updated_at: { + type: Date, + default: Date.now + } +}); + +module.exports.actionSchema.index({ + space: 1, + created_at: 1 +}); + diff --git a/models/artifact.js b/models/artifact.js new file mode 100644 index 0000000..604a372 --- /dev/null +++ b/models/artifact.js @@ -0,0 +1,88 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.artifactSchema = mongoose.model('Artifact', { + mime: String, + thumbnail_uri: String, + space_id: Schema.Types.ObjectId, + user_id: {type: Schema.Types.ObjectId, ref: 'User' }, + last_update_user_id: {type: Schema.Types.ObjectId, ref: 'User' }, + editor_name: String, + last_update_editor_name: String, + description: String, + state: {type: String, default: "idle"}, + meta: { + linked_to: [String], + title: String, + tags: [String], + search_text: String, + link_uri: String, + play_from: Number, + play_to: Number, + }, + board: { + x: {type: Number, default: 0.0}, + y: {type: Number, default: 0.0}, + z: {type: Number, default: 0.0}, + r: {type: Number, default: 0.0}, + w: {type: Number, default: 100}, + h: {type: Number, default: 100}, + }, + control_points: [{ + dx: Number, dy: Number + }], + group:{type: String, default: ""}, + locked: {type: Boolean, default: false}, + payload_uri: String, + payload_thumbnail_web_uri: String, + payload_thumbnail_medium_uri: String, + payload_thumbnail_big_uri: String, + payload_size: Number, // file size in bytes + style: { + fill_color: {type: String, default: "transparent"}, + stroke_color:{type: String, default: "#000000"}, + text_color: String, + stroke: {type: Number, default: 0.0}, + stroke_style: {type: String, default: "solid"}, + alpha: {type: Number, default: 1.0}, + order: {type: Number, default: 0}, + crop: { + x: Number, + y: Number, + w: Number, + h: Number + }, + shape: String, + shape_svg: String, + padding_left: Number, + padding_right: Number, + padding_top: Number, + padding_bottom: Number, + margin_left: Number, + margin_right: Number, + margin_top: Number, + margin_bottom: Number, + border_radius: Number, + align: {type: String, default: "left"}, + valign: {type: String, default: "top"}, + brightness: Number, + contrast: Number, + saturation: Number, + blur: Number, + hue: Number, + opacity: Number + }, + payload_alternatives: [{ + mime: String, + payload_uri: String, + payload_thumbnail_web_uri: String, + payload_thumbnail_medium_uri: String, + payload_thumbnail_big_uri: String, + payload_size: Number + }], + created_at: {type: Date, default: Date.now}, + created_from_ip: {type: String}, + updated_at: {type: Date, default: Date.now} +}); diff --git a/models/domain.js b/models/domain.js new file mode 100644 index 0000000..b2be7db --- /dev/null +++ b/models/domain.js @@ -0,0 +1,21 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.domainSchema = mongoose.Schema({ + domain: String, + edu: Boolean, + created_at: { + type: Date, + default: Date.now + }, + updated_at: { + type: Date, + default: Date.now + } +}); + +module.exports.domainSchema.index({ + domain: 1 +}); diff --git a/models/membership.js b/models/membership.js new file mode 100644 index 0000000..44b9b70 --- /dev/null +++ b/models/membership.js @@ -0,0 +1,45 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.membershipSchema = mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + space: { + type: Schema.Types.ObjectId, + ref: 'Space' + }, + team: { + type: Schema.Types.ObjectId, + ref: 'Team' + }, + role: { + type: String, + default: "viewer" + }, + state: { + type: String, + default: "active" + }, + email_invited: String, + code: String, + created_at: { + type: Date, + default: Date.now + }, + updated_at: { + type: Date, + default: Date.now + } +}); + +module.exports.membershipSchema.index({ + user: 1, + space: 1, + team: 1, + code: 1 +}); + diff --git a/models/message.js b/models/message.js new file mode 100644 index 0000000..5cbfe0e --- /dev/null +++ b/models/message.js @@ -0,0 +1,31 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.messageSchema = mongoose.Schema({ + user: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + editor_name: String, + space: { + type: Schema.Types.ObjectId, + ref: 'Space' + }, + message: String, + created_from_ip: {type: String}, + created_at: { + type: Date, + default: Date.now + }, + updated_at: { + type: Date, + default: Date.now + } +}); + +module.exports.messageSchema.index({ + space: 1, + user: 1 +}); diff --git a/models/plan.js b/models/plan.js new file mode 100644 index 0000000..b0c3815 --- /dev/null +++ b/models/plan.js @@ -0,0 +1,44 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +Plan = mongoose.model('Plan', { + key: String, + description: String, + limit_folders: { + type: Number, + default: 200 + }, + limit_spaces: { + type: Number, + default: 500 + }, + limit_storage_bytes: { + type: Number, + default: 10737418240 + }, + plan_type: { + type: String, + default: "org" + }, + price: Number, + public: Boolean, + recurring: { + type: String, + default: "month" + }, + title: String, + trial_days: Number, + voucher_code: String, + created_at: { + type: Date, + default: Date.now + }, + updated_at: { + type: Date, + default: Date.now + } +}); + +exports.planModel = Plan; \ No newline at end of file diff --git a/models/schema.js b/models/schema.js new file mode 100644 index 0000000..e64911b --- /dev/null +++ b/models/schema.js @@ -0,0 +1,12 @@ +//'use strict'; + +var mongoose = require('mongoose'); + +User = mongoose.model('User', require('./user').userSchema); +Action = mongoose.model('Action', require('./action').actionSchema); +Space = mongoose.model('Space', require('./space').spaceSchema); +Artifact = mongoose.model('Artifact', require('./artifact').artifactSchema); +Team = mongoose.model('Team', require('./team').teamSchema); +Message = mongoose.model('Message', require('./message').messageSchema); +Membership = mongoose.model('Membership', require('./membership').membershipSchema); +Domain = mongoose.model('Domain', require('./domain').domainSchema); diff --git a/models/space.js b/models/space.js new file mode 100644 index 0000000..e8ff3f6 --- /dev/null +++ b/models/space.js @@ -0,0 +1,273 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; +var async = require('async'); +var _ = require("underscore"); +var crypto = require('crypto'); + +module.exports.spaceSchema = Schema({ + name: {type: String, default: "New Space"}, + space_type: {type: String, default: "space"}, + + creator : { type: Schema.Types.ObjectId, ref: 'User' }, + parent_space_id: Schema.Types.ObjectId, + + access_mode: {type: String, default: "private"}, // "public" || "private" + password: String, + edit_hash: String, + edit_slug: String, + editors_locking: Boolean, + + thumbnail_uri: String, + stats: { + num_children: Number, + total_spaces: Number, + total_folders: Number, + storage_bytes: Number, + }, + + advanced: { + type: { + width: Number, + height: Number, + margin: Number, + background_color: String, + background_uri: String, + background_repeat: Boolean, + grid_size: Number, + grid_divisions: Number, + gutter: Number, + columns: Number, + column_max_width: Number, + columns_responsive: Number, + row_max_height: Number, + padding_horz: Number, + padding_vert: Number + }, + default: { + width: 200, + height: 400, + margin: 0, + background_color: "rgba(255,255,255,1)" + } + }, + blocked_at: {type: Date, default: Date.now}, + created_at: {type: Date, default: Date.now}, + updated_at: {type: Date, default: Date.now}, + thumbnail_updated_at: {type: Date}, + thumbnail_url: String +}); + +module.exports.spaceSchema.index({ creator: 1, parent_space_id: 1, created_at: 1, updated_at: 1, edit_hash: 1}); +module.exports.spaceSchema.statics.allForUser = function (user, callback) { + return this.find({user_id: user_id}, callback); +}; + +module.exports.spaceSchema.statics.getMemberships = function (err, callback) { + callback(null, {}); +}; + +var getRecursiveSubspacesForSpace = (parentSpace, cb) => { + if (parentSpace.space_type == "folder") { + Space.find({ + "parent_space_id": parentSpace._id + }).exec((err, subspaces) => { + async.map(subspaces, (space, innerCb) => { + getRecursiveSubspacesForSpace(space, (err, spaces) => { + innerCb(err, spaces); + }); + }, (err, subspaces) => { + var flattenSubspaces = _.flatten(subspaces); + flattenSubspaces.push(parentSpace); + cb(null, flattenSubspaces); + }); + }); + } else { + cb(null, [parentSpace]); + } +}; + +module.exports.spaceSchema.statics.getRecursiveSubspacesForSpace = getRecursiveSubspacesForSpace; + +var roleMapping = { + "none": 0, + "viewer": 1, + "editor": 2, + "admin": 3 +} + +module.exports.spaceSchema.statics.roleInSpace = (originalSpace, user, cb) => { + if (user.home_folder_id.toString() === originalSpace._id.toString()) { + cb(null, "admin"); + return; + } + + if (originalSpace.creator) { + if (originalSpace.creator._id.toString() === user._id.toString()) { + cb(null, "admin"); + return; + } + } + + var findMembershipsForSpace = function(space, allMemberships, prevRole) { + Membership.find({ + "space": space._id + }, (err, parentMemberships) => { + var currentMemberships = parentMemberships.concat(allMemberships); + + if (space.parent_space_id) { + Space.findOne({ + "_id": space.parent_space_id + }, function(err, parentSpace) { + + var role = prevRole; + if(role == "none"){ + if(originalSpace.access_mode == "public") { + role = "viewer"; + } + } + + findMembershipsForSpace(parentSpace, currentMemberships, role); + }); + } else { + // reached the top + var role = prevRole; + space.memberships = currentMemberships; + currentMemberships.forEach(function(m, i) { + if (m.user && m.user.equals(user._id)) { + if (m.role != null) { + if (roleMapping[m.role] > roleMapping[role]) { + role = m.role; + } + } + } + }); + + cb(err, role); + } + }); + }; + findMembershipsForSpace(originalSpace, [], "none"); +} + +module.exports.spaceSchema.statics.recursiveDelete = (space, cb) => { + space.remove(function(err) { + + Action.remove({ + space: space + }, function(err) { + if (err) + console.error("removed actions for space: ", err); + }); + + Membership.remove({ + space: space + }, function(err) { + if (err) + console.error("removed memberships for space: ", err); + }); + + if (space.space_type === "folder") { + Space + .find({ + parent_space_id: space._id + }) + .exec(function(err, spaces) { + async.eachLimit(spaces, 10, function(subSpace, innerCb) { + module.exports.spaceSchema.statics.recursiveDelete(subSpace, function(err) { + innerCb(err); + }); + }, function(err) { + cb(err); + }); + }); + + } else { + + Artifact.find({ + space_id: space._id + }, function(err, artifacts) { + if (err) cb(err); + else { + async.eachLimit(artifacts, 20, function(a, innerCb) { + a.remove(function(err) { + innerCb(null, a); + }); + }, function(err) { + cb(err); + }); + + } + }); + } + }); +}; + +var duplicateRecursiveSpace = (space, user, depth, cb, newParentSpace) => { + var newSpace = new Space(space); + newSpace._id = mongoose.Types.ObjectId(); + + if (newParentSpace) { + newSpace.parent_space_id = newParentSpace._id; + } else { + newSpace.name = newSpace.name + " (b)"; + } + + newSpace.creator = user; + newSpace.created_at = new Date(); + newSpace.updated_at = new Date(); + + if (newSpace.space_type === "space") { + newSpace.edit_hash = crypto.randomBytes(64).toString('hex').substring(0, 7); + } + + newSpace.save(function(err) { + + if (newSpace.space_type === "folder" && depth < 10) { + + Space + .find({ + parent_space_id: space._id + }) + .exec(function(err, spaces) { + async.eachLimit(spaces, 10, function(subSpace, innerCb) { + + duplicateRecursiveSpace(subSpace, user, ++depth, function(err, newSubSpace) { + innerCb(err, newSubSpace); + }, newSpace); + + }, function(err, allNewSubspaces) { + cb(err, newSpace); + }); + }); + + } else { + + Artifact.find({ + space_id: space._id + }, function(err, artifacts) { + if (err) innerCb(err); + else { + async.eachLimit(artifacts, 20, function(a, innerCb) { + var newArtifact = new Artifact(a); + newArtifact._id = mongoose.Types.ObjectId(); + newArtifact.space_id = newSpace._id; + newArtifact.created_at = new Date(); + newArtifact.updated_at = new Date(); + + newArtifact.save(function(err) { + innerCb(null, newArtifact); + }); + + }, function(err, allNewArtifacts) { + cb(err, newSpace); + }); + } + }); + } + + }); +}; + +module.exports.spaceSchema.statics.duplicateSpace = duplicateRecursiveSpace; diff --git a/models/team.js b/models/team.js new file mode 100644 index 0000000..a1f3cc7 --- /dev/null +++ b/models/team.js @@ -0,0 +1,70 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.teamSchema = mongoose.Schema({ + name: String, + subdomain: String, + creator: { + type: Schema.Types.ObjectId, + ref: 'User' + }, + admins: [{ + type: Schema.Types.ObjectId, + ref: 'User' + }], + invitation_codes: [String], + avatar_thumb_uri: String, + avatar_uri: String, + payment_type: { + type: String, + default: "auto" + }, + payment_plan_key: String, + payment_subscription_id: String, + blocked_at: { + type: Date + }, + upgraded_at: { + type: Date + }, + created_at: { + type: Date, + default: Date.now + }, + updated_at: { + type: Date, + default: Date.now + } +}); + +module.exports.teamSchema.index({ + creator: 1 +}); + +module.exports.teamSchema.statics.getTeamForHost = (host, cb) => { + + if (host != "127.0.0.1:9000") { //phantomjs check + let subDomainParts = host.split('.'); + + if (subDomainParts.length > 2) { + const subdomain = subDomainParts[0]; + + if (subdomain != "www") { + Team.findOne({ + subdomain: subdomain + }).exec((err, team) => { + cb(err, team, subdomain) + }); + } else { + cb(null, null) + } + + } else { + cb(null, null); + } + } else { + cb(null, null); + } +} diff --git a/models/user.js b/models/user.js new file mode 100644 index 0000000..e1c6887 --- /dev/null +++ b/models/user.js @@ -0,0 +1,53 @@ +'use strict'; + +var mongoose = require('mongoose'); +var Schema = mongoose.Schema; + +module.exports.userSchema = mongoose.Schema({ + email: String, + password_hash: String, + nickname: String, + account_type: {type: String, default: "email"}, + created_at: {type: Date, default: Date.now}, + updated_at: {type: Date, default: Date.now}, + avatar_original_uri: String, + avatar_thumb_uri: String, + src: String, + confirmation_token: String, + confirmed_at: Date, + password_reset_token: String, + home_folder_id: Schema.Types.ObjectId, + team : { type: Schema.Types.ObjectId, ref: 'Team' }, + preferences: { + language: String, + email_notifications: {type: Boolean, default: true}, + daily_digest_last_send: Date, + daily_digest: {type: Boolean, default: true} + }, + sessions: [ + { + token: String, + expires: Date, + device: String, + ip: String, + created_at: Date + } + ], + payment_info: String, + payment_plan_key: {type: String, default: "free"}, + payment_customer_id: String, + payment_subscription_id: String, + payment_notification_state: Number +}); + +module.exports.userSchema.index({ + email: 1, + "sessions.token": 1, + team: 1, + created_at: 1, + home_folder_id: 1 +}); + +module.exports.userSchema.statics.findBySessionToken = function (token, cb) { + return this.findOne({ "sessions.token": token}, cb); +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..d067495 --- /dev/null +++ b/package.json @@ -0,0 +1,91 @@ +{ + "name": "spacedeck-open", + "version": "1.0.0", + "private": true, + "scripts": { + "start": "nodemon -e .js,.html bin/www", + "test": "mocha" + }, + "dependencies": { + "archiver": "0.14.4", + "async": "1.3.0", + "aws-sdk": "2.1.39", + "basic-auth": "1.0.3", + "bcrypt": "*", + "body-parser": "~1.13.1", + "cheerio": "0.19.0", + "config": "1.14.0", + "cookie-parser": "~1.3.5", + "csurf": "1.8.3", + "debug": "~2.2.0", + "execSync": "latest", + "express": "~4.13.0", + "glob": "5.0.14", + "gm": "1.18.1", + "googleapis": "2.1.3", + "gulp": "^3.9.0", + "gulp-concat": "2.6.0", + "gulp-eslint": "*", + "gulp-express": "0.3.0", + "gulp-nodemon": "*", + "gulp-sass": "^2.0.3", + "gulp-uglify": "^1.5.1", + "gulp-util": "^3.0.6", + "helmet": "^1.1.0", + "i18n-2": "0.4.6", + "ioredis": "1.6.1", + "lodash": "^4.3.0", + "log-timestamp": "latest", + "md5": "2.0.0", + "moment": "2.10.6", + "mongoose": "4.4.3", + "morgan": "1.6.1", + "node-sass-middleware": "0.8.0", + "pdfkit": "0.7.1", + "validator": "5.2.0", + "node-phantom-simple": "2.2.4", + "phantomjs-prebuilt": "2.1.7", + "pm2": "latest", + "qr-image": "3.1.0", + "raven": "0.8.1", + "request": "2.60.0", + "sanitize-html": "^1.11.1", + "serve-favicon": "~2.3.0", + "swig": "1.4.2", + "slug": "0.9.1", + "underscore": "1.8.3", + "weak": "1.0.0", + "ws": "0.7.2" + }, + "devDependencies": { + "express": "^4.13.3", + "gulp": "^3.9.0", + "gulp-clean": "^0.3.2", + "gulp-concat": "^2.6.0", + "gulp-eslint": "^1.1.0", + "gulp-express": "^0.3.0", + "gulp-fingerprint": "^0.3.2", + "gulp-nodemon": "^2.0.4", + "gulp-rev": "^6.0.1", + "gulp-rev-all": "^0.8.22", + "gulp-rev-replace": "^0.4.3", + "gulp-sass": "^2.1.0", + "gulp-uglify": "^1.4.2", + "mocha": "*", + "nodemon": "*", + "should": "^7.1.0", + "supertest": "^1.1.0", + "winston": "^1.0.1" + }, + "description": "", + "main": "Gulpfile.js", + "directories": { + }, + "repository": { + "type": "git", + "url": "https://github.com/spacedeck/spacedeck-open.git" + }, + "keywords": [], + "author": "Lukas F. Hartmann, Martin Güther", + "license": "AGPLv3" +} diff --git a/public/fonts/OpenSans-Regular.ttf b/public/fonts/OpenSans-Regular.ttf new file mode 100755 index 0000000..db43334 Binary files /dev/null and b/public/fonts/OpenSans-Regular.ttf differ diff --git a/public/fonts/font.scss b/public/fonts/font.scss new file mode 100755 index 0000000..e288e66 --- /dev/null +++ b/public/fonts/font.scss @@ -0,0 +1,10 @@ +@font-face { + font-family: 'icon'; + src: url('../fonts/icon-regular-webfont.eot'); + src: url('../fonts/icon-regular-webfont.eot?#iefix') format('embedded-opentype'), + url('../fonts/icon-regular-webfont.woff') format('woff'), + url('../fonts/icon-regular-webfont.ttf') format('truetype'), + url('../fonts/icon-regular-webfont.svg#iconregular') format('svg'); + font-weight: normal; + font-style: normal; +} diff --git a/public/fonts/icon-regular-webfont.eot b/public/fonts/icon-regular-webfont.eot new file mode 100755 index 0000000..7e4fd2a Binary files /dev/null and b/public/fonts/icon-regular-webfont.eot differ diff --git a/public/fonts/icon-regular-webfont.svg b/public/fonts/icon-regular-webfont.svg new file mode 100755 index 0000000..f4d7082 --- /dev/null +++ b/public/fonts/icon-regular-webfont.svg @@ -0,0 +1,670 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/fonts/icon-regular-webfont.ttf b/public/fonts/icon-regular-webfont.ttf new file mode 100755 index 0000000..e439386 Binary files /dev/null and b/public/fonts/icon-regular-webfont.ttf differ diff --git a/public/fonts/icon-regular-webfont.woff b/public/fonts/icon-regular-webfont.woff new file mode 100755 index 0000000..72bd2bc Binary files /dev/null and b/public/fonts/icon-regular-webfont.woff differ diff --git a/public/fonts/unicode.scss b/public/fonts/unicode.scss new file mode 100644 index 0000000..709d8df --- /dev/null +++ b/public/fonts/unicode.scss @@ -0,0 +1,2648 @@ +.icon-bullet:before { + content: "\2022"; +} + +.icon-hyphen:before { + content: "\002D"; +} + +.icon-Euro:before { + content: "\20AC"; +} + +.icon-minus:before { + content: "\2212"; +} + +.icon-plus:before { + content: "\002B"; +} + +.icon-cloud:before { + content: "\2601"; +} + +.icon-lightning:before { + content: "\2607"; +} + +.icon-heartsuitblack:before { + content: "\2665"; +} + +.icon-whiteFlag:before { + content: "\2690"; +} + +.icon-blackFlag:before { + content: "\2691"; +} + +.icon-crossedSwords:before { + content: "\2694"; +} + +.icon-blackScissorsRight:before { + content: "\2702"; +} + +.icon-airplane:before { + content: "\2708"; +} + +.icon-envelope:before { + content: "\2709"; +} + +.icon-pencil:before { + content: "\270F"; +} + +.icon-sparkle:before { + content: "\2747"; +} + +.icon-at:before { + content: "\0040"; +} + +.icon-copyright:before { + content: "\00A9"; +} + +.icon-registered:before { + content: "\00AE"; +} + +.icon-section:before { + content: "\00A7"; +} + +.icon-battery:before { + content: "\1F50B"; +} + +.icon-bell:before { + content: "\1F514"; +} + +.icon-bookmark:before { + content: "\1F516"; +} + +.icon-calendar:before { + content: "\1F4C5"; +} + +.icon-camera:before { + content: "\1F4F7"; +} + +.icon-clipboard:before { + content: "\1F4CB"; +} + +.icon-hourglass:before { + content: "\231B"; +} + +.icon-microphone:before { + content: "\1F3A4"; +} + +.icon-newspaper:before { + content: "\1F4F0"; +} + +.icon-paperclip:before { + content: "\1F4CE"; +} + +.icon-radio:before { + content: "\1F4FB"; +} + +.icon-wrench:before { + content: "\1F527"; +} + +.icon-align-bottom:before { + content: "\E004"; +} + +.icon-align-center-horizontal:before { + content: "\E005"; +} + +.icon-align-center-vertical:before { + content: "\E006"; +} + +.icon-align-left:before { + content: "\E007"; +} + +.icon-align-right:before { + content: "\E008"; +} + +.icon-align-to-canvas:before { + content: "\E009"; +} + +.icon-align-top:before { + content: "\E00A"; +} + +.icon-align-vertical-bottom:before { + content: "\E00B"; +} + +.icon-align-vertical-middle:before { + content: "\E00C"; +} + +.icon-align-vertical-top:before { + content: "\E00D"; +} + +.icon-antenna:before { + content: "\E00E"; +} + +.icon-application:before { + content: "\E00F"; +} + +.icon-arrow-0:before { + content: "\E010"; +} + +.icon-arrow-1:before { + content: "\E011"; +} + +.icon-arrow-2:before { + content: "\E012"; +} + +.icon-arrow-3:before { + content: "\E013"; +} + +.icon-arrow-4:before { + content: "\E014"; +} + +.icon-arrow-bottom-light:before { + content: "\E015"; +} + +.icon-arrow-down:before { + content: "\E016"; +} + +.icon-arrow-fork:before { + content: "\E017"; +} + +.icon-arrow-horizontal:before { + content: "\E018"; +} + +.icon-arrow-left:before { + content: "\E019"; +} + +.icon-arrow-left-down:before { + content: "\E01A"; +} + +.icon-arrow-left-light:before { + content: "\E01B"; +} + +.icon-arrow-left-up:before { + content: "\E01C"; +} + +.icon-arrow-merge:before { + content: "\E01D"; +} + +.icon-arrow-right:before { + content: "\E01E"; +} + +.icon-arrow-right-down:before { + content: "\E01F"; +} + +.icon-arrow-right-light:before { + content: "\E020"; +} + +.icon-arrow-right-up:before { + content: "\E021"; +} + +.icon-arrow-thin-down:before { + content: "\E022"; +} + +.icon-arrow-thin-left:before { + content: "\E023"; +} + +.icon-arrow-thin-right:before { + content: "\E024"; +} + +.icon-arrow-thin-up:before { + content: "\E025"; +} + +.icon-arrow-top:before { + content: "\E026"; +} + +.icon-arrow-top-light:before { + content: "\E027"; +} + +.icon-arrow-vertical:before { + content: "\E028"; +} + +.icon-arrows-horizontal:before { + content: "\E029"; +} + +.icon-arrows-in:before { + content: "\E02A"; +} + +.icon-arrows-out:before { + content: "\E02B"; +} + +.icon-arrows-thin-horizontal:before { + content: "\E02C"; +} + +.icon-arrows-thin-vertical:before { + content: "\E02D"; +} + +.icon-arrows-vertical:before { + content: "\E02E"; +} + +.icon-ban-0:before { + content: "\E02F"; +} + +.icon-ban-1:before { + content: "\E030"; +} + +.icon-ban-2:before { + content: "\E031"; +} + +.icon-ban-3:before { + content: "\E032"; +} + +.icon-ban-4:before { + content: "\E033"; +} + +.icon-bar-code:before { + content: "\E034"; +} + +.icon-battery-0:before { + content: "\E035"; +} + +.icon-battery-1:before { + content: "\E036"; +} + +.icon-battery-2:before { + content: "\E037"; +} + +.icon-battery-3:before { + content: "\E038"; +} + +.icon-battery-4:before { + content: "\E039"; +} + +.icon-battery-5:before { + content: "\E03A"; +} + +.icon-battery-6:before { + content: "\E03B"; +} + +.icon-battery-7:before { + content: "\E03C"; +} + +.icon-battery-8:before { + content: "\E03D"; +} + +.icon-battery-loading:before { + content: "\E03E"; +} + +.icon-beverage-beer:before { + content: "\E03F"; +} + +.icon-beverage-cocktail:before { + content: "\E040"; +} + +.icon-beverage-wine:before { + content: "\E041"; +} + +.icon-book:before { + content: "\E042"; +} + +.icon-book-contacts:before { + content: "\E043"; +} + +.icon-book-open:before { + content: "\E044"; +} + +.icon-border-dashed:before { + content: "\E045"; +} + +.icon-border-dotted:before { + content: "\E046"; +} + +.icon-border-radius:before { + content: "\E047"; +} + +.icon-border-solid:before { + content: "\E048"; +} + +.icon-brightness:before { + content: "\E049"; +} + +.icon-calculator:before { + content: "\E04A"; +} + +.icon-calculator-black:before { + content: "\E04B"; +} + +.icon-camera-black:before { + content: "\E04C"; +} + +.icon-canvas:before { + content: "\E04D"; +} + +.icon-canvas-square:before { + content: "\E04E"; +} + +.icon-cd:before { + content: "\E04F"; +} + +.icon-certificate:before { + content: "\E050"; +} + +.icon-chart-0:before { + content: "\E051"; +} + +.icon-chart-1:before { + content: "\E052"; +} + +.icon-chart-2:before { + content: "\E053"; +} + +.icon-chart-3:before { + content: "\E054"; +} + +.icon-chart-4:before { + content: "\E055"; +} + +.icon-chart-5:before { + content: "\E056"; +} + +.icon-chart-6:before { + content: "\E057"; +} + +.icon-chart-7:before { + content: "\E058"; +} + +.icon-chart-8:before { + content: "\E059"; +} + +.icon-chart-bars:before { + content: "\E05A"; +} + +.icon-check:before { + content: "\E05B"; +} + +.icon-chevron-down-0:before { + content: "\E05C"; +} + +.icon-chevron-down-1:before { + content: "\E05D"; +} + +.icon-chevron-down-2:before { + content: "\E05E"; +} + +.icon-chevron-down-3:before { + content: "\E05F"; +} + +.icon-chevron-down-4:before { + content: "\E060"; +} + +.icon-chevron-left-0:before { + content: "\E061"; +} + +.icon-chevron-left-1:before { + content: "\E062"; +} + +.icon-chevron-left-2:before { + content: "\E063"; +} + +.icon-chevron-left-3:before { + content: "\E064"; +} + +.icon-chevron-left-4:before { + content: "\E065"; +} + +.icon-chevron-right-0:before { + content: "\E066"; +} + +.icon-chevron-right-1:before { + content: "\E067"; +} + +.icon-chevron-right-2:before { + content: "\E068"; +} + +.icon-chevron-right-3:before { + content: "\E069"; +} + +.icon-chevron-right-4:before { + content: "\E06A"; +} + +.icon-chevron-up-0:before { + content: "\E06B"; +} + +.icon-chevron-up-1:before { + content: "\E06C"; +} + +.icon-chevron-up-2:before { + content: "\E06D"; +} + +.icon-chevron-up-3:before { + content: "\E06E"; +} + +.icon-chevron-up-4:before { + content: "\E06F"; +} + +.icon-circle-0:before { + content: "\E070"; +} + +.icon-circle-1:before { + content: "\E071"; +} + +.icon-circle-2:before { + content: "\E072"; +} + +.icon-circle-3:before { + content: "\E073"; +} + +.icon-circle-4:before { + content: "\E074"; +} + +.icon-circle-check:before { + content: "\E075"; +} + +.icon-circle-down:before { + content: "\E076"; +} + +.icon-circle-exclude:before { + content: "\E077"; +} + +.icon-circle-info:before { + content: "\E078"; +} + +.icon-circle-intersect:before { + content: "\E079"; +} + +.icon-circle-left:before { + content: "\E07A"; +} + +.icon-circle-minus:before { + content: "\E07B"; +} + +.icon-circle-plus:before { + content: "\E07C"; +} + +.icon-circle-question:before { + content: "\E07D"; +} + +.icon-circle-remove:before { + content: "\E07E"; +} + +.icon-circle-right:before { + content: "\E07F"; +} + +.icon-circle-subtract:before { + content: "\E080"; +} + +.icon-circle-unite:before { + content: "\E081"; +} + +.icon-circle-up:before { + content: "\E082"; +} + +.icon-circle-warning:before { + content: "\E083"; +} + +.icon-clock:before { + content: "\E084"; +} + +.icon-cloud-down:before { + content: "\E085"; +} + +.icon-cloud-upload:before { + content: "\E086"; +} + +.icon-cluster:before { + content: "\E087"; +} + +.icon-cogwheel:before { + content: "\E088"; +} + +.icon-columns:before { + content: "\E089"; +} + +.icon-columns-3:before { + content: "\E08A"; +} + +.icon-compass:before { + content: "\E08B"; +} + +.icon-compass-east:before { + content: "\E08C"; +} + +.icon-compass-north:before { + content: "\E08D"; +} + +.icon-compass-north-east:before { + content: "\E08E"; +} + +.icon-compass-north-west:before { + content: "\E08F"; +} + +.icon-compass-south:before { + content: "\E090"; +} + +.icon-compass-south-east:before { + content: "\E091"; +} + +.icon-compass-south-west:before { + content: "\E092"; +} + +.icon-compass-west:before { + content: "\E093"; +} + +.icon-contrast:before { + content: "\E094"; +} + +.icon-controls-backward:before { + content: "\E095"; +} + +.icon-controls-eject:before { + content: "\E096"; +} + +.icon-controls-fast-backward:before { + content: "\E097"; +} + +.icon-controls-fast-forward:before { + content: "\E098"; +} + +.icon-controls-forward:before { + content: "\E099"; +} + +.icon-controls-pause:before { + content: "\E09A"; +} + +.icon-controls-play:before { + content: "\E09B"; +} + +.icon-controls-stop:before { + content: "\E09C"; +} + +.icon-couple:before { + content: "\E09D"; +} + +.icon-creditcard:before { + content: "\E09E"; +} + +.icon-cross-0:before { + content: "\E09F"; +} + +.icon-cross-1:before { + content: "\E0A0"; +} + +.icon-cross-2:before { + content: "\E0A1"; +} + +.icon-cross-3:before { + content: "\E0A2"; +} + +.icon-cross-4:before { + content: "\E0A3"; +} + +.icon-cross-hair:before { + content: "\E0A4"; +} + +.icon-cullumn-spacing:before { + content: "\E0A5"; +} + +.icon-currency-dollar:before { + content: "\E0A6"; +} + +.icon-currency-pound:before { + content: "\E0A7"; +} + +.icon-cursor-text:before { + content: "\E0A8"; +} + +.icon-cursor-text-frame:before { + content: "\E0A9"; +} + +.icon-curved-arrow:before { + content: "\E0AA"; +} + +.icon-cuttlery:before { + content: "\E0AB"; +} + +.icon-dashboard:before { + content: "\E0AC"; +} + +.icon-device-desktop:before { + content: "\E0AD"; +} + +.icon-device-mobile:before { + content: "\E0AE"; +} + +.icon-device-mobile-down:before { + content: "\E0AF"; +} + +.icon-device-mobile-transfer:before { + content: "\E0B0"; +} + +.icon-device-mobile-up:before { + content: "\E0B1"; +} + +.icon-device-mp3:before { + content: "\E0B2"; +} + +.icon-device-notebook:before { + content: "\E0B3"; +} + +.icon-device-tablet:before { + content: "\E0B4"; +} + +.icon-directions-axis:before { + content: "\E0B5"; +} + +.icon-directions-omni:before { + content: "\E0B6"; +} + +.icon-directions-seperate:before { + content: "\E0B7"; +} + +.icon-distribute-horizontal:before { + content: "\E0B8"; +} + +.icon-distribute-vertical:before { + content: "\E0B9"; +} + +.icon-download:before { + content: "\E0BA"; +} + +.icon-duplicate:before { + content: "\E0BB"; +} + +.icon-duplicate-alt:before { + content: "\E0BC"; +} + +.icon-edge-bottom:before { + content: "\E0BD"; +} + +.icon-edge-left:before { + content: "\E0BE"; +} + +.icon-edge-right:before { + content: "\E0BF"; +} + +.icon-edge-top:before { + content: "\E0C0"; +} + +.icon-ellipse-horizontal:before { + content: "\E0C1"; +} + +.icon-ellipse-vertical:before { + content: "\E0C2"; +} + +.icon-embed:before { + content: "\E0C3"; +} + +.icon-embed-alt:before { + content: "\E0C4"; +} + +.icon-emote-1:before { + content: "\E0C5"; +} + +.icon-emote-2:before { + content: "\E0C6"; +} + +.icon-emote-3:before { + content: "\E0C7"; +} + +.icon-emote-4:before { + content: "\E0C8"; +} + +.icon-emote-big-smile:before { + content: "\E0C9"; +} + +.icon-emote-only-happy:before { + content: "\E0CA"; +} + +.icon-emote-sad:before { + content: "\E0CB"; +} + +.icon-emote-smile:before { + content: "\E0CC"; +} + +.icon-emote-tongue:before { + content: "\E0CD"; +} + +.icon-escape:before { + content: "\E0CE"; +} + +.icon-eye-closed:before { + content: "\E0CF"; +} + +.icon-eye-open:before { + content: "\E0D0"; +} + +.icon-feather:before { + content: "\E0D1"; +} + +.icon-female:before { + content: "\E0D2"; +} + +.icon-female-afro:before { + content: "\E0D3"; +} + +.icon-female-bun:before { + content: "\E0D4"; +} + +.icon-female-emo:before { + content: "\E0D5"; +} + +.icon-female-long-hair:before { + content: "\E0D6"; +} + +.icon-female-moslem:before { + content: "\E0D7"; +} + +.icon-female-pigtails:before { + content: "\E0D8"; +} + +.icon-female-pigtails-alt:before { + content: "\E0D9"; +} + +.icon-female-user:before { + content: "\E0DA"; +} + +.icon-fetch:before { + content: "\E0DB"; +} + +.icon-file-check:before { + content: "\E0DC"; +} + +.icon-file-edit:before { + content: "\E0DD"; +} + +.icon-file-share:before { + content: "\E0DE"; +} + +.icon-fit-to:before { + content: "\E0DF"; +} + +.icon-flag:before { + content: "\E0E0"; +} + +.icon-flip-down:before { + content: "\E0E1"; +} + +.icon-flip-left:before { + content: "\E0E2"; +} + +.icon-flip-right:before { + content: "\E0E3"; +} + +.icon-flip-up:before { + content: "\E0E4"; +} + +.icon-floppydisk:before { + content: "\E0E5"; +} + +.icon-flow:before { + content: "\E0E6"; +} + +.icon-folder:before { + content: "\E0E7"; +} + +.icon-folder-down:before { + content: "\E0E8"; +} + +.icon-folder-favorites:before { + content: "\E0E9"; +} + +.icon-folder-flagged:before { + content: "\E0EA"; +} + +.icon-folder-leave:before { + content: "\E0EB"; +} + +.icon-folder-locked:before { + content: "\E0EC"; +} + +.icon-folder-minus:before { + content: "\E0ED"; +} + +.icon-folder-open:before { + content: "\E0EE"; +} + +.icon-folder-open-2:before { + content: "\E0EF"; +} + +.icon-folder-plus:before { + content: "\E0F0"; +} + +.icon-folder-remove:before { + content: "\E0F1"; +} + +.icon-folder-shared:before { + content: "\E0F2"; +} + +.icon-folder-up:before { + content: "\E0F3"; +} + +.icon-form-exclude:before { + content: "\E0F4"; +} + +.icon-form-intersect:before { + content: "\E0F5"; +} + +.icon-form-subtract:before { + content: "\E0F6"; +} + +.icon-form-unite:before { + content: "\E0F7"; +} + +.icon-full-screen:before { + content: "\E0F8"; +} + +.icon-fullscreen-off:before { + content: "\E0F9"; +} + +.icon-fullscreen-on:before { + content: "\E0FA"; +} + +.icon-globe:before { + content: "\E0FB"; +} + +.icon-grid:before { + content: "\E0FC"; +} + +.icon-grid-free:before { + content: "\E0FD"; +} + +.icon-grid-small:before { + content: "\E0FE"; +} + +.icon-group:before { + content: "\E0FF"; +} + +.icon-handle:before { + content: "\E100"; +} + +.icon-harddrive:before { + content: "\E101"; +} + +.icon-heading:before { + content: "\E102"; +} + +.icon-headphones:before { + content: "\E103"; +} + +.icon-heart:before { + content: "\E104"; +} + +.icon-heart-hollow:before { + content: "\E105"; +} + +.icon-history:before { + content: "\E106"; +} + +.icon-home:before { + content: "\E107"; +} + +.icon-human:before { + content: "\E108"; +} + +.icon-inbox:before { + content: "\E109"; +} + +.icon-info:before { + content: "\E10A"; +} + +.icon-input-checkbox:before { + content: "\E10B"; +} + +.icon-input-checkbox-checked:before { + content: "\E10C"; +} + +.icon-input-radio:before { + content: "\E10D"; +} + +.icon-input-radio-checked:before { + content: "\E10E"; +} + +.icon-leave:before { + content: "\E10F"; +} + +.icon-lightbulb:before { + content: "\E110"; +} + +.icon-line-0:before { + content: "\E111"; +} + +.icon-line-1:before { + content: "\E112"; +} + +.icon-line-2:before { + content: "\E113"; +} + +.icon-line-3:before { + content: "\E114"; +} + +.icon-line-4:before { + content: "\E115"; +} + +.icon-line-half-0:before { + content: "\E116"; +} + +.icon-line-half-1:before { + content: "\E117"; +} + +.icon-line-half-2:before { + content: "\E118"; +} + +.icon-line-half-3:before { + content: "\E119"; +} + +.icon-line-half-4:before { + content: "\E11A"; +} + +.icon-lines:before { + content: "\E11B"; +} + +.icon-link:before { + content: "\E11C"; +} + +.icon-link-closed:before { + content: "\E11D"; +} + +.icon-link-closed-alt:before { + content: "\E11E"; +} + +.icon-link-open:before { + content: "\E11F"; +} + +.icon-link-open-alt:before { + content: "\E120"; +} + +.icon-list:before { + content: "\E121"; +} + +.icon-location:before { + content: "\E122"; +} + +.icon-lock-closed:before { + content: "\E123"; +} + +.icon-lock-open:before { + content: "\E124"; +} + +.icon-login:before { + content: "\E125"; +} + +.icon-logout:before { + content: "\E126"; +} + +.icon-loop:before { + content: "\E127"; +} + +.icon-magnet:before { + content: "\E128"; +} + +.icon-mail:before { + content: "\E129"; +} + +.icon-male:before { + content: "\E12A"; +} + +.icon-male-add:before { + content: "\E12B"; +} + +.icon-male-afro:before { + content: "\E12C"; +} + +.icon-male-business:before { + content: "\E12D"; +} + +.icon-male-cool:before { + content: "\E12E"; +} + +.icon-male-emo:before { + content: "\E12F"; +} + +.icon-male-flathat:before { + content: "\E130"; +} + +.icon-male-gentleman:before { + content: "\E131"; +} + +.icon-male-hoodie:before { + content: "\E132"; +} + +.icon-male-jew:before { + content: "\E133"; +} + +.icon-male-monk:before { + content: "\E134"; +} + +.icon-male-moslem:before { + content: "\E135"; +} + +.icon-male-mustache:before { + content: "\E136"; +} + +.icon-male-punk:before { + content: "\E137"; +} + +.icon-male-remove:before { + content: "\E138"; +} + +.icon-male-scuba:before { + content: "\E139"; +} + +.icon-male-soldier:before { + content: "\E13A"; +} + +.icon-male-techno:before { + content: "\E13B"; +} + +.icon-male-user:before { + content: "\E13C"; +} + +.icon-megaphone:before { + content: "\E13D"; +} + +.icon-menu:before { + content: "\E13E"; +} + +.icon-menu-2:before { + content: "\E13F"; +} + +.icon-message:before { + content: "\E140"; +} + +.icon-message-add:before { + content: "\E141"; +} + +.icon-message-blank:before { + content: "\E142"; +} + +.icon-messages:before { + content: "\E143"; +} + +.icon-microphone-off:before { + content: "\E144"; +} + +.icon-mirror-horizontal:before { + content: "\E145"; +} + +.icon-mirror-horizontal-alt:before { + content: "\E146"; +} + +.icon-mirror-vertical:before { + content: "\E147"; +} + +.icon-mirror-vertical-alt:before { + content: "\E148"; +} + +.icon-moon-first-quarter:before { + content: "\E149"; +} + +.icon-moon-full:before { + content: "\E14A"; +} + +.icon-moon-new:before { + content: "\E14B"; +} + +.icon-moon-third-quarter:before { + content: "\E14C"; +} + +.icon-moon-waning-crescent:before { + content: "\E14D"; +} + +.icon-moon-waning-gibbous:before { + content: "\E14E"; +} + +.icon-moon-waxing-crescent:before { + content: "\E14F"; +} + +.icon-moon-waxing-gibbous:before { + content: "\E150"; +} + +.icon-move:before { + content: "\E151"; +} + +.icon-move-down:before { + content: "\E152"; +} + +.icon-move-left:before { + content: "\E153"; +} + +.icon-move-right:before { + content: "\E154"; +} + +.icon-move-up:before { + content: "\E155"; +} + +.icon-movie:before { + content: "\E156"; +} + +.icon-music-note:before { + content: "\E157"; +} + +.icon-music-upload:before { + content: "\E158"; +} + +.icon-newspaper-alt:before { + content: "\E159"; +} + +.icon-padding:before { + content: "\E15A"; +} + +.icon-padding-bottom:before { + content: "\E15B"; +} + +.icon-padding-left:before { + content: "\E15C"; +} + +.icon-padding-right:before { + content: "\E15D"; +} + +.icon-padding-top:before { + content: "\E15E"; +} + +.icon-page-horizontal:before { + content: "\E15F"; +} + +.icon-page-horizontal-down:before { + content: "\E160"; +} + +.icon-page-horizontal-flag:before { + content: "\E161"; +} + +.icon-page-horizontal-locked:before { + content: "\E162"; +} + +.icon-page-horizontal-minus:before { + content: "\E163"; +} + +.icon-page-horizontal-plus:before { + content: "\E164"; +} + +.icon-page-horizontal-remove:before { + content: "\E165"; +} + +.icon-page-horizontal-up:before { + content: "\E166"; +} + +.icon-page-vert:before { + content: "\E167"; +} + +.icon-page-vertical-double:before { + content: "\E168"; +} + +.icon-page-vertical-down:before { + content: "\E169"; +} + +.icon-page-vertical-flag:before { + content: "\E16A"; +} + +.icon-page-vertical-image:before { + content: "\E16B"; +} + +.icon-page-vertical-locked:before { + content: "\E16C"; +} + +.icon-page-vertical-minus:before { + content: "\E16D"; +} + +.icon-page-vertical-plus:before { + content: "\E16E"; +} + +.icon-page-vertical-remove:before { + content: "\E16F"; +} + +.icon-page-vertical-table:before { + content: "\E170"; +} + +.icon-page-vertical-text:before { + content: "\E171"; +} + +.icon-page-vertical-up:before { + content: "\E172"; +} + +.icon-palette:before { + content: "\E173"; +} + +.icon-palette-alt:before { + content: "\E174"; +} + +.icon-paperplane:before { + content: "\E175"; +} + +.icon-photofilm:before { + content: "\E176"; +} + +.icon-picture:before { + content: "\E177"; +} + +.icon-picture-landscape:before { + content: "\E178"; +} + +.icon-picture-portrait:before { + content: "\E179"; +} + +.icon-picture-upload:before { + content: "\E17A"; +} + +.icon-pictures:before { + content: "\E17B"; +} + +.icon-pin:before { + content: "\E17C"; +} + +.icon-planet:before { + content: "\E001"; +} + +.icon-point:before { + content: "\E17D"; +} + +.icon-pointing-down:before { + content: "\E17E"; +} + +.icon-pointing-left:before { + content: "\E17F"; +} + +.icon-pointing-right:before { + content: "\E180"; +} + +.icon-pointing-up:before { + content: "\E181"; +} + +.icon-postcard:before { + content: "\E182"; +} + +.icon-power-off:before { + content: "\E183"; +} + +.icon-present:before { + content: "\E184"; +} + +.icon-presentation:before { + content: "\E185"; +} + +.icon-printer:before { + content: "\E186"; +} + +.icon-pull:before { + content: "\E187"; +} + +.icon-push:before { + content: "\E188"; +} + +.icon-qr-code:before { + content: "\E189"; +} + +.icon-quote:before { + content: "\E18A"; +} + +.icon-radio-black:before { + content: "\E18B"; +} + +.icon-random:before { + content: "\E18C"; +} + +.icon-record:before { + content: "\E18D"; +} + +.icon-redo:before { + content: "\E18E"; +} + +.icon-resize-full:before { + content: "\E18F"; +} + +.icon-resize-small:before { + content: "\E190"; +} + +.icon-retweet:before { + content: "\E191"; +} + +.icon-rings:before { + content: "\E192"; +} + +.icon-road:before { + content: "\E193"; +} + +.icon-rotate:before { + content: "\E194"; +} + +.icon-rotate-2:before { + content: "\E195"; +} + +.icon-rotate-3:before { + content: "\E196"; +} + +.icon-rotate-left:before { + content: "\E197"; +} + +.icon-rotate-right:before { + content: "\E198"; +} + +.icon-rss:before { + content: "\E199"; +} + +.icon-search:before { + content: "\E000"; +} + +.icon-section-alt:before { + content: "\E19A"; +} + +.icon-section-alt-2:before { + content: "\E19B"; +} + +.icon-section-edit:before { + content: "\E19C"; +} + +.icon-selection:before { + content: "\E19D"; +} + +.icon-selection-circle:before { + content: "\E19E"; +} + +.icon-selection-circle-strong:before { + content: "\E19F"; +} + +.icon-selection-square:before { + content: "\E1A0"; +} + +.icon-selection-square-strong:before { + content: "\E1A1"; +} + +.icon-server:before { + content: "\E1A2"; +} + +.icon-shape-bubble:before { + content: "\E1A3"; +} + +.icon-shape-burst:before { + content: "\E1A4"; +} + +.icon-shape-circle:before { + content: "\E1A5"; +} + +.icon-shape-cloud:before { + content: "\E1A6"; +} + +.icon-shape-cross:before { + content: "\E1A7"; +} + +.icon-shape-heart:before { + content: "\E1A8"; +} + +.icon-shape-hexagon:before { + content: "\E1A9"; +} + +.icon-shape-octagon:before { + content: "\E1AA"; +} + +.icon-shape-pentagon:before { + content: "\E1AB"; +} + +.icon-shape-plus:before { + content: "\E1AC"; +} + +.icon-shape-square:before { + content: "\E1AD"; +} + +.icon-shape-star:before { + content: "\E1AE"; +} + +.icon-shape-triangle:before { + content: "\E1AF"; +} + +.icon-shapes:before { + content: "\E1B0"; +} + +.icon-share:before { + content: "\E1B1"; +} + +.icon-shopping-cart:before { + content: "\E1B2"; +} + +.icon-signal:before { + content: "\E1B3"; +} + +.icon-signal-alt:before { + content: "\E1B4"; +} + +.icon-size:before { + content: "\E1B5"; +} + +.icon-size-both:before { + content: "\E1B6"; +} + +.icon-size-horizontal:before { + content: "\E1B7"; +} + +.icon-size-vertical:before { + content: "\E1B8"; +} + +.icon-social-dribbble:before { + content: "\E1B9"; +} + +.icon-social-dropbox:before { + content: "\E1BA"; +} + +.icon-social-facebook:before { + content: "\E1BB"; +} + +.icon-social-flickr:before { + content: "\E1BC"; +} + +.icon-social-google:before { + content: "\E1BD"; +} + +.icon-social-soundcloud:before { + content: "\E002"; +} + +.icon-social-square-dribbble:before { + content: "\E1BE"; +} + +.icon-social-square-dropbox:before { + content: "\E1BF"; +} + +.icon-social-square-facebook:before { + content: "\E1C0"; +} + +.icon-social-square-flickr:before { + content: "\E1C1"; +} + +.icon-social-square-google:before { + content: "\E1C2"; +} + +.icon-social-square-soundcloud:before { + content: "\E003"; +} + +.icon-social-square-tumblr:before { + content: "\E1C3"; +} + +.icon-social-square-twitter:before { + content: "\E1C4"; +} + +.icon-social-square-vimeo:before { + content: "\E1C5"; +} + +.icon-social-square-vine:before { + content: "\E1C6"; +} + +.icon-social-square-youtube:before { + content: "\E1C7"; +} + +.icon-social-tumblr:before { + content: "\E1C8"; +} + +.icon-social-twitter:before { + content: "\E1C9"; +} + +.icon-social-vimeo:before { + content: "\E1CA"; +} + +.icon-social-vine:before { + content: "\E1CB"; +} + +.icon-social-youtube:before { + content: "\E1CC"; +} + +.icon-sort-alphabetical-ascending:before { + content: "\E1CD"; +} + +.icon-sort-alphabetical-descending:before { + content: "\E1CE"; +} + +.icon-sort-ascending:before { + content: "\E1CF"; +} + +.icon-sort-descending:before { + content: "\E1D0"; +} + +.icon-sort-numerical-ascending:before { + content: "\E1D1"; +} + +.icon-sort-numerical-descending:before { + content: "\E1D2"; +} + +.icon-sorting:before { + content: "\E1D3"; +} + +.icon-sorting-ascending:before { + content: "\E1D4"; +} + +.icon-sorting-descending:before { + content: "\E1D5"; +} + +.icon-sound-5-1:before { + content: "\E1D6"; +} + +.icon-sound-6-1:before { + content: "\E1D7"; +} + +.icon-sound-7-1:before { + content: "\E1D8"; +} + +.icon-sound-dolby:before { + content: "\E1D9"; +} + +.icon-sound-full:before { + content: "\E1DA"; +} + +.icon-sound-off:before { + content: "\E1DB"; +} + +.icon-sound-silent:before { + content: "\E1DC"; +} + +.icon-sound-stereo:before { + content: "\E1DD"; +} + +.icon-space-horizontal:before { + content: "\E1DE"; +} + +.icon-space-shared:before { + content: "\E1DF"; +} + +.icon-space-vertical:before { + content: "\E1E0"; +} + +.icon-spacedeck-logo:before { + content: "\E1E1"; +} + +.icon-spacing-horizontal:before { + content: "\E1E2"; +} + +.icon-spacing-vertical:before { + content: "\E1E3"; +} + +.icon-square-down:before { + content: "\E1E4"; +} + +.icon-square-left:before { + content: "\E1E5"; +} + +.icon-square-right:before { + content: "\E1E6"; +} + +.icon-square-up:before { + content: "\E1E7"; +} + +.icon-stack-3d:before { + content: "\E1E8"; +} + +.icon-stack-3d-bottom:before { + content: "\E1E9"; +} + +.icon-stack-3d-top:before { + content: "\E1EA"; +} + +.icon-stack-bottom:before { + content: "\E1EB"; +} + +.icon-stack-down:before { + content: "\E1EC"; +} + +.icon-stack-top:before { + content: "\E1ED"; +} + +.icon-stack-up:before { + content: "\E1EE"; +} + +.icon-star:before { + content: "\E1EF"; +} + +.icon-star-hollow:before { + content: "\E1F0"; +} + +.icon-stash-apply:before { + content: "\E1F1"; +} + +.icon-stash-save:before { + content: "\E1F2"; +} + +.icon-statistic:before { + content: "\E1F3"; +} + +.icon-step-backward:before { + content: "\E1F4"; +} + +.icon-step-forward:before { + content: "\E1F5"; +} + +.icon-stroke-weight:before { + content: "\E1F6"; +} + +.icon-subtitles:before { + content: "\E1F7"; +} + +.icon-suitcase:before { + content: "\E1F8"; +} + +.icon-table:before { + content: "\E1F9"; +} + +.icon-tag:before { + content: "\E1FA"; +} + +.icon-tags:before { + content: "\E1FB"; +} + +.icon-text-align-center:before { + content: "\E1FC"; +} + +.icon-text-align-center-alt:before { + content: "\E1FD"; +} + +.icon-text-align-justify:before { + content: "\E1FE"; +} + +.icon-text-align-justify-alt:before { + content: "\E1FF"; +} + +.icon-text-align-left:before { + content: "\E200"; +} + +.icon-text-align-left-alt:before { + content: "\E201"; +} + +.icon-text-align-right:before { + content: "\E202"; +} + +.icon-text-align-right-alt:before { + content: "\E203"; +} + +.icon-text-allcaps:before { + content: "\E204"; +} + +.icon-text-bold:before { + content: "\E205"; +} + +.icon-text-frame:before { + content: "\E206"; +} + +.icon-text-indent:before { + content: "\E207"; +} + +.icon-text-italic:before { + content: "\E208"; +} + +.icon-text-list:before { + content: "\E209"; +} + +.icon-text-list-alphabetical:before { + content: "\E20A"; +} + +.icon-text-list-alt:before { + content: "\E20B"; +} + +.icon-text-list-bullet:before { + content: "\E20C"; +} + +.icon-text-list-numbered:before { + content: "\E20D"; +} + +.icon-text-normal:before { + content: "\E20E"; +} + +.icon-text-remove-format:before { + content: "\E20F"; +} + +.icon-text-roman:before { + content: "\E210"; +} + +.icon-text-size:before { + content: "\E211"; +} + +.icon-text-strike:before { + content: "\E212"; +} + +.icon-text-styles:before { + content: "\E213"; +} + +.icon-text-sub:before { + content: "\E214"; +} + +.icon-text-super:before { + content: "\E215"; +} + +.icon-text-typeface:before { + content: "\E216"; +} + +.icon-text-underline:before { + content: "\E217"; +} + +.icon-text-unindent:before { + content: "\E218"; +} + +.icon-text-uppercase:before { + content: "\E219"; +} + +.icon-text-width:before { + content: "\E21A"; +} + +.icon-thumbs-down:before { + content: "\E21B"; +} + +.icon-thumbs-up:before { + content: "\E21C"; +} + +.icon-tint:before { + content: "\E21D"; +} + +.icon-tint-hollow:before { + content: "\E21E"; +} + +.icon-tint-none:before { + content: "\E21F"; +} + +.icon-tool-arrow:before { + content: "\E220"; +} + +.icon-tool-circle:before { + content: "\E221"; +} + +.icon-tool-crop:before { + content: "\E222"; +} + +.icon-tool-draw:before { + content: "\E223"; +} + +.icon-tool-erase:before { + content: "\E224"; +} + +.icon-tool-eyedrop:before { + content: "\E225"; +} + +.icon-tool-fill:before { + content: "\E226"; +} + +.icon-tool-hand:before { + content: "\E227"; +} + +.icon-tool-library:before { + content: "\E228"; +} + +.icon-tool-line:before { + content: "\E229"; +} + +.icon-tool-magic-wand:before { + content: "\E22A"; +} + +.icon-tool-pointer:before { + content: "\E22B"; +} + +.icon-tool-rectangle:before { + content: "\E22C"; +} + +.icon-tool-scribble:before { + content: "\E22D"; +} + +.icon-tool-stroke:before { + content: "\E22E"; +} + +.icon-tool-text:before { + content: "\E22F"; +} + +.icon-tool-text-alt:before { + content: "\E230"; +} + +.icon-tool-zone:before { + content: "\E231"; +} + +.icon-tool-zones:before { + content: "\E232"; +} + +.icon-transfer:before { + content: "\E233"; +} + +.icon-trash:before { + content: "\E234"; +} + +.icon-tree-conifer:before { + content: "\E235"; +} + +.icon-tree-deciduous:before { + content: "\E236"; +} + +.icon-triangle-0:before { + content: "\E237"; +} + +.icon-triangle-0-bottom:before { + content: "\E238"; +} + +.icon-triangle-0-left:before { + content: "\E239"; +} + +.icon-triangle-0-right:before { + content: "\E23A"; +} + +.icon-triangle-0-top:before { + content: "\E23B"; +} + +.icon-triangle-1:before { + content: "\E23C"; +} + +.icon-triangle-1-bottom:before { + content: "\E23D"; +} + +.icon-triangle-1-left:before { + content: "\E23E"; +} + +.icon-triangle-1-right:before { + content: "\E23F"; +} + +.icon-triangle-1-top:before { + content: "\E240"; +} + +.icon-triangle-2:before { + content: "\E241"; +} + +.icon-triangle-2-bottom:before { + content: "\E242"; +} + +.icon-triangle-2-left:before { + content: "\E243"; +} + +.icon-triangle-2-right:before { + content: "\E244"; +} + +.icon-triangle-2-top:before { + content: "\E245"; +} + +.icon-triangle-3:before { + content: "\E246"; +} + +.icon-triangle-3-bottom:before { + content: "\E247"; +} + +.icon-triangle-3-left:before { + content: "\E248"; +} + +.icon-triangle-3-right:before { + content: "\E249"; +} + +.icon-triangle-3-top:before { + content: "\E24A"; +} + +.icon-triangle-4:before { + content: "\E24B"; +} + +.icon-triangle-4-bottom:before { + content: "\E24C"; +} + +.icon-triangle-4-left:before { + content: "\E24D"; +} + +.icon-triangle-4-right:before { + content: "\E24E"; +} + +.icon-triangle-4-top:before { + content: "\E24F"; +} + +.icon-triangle-down:before { + content: "\E250"; +} + +.icon-triangle-left:before { + content: "\E251"; +} + +.icon-triangle-right:before { + content: "\E252"; +} + +.icon-triangle-up:before { + content: "\E253"; +} + +.icon-triangles-horizontal:before { + content: "\E254"; +} + +.icon-triangles-vertical:before { + content: "\E255"; +} + +.icon-undo:before { + content: "\E256"; +} + +.icon-ungroup:before { + content: "\E257"; +} + +.icon-uniE000:before { + content: "\E258"; +} + +.icon-upload:before { + content: "\E259"; +} + +.icon-upload-alt:before { + content: "\E25A"; +} + +.icon-user:before { + content: "\E25B"; +} + +.icon-user-add:before { + content: "\E25C"; +} + +.icon-user-group:before { + content: "\E25D"; +} + +.icon-user-group-2:before { + content: "\E25E"; +} + +.icon-user-group-3:before { + content: "\E25F"; +} + +.icon-user-group-gentlemen:before { + content: "\E260"; +} + +.icon-user-minus:before { + content: "\E261"; +} + +.icon-user-remove:before { + content: "\E262"; +} + +.icon-video-camera:before { + content: "\E263"; +} + +.icon-video-camera-off:before { + content: "\E264"; +} + +.icon-video-hd:before { + content: "\E265"; +} + +.icon-video-sd:before { + content: "\E266"; +} + +.icon-video-upload:before { + content: "\E267"; +} + +.icon-vinyl:before { + content: "\E268"; +} + +.icon-warning:before { + content: "\E269"; +} + +.icon-wifi:before { + content: "\E26A"; +} + +.icon-window:before { + content: "\E26B"; +} + +.icon-window-new:before { + content: "\E26C"; +} + +.icon-window-new-alt:before { + content: "\E26D"; +} + +.icon-zone:before { + content: "\E26E"; +} + +.icon-zone-edit:before { + content: "\E26F"; +} + +.icon-zoom:before { + content: "\E270"; +} + +.icon-zoom-in:before { + content: "\E271"; +} + +.icon-zoom-out:before { + content: "\E272"; +} + +.icon-helper-bar:before { + content: "\E273"; +} + +.icon-helper-needle:before { + content: "\E274"; +} + +.icon-helper-screen:before { + content: "\E275"; +} + diff --git a/public/images/border-dotted.png b/public/images/border-dotted.png new file mode 100644 index 0000000..40d8e7f Binary files /dev/null and b/public/images/border-dotted.png differ diff --git a/public/images/cursors/crosshair.png b/public/images/cursors/crosshair.png new file mode 100644 index 0000000..2289995 Binary files /dev/null and b/public/images/cursors/crosshair.png differ diff --git a/public/images/cursors/crosshair.psd b/public/images/cursors/crosshair.psd new file mode 100644 index 0000000..9a1a62a Binary files /dev/null and b/public/images/cursors/crosshair.psd differ diff --git a/public/images/cursors/eyedrop.png b/public/images/cursors/eyedrop.png new file mode 100644 index 0000000..02e3771 Binary files /dev/null and b/public/images/cursors/eyedrop.png differ diff --git a/public/images/cursors/eyedropper.png b/public/images/cursors/eyedropper.png new file mode 100644 index 0000000..0ddf179 Binary files /dev/null and b/public/images/cursors/eyedropper.png differ diff --git a/public/images/cursors/eyedropper.psd b/public/images/cursors/eyedropper.psd new file mode 100644 index 0000000..a9be524 Binary files /dev/null and b/public/images/cursors/eyedropper.psd differ diff --git a/public/images/cursors/pencil.png b/public/images/cursors/pencil.png new file mode 100644 index 0000000..113ea62 Binary files /dev/null and b/public/images/cursors/pencil.png differ diff --git a/public/images/cursors/pencil.psd b/public/images/cursors/pencil.psd new file mode 100644 index 0000000..c4d848f Binary files /dev/null and b/public/images/cursors/pencil.psd differ diff --git a/public/images/cursors/text-frame.png b/public/images/cursors/text-frame.png new file mode 100644 index 0000000..180a8b0 Binary files /dev/null and b/public/images/cursors/text-frame.png differ diff --git a/public/images/cursors/text-frame.psd b/public/images/cursors/text-frame.psd new file mode 100644 index 0000000..8691b66 Binary files /dev/null and b/public/images/cursors/text-frame.psd differ diff --git a/public/images/diamond.svg b/public/images/diamond.svg new file mode 100644 index 0000000..c13dd48 --- /dev/null +++ b/public/images/diamond.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/public/images/favicon.png b/public/images/favicon.png new file mode 100644 index 0000000..5b28e0c Binary files /dev/null and b/public/images/favicon.png differ diff --git a/public/images/hourglass.gif b/public/images/hourglass.gif new file mode 100644 index 0000000..b12adbd Binary files /dev/null and b/public/images/hourglass.gif differ diff --git a/public/images/hue.png b/public/images/hue.png new file mode 100644 index 0000000..e734dd6 Binary files /dev/null and b/public/images/hue.png differ diff --git a/public/images/huevalue.png b/public/images/huevalue.png new file mode 100644 index 0000000..e63f7e9 Binary files /dev/null and b/public/images/huevalue.png differ diff --git a/public/images/ideate-char 2.svg b/public/images/ideate-char 2.svg new file mode 100644 index 0000000..e77b280 --- /dev/null +++ b/public/images/ideate-char 2.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/offline-char.svg b/public/images/offline-char.svg new file mode 100644 index 0000000..5eb0010 --- /dev/null +++ b/public/images/offline-char.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/opacity-grid.png b/public/images/opacity-grid.png new file mode 100644 index 0000000..4ce973b Binary files /dev/null and b/public/images/opacity-grid.png differ diff --git a/public/images/opacity-strip.png b/public/images/opacity-strip.png new file mode 100644 index 0000000..156aee2 Binary files /dev/null and b/public/images/opacity-strip.png differ diff --git a/public/images/placeholder-4x3.gif b/public/images/placeholder-4x3.gif new file mode 100644 index 0000000..6f6d278 Binary files /dev/null and b/public/images/placeholder-4x3.gif differ diff --git a/public/images/sd5-hero2-compressed.jpg b/public/images/sd5-hero2-compressed.jpg new file mode 100644 index 0000000..50b2287 Binary files /dev/null and b/public/images/sd5-hero2-compressed.jpg differ diff --git a/public/images/sd5-keyvisual-compressed.jpg b/public/images/sd5-keyvisual-compressed.jpg new file mode 100644 index 0000000..6da1388 Binary files /dev/null and b/public/images/sd5-keyvisual-compressed.jpg differ diff --git a/public/images/sd5-logo-inverted.svg b/public/images/sd5-logo-inverted.svg new file mode 100644 index 0000000..cd009f4 --- /dev/null +++ b/public/images/sd5-logo-inverted.svg @@ -0,0 +1,124 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/sd5-logo.png b/public/images/sd5-logo.png new file mode 100644 index 0000000..c3b5215 Binary files /dev/null and b/public/images/sd5-logo.png differ diff --git a/public/images/sd5-logo.svg b/public/images/sd5-logo.svg new file mode 100644 index 0000000..ae7a5e8 --- /dev/null +++ b/public/images/sd5-logo.svg @@ -0,0 +1,118 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/spacedeck-logo.svg b/public/images/spacedeck-logo.svg new file mode 100644 index 0000000..9c16f5c --- /dev/null +++ b/public/images/spacedeck-logo.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/images/spinner.gif b/public/images/spinner.gif new file mode 100644 index 0000000..26c67fd Binary files /dev/null and b/public/images/spinner.gif differ diff --git a/public/images/spinner2.gif b/public/images/spinner2.gif new file mode 100644 index 0000000..efe2b16 Binary files /dev/null and b/public/images/spinner2.gif differ diff --git a/public/images/triangle.svg b/public/images/triangle.svg new file mode 100644 index 0000000..89b0e0f --- /dev/null +++ b/public/images/triangle.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/public/javascripts/backend.js b/public/javascripts/backend.js new file mode 100644 index 0000000..9b7bb08 --- /dev/null +++ b/public/javascripts/backend.js @@ -0,0 +1,331 @@ +var api_endpoint = ENV.apiEndpoint; +var api_socket_endpoint = ENV.websocketsEndpoint; + +var api_token = null; +var websocket = null; +var channel_id = null; +var space_auth = null; + +function load_resource(method, path, data, on_success, on_error, on_progress) { + var req = new XMLHttpRequest(); + req.onload = function(evt,b,c) { + if (req.status>=200 && req.status<=299) { + var parsed = null; + + try { + var parsed = JSON.parse(req.response); + } catch(e) {}; + + if (data && parsed && parsed._id) { + // mutate the local object and update its _id + data._id = parsed._id; + } + if (on_success) { + on_success(parsed,req); + } + } else { + if (on_error) { + on_error(req); + } + } + }; + + req.onerror = function(err) { + console.log(err,err.target); + // window._spacedeck_location_change is a flag set by redirect / reload functions + if (!window._spacedeck_location_change) { + if (window.spacedeck && window.spacedeck.active_space) { + window.spacedeck.offline = true; + } else { + alert("Could not connect to Spacedeck. Please reconnect and try again."); + } + } + if (on_error) on_error(req); + } + + req.withCredentials = true; + + req.open(method, api_endpoint+"/api"+path, true); + + if (api_token) { + req.setRequestHeader("X-Spacedeck-Auth", api_token); + } + + if (space_auth) { + console.log("set space auth", space_auth); + req.setRequestHeader("X-Spacedeck-Space-Auth", space_auth); + } + + if (channel_id) { + req.setRequestHeader("X-Spacedeck-Channel", channel_id); + } + if (csrf_token) { + req.setRequestHeader("X-csrf-token", csrf_token); + } + + try { + if (data) { + if (data.toString() == "[object File]") { + req.setRequestHeader("Content-type", data.type); + req.setRequestHeader("Accepts", "application/json"); + req.upload.onprogress = function(e) { + console.log("upload progress: ",e.loaded,e.total); + if (on_progress) on_progress(e); + } + req.send(data); + } else { + req.setRequestHeader("Content-type", "application/json"); + req.send(JSON.stringify(data)); + } + } else { + req.send(); + } + } catch (e) { + if (on_error) { + on_error(req, e); + } else { + throw(e); + } + } +} + + +function get_resource(path, on_success, on_error, on_progress) { + load_resource("get", path, null, on_success, on_error, on_progress); +} + +function load_profile(name, on_success, on_error) { + load_resource("get", "/users/slug?slug="+name, null, on_success, on_error); +} + +function load_current_user(on_success, on_error) { + load_resource("get", "/users/current", null, on_success, on_error); +} + +function load_space(id, on_success, on_error) { + if (!id || id=="undefined") { + console.error("load_space id:", id); + return; + } + var url = "/spaces/"+id; + load_resource("get", url, null, function(space, req) { + var role = req.getResponseHeader("x-spacedeck-space-role"); + on_success(space, role); + }, on_error); +} + +function load_space_path(id, on_success, on_error) { + var url = "/spaces/"+id+"/path"; + load_resource("get", url, null, function(space, req) { + on_success(space); + }, on_error); +} + +function load_spaces(id, is_home, on_success, on_error) { + if (!id || id=="undefined") { + console.error("load_spaces id:", id); + return; + } + + var q = "?parent_space_id="+id; + load_resource("get", "/spaces"+q, null, function(spaces) { + on_success(spaces); + }, on_error); +} + +function load_writable_folders( on_success, on_error) { + load_resource("get", "/spaces?writablefolders=true", null, on_success, on_error); +} + +function load_history(s, on_success, on_error) { + load_resource("get", "/spaces/"+ s._id +"/digest", null, on_success, on_error); +} + +function load_filtered_spaces(filter, on_success, on_error) { + load_resource("get", "/spaces?filter="+filter, null, on_success, on_error); +} + +function load_spaces_search(query, on_success, on_error) { + load_resource("get", "/spaces?search="+query, null, on_success, on_error); +} + +function load_artifacts(id, on_success, on_error) { + load_resource("get", "/spaces/"+id+"/artifacts", null, on_success, on_error); +} + +function save_artifact(a, on_success, on_error) { + if (a._id) { + load_resource("put", "/spaces/"+a.space_id+"/artifacts/"+a._id,a,on_success,on_error); + } else { + load_resource("post", "/spaces/"+a.space_id+"/artifacts",a,on_success,on_error); + } +} + +function save_pdf_file(space, point, file, zones, on_success, on_error, on_progress) { + load_resource("post", "/spaces/"+space._id+"/artifacts-pdf?filename="+file.name + "&x="+point.x+"&y="+point.y + "&zones="+zones,file,on_success,on_error,on_progress); +} + +function save_artifact_file(a, file,filename, on_success, on_error, on_progress) { + load_resource("post", "/spaces/"+a.space_id+"/artifacts/"+a._id+"/payload?filename="+filename,file,on_success,on_error,on_progress); +} + +function save_space(s, on_success, on_error) { + if (s._id) { + delete s['artifacts']; + load_resource("put", "/spaces/"+s._id,s,on_success,on_error); + } else { + load_resource("post", "/spaces",s,on_success,on_error); + } +} + +function delete_space(s, on_success, on_error) { + load_resource("delete", "/spaces/"+s._id, null, on_success, on_error); +} + + +function delete_artifact(a, on_success, on_error) { + load_resource("delete", "/spaces/"+a.space_id+"/artifacts/"+a._id); +} + + +function duplicate_space(s, to_space_id, on_success, on_error) { + var path = "/spaces/"+s._id+"/duplicate"; + if(to_space_id) { + path += "?parent_space_id=" + to_space_id + } + load_resource("post", path, null,on_success,on_error); +} + +function load_members(space, on_success, on_error) { + load_resource("get", "/spaces/"+ space._id +"/memberships", null, on_success, on_error); +} + +function create_membership(space, m, on_success, on_error) { + load_resource("post", "/spaces/"+ space._id +"/memberships", m, on_success, on_error); +} + +function save_membership(space, m, on_success, on_error) { + load_resource("put", "/spaces/"+ space._id +"/memberships/" + m._id, m, on_success, on_error); +} + +function delete_membership(space, m, on_success, on_error) { + load_resource("delete", "/spaces/"+ space._id +"/memberships/"+m._id, m, on_success, on_error); +} + +function accept_invitation(id, code, on_success, on_error) { + load_resource("get", "/memberships/"+ id +"/accept?code="+code, null, on_success, on_error); +} + +function get_join_link(space_id, on_success, on_error) { + load_resource("get", "/invitation_codes?space_id="+space_id, null, on_success, on_error); +} + +function create_join_link(space_id, role, on_success, on_error) { + load_resource("post", "/invitation_codes", {join_role:role, sticky:true, space_id:space_id}, on_success, on_error); +} + +function delete_join_link(link_id, on_success, on_error) { + load_resource("delete", "/invitation_codes/"+link_id, null, on_success, on_error); +} + +function load_team_members(id, on_success, on_error) { + load_resource("get", "/teams/"+ id +"/memberships", null, function(team) { + on_success(team); + }, on_error); +} + +function save_avatar_file(type, o, file, on_success, on_error) { + load_resource("post", "/"+type+"s/"+o._id+"/avatar", file, on_success,on_error); +} + +function remove_avatar_file(type, o, on_success, on_error) { + load_resource("delete", "/"+type+"s/"+o._id+"/avatar", null, on_success,on_error); +} + +function save_space_background_file(space, file, on_success, on_error) { + load_resource("post", "/spaces/"+space._id+"/background?filename="+file.name, file, on_success,on_error); +} + +function save_user_background_file(user, file, on_success, on_error) { + load_resource("post", "/users/"+user._id+"/background", file, on_success,on_error); +} + +function save_user_password(u, pass, newPass, on_success, on_error) { + load_resource("post", "/users/" + u._id + "/password", {old_password:pass, new_password:newPass}, on_success, on_error); +} + +function get_featured_users(on_success, on_error) { + load_resource("get", "/users/featured", null, on_success, on_error); +} + +function save_user(u, on_success, on_error) { + load_resource("put", "/users/"+u._id,u,on_success,on_error); +} + +function delete_user(u, password, on_success, on_error) { + load_resource("delete", "/users/"+u._id +"?password="+password,null,on_success,on_error); +} + +function create_user(name, email, password, password_confirmation, on_success, on_error) { + load_resource("post", "/users", {email:email, nickname:name, password:password, password_confirmation: password_confirmation}, on_success, on_error); +} + +function create_session(email, password, on_success, on_error) { + load_resource("post", "/sessions", {email:email, password:password}, on_success, on_error); +} + +function delete_session(on_success, on_error) { + load_resource("delete", "/sessions/current", null, on_success, on_error); +} + +function create_oauthtoken(on_success, on_error) { + load_resource("get", "/users/oauth2callback/url", null, on_success, on_error); +} + +function create_session_for_oauthtoken(token, on_success, on_error) { + load_resource("get", "/users/loginorsignupviagoogle?code="+token, null, on_success, on_error); +} + +function create_password_reset(email, on_success, on_error) { + load_resource("post", "/users/password_reset_requests?email=" + encodeURIComponent(email), null, on_success, on_error); +} + +function confirm_password_reset(password, confirm, on_success, on_error) { + load_resource("post", "/users/password_reset_requests/"+confirm+"/confirm", {password:password}, on_success, on_error); +} + +function confirm_user(user, token, on_success, on_error) { + load_resource("put", "/users/"+user._id+"/confirm", {token:token}, on_success, on_error); +} + +function resent_confirm_mail(user, on_success, on_error) { + load_resource("post", "/users/"+user._id+"/confirm", {}, on_success, on_error); +} + +function create_feedback(user, m, on_success, on_error) { + load_resource("post", "/users/feedback", {text: m}, on_success, on_error); +} + +function save_team(u, on_success, on_error) { + load_resource("put", "/teams/"+u._id,u,on_success,on_error); +} + +function load_comments(space_id, on_success, on_error) { + load_resource("get", "/spaces/"+space_id+"/messages", null, on_success, on_error); +} + +function save_comment(space_id, data, on_success, on_error) { + load_resource("post", "/spaces/"+space_id +"/messages", data, on_success, on_error); +} + +function delete_comment(space_id, message_id,on_success, on_error) { + load_resource("delete", "/spaces/"+space_id +"/messages/"+ message_id, null , on_success, on_error); +} + +function update_comment(space_id, data, on_success, on_error) { + load_resource("post", "/spaces/"+space_id+"/messages/" + data._id , data, on_success, on_error); +} + +function load_notifications(u, on_success, on_error) { + load_resource("get", "/notifications", null, on_success, on_error); +} diff --git a/public/javascripts/clipboard.js b/public/javascripts/clipboard.js new file mode 100755 index 0000000..6a28dfb --- /dev/null +++ b/public/javascripts/clipboard.js @@ -0,0 +1,745 @@ +/*! + * clipboard.js v1.5.5 + * https://zenorocha.github.io/clipboard.js + * + * Licensed MIT © Zeno Rocha + */ +(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Clipboard = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0; + + /** + * Android requires exceptions. + * + * @type boolean + */ + var deviceIsAndroid = navigator.userAgent.indexOf('Android') > 0 && !deviceIsWindowsPhone; + + + /** + * iOS requires exceptions. + * + * @type boolean + */ + var deviceIsIOS = /iP(ad|hone|od)/.test(navigator.userAgent) && !deviceIsWindowsPhone; + + + /** + * iOS 4 requires an exception for select elements. + * + * @type boolean + */ + var deviceIsIOS4 = deviceIsIOS && (/OS 4_\d(_\d)?/).test(navigator.userAgent); + + + /** + * iOS 6.0-7.* requires the target element to be manually derived + * + * @type boolean + */ + var deviceIsIOSWithBadTarget = deviceIsIOS && (/OS [6-7]_\d/).test(navigator.userAgent); + + /** + * BlackBerry requires exceptions. + * + * @type boolean + */ + var deviceIsBlackBerry10 = navigator.userAgent.indexOf('BB10') > 0; + + /** + * 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) { + 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 ((deviceIsIOS && target.type === 'file') || target.disabled) { + return true; + } + + break; + case 'label': + case 'iframe': // iOS8 homescreen apps can prevent events bubbling into frames + 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) { + switch (target.nodeName.toLowerCase()) { + case 'textarea': + return true; + case 'select': + return !deviceIsAndroid; + 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) { + 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(this.determineEventType(targetElement), true, true, window, 1, touch.screenX, touch.screenY, touch.clientX, touch.clientY, false, false, false, false, 0, null); + clickEvent.forwardedTouchEvent = true; + targetElement.dispatchEvent(clickEvent); + }; + + FastClick.prototype.determineEventType = function(targetElement) { + + //Issue #159: Android Chrome Select Box does not open with a synthetic click event + if (deviceIsAndroid && targetElement.tagName.toLowerCase() === 'select') { + return 'mousedown'; + } + + return 'click'; + }; + + + /** + * @param {EventTarget|Element} targetElement + */ + FastClick.prototype.focus = function(targetElement) { + var length; + + // Issue #160: on iOS 7, some input elements (e.g. date datetime month) throw a vague TypeError on setSelectionRange. These elements don't have an integer value for the selectionStart and selectionEnd properties, but unfortunately that can't be used for detection because accessing the properties also throws a TypeError. Just check the type instead. Filed as Apple bug #15122724. + if (deviceIsIOS && targetElement.setSelectionRange && targetElement.type.indexOf('date') !== 0 && targetElement.type !== 'time' && targetElement.type !== 'month') { + 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) { + 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) { + + // 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) { + 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 (deviceIsIOS) { + + // Only trusted events will deselect text on iOS (issue #49) + selection = window.getSelection(); + if (selection.rangeCount && !selection.isCollapsed) { + return true; + } + + if (!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. + // Issue 120: touch.identifier is 0 when Chrome dev tools 'Emulate touch events' is set with an iOS device UA string, + // which causes all touch events to be ignored. As this block only applies to iOS, and iOS identifiers are always long, + // random integers, it's safe to to continue if the identifier is 0 here. + if (touch.identifier && 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) < this.tapDelay) { + 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) { + 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; + }; + + + /** + * Update the last position. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onTouchMove = function(event) { + if (!this.trackingClick) { + return true; + } + + // If the touch has moved, cancel the click tracking + if (this.targetElement !== this.getTargetElementFromEventTarget(event.target) || this.touchHasMoved(event)) { + this.trackingClick = false; + this.targetElement = null; + } + + return true; + }; + + + /** + * Attempt to find the labelled control for the given label element. + * + * @param {EventTarget|HTMLLabelElement} labelElement + * @returns {Element|null} + */ + FastClick.prototype.findControl = function(labelElement) { + + // 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) { + var forElement, trackingClickStart, targetTagName, scrollParent, touch, targetElement = this.targetElement; + + if (!this.trackingClick) { + return true; + } + + // Prevent phantom clicks on fast double-tap (issue #36) + if ((event.timeStamp - this.lastClickTime) < this.tapDelay) { + this.cancelNextClick = true; + return true; + } + + if ((event.timeStamp - this.trackingClickStart) > this.tapTimeout) { + return true; + } + + // Reset to prevent wrong click cancel on input (issue #156). + this.cancelNextClick = false; + + 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 (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 (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 || (deviceIsIOS && window.top !== window && targetTagName === 'input')) { + this.targetElement = null; + return false; + } + + this.focus(targetElement); + this.sendClick(targetElement, event); + + // Select elements need the event to go through on iOS 4, otherwise the selector menu won't open. + // Also this breaks opening selects when VoiceOver is active on iOS6, iOS7 (and possibly others) + if (!deviceIsIOS || targetTagName !== 'select') { + this.targetElement = null; + event.preventDefault(); + } + + return false; + } + + if (deviceIsIOS && !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() { + this.trackingClick = false; + this.targetElement = null; + }; + + + /** + * Determine mouse events which should be permitted. + * + * @param {Event} event + * @returns {boolean} + */ + FastClick.prototype.onMouse = function(event) { + + // 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) { + 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() { + var layer = this.layer; + + if (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('touchmove', this.onTouchMove, 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) { + var metaViewport; + var chromeVersion; + var blackberryVersion; + var firefoxVersion; + + // Devices that don't support touch don't need FastClick + if (typeof window.ontouchstart === 'undefined') { + return true; + } + + // Chrome version - zero for other browsers + chromeVersion = +(/Chrome\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; + + if (chromeVersion) { + + if (deviceIsAndroid) { + metaViewport = document.querySelector('meta[name=viewport]'); + + if (metaViewport) { + // Chrome on Android with user-scalable="no" doesn't need FastClick (issue #89) + if (metaViewport.content.indexOf('user-scalable=no') !== -1) { + return true; + } + // Chrome 32 and above with width=device-width or less don't need FastClick + if (chromeVersion > 31 && document.documentElement.scrollWidth <= window.outerWidth) { + return true; + } + } + + // Chrome desktop doesn't need FastClick (issue #15) + } else { + return true; + } + } + + if (deviceIsBlackBerry10) { + blackberryVersion = navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/); + + // BlackBerry 10.3+ does not require Fastclick library. + // https://github.com/ftlabs/fastclick/issues/251 + if (blackberryVersion[1] >= 10 && blackberryVersion[2] >= 3) { + metaViewport = document.querySelector('meta[name=viewport]'); + + if (metaViewport) { + // user-scalable=no eliminates click delay. + if (metaViewport.content.indexOf('user-scalable=no') !== -1) { + return true; + } + // width=device-width (or less than device-width) eliminates click delay. + if (document.documentElement.scrollWidth <= window.outerWidth) { + return true; + } + } + } + } + + // IE10 with -ms-touch-action: none or manipulation, which disables double-tap-to-zoom (issue #97) + if (layer.style.msTouchAction === 'none' || layer.style.touchAction === 'manipulation') { + return true; + } + + // Firefox version - zero for other browsers + firefoxVersion = +(/Firefox\/([0-9]+)/.exec(navigator.userAgent) || [,0])[1]; + + if (firefoxVersion >= 27) { + // Firefox 27+ does not have tap delay if the content is not zoomable - https://bugzilla.mozilla.org/show_bug.cgi?id=922896 + + metaViewport = document.querySelector('meta[name=viewport]'); + if (metaViewport && (metaViewport.content.indexOf('user-scalable=no') !== -1 || document.documentElement.scrollWidth <= window.outerWidth)) { + return true; + } + } + + // IE11: prefixed -ms-touch-action is no longer supported and it's recomended to use non-prefixed version + // http://msdn.microsoft.com/en-us/library/windows/apps/Hh767313.aspx + if (layer.style.touchAction === 'none' || layer.style.touchAction === 'manipulation') { + return true; + } + + return false; + }; + + + /** + * Factory method for creating a FastClick object + * + * @param {Element} layer The layer to listen on + * @param {Object} [options={}] The options to override the defaults + */ + FastClick.attach = function(layer, options) { + return new FastClick(layer, options); + }; + + + if (typeof define === 'function' && typeof define.amd === 'object' && define.amd) { + + // AMD. Register as an anonymous module. + define(function() { + return FastClick; + }); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = FastClick.attach; + module.exports.FastClick = FastClick; + } else { + window.FastClick = FastClick; + } +}()); diff --git a/public/javascripts/helper.js b/public/javascripts/helper.js new file mode 100644 index 0000000..576268c --- /dev/null +++ b/public/javascripts/helper.js @@ -0,0 +1,249 @@ +function validateEmail(email) { + var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + return re.test(email); +} + +function zero_pad(num) { + zero = 2 - num.toString().length + 1; + return Array(+(zero > 0 && zero)).join("0") + num; +} + +function format_time(seconds) { + if (isNaN(seconds)) seconds = 0; + return zero_pad(parseInt(seconds/60)) + ":" + zero_pad(parseInt(seconds%60)); +} + +var url_to_links_rx = /(^|[\s\n]|>)((?:https?|ftp):\/\/[\-A-Z0-9+\u0026\u2019@#\/%?=()~_|!:,.;]*[\-A-Z0-9+\u0026@#\/%=~()_|])/gi; +function urls_to_links(text) { + return text.replace(url_to_links_rx, "$1$2"); +} + +// http://stackoverflow.com/questions/901115/how-can-i-get-query-string-values-in-javascript +function get_query_param(name) { + name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); + var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), + results = regex.exec(location.search); + return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); +} + +// http://stackoverflow.com/questions/1349404/generate-a-string-of-5-random-characters-in-javascript +function random_string(len) { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!-_"; + + for (var i=0; i < len; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + + return text; +} + +function fixup_touches(evt) { + // convert touch events + var e = evt; + if (evt.originalEvent) e = evt.originalEvent; + evt = { + pageX: evt.pageX, + pageY: evt.pageY, + offsetX: evt.offsetX, + offsetY: evt.offsetY, + clientX: evt.clientX, + clientY: evt.clientY, + layerX: evt.layerX, + layerY: evt.layerY, + target: evt.target, + currentTarget: evt.currentTarget + }; + + if (e.changedTouches && e.changedTouches.length) { + evt.pageX = e.changedTouches[0].pageX; + evt.pageY = e.changedTouches[0].pageY; + } else if (e.touches && e.touches.length) { + evt.pageX = e.touches[0].pageX; + evt.pageY = e.touches[0].pageY; + } + return evt; +} + +function rgb_to_hex(r, g, b) { + return ((1 << 24) + (parseInt(r) << 16) + (parseInt(g) << 8) + parseInt(b)).toString(16).slice(1); +} + +function hex_to_rgba(color) { + + if (!color || color == "transparent") { + return {r:0,g:0,b:0,a:0}; + } + + if (color.match("rgb\\(")) { + color = color.replace("rgb(","").replace(")","").split(","); + return { + r: color[0], + g: color[1], + b: color[2], + a: 255 + }; + } + + if (color.match("rgba\\(")) { + color = color.replace("rgba(","").replace(")","").split(","); + return { + r: color[0], + g: color[1], + b: color[2], + a: color[3]*255 + }; + } + + var r = parseInt(color.substr(1,2), 16); + var g = parseInt(color.substr(3,2), 16); + var b = parseInt(color.substr(5,2), 16); + var a = 255; + if (color.length>7) { + a = parseInt(color.substr(7,2), 16); + } + return {r:r,g:g,b:b,a:a}; +} + +function rgb_to_hsv () { + var rr, gg, bb, + r = arguments[0] / 255, + g = arguments[1] / 255, + b = arguments[2] / 255, + h, s, + v = Math.max(r, g, b), + diff = v - Math.min(r, g, b), + diffc = function(c) { + return (v - c) / 6 / diff + 1 / 2; + }; + + if (diff == 0) { + h = s = 0; + } else { + s = diff / v; + rr = diffc(r); + gg = diffc(g); + bb = diffc(b); + + if (r === v) { + h = bb - gg; + } else if (g === v) { + h = (1 / 3) + rr - bb; + } else if (b === v) { + h = (2 / 3) + gg - rr; + } + + if (h < 0) { + h += 1; + } else if (h > 1) { + h -= 1; + } + } + return { + h: h || 0, + s: s || 0, + v: v || 0 + }; +} + +// values? +function hsv_to_rgb(h, s, v) { + var r, g, b, i, f, p, q, t; + if (h && s === undefined && v === undefined) { + s = h.s, v = h.v, h = h.h; + } + i = Math.floor(h * 6); + f = h * 6 - i; + p = v * (1 - s); + q = v * (1 - f * s); + t = v * (1 - (1 - f) * s); + switch (i % 6) { + case 0: r = v, g = t, b = p; break; + case 1: r = q, g = v, b = p; break; + case 2: r = p, g = v, b = t; break; + case 3: r = p, g = q, b = v; break; + case 4: r = t, g = p, b = v; break; + case 5: r = v, g = p, b = q; break; + } + return { + r: Math.round(r * 255), + g: Math.round(g * 255), + b: Math.round(b * 255) + }; +} + +temp_grid_canvas = document.createElement("canvas"); + +function render_grid(w,h,divisions) { + temp_grid_canvas.width = w; + temp_grid_canvas.height = h; + + var step = w / divisions; + + var ctx = temp_grid_canvas.getContext('2d'); + ctx.strokeStyle = "#f0f0f0"; + ctx.lineWidth = 1; + + var gc1 = "rgba(60,60,60,0.125)"; + var gc2 = "rgba(60,60,60,0.075)"; + + for (var y=0; y>> 0; + if (len === 0) { + return -1; + } + var n = 0; + if (arguments.length > 0) { + n = Number(arguments[1]); + if (n != n) { // shortcut for verifying if it's NaN + n = 0; + } else if (n != 0 && n != Infinity && n != -Infinity) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + if (n >= len) { + return -1; + } + var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); + for (; k < len; k++) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + } + } + + // add lastIndexOf to non ECMA-262 standard compliant browsers + if (!Array.prototype.lastIndexOf) { + Array.prototype.lastIndexOf = function(searchElement /*, fromIndex*/) { + "use strict"; + if (this == null) { + throw new TypeError(); + } + var t = Object(this); + var len = t.length >>> 0; + if (len === 0) { + return -1; + } + var n = len; + if (arguments.length > 1) { + n = Number(arguments[1]); + if (n != n) { + n = 0; + } else if (n != 0 && n != (1 / 0) && n != -(1 / 0)) { + n = (n > 0 || -1) * Math.floor(Math.abs(n)); + } + } + var k = n >= 0 ? Math.min(n, len - 1) : len - Math.abs(n); + for (; k >= 0; k--) { + if (k in t && t[k] === searchElement) { + return k; + } + } + return -1; + }; + } + + // Add string trim for IE8. + if (typeof String.prototype.trim !== 'function') { + String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g, ''); + } + } + + var $ = root.jQuery || root.Zepto + , i18n = {} + , resStore = {} + , currentLng + , replacementCounter = 0 + , languages = [] + , initialized = false + , sync = {} + , conflictReference = null; + + + + // Export the i18next object for **CommonJS**. + // If we're not in CommonJS, add `i18n` to the + // global object or to jquery. + if (typeof module !== 'undefined' && module.exports) { + module.exports = i18n; + } else { + if ($) { + $.i18n = $.i18n || i18n; + } + + if (root.i18n) { + conflictReference = root.i18n; + } + root.i18n = i18n; + } + sync = { + + load: function(lngs, options, cb) { + if (options.useLocalStorage) { + sync._loadLocal(lngs, options, function(err, store) { + var missingLngs = []; + for (var i = 0, len = lngs.length; i < len; i++) { + if (!store[lngs[i]]) missingLngs.push(lngs[i]); + } + + if (missingLngs.length > 0) { + sync._fetch(missingLngs, options, function(err, fetched) { + f.extend(store, fetched); + sync._storeLocal(fetched); + + cb(err, store); + }); + } else { + cb(err, store); + } + }); + } else { + sync._fetch(lngs, options, function(err, store){ + cb(err, store); + }); + } + }, + + _loadLocal: function(lngs, options, cb) { + var store = {} + , nowMS = new Date().getTime(); + + if(window.localStorage) { + + var todo = lngs.length; + + f.each(lngs, function(key, lng) { + var local = f.localStorage.getItem('res_' + lng); + + if (local) { + local = JSON.parse(local); + + if (local.i18nStamp && local.i18nStamp + options.localStorageExpirationTime > nowMS) { + store[lng] = local; + } + } + + todo--; // wait for all done befor callback + if (todo === 0) cb(null, store); + }); + } + }, + + _storeLocal: function(store) { + if(window.localStorage) { + for (var m in store) { + store[m].i18nStamp = new Date().getTime(); + f.localStorage.setItem('res_' + m, JSON.stringify(store[m])); + } + } + return; + }, + + _fetch: function(lngs, options, cb) { + var ns = options.ns + , store = {}; + + if (!options.dynamicLoad) { + var todo = ns.namespaces.length * lngs.length + , errors; + + // load each file individual + f.each(ns.namespaces, function(nsIndex, nsValue) { + f.each(lngs, function(lngIndex, lngValue) { + + // Call this once our translation has returned. + var loadComplete = function(err, data) { + if (err) { + errors = errors || []; + errors.push(err); + } + store[lngValue] = store[lngValue] || {}; + store[lngValue][nsValue] = data; + + todo--; // wait for all done befor callback + if (todo === 0) cb(errors, store); + }; + + if(typeof options.customLoad == 'function'){ + // Use the specified custom callback. + options.customLoad(lngValue, nsValue, options, loadComplete); + } else { + //~ // Use our inbuilt sync. + sync._fetchOne(lngValue, nsValue, options, loadComplete); + } + }); + }); + } else { + // Call this once our translation has returned. + var loadComplete = function(err, data) { + cb(err, data); + }; + + if(typeof options.customLoad == 'function'){ + // Use the specified custom callback. + options.customLoad(lngs, ns.namespaces, options, loadComplete); + } else { + var url = applyReplacement(options.resGetPath, { lng: lngs.join('+'), ns: ns.namespaces.join('+') }); + // load all needed stuff once + f.ajax({ + url: url, + cache: options.cache, + success: function(data, status, xhr) { + f.log('loaded: ' + url); + loadComplete(null, data); + }, + error : function(xhr, status, error) { + f.log('failed loading: ' + url); + loadComplete('failed loading resource.json error: ' + error); + }, + dataType: "json", + async : options.getAsync, + timeout: options.ajaxTimeout + }); + } + } + }, + + _fetchOne: function(lng, ns, options, done) { + var url = applyReplacement(options.resGetPath, { lng: lng, ns: ns }); + f.ajax({ + url: url, + cache: options.cache, + success: function(data, status, xhr) { + f.log('loaded: ' + url); + done(null, data); + }, + error : function(xhr, status, error) { + if ((status && status == 200) || (xhr && xhr.status && xhr.status == 200)) { + // file loaded but invalid json, stop waste time ! + f.error('There is a typo in: ' + url); + } else if ((status && status == 404) || (xhr && xhr.status && xhr.status == 404)) { + f.log('Does not exist: ' + url); + } else { + var theStatus = status ? status : ((xhr && xhr.status) ? xhr.status : null); + f.log(theStatus + ' when loading ' + url); + } + + done(error, {}); + }, + dataType: "json", + async : options.getAsync, + timeout: options.ajaxTimeout, + headers: options.headers + }); + }, + + postMissing: function(lng, ns, key, defaultValue, lngs) { + var payload = {}; + payload[key] = defaultValue; + + var urls = []; + + if (o.sendMissingTo === 'fallback' && o.fallbackLng[0] !== false) { + for (var i = 0; i < o.fallbackLng.length; i++) { + urls.push({lng: o.fallbackLng[i], url: applyReplacement(o.resPostPath, { lng: o.fallbackLng[i], ns: ns })}); + } + } else if (o.sendMissingTo === 'current' || (o.sendMissingTo === 'fallback' && o.fallbackLng[0] === false) ) { + urls.push({lng: lng, url: applyReplacement(o.resPostPath, { lng: lng, ns: ns })}); + } else if (o.sendMissingTo === 'all') { + for (var i = 0, l = lngs.length; i < l; i++) { + urls.push({lng: lngs[i], url: applyReplacement(o.resPostPath, { lng: lngs[i], ns: ns })}); + } + } + + for (var y = 0, len = urls.length; y < len; y++) { + var item = urls[y]; + f.ajax({ + url: item.url, + type: o.sendType, + data: payload, + success: function(data, status, xhr) { + f.log('posted missing key \'' + key + '\' to: ' + item.url); + + // add key to resStore + var keys = key.split('.'); + var x = 0; + var value = resStore[item.lng][ns]; + while (keys[x]) { + if (x === keys.length - 1) { + value = value[keys[x]] = defaultValue; + } else { + value = value[keys[x]] = value[keys[x]] || {}; + } + x++; + } + }, + error : function(xhr, status, error) { + f.log('failed posting missing key \'' + key + '\' to: ' + item.url); + }, + dataType: "json", + async : o.postAsync, + timeout: o.ajaxTimeout + }); + } + }, + + reload: reload + }; + // defaults + var o = { + lng: undefined, + load: 'all', + preload: [], + lowerCaseLng: false, + returnObjectTrees: false, + fallbackLng: ['dev'], + fallbackNS: [], + detectLngQS: 'setLng', + detectLngFromLocalStorage: false, + ns: { + namespaces: ['translation'], + defaultNs: 'translation' + }, + fallbackOnNull: true, + fallbackOnEmpty: false, + fallbackToDefaultNS: false, + showKeyIfEmpty: false, + nsseparator: ':', + keyseparator: '.', + selectorAttr: 'data-i18n', + debug: false, + + resGetPath: 'locales/__lng__/__ns__.json', + resPostPath: 'locales/add/__lng__/__ns__', + + getAsync: true, + postAsync: true, + + resStore: undefined, + useLocalStorage: false, + localStorageExpirationTime: 7*24*60*60*1000, + + dynamicLoad: false, + sendMissing: false, + sendMissingTo: 'fallback', // current | all + sendType: 'POST', + + interpolationPrefix: '__', + interpolationSuffix: '__', + defaultVariables: false, + reusePrefix: '$t(', + reuseSuffix: ')', + pluralSuffix: '_plural', + pluralNotFound: ['plural_not_found', Math.random()].join(''), + contextNotFound: ['context_not_found', Math.random()].join(''), + escapeInterpolation: false, + indefiniteSuffix: '_indefinite', + indefiniteNotFound: ['indefinite_not_found', Math.random()].join(''), + + setJqueryExt: true, + defaultValueFromContent: true, + useDataAttrOptions: false, + cookieExpirationTime: undefined, + useCookie: true, + cookieName: 'i18next', + cookieDomain: undefined, + + objectTreeKeyHandler: undefined, + postProcess: undefined, + parseMissingKey: undefined, + missingKeyHandler: sync.postMissing, + ajaxTimeout: 0, + + shortcutFunction: 'sprintf' // or: defaultValue + }; + function _extend(target, source) { + if (!source || typeof source === 'function') { + return target; + } + + for (var attr in source) { target[attr] = source[attr]; } + return target; + } + + function _deepExtend(target, source, overwrite) { + for (var prop in source) + if (prop in target) { + // If we reached a leaf string in target or source then replace with source or skip depending on the 'overwrite' switch + if (typeof target[prop] === 'string' || target[prop] instanceof String || typeof source[prop] === 'string' || source[prop] instanceof String) { + if (overwrite) { + target[prop] = source[prop]; + } + } else { + _deepExtend(target[prop], source[prop], overwrite); + } + } else { + target[prop] = source[prop]; + } + return target; + } + + function _each(object, callback, args) { + var name, i = 0, + length = object.length, + isObj = length === undefined || Object.prototype.toString.apply(object) !== '[object Array]' || typeof object === "function"; + + if (args) { + if (isObj) { + for (name in object) { + if (callback.apply(object[name], args) === false) { + break; + } + } + } else { + for ( ; i < length; ) { + if (callback.apply(object[i++], args) === false) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if (isObj) { + for (name in object) { + if (callback.call(object[name], name, object[name]) === false) { + break; + } + } + } else { + for ( ; i < length; ) { + if (callback.call(object[i], i, object[i++]) === false) { + break; + } + } + } + } + + return object; + } + + var _entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' + }; + + function _escape(data) { + if (typeof data === 'string') { + return data.replace(/[&<>"'\/]/g, function (s) { + return _entityMap[s]; + }); + }else{ + return data; + } + } + + function _ajax(options) { + + // v0.5.0 of https://github.com/goloroden/http.js + var getXhr = function (callback) { + // Use the native XHR object if the browser supports it. + if (window.XMLHttpRequest) { + return callback(null, new XMLHttpRequest()); + } else if (window.ActiveXObject) { + // In Internet Explorer check for ActiveX versions of the XHR object. + try { + return callback(null, new ActiveXObject("Msxml2.XMLHTTP")); + } catch (e) { + return callback(null, new ActiveXObject("Microsoft.XMLHTTP")); + } + } + + // If no XHR support was found, throw an error. + return callback(new Error()); + }; + + var encodeUsingUrlEncoding = function (data) { + if(typeof data === 'string') { + return data; + } + + var result = []; + for(var dataItem in data) { + if(data.hasOwnProperty(dataItem)) { + result.push(encodeURIComponent(dataItem) + '=' + encodeURIComponent(data[dataItem])); + } + } + + return result.join('&'); + }; + + var utf8 = function (text) { + text = text.replace(/\r\n/g, '\n'); + var result = ''; + + for(var i = 0; i < text.length; i++) { + var c = text.charCodeAt(i); + + if(c < 128) { + result += String.fromCharCode(c); + } else if((c > 127) && (c < 2048)) { + result += String.fromCharCode((c >> 6) | 192); + result += String.fromCharCode((c & 63) | 128); + } else { + result += String.fromCharCode((c >> 12) | 224); + result += String.fromCharCode(((c >> 6) & 63) | 128); + result += String.fromCharCode((c & 63) | 128); + } + } + + return result; + }; + + var base64 = function (text) { + var keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + + text = utf8(text); + var result = '', + chr1, chr2, chr3, + enc1, enc2, enc3, enc4, + i = 0; + + do { + chr1 = text.charCodeAt(i++); + chr2 = text.charCodeAt(i++); + chr3 = text.charCodeAt(i++); + + enc1 = chr1 >> 2; + enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); + enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); + enc4 = chr3 & 63; + + if(isNaN(chr2)) { + enc3 = enc4 = 64; + } else if(isNaN(chr3)) { + enc4 = 64; + } + + result += + keyStr.charAt(enc1) + + keyStr.charAt(enc2) + + keyStr.charAt(enc3) + + keyStr.charAt(enc4); + chr1 = chr2 = chr3 = ''; + enc1 = enc2 = enc3 = enc4 = ''; + } while(i < text.length); + + return result; + }; + + var mergeHeaders = function () { + // Use the first header object as base. + var result = arguments[0]; + + // Iterate through the remaining header objects and add them. + for(var i = 1; i < arguments.length; i++) { + var currentHeaders = arguments[i]; + for(var header in currentHeaders) { + if(currentHeaders.hasOwnProperty(header)) { + result[header] = currentHeaders[header]; + } + } + } + + // Return the merged headers. + return result; + }; + + var ajax = function (method, url, options, callback) { + // Adjust parameters. + if(typeof options === 'function') { + callback = options; + options = {}; + } + + // Set default parameter values. + options.cache = options.cache || false; + options.data = options.data || {}; + options.headers = options.headers || {}; + options.jsonp = options.jsonp || false; + options.async = options.async === undefined ? true : options.async; + + // Merge the various header objects. + var headers = mergeHeaders({ + 'accept': '*/*', + 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' + }, ajax.headers, options.headers); + + // Encode the data according to the content-type. + var payload; + if (headers['content-type'] === 'application/json') { + payload = JSON.stringify(options.data); + } else { + payload = encodeUsingUrlEncoding(options.data); + } + + // Specially prepare GET requests: Setup the query string, handle caching and make a JSONP call + // if neccessary. + if(method === 'GET') { + // Setup the query string. + var queryString = []; + if(payload) { + queryString.push(payload); + payload = null; + } + + // Handle caching. + if(!options.cache) { + queryString.push('_=' + (new Date()).getTime()); + } + + // If neccessary prepare the query string for a JSONP call. + if(options.jsonp) { + queryString.push('callback=' + options.jsonp); + queryString.push('jsonp=' + options.jsonp); + } + + // Merge the query string and attach it to the url. + queryString = queryString.join('&'); + if (queryString.length > 1) { + if (url.indexOf('?') > -1) { + url += '&' + queryString; + } else { + url += '?' + queryString; + } + } + + // Make a JSONP call if neccessary. + if(options.jsonp) { + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + script.type = 'text/javascript'; + script.src = url; + head.appendChild(script); + return; + } + } + + // Since we got here, it is no JSONP request, so make a normal XHR request. + getXhr(function (err, xhr) { + if(err) return callback(err); + + // Open the request. + xhr.open(method, url, options.async); + + // Set the request headers. + for(var header in headers) { + if(headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header, headers[header]); + } + } + + // Handle the request events. + xhr.onreadystatechange = function () { + if(xhr.readyState === 4) { + var data = xhr.responseText || ''; + + // If no callback is given, return. + if(!callback) { + return; + } + + // Return an object that provides access to the data as text and JSON. + callback(xhr.status, { + text: function () { + return data; + }, + + json: function () { + try { + return JSON.parse(data) + } catch (e) { + f.error('Can not parse JSON. URL: ' + url); + return {}; + } + } + }); + } + }; + + // Actually send the XHR request. + xhr.send(payload); + }); + }; + + // Define the external interface. + var http = { + authBasic: function (username, password) { + ajax.headers['Authorization'] = 'Basic ' + base64(username + ':' + password); + }, + + connect: function (url, options, callback) { + return ajax('CONNECT', url, options, callback); + }, + + del: function (url, options, callback) { + return ajax('DELETE', url, options, callback); + }, + + get: function (url, options, callback) { + return ajax('GET', url, options, callback); + }, + + head: function (url, options, callback) { + return ajax('HEAD', url, options, callback); + }, + + headers: function (headers) { + ajax.headers = headers || {}; + }, + + isAllowed: function (url, verb, callback) { + this.options(url, function (status, data) { + callback(data.text().indexOf(verb) !== -1); + }); + }, + + options: function (url, options, callback) { + return ajax('OPTIONS', url, options, callback); + }, + + patch: function (url, options, callback) { + return ajax('PATCH', url, options, callback); + }, + + post: function (url, options, callback) { + return ajax('POST', url, options, callback); + }, + + put: function (url, options, callback) { + return ajax('PUT', url, options, callback); + }, + + trace: function (url, options, callback) { + return ajax('TRACE', url, options, callback); + } + }; + + + var methode = options.type ? options.type.toLowerCase() : 'get'; + + http[methode](options.url, options, function (status, data) { + // file: protocol always gives status code 0, so check for data + if (status === 200 || (status === 0 && data.text())) { + options.success(data.json(), status, null); + } else { + options.error(data.text(), status, null); + } + }); + } + + var _cookie = { + create: function(name,value,minutes,domain) { + var expires; + if (minutes) { + var date = new Date(); + date.setTime(date.getTime()+(minutes*60*1000)); + expires = "; expires="+date.toGMTString(); + } + else expires = ""; + domain = (domain)? "domain="+domain+";" : ""; + document.cookie = name+"="+value+expires+";"+domain+"path=/"; + }, + + read: function(name) { + var nameEQ = name + "="; + var ca = document.cookie.split(';'); + for(var i=0;i < ca.length;i++) { + var c = ca[i]; + while (c.charAt(0)==' ') c = c.substring(1,c.length); + if (c.indexOf(nameEQ) === 0) return c.substring(nameEQ.length,c.length); + } + return null; + }, + + remove: function(name) { + this.create(name,"",-1); + } + }; + + var cookie_noop = { + create: function(name,value,minutes,domain) {}, + read: function(name) { return null; }, + remove: function(name) {} + }; + + + + // move dependent functions to a container so that + // they can be overriden easier in no jquery environment (node.js) + var f = { + extend: $ ? $.extend : _extend, + deepExtend: _deepExtend, + each: $ ? $.each : _each, + ajax: $ ? $.ajax : (typeof document !== 'undefined' ? _ajax : function() {}), + cookie: typeof document !== 'undefined' ? _cookie : cookie_noop, + detectLanguage: detectLanguage, + escape: _escape, + log: function(str) { + if (o.debug && typeof console !== "undefined") console.log(str); + }, + error: function(str) { + if (typeof console !== "undefined") console.error(str); + }, + getCountyIndexOfLng: function(lng) { + var lng_index = 0; + if (lng === 'nb-NO' || lng === 'nn-NO' || lng === 'nb-no' || lng === 'nn-no') lng_index = 1; + return lng_index; + }, + toLanguages: function(lng, fallbackLng) { + var log = this.log; + + fallbackLng = fallbackLng || o.fallbackLng; + if (typeof fallbackLng === 'string') + fallbackLng = [fallbackLng]; + + function applyCase(l) { + var ret = l; + + if (typeof l === 'string' && l.indexOf('-') > -1) { + var parts = l.split('-'); + + ret = o.lowerCaseLng ? + parts[0].toLowerCase() + '-' + parts[1].toLowerCase() : + parts[0].toLowerCase() + '-' + parts[1].toUpperCase(); + } else { + ret = o.lowerCaseLng ? l.toLowerCase() : l; + } + + return ret; + } + + var languages = []; + var whitelist = o.lngWhitelist || false; + var addLanguage = function(language){ + //reject langs not whitelisted + if(!whitelist || whitelist.indexOf(language) > -1){ + languages.push(language); + }else{ + log('rejecting non-whitelisted language: ' + language); + } + }; + if (typeof lng === 'string' && lng.indexOf('-') > -1) { + var parts = lng.split('-'); + + if (o.load !== 'unspecific') addLanguage(applyCase(lng)); + if (o.load !== 'current') addLanguage(applyCase(parts[this.getCountyIndexOfLng(lng)])); + } else { + addLanguage(applyCase(lng)); + } + + for (var i = 0; i < fallbackLng.length; i++) { + if (languages.indexOf(fallbackLng[i]) === -1 && fallbackLng[i]) languages.push(applyCase(fallbackLng[i])); + } + return languages; + }, + regexEscape: function(str) { + return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); + }, + regexReplacementEscape: function(strOrFn) { + if (typeof strOrFn === 'string') { + return strOrFn.replace(/\$/g, "$$$$"); + } else { + return strOrFn; + } + }, + localStorage: { + setItem: function(key, value) { + if (window.localStorage) { + try { + window.localStorage.setItem(key, value); + } catch (e) { + f.log('failed to set value for key "' + key + '" to localStorage.'); + } + } + }, + getItem: function(key, value) { + if (window.localStorage) { + try { + return window.localStorage.getItem(key, value); + } catch (e) { + f.log('failed to get value for key "' + key + '" from localStorage.'); + return undefined; + } + } + } + } + }; + function init(options, cb) { + + if (typeof options === 'function') { + cb = options; + options = {}; + } + options = options || {}; + + // override defaults with passed in options + f.extend(o, options); + delete o.fixLng; /* passed in each time */ + + // override functions: .log(), .detectLanguage(), etc + if (o.functions) { + delete o.functions; + f.extend(f, options.functions); + } + + // create namespace object if namespace is passed in as string + if (typeof o.ns == 'string') { + o.ns = { namespaces: [o.ns], defaultNs: o.ns}; + } + + // fallback namespaces + if (typeof o.fallbackNS == 'string') { + o.fallbackNS = [o.fallbackNS]; + } + + // fallback languages + if (typeof o.fallbackLng == 'string' || typeof o.fallbackLng == 'boolean') { + o.fallbackLng = [o.fallbackLng]; + } + + // escape prefix/suffix + o.interpolationPrefixEscaped = f.regexEscape(o.interpolationPrefix); + o.interpolationSuffixEscaped = f.regexEscape(o.interpolationSuffix); + + if (!o.lng) o.lng = f.detectLanguage(); + + languages = f.toLanguages(o.lng); + currentLng = languages[0]; + f.log('currentLng set to: ' + currentLng); + + if (o.useCookie && f.cookie.read(o.cookieName) !== currentLng){ //cookie is unset or invalid + f.cookie.create(o.cookieName, currentLng, o.cookieExpirationTime, o.cookieDomain); + } + if (o.detectLngFromLocalStorage && typeof document !== 'undefined' && window.localStorage) { + f.localStorage.setItem('i18next_lng', currentLng); + } + + var lngTranslate = translate; + if (options.fixLng) { + lngTranslate = function(key, options) { + options = options || {}; + options.lng = options.lng || lngTranslate.lng; + return translate(key, options); + }; + lngTranslate.lng = currentLng; + } + + pluralExtensions.setCurrentLng(currentLng); + + // add JQuery extensions + if ($ && o.setJqueryExt) { + addJqueryFunct && addJqueryFunct(); + } else { + addJqueryLikeFunctionality && addJqueryLikeFunctionality(); + } + + // jQuery deferred + var deferred; + if ($ && $.Deferred) { + deferred = $.Deferred(); + } + + // return immidiatly if res are passed in + if (o.resStore) { + resStore = o.resStore; + initialized = true; + if (cb) cb(null, lngTranslate); + if (deferred) deferred.resolve(lngTranslate); + if (deferred) return deferred.promise(); + return; + } + + // languages to load + var lngsToLoad = f.toLanguages(o.lng); + if (typeof o.preload === 'string') o.preload = [o.preload]; + for (var i = 0, l = o.preload.length; i < l; i++) { + var pres = f.toLanguages(o.preload[i]); + for (var y = 0, len = pres.length; y < len; y++) { + if (lngsToLoad.indexOf(pres[y]) < 0) { + lngsToLoad.push(pres[y]); + } + } + } + + // else load them + i18n.sync.load(lngsToLoad, o, function(err, store) { + resStore = store; + initialized = true; + + if (cb) cb(err, lngTranslate); + if (deferred) (!err ? deferred.resolve : deferred.reject)(err || lngTranslate); + }); + + if (deferred) return deferred.promise(); + } + + function isInitialized() { + return initialized; + } + function preload(lngs, cb) { + if (typeof lngs === 'string') lngs = [lngs]; + for (var i = 0, l = lngs.length; i < l; i++) { + if (o.preload.indexOf(lngs[i]) < 0) { + o.preload.push(lngs[i]); + } + } + return init(cb); + } + + function addResourceBundle(lng, ns, resources, deep, overwrite) { + if (typeof ns !== 'string') { + resources = ns; + ns = o.ns.defaultNs; + } else if (o.ns.namespaces.indexOf(ns) < 0) { + o.ns.namespaces.push(ns); + } + + resStore[lng] = resStore[lng] || {}; + resStore[lng][ns] = resStore[lng][ns] || {}; + + if (deep) { + f.deepExtend(resStore[lng][ns], resources, overwrite); + } else { + f.extend(resStore[lng][ns], resources); + } + if (o.useLocalStorage) { + sync._storeLocal(resStore); + } + } + + function hasResourceBundle(lng, ns) { + if (typeof ns !== 'string') { + ns = o.ns.defaultNs; + } + + resStore[lng] = resStore[lng] || {}; + var res = resStore[lng][ns] || {}; + + var hasValues = false; + for(var prop in res) { + if (res.hasOwnProperty(prop)) { + hasValues = true; + } + } + + return hasValues; + } + + function getResourceBundle(lng, ns) { + if (typeof ns !== 'string') { + ns = o.ns.defaultNs; + } + + resStore[lng] = resStore[lng] || {}; + return f.extend({}, resStore[lng][ns]); + } + + function removeResourceBundle(lng, ns) { + if (typeof ns !== 'string') { + ns = o.ns.defaultNs; + } + + resStore[lng] = resStore[lng] || {}; + resStore[lng][ns] = {}; + if (o.useLocalStorage) { + sync._storeLocal(resStore); + } + } + + function addResource(lng, ns, key, value) { + if (typeof ns !== 'string') { + resource = ns; + ns = o.ns.defaultNs; + } else if (o.ns.namespaces.indexOf(ns) < 0) { + o.ns.namespaces.push(ns); + } + + resStore[lng] = resStore[lng] || {}; + resStore[lng][ns] = resStore[lng][ns] || {}; + + var keys = key.split(o.keyseparator); + var x = 0; + var node = resStore[lng][ns]; + var origRef = node; + + while (keys[x]) { + if (x == keys.length - 1) + node[keys[x]] = value; + else { + if (node[keys[x]] == null) + node[keys[x]] = {}; + + node = node[keys[x]]; + } + x++; + } + if (o.useLocalStorage) { + sync._storeLocal(resStore); + } + } + + function addResources(lng, ns, resources) { + if (typeof ns !== 'string') { + resources = ns; + ns = o.ns.defaultNs; + } else if (o.ns.namespaces.indexOf(ns) < 0) { + o.ns.namespaces.push(ns); + } + + for (var m in resources) { + if (typeof resources[m] === 'string') addResource(lng, ns, m, resources[m]); + } + } + + function setDefaultNamespace(ns) { + o.ns.defaultNs = ns; + } + + function loadNamespace(namespace, cb) { + loadNamespaces([namespace], cb); + } + + function loadNamespaces(namespaces, cb) { + var opts = { + dynamicLoad: o.dynamicLoad, + resGetPath: o.resGetPath, + getAsync: o.getAsync, + customLoad: o.customLoad, + ns: { namespaces: namespaces, defaultNs: ''} /* new namespaces to load */ + }; + + // languages to load + var lngsToLoad = f.toLanguages(o.lng); + if (typeof o.preload === 'string') o.preload = [o.preload]; + for (var i = 0, l = o.preload.length; i < l; i++) { + var pres = f.toLanguages(o.preload[i]); + for (var y = 0, len = pres.length; y < len; y++) { + if (lngsToLoad.indexOf(pres[y]) < 0) { + lngsToLoad.push(pres[y]); + } + } + } + + // check if we have to load + var lngNeedLoad = []; + for (var a = 0, lenA = lngsToLoad.length; a < lenA; a++) { + var needLoad = false; + var resSet = resStore[lngsToLoad[a]]; + if (resSet) { + for (var b = 0, lenB = namespaces.length; b < lenB; b++) { + if (!resSet[namespaces[b]]) needLoad = true; + } + } else { + needLoad = true; + } + + if (needLoad) lngNeedLoad.push(lngsToLoad[a]); + } + + if (lngNeedLoad.length) { + i18n.sync._fetch(lngNeedLoad, opts, function(err, store) { + var todo = namespaces.length * lngNeedLoad.length; + + // load each file individual + f.each(namespaces, function(nsIndex, nsValue) { + + // append namespace to namespace array + if (o.ns.namespaces.indexOf(nsValue) < 0) { + o.ns.namespaces.push(nsValue); + } + + f.each(lngNeedLoad, function(lngIndex, lngValue) { + resStore[lngValue] = resStore[lngValue] || {}; + resStore[lngValue][nsValue] = store[lngValue][nsValue]; + + todo--; // wait for all done befor callback + if (todo === 0 && cb) { + if (o.useLocalStorage) i18n.sync._storeLocal(resStore); + cb(); + } + }); + }); + }); + } else { + if (cb) cb(); + } + } + + function setLng(lng, options, cb) { + if (typeof options === 'function') { + cb = options; + options = {}; + } else if (!options) { + options = {}; + } + + options.lng = lng; + return init(options, cb); + } + + function lng() { + return currentLng; + } + + function dir() { + var rtlLangs = [ "ar", "shu", "sqr", "ssh", "xaa", "yhd", "yud", "aao", "abh", "abv", "acm", + "acq", "acw", "acx", "acy", "adf", "ads", "aeb", "aec", "afb", "ajp", "apc", "apd", "arb", + "arq", "ars", "ary", "arz", "auz", "avl", "ayh", "ayl", "ayn", "ayp", "bbz", "pga", "he", + "iw", "ps", "pbt", "pbu", "pst", "prp", "prd", "ur", "ydd", "yds", "yih", "ji", "yi", "hbo", + "men", "xmn", "fa", "jpr", "peo", "pes", "prs", "dv", "sam" + ]; + + if ( rtlLangs.some( function( lang ) { + return new RegExp( '^' + lang ).test( currentLng ); + } ) ) { + return 'rtl'; + } + return 'ltr'; + } + + function reload(cb) { + resStore = {}; + setLng(currentLng, cb); + } + + function noConflict() { + + window.i18next = window.i18n; + + if (conflictReference) { + window.i18n = conflictReference; + } else { + delete window.i18n; + } + } + function addJqueryFunct() { + // $.t shortcut + $.t = $.t || translate; + + function parse(ele, key, options) { + if (key.length === 0) return; + + var attr = 'text'; + + if (key.indexOf('[') === 0) { + var parts = key.split(']'); + key = parts[1]; + attr = parts[0].substr(1, parts[0].length-1); + } + + if (key.indexOf(';') === key.length-1) { + key = key.substr(0, key.length-2); + } + + var optionsToUse; + if (attr === 'html') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; + ele.html($.t(key, optionsToUse)); + } else if (attr === 'text') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.text() }, options) : options; + ele.text($.t(key, optionsToUse)); + } else if (attr === 'prepend') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; + ele.prepend($.t(key, optionsToUse)); + } else if (attr === 'append') { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.html() }, options) : options; + ele.append($.t(key, optionsToUse)); + } else if (attr.indexOf("data-") === 0) { + var dataAttr = attr.substr(("data-").length); + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.data(dataAttr) }, options) : options; + var translated = $.t(key, optionsToUse); + //we change into the data cache + ele.data(dataAttr, translated); + //we change into the dom + ele.attr(attr, translated); + } else { + optionsToUse = o.defaultValueFromContent ? $.extend({ defaultValue: ele.attr(attr) }, options) : options; + ele.attr(attr, $.t(key, optionsToUse)); + } + } + + function localize(ele, options) { + var key = ele.attr(o.selectorAttr); + if (!key && typeof key !== 'undefined' && key !== false) key = ele.text() || ele.val(); + if (!key) return; + + var target = ele + , targetSelector = ele.data("i18n-target"); + if (targetSelector) { + target = ele.find(targetSelector) || ele; + } + + if (!options && o.useDataAttrOptions === true) { + options = ele.data("i18n-options"); + } + options = options || {}; + + if (key.indexOf(';') >= 0) { + var keys = key.split(';'); + + $.each(keys, function(m, k) { + if (k !== '') parse(target, k, options); + }); + + } else { + parse(target, key, options); + } + + if (o.useDataAttrOptions === true) { + var clone = $.extend({lng: 'non', lngs: [], _origLng: 'non'}, options); + delete clone.lng; + delete clone.lngs; + delete clone._origLng; + ele.data("i18n-options", clone); + } + } + + // fn + $.fn.i18n = function (options) { + return this.each(function() { + // localize element itself + localize($(this), options); + + // localize childs + var elements = $(this).find('[' + o.selectorAttr + ']'); + elements.each(function() { + localize($(this), options); + }); + }); + }; + } + function addJqueryLikeFunctionality() { + + function parse(ele, key, options) { + if (key.length === 0) return; + + var attr = 'text'; + + if (key.indexOf('[') === 0) { + var parts = key.split(']'); + key = parts[1]; + attr = parts[0].substr(1, parts[0].length-1); + } + + if (key.indexOf(';') === key.length-1) { + key = key.substr(0, key.length-2); + } + + if (attr === 'html') { + ele.innerHTML = translate(key, options); + } else if (attr === 'text') { + ele.textContent = translate(key, options); + } else if (attr === 'prepend') { + ele.insertAdjacentHTML(translate(key, options), 'afterbegin'); + } else if (attr === 'append') { + ele.insertAdjacentHTML(translate(key, options), 'beforeend'); + } else { + ele.setAttribute(attr, translate(key, options)); + } + } + + function localize(ele, options) { + var key = ele.getAttribute(o.selectorAttr); + if (!key && typeof key !== 'undefined' && key !== false) key = ele.textContent || ele.value; + if (!key) return; + + var target = ele + , targetSelector = ele.getAttribute("i18n-target"); + if (targetSelector) { + target = ele.querySelector(targetSelector) || ele; + } + + if (key.indexOf(';') >= 0) { + var keys = key.split(';'), index = 0, length = keys.length; + + for ( ; index < length; index++) { + if (keys[index] !== '') parse(target, keys[index], options); + } + + } else { + parse(target, key, options); + } + } + + // fn + i18n.translateObject = function (object, options) { + // localize childs + var elements = object.querySelectorAll('[' + o.selectorAttr + ']'); + var index = 0, length = elements.length; + for ( ; index < length; index++) { + localize(elements[index], options); + } + }; + } + function applyReplacement(str, replacementHash, nestedKey, options) { + if (!str) return str; + + options = options || replacementHash; // first call uses replacement hash combined with options + if (str.indexOf(options.interpolationPrefix || o.interpolationPrefix) < 0) return str; + + var prefix = options.interpolationPrefix ? f.regexEscape(options.interpolationPrefix) : o.interpolationPrefixEscaped + , suffix = options.interpolationSuffix ? f.regexEscape(options.interpolationSuffix) : o.interpolationSuffixEscaped + , keyseparator = options.keyseparator || o.keyseparator + , unEscapingSuffix = 'HTML'+suffix; + + var hash = replacementHash.replace && typeof replacementHash.replace === 'object' ? replacementHash.replace : replacementHash; + var replacementRegex = new RegExp([prefix, '(.+?)', '(HTML)?', suffix].join(''), 'g'); + var escapeInterpolation = options.escapeInterpolation || o.escapeInterpolation; + return str.replace(replacementRegex, function (wholeMatch, keyMatch, htmlMatched) { + // Check for recursive matches of object + var objectMatching = hash; + var keyLeaf = keyMatch; + while (keyLeaf.indexOf(keyseparator) >= 0 && typeof objectMatching === 'object' && objectMatching) { + var propName = keyLeaf.slice(0, keyLeaf.indexOf(keyseparator)); + keyLeaf = keyLeaf.slice(keyLeaf.indexOf(keyseparator) + 1); + objectMatching = objectMatching[propName]; + } + if (objectMatching && typeof objectMatching === 'object' && objectMatching.hasOwnProperty(keyLeaf)) { + var value = objectMatching[keyLeaf]; + if (escapeInterpolation && !htmlMatched) { + return f.escape(objectMatching[keyLeaf]); + } else { + return objectMatching[keyLeaf]; + } + } else { + return wholeMatch; + } + }); + } + + // append it to functions + f.applyReplacement = applyReplacement; + + function applyReuse(translated, options) { + var comma = ','; + var options_open = '{'; + var options_close = '}'; + + var opts = f.extend({}, options); + delete opts.postProcess; + delete opts.isFallbackLookup; + + while (translated.indexOf(o.reusePrefix) != -1) { + replacementCounter++; + if (replacementCounter > o.maxRecursion) { break; } // safety net for too much recursion + var index_of_opening = translated.lastIndexOf(o.reusePrefix); + var index_of_end_of_closing = translated.indexOf(o.reuseSuffix, index_of_opening) + o.reuseSuffix.length; + var token = translated.substring(index_of_opening, index_of_end_of_closing); + var token_without_symbols = token.replace(o.reusePrefix, '').replace(o.reuseSuffix, ''); + + if (index_of_end_of_closing <= index_of_opening) { + f.error('there is an missing closing in following translation value', translated); + return ''; + } + + if (token_without_symbols.indexOf(comma) != -1) { + var index_of_token_end_of_closing = token_without_symbols.indexOf(comma); + if (token_without_symbols.indexOf(options_open, index_of_token_end_of_closing) != -1 && token_without_symbols.indexOf(options_close, index_of_token_end_of_closing) != -1) { + var index_of_opts_opening = token_without_symbols.indexOf(options_open, index_of_token_end_of_closing); + var index_of_opts_end_of_closing = token_without_symbols.indexOf(options_close, index_of_opts_opening) + options_close.length; + try { + opts = f.extend(opts, JSON.parse(token_without_symbols.substring(index_of_opts_opening, index_of_opts_end_of_closing))); + token_without_symbols = token_without_symbols.substring(0, index_of_token_end_of_closing); + } catch (e) { + } + } + } + + var translated_token = _translate(token_without_symbols, opts); + translated = translated.replace(token, f.regexReplacementEscape(translated_token)); + } + return translated; + } + + function hasContext(options) { + return (options.context && (typeof options.context == 'string' || typeof options.context == 'number')); + } + + function needsPlural(options, lng) { + return (options.count !== undefined && typeof options.count != 'string'/* && pluralExtensions.needsPlural(lng, options.count)*/); + } + + function needsIndefiniteArticle(options) { + return (options.indefinite_article !== undefined && typeof options.indefinite_article != 'string' && options.indefinite_article); + } + + function exists(key, options) { + options = options || {}; + + var notFound = _getDefaultValue(key, options) + , found = _find(key, options); + + return found !== undefined || found === notFound; + } + + function translate(key, options) { + if (!initialized) { + f.log('i18next not finished initialization. you might have called t function before loading resources finished.') + + if (options && options.defaultValue) { + return options.detaultValue; + } else { + return ''; + } + }; + replacementCounter = 0; + return _translate.apply(null, arguments); + } + + function _getDefaultValue(key, options) { + return (options.defaultValue !== undefined) ? options.defaultValue : key; + } + + function _injectSprintfProcessor() { + + var values = []; + + // mh: build array from second argument onwards + for (var i = 1; i < arguments.length; i++) { + values.push(arguments[i]); + } + + return { + postProcess: 'sprintf', + sprintf: values + }; + } + + function _translate(potentialKeys, options) { + if (typeof options !== 'undefined' && typeof options !== 'object') { + if (o.shortcutFunction === 'sprintf') { + // mh: gettext like sprintf syntax found, automatically create sprintf processor + options = _injectSprintfProcessor.apply(null, arguments); + } else if (o.shortcutFunction === 'defaultValue') { + options = { + defaultValue: options + } + } + } else { + options = options || {}; + } + + if (typeof o.defaultVariables === 'object') { + options = f.extend({}, o.defaultVariables, options); + } + + if (potentialKeys === undefined || potentialKeys === null || potentialKeys === '') return ''; + + if (typeof potentialKeys === 'number') { + potentialKeys = String(potentialKeys); + } + + if (typeof potentialKeys === 'string') { + potentialKeys = [potentialKeys]; + } + + var key = potentialKeys[0]; + + if (potentialKeys.length > 1) { + for (var i = 0; i < potentialKeys.length; i++) { + key = potentialKeys[i]; + if (exists(key, options)) { + break; + } + } + } + + var notFound = _getDefaultValue(key, options) + , found = _find(key, options) + , nsseparator = options.nsseparator || o.nsseparator + , lngs = options.lng ? f.toLanguages(options.lng, options.fallbackLng) : languages + , ns = options.ns || o.ns.defaultNs + , parts; + + // split ns and key + if (key.indexOf(nsseparator) > -1) { + parts = key.split(nsseparator); + ns = parts[0]; + key = parts[1]; + } + + if (found === undefined && o.sendMissing && typeof o.missingKeyHandler === 'function') { + if (options.lng) { + o.missingKeyHandler(lngs[0], ns, key, notFound, lngs); + } else { + o.missingKeyHandler(o.lng, ns, key, notFound, lngs); + } + } + + var postProcessorsToApply, + postProcessor, + j; + if (typeof o.postProcess === 'string' && o.postProcess !== '') { + postProcessorsToApply = [o.postProcess]; + } else if (typeof o.postProcess === 'array' || typeof o.postProcess === 'object') { + postProcessorsToApply = o.postProcess; + } else { + postProcessorsToApply = []; + } + + if (typeof options.postProcess === 'string' && options.postProcess !== '') { + postProcessorsToApply = postProcessorsToApply.concat([options.postProcess]); + } else if (typeof options.postProcess === 'array' || typeof options.postProcess === 'object') { + postProcessorsToApply = postProcessorsToApply.concat(options.postProcess); + } + + if (found !== undefined && postProcessorsToApply.length) { + for (j = 0; j < postProcessorsToApply.length; j += 1) { + postProcessor = postProcessorsToApply[j]; + if (postProcessors[postProcessor]) { + found = postProcessors[postProcessor](found, key, options); + } + } + } + + // process notFound if function exists + var splitNotFound = notFound; + if (notFound.indexOf(nsseparator) > -1) { + parts = notFound.split(nsseparator); + splitNotFound = parts[1]; + } + if (splitNotFound === key && o.parseMissingKey) { + notFound = o.parseMissingKey(notFound); + } + + if (found === undefined) { + notFound = applyReplacement(notFound, options); + notFound = applyReuse(notFound, options); + + if (postProcessorsToApply.length) { + found = _getDefaultValue(key, options); + for (j = 0; j < postProcessorsToApply.length; j += 1) { + postProcessor = postProcessorsToApply[j]; + if (postProcessors[postProcessor]) { + found = postProcessors[postProcessor](found, key, options); + } + } + } + } + + return (found !== undefined) ? found : notFound; + } + + function _find(key, options) { + options = options || {}; + + var optionWithoutCount, translated + , notFound = _getDefaultValue(key, options) + , lngs = languages; + + if (!resStore) { return notFound; } // no resStore to translate from + + // CI mode + if (lngs[0].toLowerCase() === 'cimode') return notFound; + + // passed in lng + if (options.lngs) lngs = options.lngs; + if (options.lng) { + lngs = f.toLanguages(options.lng, options.fallbackLng); + + if (!resStore[lngs[0]]) { + var oldAsync = o.getAsync; + o.getAsync = false; + + i18n.sync.load(lngs, o, function(err, store) { + f.extend(resStore, store); + o.getAsync = oldAsync; + }); + } + } + + var ns = options.ns || o.ns.defaultNs; + var nsseparator = options.nsseparator || o.nsseparator; + if (key.indexOf(nsseparator) > -1) { + var parts = key.split(nsseparator); + ns = parts[0]; + key = parts[1]; + } + + if (hasContext(options)) { + optionWithoutCount = f.extend({}, options); + delete optionWithoutCount.context; + optionWithoutCount.defaultValue = o.contextNotFound; + + var contextKey = ns + nsseparator + key + '_' + options.context; + + translated = translate(contextKey, optionWithoutCount); + if (translated != o.contextNotFound) { + return applyReplacement(translated, { context: options.context }); // apply replacement for context only + } // else continue translation with original/nonContext key + } + + if (needsPlural(options, lngs[0])) { + optionWithoutCount = f.extend({ lngs: [lngs[0]]}, options); + delete optionWithoutCount.count; + optionWithoutCount._origLng = optionWithoutCount._origLng || optionWithoutCount.lng || lngs[0]; + delete optionWithoutCount.lng; + optionWithoutCount.defaultValue = o.pluralNotFound; + + var pluralKey; + if (!pluralExtensions.needsPlural(lngs[0], options.count)) { + pluralKey = ns + nsseparator + key; + } else { + pluralKey = ns + nsseparator + key + o.pluralSuffix; + var pluralExtension = pluralExtensions.get(lngs[0], options.count); + if (pluralExtension >= 0) { + pluralKey = pluralKey + '_' + pluralExtension; + } else if (pluralExtension === 1) { + pluralKey = ns + nsseparator + key; // singular + } + } + + translated = translate(pluralKey, optionWithoutCount); + + if (translated != o.pluralNotFound) { + return applyReplacement(translated, { + count: options.count, + interpolationPrefix: options.interpolationPrefix, + interpolationSuffix: options.interpolationSuffix + }); // apply replacement for count only + } else if (lngs.length > 1) { + // remove failed lng + var clone = lngs.slice(); + clone.shift(); + options = f.extend(options, { lngs: clone }); + options._origLng = optionWithoutCount._origLng; + delete options.lng; + // retry with fallbacks + translated = translate(ns + nsseparator + key, options); + if (translated != o.pluralNotFound) return translated; + } else { + optionWithoutCount.lng = optionWithoutCount._origLng; + delete optionWithoutCount._origLng; + translated = translate(ns + nsseparator + key, optionWithoutCount); + + return applyReplacement(translated, { + count: options.count, + interpolationPrefix: options.interpolationPrefix, + interpolationSuffix: options.interpolationSuffix + }); + } + } + + if (needsIndefiniteArticle(options)) { + var optionsWithoutIndef = f.extend({}, options); + delete optionsWithoutIndef.indefinite_article; + optionsWithoutIndef.defaultValue = o.indefiniteNotFound; + // If we don't have a count, we want the indefinite, if we do have a count, and needsPlural is false + var indefiniteKey = ns + nsseparator + key + (((options.count && !needsPlural(options, lngs[0])) || !options.count) ? o.indefiniteSuffix : ""); + translated = translate(indefiniteKey, optionsWithoutIndef); + if (translated != o.indefiniteNotFound) { + return translated; + } + } + + var found; + var keyseparator = options.keyseparator || o.keyseparator; + var keys = key.split(keyseparator); + for (var i = 0, len = lngs.length; i < len; i++ ) { + if (found !== undefined) break; + + var l = lngs[i]; + + var x = 0; + var value = resStore[l] && resStore[l][ns]; + while (keys[x]) { + value = value && value[keys[x]]; + x++; + } + if (value !== undefined && (!o.showKeyIfEmpty || value !== '')) { + var valueType = Object.prototype.toString.apply(value); + if (typeof value === 'string') { + value = applyReplacement(value, options); + value = applyReuse(value, options); + } else if (valueType === '[object Array]' && !o.returnObjectTrees && !options.returnObjectTrees) { + value = value.join('\n'); + value = applyReplacement(value, options); + value = applyReuse(value, options); + } else if (value === null && o.fallbackOnNull === true) { + value = undefined; + } else if (value !== null) { + if (!o.returnObjectTrees && !options.returnObjectTrees) { + if (o.objectTreeKeyHandler && typeof o.objectTreeKeyHandler == 'function') { + value = o.objectTreeKeyHandler(key, value, l, ns, options); + } else { + value = 'key \'' + ns + ':' + key + ' (' + l + ')\' ' + + 'returned an object instead of string.'; + f.log(value); + } + } else if (valueType !== '[object Number]' && valueType !== '[object Function]' && valueType !== '[object RegExp]') { + var copy = (valueType === '[object Array]') ? [] : {}; // apply child translation on a copy + f.each(value, function(m) { + copy[m] = _translate(ns + nsseparator + key + keyseparator + m, options); + }); + value = copy; + } + } + + if (typeof value === 'string' && value.trim() === '' && o.fallbackOnEmpty === true) + value = undefined; + + found = value; + } + } + + if (found === undefined && !options.isFallbackLookup && (o.fallbackToDefaultNS === true || (o.fallbackNS && o.fallbackNS.length > 0))) { + // set flag for fallback lookup - avoid recursion + options.isFallbackLookup = true; + + if (o.fallbackNS.length) { + + for (var y = 0, lenY = o.fallbackNS.length; y < lenY; y++) { + found = _find(o.fallbackNS[y] + nsseparator + key, options); + + if (found || (found==="" && o.fallbackOnEmpty === false)) { + /* compare value without namespace */ + var foundValue = found.indexOf(nsseparator) > -1 ? found.split(nsseparator)[1] : found + , notFoundValue = notFound.indexOf(nsseparator) > -1 ? notFound.split(nsseparator)[1] : notFound; + + if (foundValue !== notFoundValue) break; + } + } + } else { + options.ns = o.ns.defaultNs; + found = _find(key, options); // fallback to default NS + } + options.isFallbackLookup = false; + } + + return found; + } + function detectLanguage() { + var detectedLng; + var whitelist = o.lngWhitelist || []; + var userLngChoices = []; + + // get from qs + var qsParm = []; + if (typeof window !== 'undefined') { + (function() { + var query = window.location.search.substring(1); + var params = query.split('&'); + for (var i=0; i 0) { + var key = params[i].substring(0,pos); + if (key == o.detectLngQS) { + userLngChoices.push(params[i].substring(pos+1)); + } + } + } + })(); + } + + // get from cookie + if (o.useCookie && typeof document !== 'undefined') { + var c = f.cookie.read(o.cookieName); + if (c) userLngChoices.push(c); + } + + // get from localStorage + if (o.detectLngFromLocalStorage && typeof window !== 'undefined' && window.localStorage) { + var lang = f.localStorage.getItem('i18next_lng'); + if (lang) { + userLngChoices.push(lang); + } + } + + // get from navigator + if (typeof navigator !== 'undefined') { + if (navigator.languages) { // chrome only; not an array, so can't use .push.apply instead of iterating + for (var i=0;i -1) { + var parts = lng.split('-'); + lng = o.lowerCaseLng ? + parts[0].toLowerCase() + '-' + parts[1].toLowerCase() : + parts[0].toLowerCase() + '-' + parts[1].toUpperCase(); + } + + if (whitelist.length === 0 || whitelist.indexOf(lng) > -1) { + detectedLng = lng; + break; + } + } + })(); + + //fallback + if (!detectedLng){ + detectedLng = o.fallbackLng[0]; + } + + return detectedLng; + } + // definition http://translate.sourceforge.net/wiki/l10n/pluralforms + + /* [code, name, numbers, pluralsType] */ + var _rules = [ + ["ach", "Acholi", [1,2], 1], + ["af", "Afrikaans",[1,2], 2], + ["ak", "Akan", [1,2], 1], + ["am", "Amharic", [1,2], 1], + ["an", "Aragonese",[1,2], 2], + ["ar", "Arabic", [0,1,2,3,11,100],5], + ["arn", "Mapudungun",[1,2], 1], + ["ast", "Asturian", [1,2], 2], + ["ay", "Aymará", [1], 3], + ["az", "Azerbaijani",[1,2],2], + ["be", "Belarusian",[1,2,5],4], + ["bg", "Bulgarian",[1,2], 2], + ["bn", "Bengali", [1,2], 2], + ["bo", "Tibetan", [1], 3], + ["br", "Breton", [1,2], 1], + ["bs", "Bosnian", [1,2,5],4], + ["ca", "Catalan", [1,2], 2], + ["cgg", "Chiga", [1], 3], + ["cs", "Czech", [1,2,5],6], + ["csb", "Kashubian",[1,2,5],7], + ["cy", "Welsh", [1,2,3,8],8], + ["da", "Danish", [1,2], 2], + ["de", "German", [1,2], 2], + ["dev", "Development Fallback", [1,2], 2], + ["dz", "Dzongkha", [1], 3], + ["el", "Greek", [1,2], 2], + ["en", "English", [1,2], 2], + ["eo", "Esperanto",[1,2], 2], + ["es", "Spanish", [1,2], 2], + ["es_ar","Argentinean Spanish", [1,2], 2], + ["et", "Estonian", [1,2], 2], + ["eu", "Basque", [1,2], 2], + ["fa", "Persian", [1], 3], + ["fi", "Finnish", [1,2], 2], + ["fil", "Filipino", [1,2], 1], + ["fo", "Faroese", [1,2], 2], + ["fr", "French", [1,2], 9], + ["fur", "Friulian", [1,2], 2], + ["fy", "Frisian", [1,2], 2], + ["ga", "Irish", [1,2,3,7,11],10], + ["gd", "Scottish Gaelic",[1,2,3,20],11], + ["gl", "Galician", [1,2], 2], + ["gu", "Gujarati", [1,2], 2], + ["gun", "Gun", [1,2], 1], + ["ha", "Hausa", [1,2], 2], + ["he", "Hebrew", [1,2], 2], + ["hi", "Hindi", [1,2], 2], + ["hr", "Croatian", [1,2,5],4], + ["hu", "Hungarian",[1,2], 2], + ["hy", "Armenian", [1,2], 2], + ["ia", "Interlingua",[1,2],2], + ["id", "Indonesian",[1], 3], + ["is", "Icelandic",[1,2], 12], + ["it", "Italian", [1,2], 2], + ["ja", "Japanese", [1], 3], + ["jbo", "Lojban", [1], 3], + ["jv", "Javanese", [0,1], 13], + ["ka", "Georgian", [1], 3], + ["kk", "Kazakh", [1], 3], + ["km", "Khmer", [1], 3], + ["kn", "Kannada", [1,2], 2], + ["ko", "Korean", [1], 3], + ["ku", "Kurdish", [1,2], 2], + ["kw", "Cornish", [1,2,3,4],14], + ["ky", "Kyrgyz", [1], 3], + ["lb", "Letzeburgesch",[1,2],2], + ["ln", "Lingala", [1,2], 1], + ["lo", "Lao", [1], 3], + ["lt", "Lithuanian",[1,2,10],15], + ["lv", "Latvian", [1,2,0],16], + ["mai", "Maithili", [1,2], 2], + ["mfe", "Mauritian Creole",[1,2],1], + ["mg", "Malagasy", [1,2], 1], + ["mi", "Maori", [1,2], 1], + ["mk", "Macedonian",[1,2],17], + ["ml", "Malayalam",[1,2], 2], + ["mn", "Mongolian",[1,2], 2], + ["mnk", "Mandinka", [0,1,2],18], + ["mr", "Marathi", [1,2], 2], + ["ms", "Malay", [1], 3], + ["mt", "Maltese", [1,2,11,20],19], + ["nah", "Nahuatl", [1,2], 2], + ["nap", "Neapolitan",[1,2], 2], + ["nb", "Norwegian Bokmal",[1,2],2], + ["ne", "Nepali", [1,2], 2], + ["nl", "Dutch", [1,2], 2], + ["nn", "Norwegian Nynorsk",[1,2],2], + ["no", "Norwegian",[1,2], 2], + ["nso", "Northern Sotho",[1,2],2], + ["oc", "Occitan", [1,2], 1], + ["or", "Oriya", [2,1], 2], + ["pa", "Punjabi", [1,2], 2], + ["pap", "Papiamento",[1,2], 2], + ["pl", "Polish", [1,2,5],7], + ["pms", "Piemontese",[1,2], 2], + ["ps", "Pashto", [1,2], 2], + ["pt", "Portuguese",[1,2], 2], + ["pt_br","Brazilian Portuguese",[1,2], 2], + ["rm", "Romansh", [1,2], 2], + ["ro", "Romanian", [1,2,20],20], + ["ru", "Russian", [1,2,5],4], + ["sah", "Yakut", [1], 3], + ["sco", "Scots", [1,2], 2], + ["se", "Northern Sami",[1,2], 2], + ["si", "Sinhala", [1,2], 2], + ["sk", "Slovak", [1,2,5],6], + ["sl", "Slovenian",[5,1,2,3],21], + ["so", "Somali", [1,2], 2], + ["son", "Songhay", [1,2], 2], + ["sq", "Albanian", [1,2], 2], + ["sr", "Serbian", [1,2,5],4], + ["su", "Sundanese",[1], 3], + ["sv", "Swedish", [1,2], 2], + ["sw", "Swahili", [1,2], 2], + ["ta", "Tamil", [1,2], 2], + ["te", "Telugu", [1,2], 2], + ["tg", "Tajik", [1,2], 1], + ["th", "Thai", [1], 3], + ["ti", "Tigrinya", [1,2], 1], + ["tk", "Turkmen", [1,2], 2], + ["tr", "Turkish", [1,2], 1], + ["tt", "Tatar", [1], 3], + ["ug", "Uyghur", [1], 3], + ["uk", "Ukrainian",[1,2,5],4], + ["ur", "Urdu", [1,2], 2], + ["uz", "Uzbek", [1,2], 1], + ["vi", "Vietnamese",[1], 3], + ["wa", "Walloon", [1,2], 1], + ["wo", "Wolof", [1], 3], + ["yo", "Yoruba", [1,2], 2], + ["zh", "Chinese", [1], 3] + ]; + + var _rulesPluralsTypes = { + 1: function(n) {return Number(n > 1);}, + 2: function(n) {return Number(n != 1);}, + 3: function(n) {return 0;}, + 4: function(n) {return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);}, + 5: function(n) {return Number(n===0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 ? 4 : 5);}, + 6: function(n) {return Number((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);}, + 7: function(n) {return Number(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);}, + 8: function(n) {return Number((n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3);}, + 9: function(n) {return Number(n >= 2);}, + 10: function(n) {return Number(n==1 ? 0 : n==2 ? 1 : n<7 ? 2 : n<11 ? 3 : 4) ;}, + 11: function(n) {return Number((n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3);}, + 12: function(n) {return Number(n%10!=1 || n%100==11);}, + 13: function(n) {return Number(n !== 0);}, + 14: function(n) {return Number((n==1) ? 0 : (n==2) ? 1 : (n == 3) ? 2 : 3);}, + 15: function(n) {return Number(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);}, + 16: function(n) {return Number(n%10==1 && n%100!=11 ? 0 : n !== 0 ? 1 : 2);}, + 17: function(n) {return Number(n==1 || n%10==1 ? 0 : 1);}, + 18: function(n) {return Number(n==0 ? 0 : n==1 ? 1 : 2);}, + 19: function(n) {return Number(n==1 ? 0 : n===0 || ( n%100>1 && n%100<11) ? 1 : (n%100>10 && n%100<20 ) ? 2 : 3);}, + 20: function(n) {return Number(n==1 ? 0 : (n===0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2);}, + 21: function(n) {return Number(n%100==1 ? 1 : n%100==2 ? 2 : n%100==3 || n%100==4 ? 3 : 0); } + }; + + var pluralExtensions = { + + rules: (function () { + var l, rules = {}; + for (l=_rules.length; l-- ;) { + rules[_rules[l][0]] = { + name: _rules[l][1], + numbers: _rules[l][2], + plurals: _rulesPluralsTypes[_rules[l][3]] + } + } + return rules; + }()), + + // you can add your own pluralExtensions + addRule: function(lng, obj) { + pluralExtensions.rules[lng] = obj; + }, + + setCurrentLng: function(lng) { + if (!pluralExtensions.currentRule || pluralExtensions.currentRule.lng !== lng) { + var parts = lng.split('-'); + + pluralExtensions.currentRule = { + lng: lng, + rule: pluralExtensions.rules[parts[0]] + }; + } + }, + + needsPlural: function(lng, count) { + var parts = lng.split('-'); + + var ext; + if (pluralExtensions.currentRule && pluralExtensions.currentRule.lng === lng) { + ext = pluralExtensions.currentRule.rule; + } else { + ext = pluralExtensions.rules[parts[f.getCountyIndexOfLng(lng)]]; + } + + if (ext && ext.numbers.length <= 1) { + return false; + } else { + return this.get(lng, count) !== 1; + } + }, + + get: function(lng, count) { + var parts = lng.split('-'); + + function getResult(l, c) { + var ext; + if (pluralExtensions.currentRule && pluralExtensions.currentRule.lng === lng) { + ext = pluralExtensions.currentRule.rule; + } else { + ext = pluralExtensions.rules[l]; + } + if (ext) { + var i; + if (ext.noAbs) { + i = ext.plurals(c); + } else { + i = ext.plurals(Math.abs(c)); + } + + var number = ext.numbers[i]; + if (ext.numbers.length === 2 && ext.numbers[0] === 1) { + if (number === 2) { + number = -1; // regular plural + } else if (number === 1) { + number = 1; // singular + } + }//console.log(count + '-' + number); + return number; + } else { + return c === 1 ? '1' : '-1'; + } + } + + return getResult(parts[f.getCountyIndexOfLng(lng)], count); + } + + }; + var postProcessors = {}; + var addPostProcessor = function(name, fc) { + postProcessors[name] = fc; + }; + // sprintf support + var sprintf = (function() { + function get_type(variable) { + return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase(); + } + function str_repeat(input, multiplier) { + for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */} + return output.join(''); + } + + var str_format = function() { + if (!str_format.cache.hasOwnProperty(arguments[0])) { + str_format.cache[arguments[0]] = str_format.parse(arguments[0]); + } + return str_format.format.call(null, str_format.cache[arguments[0]], arguments); + }; + + str_format.format = function(parse_tree, argv) { + var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length; + for (i = 0; i < tree_length; i++) { + node_type = get_type(parse_tree[i]); + if (node_type === 'string') { + output.push(parse_tree[i]); + } + else if (node_type === 'array') { + match = parse_tree[i]; // convenience purposes only + if (match[2]) { // keyword argument + arg = argv[cursor]; + for (k = 0; k < match[2].length; k++) { + if (!arg.hasOwnProperty(match[2][k])) { + throw(sprintf('[sprintf] property "%s" does not exist', match[2][k])); + } + arg = arg[match[2][k]]; + } + } + else if (match[1]) { // positional argument (explicit) + arg = argv[match[1]]; + } + else { // positional argument (implicit) + arg = argv[cursor++]; + } + + if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) { + throw(sprintf('[sprintf] expecting number but found %s', get_type(arg))); + } + switch (match[8]) { + case 'b': arg = arg.toString(2); break; + case 'c': arg = String.fromCharCode(arg); break; + case 'd': arg = parseInt(arg, 10); break; + case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break; + case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break; + case 'o': arg = arg.toString(8); break; + case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break; + case 'u': arg = Math.abs(arg); break; + case 'x': arg = arg.toString(16); break; + case 'X': arg = arg.toString(16).toUpperCase(); break; + } + arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg); + pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' '; + pad_length = match[6] - String(arg).length; + pad = match[6] ? str_repeat(pad_character, pad_length) : ''; + output.push(match[5] ? arg + pad : pad + arg); + } + } + return output.join(''); + }; + + str_format.cache = {}; + + str_format.parse = function(fmt) { + var _fmt = fmt, match = [], parse_tree = [], arg_names = 0; + while (_fmt) { + if ((match = /^[^\x25]+/.exec(_fmt)) !== null) { + parse_tree.push(match[0]); + } + else if ((match = /^\x25{2}/.exec(_fmt)) !== null) { + parse_tree.push('%'); + } + else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) { + if (match[2]) { + arg_names |= 1; + var field_list = [], replacement_field = match[2], field_match = []; + if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') { + if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + } + else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) { + field_list.push(field_match[1]); + } + else { + throw('[sprintf] huh?'); + } + } + } + else { + throw('[sprintf] huh?'); + } + match[2] = field_list; + } + else { + arg_names |= 2; + } + if (arg_names === 3) { + throw('[sprintf] mixing positional and named placeholders is not (yet) supported'); + } + parse_tree.push(match); + } + else { + throw('[sprintf] huh?'); + } + _fmt = _fmt.substring(match[0].length); + } + return parse_tree; + }; + + return str_format; + })(); + + var vsprintf = function(fmt, argv) { + argv.unshift(fmt); + return sprintf.apply(null, argv); + }; + + addPostProcessor("sprintf", function(val, key, opts) { + if (!opts.sprintf) return val; + + if (Object.prototype.toString.apply(opts.sprintf) === '[object Array]') { + return vsprintf(val, opts.sprintf); + } else if (typeof opts.sprintf === 'object') { + return sprintf(val, opts.sprintf); + } + + return val; + }); + // public api interface + i18n.init = init; + i18n.isInitialized = isInitialized; + i18n.setLng = setLng; + i18n.preload = preload; + i18n.addResourceBundle = addResourceBundle; + i18n.hasResourceBundle = hasResourceBundle; + i18n.getResourceBundle = getResourceBundle; + i18n.addResource = addResource; + i18n.addResources = addResources; + i18n.removeResourceBundle = removeResourceBundle; + i18n.loadNamespace = loadNamespace; + i18n.loadNamespaces = loadNamespaces; + i18n.setDefaultNamespace = setDefaultNamespace; + i18n.t = translate; + i18n.translate = translate; + i18n.exists = exists; + i18n.detectLanguage = f.detectLanguage; + i18n.pluralExtensions = pluralExtensions; + i18n.sync = sync; + i18n.functions = f; + i18n.lng = lng; + i18n.dir = dir; + i18n.addPostProcessor = addPostProcessor; + i18n.applyReplacement = f.applyReplacement; + i18n.options = o; + i18n.noConflict = noConflict; + +})(typeof exports === 'undefined' ? window : exports); \ No newline at end of file diff --git a/public/javascripts/jquery-2.1.4.min.js b/public/javascripts/jquery-2.1.4.min.js new file mode 100644 index 0000000..49990d6 --- /dev/null +++ b/public/javascripts/jquery-2.1.4.min.js @@ -0,0 +1,4 @@ +/*! jQuery v2.1.4 | (c) 2005, 2015 jQuery Foundation, Inc. | jquery.org/license */ +!function(a,b){"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){var c=[],d=c.slice,e=c.concat,f=c.push,g=c.indexOf,h={},i=h.toString,j=h.hasOwnProperty,k={},l=a.document,m="2.1.4",n=function(a,b){return new n.fn.init(a,b)},o=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,p=/^-ms-/,q=/-([\da-z])/gi,r=function(a,b){return b.toUpperCase()};n.fn=n.prototype={jquery:m,constructor:n,selector:"",length:0,toArray:function(){return d.call(this)},get:function(a){return null!=a?0>a?this[a+this.length]:this[a]:d.call(this)},pushStack:function(a){var b=n.merge(this.constructor(),a);return b.prevObject=this,b.context=this.context,b},each:function(a,b){return n.each(this,a,b)},map:function(a){return this.pushStack(n.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(d.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(0>a?b:0);return this.pushStack(c>=0&&b>c?[this[c]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:f,sort:c.sort,splice:c.splice},n.extend=n.fn.extend=function(){var a,b,c,d,e,f,g=arguments[0]||{},h=1,i=arguments.length,j=!1;for("boolean"==typeof g&&(j=g,g=arguments[h]||{},h++),"object"==typeof g||n.isFunction(g)||(g={}),h===i&&(g=this,h--);i>h;h++)if(null!=(a=arguments[h]))for(b in a)c=g[b],d=a[b],g!==d&&(j&&d&&(n.isPlainObject(d)||(e=n.isArray(d)))?(e?(e=!1,f=c&&n.isArray(c)?c:[]):f=c&&n.isPlainObject(c)?c:{},g[b]=n.extend(j,f,d)):void 0!==d&&(g[b]=d));return g},n.extend({expando:"jQuery"+(m+Math.random()).replace(/\D/g,""),isReady:!0,error:function(a){throw new Error(a)},noop:function(){},isFunction:function(a){return"function"===n.type(a)},isArray:Array.isArray,isWindow:function(a){return null!=a&&a===a.window},isNumeric:function(a){return!n.isArray(a)&&a-parseFloat(a)+1>=0},isPlainObject:function(a){return"object"!==n.type(a)||a.nodeType||n.isWindow(a)?!1:a.constructor&&!j.call(a.constructor.prototype,"isPrototypeOf")?!1:!0},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},type:function(a){return null==a?a+"":"object"==typeof a||"function"==typeof a?h[i.call(a)]||"object":typeof a},globalEval:function(a){var b,c=eval;a=n.trim(a),a&&(1===a.indexOf("use strict")?(b=l.createElement("script"),b.text=a,l.head.appendChild(b).parentNode.removeChild(b)):c(a))},camelCase:function(a){return a.replace(p,"ms-").replace(q,r)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()},each:function(a,b,c){var d,e=0,f=a.length,g=s(a);if(c){if(g){for(;f>e;e++)if(d=b.apply(a[e],c),d===!1)break}else for(e in a)if(d=b.apply(a[e],c),d===!1)break}else if(g){for(;f>e;e++)if(d=b.call(a[e],e,a[e]),d===!1)break}else for(e in a)if(d=b.call(a[e],e,a[e]),d===!1)break;return a},trim:function(a){return null==a?"":(a+"").replace(o,"")},makeArray:function(a,b){var c=b||[];return null!=a&&(s(Object(a))?n.merge(c,"string"==typeof a?[a]:a):f.call(c,a)),c},inArray:function(a,b,c){return null==b?-1:g.call(b,a,c)},merge:function(a,b){for(var c=+b.length,d=0,e=a.length;c>d;d++)a[e++]=b[d];return a.length=e,a},grep:function(a,b,c){for(var d,e=[],f=0,g=a.length,h=!c;g>f;f++)d=!b(a[f],f),d!==h&&e.push(a[f]);return e},map:function(a,b,c){var d,f=0,g=a.length,h=s(a),i=[];if(h)for(;g>f;f++)d=b(a[f],f,c),null!=d&&i.push(d);else for(f in a)d=b(a[f],f,c),null!=d&&i.push(d);return e.apply([],i)},guid:1,proxy:function(a,b){var c,e,f;return"string"==typeof b&&(c=a[b],b=a,a=c),n.isFunction(a)?(e=d.call(arguments,2),f=function(){return a.apply(b||this,e.concat(d.call(arguments)))},f.guid=a.guid=a.guid||n.guid++,f):void 0},now:Date.now,support:k}),n.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(a,b){h["[object "+b+"]"]=b.toLowerCase()});function s(a){var b="length"in a&&a.length,c=n.type(a);return"function"===c||n.isWindow(a)?!1:1===a.nodeType&&b?!0:"array"===c||0===b||"number"==typeof b&&b>0&&b-1 in a}var t=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C=1<<31,D={}.hasOwnProperty,E=[],F=E.pop,G=E.push,H=E.push,I=E.slice,J=function(a,b){for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},K="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",L="[\\x20\\t\\r\\n\\f]",M="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",N=M.replace("w","w#"),O="\\["+L+"*("+M+")(?:"+L+"*([*^$|!~]?=)"+L+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+N+"))|)"+L+"*\\]",P=":("+M+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+O+")*)|.*)\\)|)",Q=new RegExp(L+"+","g"),R=new RegExp("^"+L+"+|((?:^|[^\\\\])(?:\\\\.)*)"+L+"+$","g"),S=new RegExp("^"+L+"*,"+L+"*"),T=new RegExp("^"+L+"*([>+~]|"+L+")"+L+"*"),U=new RegExp("="+L+"*([^\\]'\"]*?)"+L+"*\\]","g"),V=new RegExp(P),W=new RegExp("^"+N+"$"),X={ID:new RegExp("^#("+M+")"),CLASS:new RegExp("^\\.("+M+")"),TAG:new RegExp("^("+M.replace("w","w*")+")"),ATTR:new RegExp("^"+O),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+L+"*(even|odd|(([+-]|)(\\d*)n|)"+L+"*(?:([+-]|)"+L+"*(\\d+)|))"+L+"*\\)|)","i"),bool:new RegExp("^(?:"+K+")$","i"),needsContext:new RegExp("^"+L+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+L+"*((?:-\\d)?\\d*)"+L+"*\\)|)(?=[^-]|$)","i")},Y=/^(?:input|select|textarea|button)$/i,Z=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,aa=/[+~]/,ba=/'|\\/g,ca=new RegExp("\\\\([\\da-f]{1,6}"+L+"?|("+L+")|.)","ig"),da=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:0>d?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ea=function(){m()};try{H.apply(E=I.call(v.childNodes),v.childNodes),E[v.childNodes.length].nodeType}catch(fa){H={apply:E.length?function(a,b){G.apply(a,I.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s,w,x;if((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,d=d||[],k=b.nodeType,"string"!=typeof a||!a||1!==k&&9!==k&&11!==k)return d;if(!e&&p){if(11!==k&&(f=_.exec(a)))if(j=f[1]){if(9===k){if(h=b.getElementById(j),!h||!h.parentNode)return d;if(h.id===j)return d.push(h),d}else if(b.ownerDocument&&(h=b.ownerDocument.getElementById(j))&&t(b,h)&&h.id===j)return d.push(h),d}else{if(f[2])return H.apply(d,b.getElementsByTagName(a)),d;if((j=f[3])&&c.getElementsByClassName)return H.apply(d,b.getElementsByClassName(j)),d}if(c.qsa&&(!q||!q.test(a))){if(s=r=u,w=b,x=1!==k&&a,1===k&&"object"!==b.nodeName.toLowerCase()){o=g(a),(r=b.getAttribute("id"))?s=r.replace(ba,"\\$&"):b.setAttribute("id",s),s="[id='"+s+"'] ",l=o.length;while(l--)o[l]=s+ra(o[l]);w=aa.test(a)&&pa(b.parentNode)||b,x=o.join(",")}if(x)try{return H.apply(d,w.querySelectorAll(x)),d}catch(y){}finally{r||b.removeAttribute("id")}}}return i(a.replace(R,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("div");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=a.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&(~b.sourceIndex||C)-(~a.sourceIndex||C);if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function pa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return b?"HTML"!==b.nodeName:!1},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=g.documentElement,e=g.defaultView,e&&e!==e.top&&(e.addEventListener?e.addEventListener("unload",ea,!1):e.attachEvent&&e.attachEvent("onunload",ea)),p=!f(g),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(g.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=$.test(g.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!g.getElementsByName||!g.getElementsByName(u).length}),c.getById?(d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c&&c.parentNode?[c]:[]}},d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){return a.getAttribute("id")===b}}):(delete d.find.ID,d.filter.ID=function(a){var b=a.replace(ca,da);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){return p?b.getElementsByClassName(a):void 0},r=[],q=[],(c.qsa=$.test(g.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+L+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+L+"*(?:value|"+K+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){var b=g.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+L+"*[*^$|!~]?="),a.querySelectorAll(":enabled").length||q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=$.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"div"),s.call(a,"[s!='']:x"),r.push("!=",P)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=$.test(o.compareDocumentPosition),t=b||$.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===g||a.ownerDocument===v&&t(v,a)?-1:b===g||b.ownerDocument===v&&t(v,b)?1:k?J(k,a)-J(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,h=[a],i=[b];if(!e||!f)return a===g?-1:b===g?1:e?-1:f?1:k?J(k,a)-J(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)h.unshift(c);c=b;while(c=c.parentNode)i.unshift(c);while(h[d]===i[d])d++;return d?la(h[d],i[d]):h[d]===v?-1:i[d]===v?1:0},g):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(U,"='$1']"),!(!c.matchesSelector||!p||r&&r.test(b)||q&&q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&D.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:X,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(ca,da),a[3]=(a[3]||a[4]||a[5]||"").replace(ca,da),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return X.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&V.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(ca,da).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+L+")"+a+"("+L+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:b?(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(Q," ")+" ").indexOf(c)>-1:"|="===b?e===c||e.slice(0,c.length+1)===c+"-":!1):!0}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h;if(q){if(f){while(p){l=b;while(l=l[p])if(h?l.nodeName.toLowerCase()===r:1===l.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){k=q[u]||(q[u]={}),j=k[a]||[],n=j[0]===w&&j[1],m=j[0]===w&&j[2],l=n&&q.childNodes[n];while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if(1===l.nodeType&&++m&&l===b){k[a]=[w,n,m];break}}else if(s&&(j=(b[u]||(b[u]={}))[a])&&j[0]===w)m=j[1];else while(l=++n&&l&&l[p]||(m=n=0)||o.pop())if((h?l.nodeName.toLowerCase()===r:1===l.nodeType)&&++m&&(s&&((l[u]||(l[u]={}))[a]=[w,m]),l===b))break;return m-=e,m===d||m%d===0&&m/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=J(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(R,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(ca,da),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return W.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(ca,da).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return Z.test(a.nodeName)},input:function(a){return Y.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:oa(function(){return[0]}),last:oa(function(a,b){return[b-1]}),eq:oa(function(a,b,c){return[0>c?c+b:c]}),even:oa(function(a,b){for(var c=0;b>c;c+=2)a.push(c);return a}),odd:oa(function(a,b){for(var c=1;b>c;c+=2)a.push(c);return a}),lt:oa(function(a,b,c){for(var d=0>c?c+b:c;--d>=0;)a.push(d);return a}),gt:oa(function(a,b,c){for(var d=0>c?c+b:c;++db;b++)d+=a[b].value;return d}function sa(a,b,c){var d=b.dir,e=c&&"parentNode"===d,f=x++;return b.first?function(b,c,f){while(b=b[d])if(1===b.nodeType||e)return a(b,c,f)}:function(b,c,g){var h,i,j=[w,f];if(g){while(b=b[d])if((1===b.nodeType||e)&&a(b,c,g))return!0}else while(b=b[d])if(1===b.nodeType||e){if(i=b[u]||(b[u]={}),(h=i[d])&&h[0]===w&&h[1]===f)return j[2]=h[2];if(i[d]=j,j[2]=a(b,c,g))return!0}}}function ta(a){return a.length>1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function ua(a,b,c){for(var d=0,e=b.length;e>d;d++)ga(a,b[d],c);return c}function va(a,b,c,d,e){for(var f,g=[],h=0,i=a.length,j=null!=b;i>h;h++)(f=a[h])&&(!c||c(f,d,e))&&(g.push(f),j&&b.push(h));return g}function wa(a,b,c,d,e,f){return d&&!d[u]&&(d=wa(d)),e&&!e[u]&&(e=wa(e,f)),ia(function(f,g,h,i){var j,k,l,m=[],n=[],o=g.length,p=f||ua(b||"*",h.nodeType?[h]:h,[]),q=!a||!f&&b?p:va(p,m,a,h,i),r=c?e||(f?a:o||d)?[]:g:q;if(c&&c(q,r,h,i),d){j=va(r,n),d(j,[],h,i),k=j.length;while(k--)(l=j[k])&&(r[n[k]]=!(q[n[k]]=l))}if(f){if(e||a){if(e){j=[],k=r.length;while(k--)(l=r[k])&&j.push(q[k]=l);e(null,r=[],j,i)}k=r.length;while(k--)(l=r[k])&&(j=e?J(f,l):m[k])>-1&&(f[j]=!(g[j]=l))}}else r=va(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):H.apply(g,r)})}function xa(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=sa(function(a){return a===b},h,!0),l=sa(function(a){return J(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];f>i;i++)if(c=d.relative[a[i].type])m=[sa(ta(m),c)];else{if(c=d.filter[a[i].type].apply(null,a[i].matches),c[u]){for(e=++i;f>e;e++)if(d.relative[a[e].type])break;return wa(i>1&&ta(m),i>1&&ra(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(R,"$1"),c,e>i&&xa(a.slice(i,e)),f>e&&xa(a=a.slice(e)),f>e&&ra(a))}m.push(c)}return ta(m)}function ya(a,b){var c=b.length>0,e=a.length>0,f=function(f,g,h,i,k){var l,m,o,p=0,q="0",r=f&&[],s=[],t=j,u=f||e&&d.find.TAG("*",k),v=w+=null==t?1:Math.random()||.1,x=u.length;for(k&&(j=g!==n&&g);q!==x&&null!=(l=u[q]);q++){if(e&&l){m=0;while(o=a[m++])if(o(l,g,h)){i.push(l);break}k&&(w=v)}c&&((l=!o&&l)&&p--,f&&r.push(l))}if(p+=q,c&&q!==p){m=0;while(o=b[m++])o(r,s,g,h);if(f){if(p>0)while(q--)r[q]||s[q]||(s[q]=F.call(i));s=va(s)}H.apply(i,s),k&&!f&&s.length>0&&p+b.length>1&&ga.uniqueSort(i)}return k&&(w=v,j=t),r};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=xa(b[c]),f[u]?d.push(f):e.push(f);f=A(a,ya(e,d)),f.selector=a}return f},i=ga.select=function(a,b,e,f){var i,j,k,l,m,n="function"==typeof a&&a,o=!f&&g(a=n.selector||a);if(e=e||[],1===o.length){if(j=o[0]=o[0].slice(0),j.length>2&&"ID"===(k=j[0]).type&&c.getById&&9===b.nodeType&&p&&d.relative[j[1].type]){if(b=(d.find.ID(k.matches[0].replace(ca,da),b)||[])[0],!b)return e;n&&(b=b.parentNode),a=a.slice(j.shift().value.length)}i=X.needsContext.test(a)?0:j.length;while(i--){if(k=j[i],d.relative[l=k.type])break;if((m=d.find[l])&&(f=m(k.matches[0].replace(ca,da),aa.test(j[0].type)&&pa(b.parentNode)||b))){if(j.splice(i,1),a=f.length&&ra(j),!a)return H.apply(e,f),e;break}}}return(n||h(a,o))(f,b,!p,e,aa.test(a)&&pa(b.parentNode)||b),e},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("div"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){return c?void 0:a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){return c||"input"!==a.nodeName.toLowerCase()?void 0:a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(K,function(a,b,c){var d;return c?void 0:a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);n.find=t,n.expr=t.selectors,n.expr[":"]=n.expr.pseudos,n.unique=t.uniqueSort,n.text=t.getText,n.isXMLDoc=t.isXML,n.contains=t.contains;var u=n.expr.match.needsContext,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^.[^:#\[\.,]*$/;function x(a,b,c){if(n.isFunction(b))return n.grep(a,function(a,d){return!!b.call(a,d,a)!==c});if(b.nodeType)return n.grep(a,function(a){return a===b!==c});if("string"==typeof b){if(w.test(b))return n.filter(b,a,c);b=n.filter(b,a)}return n.grep(a,function(a){return g.call(b,a)>=0!==c})}n.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?n.find.matchesSelector(d,a)?[d]:[]:n.find.matches(a,n.grep(b,function(a){return 1===a.nodeType}))},n.fn.extend({find:function(a){var b,c=this.length,d=[],e=this;if("string"!=typeof a)return this.pushStack(n(a).filter(function(){for(b=0;c>b;b++)if(n.contains(e[b],this))return!0}));for(b=0;c>b;b++)n.find(a,e[b],d);return d=this.pushStack(c>1?n.unique(d):d),d.selector=this.selector?this.selector+" "+a:a,d},filter:function(a){return this.pushStack(x(this,a||[],!1))},not:function(a){return this.pushStack(x(this,a||[],!0))},is:function(a){return!!x(this,"string"==typeof a&&u.test(a)?n(a):a||[],!1).length}});var y,z=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,A=n.fn.init=function(a,b){var c,d;if(!a)return this;if("string"==typeof a){if(c="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:z.exec(a),!c||!c[1]&&b)return!b||b.jquery?(b||y).find(a):this.constructor(b).find(a);if(c[1]){if(b=b instanceof n?b[0]:b,n.merge(this,n.parseHTML(c[1],b&&b.nodeType?b.ownerDocument||b:l,!0)),v.test(c[1])&&n.isPlainObject(b))for(c in b)n.isFunction(this[c])?this[c](b[c]):this.attr(c,b[c]);return this}return d=l.getElementById(c[2]),d&&d.parentNode&&(this.length=1,this[0]=d),this.context=l,this.selector=a,this}return a.nodeType?(this.context=this[0]=a,this.length=1,this):n.isFunction(a)?"undefined"!=typeof y.ready?y.ready(a):a(n):(void 0!==a.selector&&(this.selector=a.selector,this.context=a.context),n.makeArray(a,this))};A.prototype=n.fn,y=n(l);var B=/^(?:parents|prev(?:Until|All))/,C={children:!0,contents:!0,next:!0,prev:!0};n.extend({dir:function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&n(a).is(c))break;d.push(a)}return d},sibling:function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c}}),n.fn.extend({has:function(a){var b=n(a,this),c=b.length;return this.filter(function(){for(var a=0;c>a;a++)if(n.contains(this,b[a]))return!0})},closest:function(a,b){for(var c,d=0,e=this.length,f=[],g=u.test(a)||"string"!=typeof a?n(a,b||this.context):0;e>d;d++)for(c=this[d];c&&c!==b;c=c.parentNode)if(c.nodeType<11&&(g?g.index(c)>-1:1===c.nodeType&&n.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?n.unique(f):f)},index:function(a){return a?"string"==typeof a?g.call(n(a),this[0]):g.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(n.unique(n.merge(this.get(),n(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function D(a,b){while((a=a[b])&&1!==a.nodeType);return a}n.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return n.dir(a,"parentNode")},parentsUntil:function(a,b,c){return n.dir(a,"parentNode",c)},next:function(a){return D(a,"nextSibling")},prev:function(a){return D(a,"previousSibling")},nextAll:function(a){return n.dir(a,"nextSibling")},prevAll:function(a){return n.dir(a,"previousSibling")},nextUntil:function(a,b,c){return n.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return n.dir(a,"previousSibling",c)},siblings:function(a){return n.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return n.sibling(a.firstChild)},contents:function(a){return a.contentDocument||n.merge([],a.childNodes)}},function(a,b){n.fn[a]=function(c,d){var e=n.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=n.filter(d,e)),this.length>1&&(C[a]||n.unique(e),B.test(a)&&e.reverse()),this.pushStack(e)}});var E=/\S+/g,F={};function G(a){var b=F[a]={};return n.each(a.match(E)||[],function(a,c){b[c]=!0}),b}n.Callbacks=function(a){a="string"==typeof a?F[a]||G(a):n.extend({},a);var b,c,d,e,f,g,h=[],i=!a.once&&[],j=function(l){for(b=a.memory&&l,c=!0,g=e||0,e=0,f=h.length,d=!0;h&&f>g;g++)if(h[g].apply(l[0],l[1])===!1&&a.stopOnFalse){b=!1;break}d=!1,h&&(i?i.length&&j(i.shift()):b?h=[]:k.disable())},k={add:function(){if(h){var c=h.length;!function g(b){n.each(b,function(b,c){var d=n.type(c);"function"===d?a.unique&&k.has(c)||h.push(c):c&&c.length&&"string"!==d&&g(c)})}(arguments),d?f=h.length:b&&(e=c,j(b))}return this},remove:function(){return h&&n.each(arguments,function(a,b){var c;while((c=n.inArray(b,h,c))>-1)h.splice(c,1),d&&(f>=c&&f--,g>=c&&g--)}),this},has:function(a){return a?n.inArray(a,h)>-1:!(!h||!h.length)},empty:function(){return h=[],f=0,this},disable:function(){return h=i=b=void 0,this},disabled:function(){return!h},lock:function(){return i=void 0,b||k.disable(),this},locked:function(){return!i},fireWith:function(a,b){return!h||c&&!i||(b=b||[],b=[a,b.slice?b.slice():b],d?i.push(b):j(b)),this},fire:function(){return k.fireWith(this,arguments),this},fired:function(){return!!c}};return k},n.extend({Deferred:function(a){var b=[["resolve","done",n.Callbacks("once memory"),"resolved"],["reject","fail",n.Callbacks("once memory"),"rejected"],["notify","progress",n.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return n.Deferred(function(c){n.each(b,function(b,f){var g=n.isFunction(a[b])&&a[b];e[f[1]](function(){var a=g&&g.apply(this,arguments);a&&n.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f[0]+"With"](this===d?c.promise():this,g?[a]:arguments)})}),a=null}).promise()},promise:function(a){return null!=a?n.extend(a,d):d}},e={};return d.pipe=d.then,n.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[1^a][2].disable,b[2][2].lock),e[f[0]]=function(){return e[f[0]+"With"](this===e?d:this,arguments),this},e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=d.call(arguments),e=c.length,f=1!==e||a&&n.isFunction(a.promise)?e:0,g=1===f?a:n.Deferred(),h=function(a,b,c){return function(e){b[a]=this,c[a]=arguments.length>1?d.call(arguments):e,c===i?g.notifyWith(b,c):--f||g.resolveWith(b,c)}},i,j,k;if(e>1)for(i=new Array(e),j=new Array(e),k=new Array(e);e>b;b++)c[b]&&n.isFunction(c[b].promise)?c[b].promise().done(h(b,k,c)).fail(g.reject).progress(h(b,j,i)):--f;return f||g.resolveWith(k,c),g.promise()}});var H;n.fn.ready=function(a){return n.ready.promise().done(a),this},n.extend({isReady:!1,readyWait:1,holdReady:function(a){a?n.readyWait++:n.ready(!0)},ready:function(a){(a===!0?--n.readyWait:n.isReady)||(n.isReady=!0,a!==!0&&--n.readyWait>0||(H.resolveWith(l,[n]),n.fn.triggerHandler&&(n(l).triggerHandler("ready"),n(l).off("ready"))))}});function I(){l.removeEventListener("DOMContentLoaded",I,!1),a.removeEventListener("load",I,!1),n.ready()}n.ready.promise=function(b){return H||(H=n.Deferred(),"complete"===l.readyState?setTimeout(n.ready):(l.addEventListener("DOMContentLoaded",I,!1),a.addEventListener("load",I,!1))),H.promise(b)},n.ready.promise();var J=n.access=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===n.type(c)){e=!0;for(h in c)n.access(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,n.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(n(a),c)})),b))for(;i>h;h++)b(a[h],c,g?d:d.call(a[h],h,b(a[h],c)));return e?a:j?b.call(a):i?b(a[0],c):f};n.acceptData=function(a){return 1===a.nodeType||9===a.nodeType||!+a.nodeType};function K(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=n.expando+K.uid++}K.uid=1,K.accepts=n.acceptData,K.prototype={key:function(a){if(!K.accepts(a))return 0;var b={},c=a[this.expando];if(!c){c=K.uid++;try{b[this.expando]={value:c},Object.defineProperties(a,b)}catch(d){b[this.expando]=c,n.extend(a,b)}}return this.cache[c]||(this.cache[c]={}),c},set:function(a,b,c){var d,e=this.key(a),f=this.cache[e];if("string"==typeof b)f[b]=c;else if(n.isEmptyObject(f))n.extend(this.cache[e],b);else for(d in b)f[d]=b[d];return f},get:function(a,b){var c=this.cache[this.key(a)];return void 0===b?c:c[b]},access:function(a,b,c){var d;return void 0===b||b&&"string"==typeof b&&void 0===c?(d=this.get(a,b),void 0!==d?d:this.get(a,n.camelCase(b))):(this.set(a,b,c),void 0!==c?c:b)},remove:function(a,b){var c,d,e,f=this.key(a),g=this.cache[f];if(void 0===b)this.cache[f]={};else{n.isArray(b)?d=b.concat(b.map(n.camelCase)):(e=n.camelCase(b),b in g?d=[b,e]:(d=e,d=d in g?[d]:d.match(E)||[])),c=d.length;while(c--)delete g[d[c]]}},hasData:function(a){return!n.isEmptyObject(this.cache[a[this.expando]]||{})},discard:function(a){a[this.expando]&&delete this.cache[a[this.expando]]}};var L=new K,M=new K,N=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,O=/([A-Z])/g;function P(a,b,c){var d;if(void 0===c&&1===a.nodeType)if(d="data-"+b.replace(O,"-$1").toLowerCase(),c=a.getAttribute(d),"string"==typeof c){try{c="true"===c?!0:"false"===c?!1:"null"===c?null:+c+""===c?+c:N.test(c)?n.parseJSON(c):c}catch(e){}M.set(a,b,c)}else c=void 0;return c}n.extend({hasData:function(a){return M.hasData(a)||L.hasData(a)},data:function(a,b,c){ +return M.access(a,b,c)},removeData:function(a,b){M.remove(a,b)},_data:function(a,b,c){return L.access(a,b,c)},_removeData:function(a,b){L.remove(a,b)}}),n.fn.extend({data:function(a,b){var c,d,e,f=this[0],g=f&&f.attributes;if(void 0===a){if(this.length&&(e=M.get(f),1===f.nodeType&&!L.get(f,"hasDataAttrs"))){c=g.length;while(c--)g[c]&&(d=g[c].name,0===d.indexOf("data-")&&(d=n.camelCase(d.slice(5)),P(f,d,e[d])));L.set(f,"hasDataAttrs",!0)}return e}return"object"==typeof a?this.each(function(){M.set(this,a)}):J(this,function(b){var c,d=n.camelCase(a);if(f&&void 0===b){if(c=M.get(f,a),void 0!==c)return c;if(c=M.get(f,d),void 0!==c)return c;if(c=P(f,d,void 0),void 0!==c)return c}else this.each(function(){var c=M.get(this,d);M.set(this,d,b),-1!==a.indexOf("-")&&void 0!==c&&M.set(this,a,b)})},null,b,arguments.length>1,null,!0)},removeData:function(a){return this.each(function(){M.remove(this,a)})}}),n.extend({queue:function(a,b,c){var d;return a?(b=(b||"fx")+"queue",d=L.get(a,b),c&&(!d||n.isArray(c)?d=L.access(a,b,n.makeArray(c)):d.push(c)),d||[]):void 0},dequeue:function(a,b){b=b||"fx";var c=n.queue(a,b),d=c.length,e=c.shift(),f=n._queueHooks(a,b),g=function(){n.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return L.get(a,c)||L.access(a,c,{empty:n.Callbacks("once memory").add(function(){L.remove(a,[b+"queue",c])})})}}),n.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.lengthx",k.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var U="undefined";k.focusinBubbles="onfocusin"in a;var V=/^key/,W=/^(?:mouse|pointer|contextmenu)|click/,X=/^(?:focusinfocus|focusoutblur)$/,Y=/^([^.]*)(?:\.(.+)|)$/;function Z(){return!0}function $(){return!1}function _(){try{return l.activeElement}catch(a){}}n.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.get(a);if(r){c.handler&&(f=c,c=f.handler,e=f.selector),c.guid||(c.guid=n.guid++),(i=r.events)||(i=r.events={}),(g=r.handle)||(g=r.handle=function(b){return typeof n!==U&&n.event.triggered!==b.type?n.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(E)||[""],j=b.length;while(j--)h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o&&(l=n.event.special[o]||{},o=(e?l.delegateType:l.bindType)||o,l=n.event.special[o]||{},k=n.extend({type:o,origType:q,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&n.expr.match.needsContext.test(e),namespace:p.join(".")},f),(m=i[o])||(m=i[o]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,p,g)!==!1||a.addEventListener&&a.addEventListener(o,g,!1)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),n.event.global[o]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,o,p,q,r=L.hasData(a)&&L.get(a);if(r&&(i=r.events)){b=(b||"").match(E)||[""],j=b.length;while(j--)if(h=Y.exec(b[j])||[],o=q=h[1],p=(h[2]||"").split(".").sort(),o){l=n.event.special[o]||{},o=(d?l.delegateType:l.bindType)||o,m=i[o]||[],h=h[2]&&new RegExp("(^|\\.)"+p.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&q!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,p,r.handle)!==!1||n.removeEvent(a,o,r.handle),delete i[o])}else for(o in i)n.event.remove(a,o+b[j],c,d,!0);n.isEmptyObject(i)&&(delete r.handle,L.remove(a,"events"))}},trigger:function(b,c,d,e){var f,g,h,i,k,m,o,p=[d||l],q=j.call(b,"type")?b.type:b,r=j.call(b,"namespace")?b.namespace.split("."):[];if(g=h=d=d||l,3!==d.nodeType&&8!==d.nodeType&&!X.test(q+n.event.triggered)&&(q.indexOf(".")>=0&&(r=q.split("."),q=r.shift(),r.sort()),k=q.indexOf(":")<0&&"on"+q,b=b[n.expando]?b:new n.Event(q,"object"==typeof b&&b),b.isTrigger=e?2:3,b.namespace=r.join("."),b.namespace_re=b.namespace?new RegExp("(^|\\.)"+r.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=d),c=null==c?[b]:n.makeArray(c,[b]),o=n.event.special[q]||{},e||!o.trigger||o.trigger.apply(d,c)!==!1)){if(!e&&!o.noBubble&&!n.isWindow(d)){for(i=o.delegateType||q,X.test(i+q)||(g=g.parentNode);g;g=g.parentNode)p.push(g),h=g;h===(d.ownerDocument||l)&&p.push(h.defaultView||h.parentWindow||a)}f=0;while((g=p[f++])&&!b.isPropagationStopped())b.type=f>1?i:o.bindType||q,m=(L.get(g,"events")||{})[b.type]&&L.get(g,"handle"),m&&m.apply(g,c),m=k&&g[k],m&&m.apply&&n.acceptData(g)&&(b.result=m.apply(g,c),b.result===!1&&b.preventDefault());return b.type=q,e||b.isDefaultPrevented()||o._default&&o._default.apply(p.pop(),c)!==!1||!n.acceptData(d)||k&&n.isFunction(d[q])&&!n.isWindow(d)&&(h=d[k],h&&(d[k]=null),n.event.triggered=q,d[q](),n.event.triggered=void 0,h&&(d[k]=h)),b.result}},dispatch:function(a){a=n.event.fix(a);var b,c,e,f,g,h=[],i=d.call(arguments),j=(L.get(this,"events")||{})[a.type]||[],k=n.event.special[a.type]||{};if(i[0]=a,a.delegateTarget=this,!k.preDispatch||k.preDispatch.call(this,a)!==!1){h=n.event.handlers.call(this,a,j),b=0;while((f=h[b++])&&!a.isPropagationStopped()){a.currentTarget=f.elem,c=0;while((g=f.handlers[c++])&&!a.isImmediatePropagationStopped())(!a.namespace_re||a.namespace_re.test(g.namespace))&&(a.handleObj=g,a.data=g.data,e=((n.event.special[g.origType]||{}).handle||g.handler).apply(f.elem,i),void 0!==e&&(a.result=e)===!1&&(a.preventDefault(),a.stopPropagation()))}return k.postDispatch&&k.postDispatch.call(this,a),a.result}},handlers:function(a,b){var c,d,e,f,g=[],h=b.delegateCount,i=a.target;if(h&&i.nodeType&&(!a.button||"click"!==a.type))for(;i!==this;i=i.parentNode||this)if(i.disabled!==!0||"click"!==a.type){for(d=[],c=0;h>c;c++)f=b[c],e=f.selector+" ",void 0===d[e]&&(d[e]=f.needsContext?n(e,this).index(i)>=0:n.find(e,this,null,[i]).length),d[e]&&d.push(f);d.length&&g.push({elem:i,handlers:d})}return h]*)\/>/gi,ba=/<([\w:]+)/,ca=/<|&#?\w+;/,da=/<(?:script|style|link)/i,ea=/checked\s*(?:[^=]|=\s*.checked.)/i,fa=/^$|\/(?:java|ecma)script/i,ga=/^true\/(.*)/,ha=/^\s*\s*$/g,ia={option:[1,""],thead:[1,"","
              "],col:[2,"","
              "],tr:[2,"","
              "],td:[3,"","
              "],_default:[0,"",""]};ia.optgroup=ia.option,ia.tbody=ia.tfoot=ia.colgroup=ia.caption=ia.thead,ia.th=ia.td;function ja(a,b){return n.nodeName(a,"table")&&n.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a.appendChild(a.ownerDocument.createElement("tbody")):a}function ka(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function la(a){var b=ga.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function ma(a,b){for(var c=0,d=a.length;d>c;c++)L.set(a[c],"globalEval",!b||L.get(b[c],"globalEval"))}function na(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(L.hasData(a)&&(f=L.access(a),g=L.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;d>c;c++)n.event.add(b,e,j[e][c])}M.hasData(a)&&(h=M.access(a),i=n.extend({},h),M.set(b,i))}}function oa(a,b){var c=a.getElementsByTagName?a.getElementsByTagName(b||"*"):a.querySelectorAll?a.querySelectorAll(b||"*"):[];return void 0===b||b&&n.nodeName(a,b)?n.merge([a],c):c}function pa(a,b){var c=b.nodeName.toLowerCase();"input"===c&&T.test(a.type)?b.checked=a.checked:("input"===c||"textarea"===c)&&(b.defaultValue=a.defaultValue)}n.extend({clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=n.contains(a.ownerDocument,a);if(!(k.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||n.isXMLDoc(a)))for(g=oa(h),f=oa(a),d=0,e=f.length;e>d;d++)pa(f[d],g[d]);if(b)if(c)for(f=f||oa(a),g=g||oa(h),d=0,e=f.length;e>d;d++)na(f[d],g[d]);else na(a,h);return g=oa(h,"script"),g.length>0&&ma(g,!i&&oa(a,"script")),h},buildFragment:function(a,b,c,d){for(var e,f,g,h,i,j,k=b.createDocumentFragment(),l=[],m=0,o=a.length;o>m;m++)if(e=a[m],e||0===e)if("object"===n.type(e))n.merge(l,e.nodeType?[e]:e);else if(ca.test(e)){f=f||k.appendChild(b.createElement("div")),g=(ba.exec(e)||["",""])[1].toLowerCase(),h=ia[g]||ia._default,f.innerHTML=h[1]+e.replace(aa,"<$1>")+h[2],j=h[0];while(j--)f=f.lastChild;n.merge(l,f.childNodes),f=k.firstChild,f.textContent=""}else l.push(b.createTextNode(e));k.textContent="",m=0;while(e=l[m++])if((!d||-1===n.inArray(e,d))&&(i=n.contains(e.ownerDocument,e),f=oa(k.appendChild(e),"script"),i&&ma(f),c)){j=0;while(e=f[j++])fa.test(e.type||"")&&c.push(e)}return k},cleanData:function(a){for(var b,c,d,e,f=n.event.special,g=0;void 0!==(c=a[g]);g++){if(n.acceptData(c)&&(e=c[L.expando],e&&(b=L.cache[e]))){if(b.events)for(d in b.events)f[d]?n.event.remove(c,d):n.removeEvent(c,d,b.handle);L.cache[e]&&delete L.cache[e]}delete M.cache[c[M.expando]]}}}),n.fn.extend({text:function(a){return J(this,function(a){return void 0===a?n.text(this):this.empty().each(function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=a)})},null,a,arguments.length)},append:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.appendChild(a)}})},prepend:function(){return this.domManip(arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=ja(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return this.domManip(arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},remove:function(a,b){for(var c,d=a?n.filter(a,this):this,e=0;null!=(c=d[e]);e++)b||1!==c.nodeType||n.cleanData(oa(c)),c.parentNode&&(b&&n.contains(c.ownerDocument,c)&&ma(oa(c,"script")),c.parentNode.removeChild(c));return this},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(n.cleanData(oa(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null==a?!1:a,b=null==b?a:b,this.map(function(){return n.clone(this,a,b)})},html:function(a){return J(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!da.test(a)&&!ia[(ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(aa,"<$1>");try{for(;d>c;c++)b=this[c]||{},1===b.nodeType&&(n.cleanData(oa(b,!1)),b.innerHTML=a);b=0}catch(e){}}b&&this.empty().append(a)},null,a,arguments.length)},replaceWith:function(){var a=arguments[0];return this.domManip(arguments,function(b){a=this.parentNode,n.cleanData(oa(this)),a&&a.replaceChild(b,this)}),a&&(a.length||a.nodeType)?this:this.remove()},detach:function(a){return this.remove(a,!0)},domManip:function(a,b){a=e.apply([],a);var c,d,f,g,h,i,j=0,l=this.length,m=this,o=l-1,p=a[0],q=n.isFunction(p);if(q||l>1&&"string"==typeof p&&!k.checkClone&&ea.test(p))return this.each(function(c){var d=m.eq(c);q&&(a[0]=p.call(this,c,d.html())),d.domManip(a,b)});if(l&&(c=n.buildFragment(a,this[0].ownerDocument,!1,this),d=c.firstChild,1===c.childNodes.length&&(c=d),d)){for(f=n.map(oa(c,"script"),ka),g=f.length;l>j;j++)h=c,j!==o&&(h=n.clone(h,!0,!0),g&&n.merge(f,oa(h,"script"))),b.call(this[j],h,j);if(g)for(i=f[f.length-1].ownerDocument,n.map(f,la),j=0;g>j;j++)h=f[j],fa.test(h.type||"")&&!L.access(h,"globalEval")&&n.contains(i,h)&&(h.src?n._evalUrl&&n._evalUrl(h.src):n.globalEval(h.textContent.replace(ha,"")))}return this}}),n.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){n.fn[a]=function(a){for(var c,d=[],e=n(a),g=e.length-1,h=0;g>=h;h++)c=h===g?this:this.clone(!0),n(e[h])[b](c),f.apply(d,c.get());return this.pushStack(d)}});var qa,ra={};function sa(b,c){var d,e=n(c.createElement(b)).appendTo(c.body),f=a.getDefaultComputedStyle&&(d=a.getDefaultComputedStyle(e[0]))?d.display:n.css(e[0],"display");return e.detach(),f}function ta(a){var b=l,c=ra[a];return c||(c=sa(a,b),"none"!==c&&c||(qa=(qa||n(""; + recommendedWidth = 640 / 2; + recommendedHeight = 390 / 2; + sourceLink = embedUri; + extraViewClasses = "external-embed"; + } else if (data.match(/http/) && data.replace(/[^<]/g, "").length < 3) { + + youtubeMatcher = /youtube\.com\/.*v=([^&<]+)/; + youtubeMatcher2 = /youtu\.be\/([^&<]+)/; + soundcloudMatcher = /soundcloud\.com\/([^<]+)/; + vimeoMatcher = /vimeo.com\/([^<]*)/; + dailyMotionMatcher = /dailymotion.com\/video\/([^<]*)/; + googleMapsMatcher = /google.com\/maps\?([^<]*)/; + spacedeckMatcher = new RegExp(location.host + "\/(spaces|folders)\/([0-9a-f]{24})"); + + if (m = data.match(youtubeMatcher) || (m = data.match(youtubeMatcher2))) { + videoId = m[1]; + html = ""; + recommendedWidth = 640 / 2; + recommendedHeight = 390 / 2; + provider_name = "youtube"; + type = "video"; + + } else if (m = data.match(dailyMotionMatcher)) { + videoId = m[1]; + html = ""; + recommendedWidth = 536 / 2; + recommendedHeight = 302 / 2; + + provider_name = "dailymotion"; + type = "video"; + + } else if (m = data.match(vimeoMatcher)) { + videoId = m[1]; + html = ""; + recommendedWidth = 536 / 2; + recommendedHeight = 302 / 2; + + provider_name = "vimeo"; + type = "video"; + + } else if (m = data.match(soundcloudMatcher)) { + var scurl = "https://" + m[0]; + var url; + if (m[0].indexOf("soundcloud.com/player")>=0) { + url = "https://w." + m[0]; + } else { + url = "https://w.soundcloud.com/player/?url="+encodeURI(scurl); + } + + html = ""; + recommendedWidth = 720 / 2; + recommendedHeight = 184; + sourceLink = scurl; + + provider_name = "soundcloud"; + type = "audio"; + + } else if ((m = data.match(googleMapsMatcher))) { + mapsParams = m[1]; + html = ""; + recommendedWidth = 640 / 2; + recommendedHeight = 390 / 2; + + provider_name = "google"; + type = "map"; + } else if ((m = data.match(genericUriMatcher)) && !isDataFileUrl(m[1])) { + uri = m[1]; + grabUri = uri; + endPoint = "/api/webgrabber/" + (encodeURIComponent(btoa(grabUri))); + html = data.replace(uri, " "); + recommendedWidth = 300; + recommendedHeight = 300; + sourceLink = uri; + } else { + plainText = true; + } + } else { + plainText = true; + } + if (plainText) { + // replace links with clickable links + return null; + } + result = { + html: html, + thumbnail_width: recommendedWidth, + thumbnail_height: recommendedHeight, + type: type, + provider_name: provider_name, + url: sourceLink + }; + return result; +}; diff --git a/public/javascripts/locales.js b/public/javascripts/locales.js new file mode 100644 index 0000000..086769f --- /dev/null +++ b/public/javascripts/locales.js @@ -0,0 +1,946 @@ +window.locales = {}; +window.locales.en = {}; +window.locales.de = {}; +window.locales.fr = {}; +window.locales.en.translation = +{ + "ok": "OK", + "cancel": "Cancel", + "close": "Close", + "open": "Open", + "folder": "Folder", + "save": "Save", + "saved": "Saved", + "created": "created", + "duplicate": "Duplicate", + "delete": "Delete", + "remove": "Remove", + "set": "set", + "reset": "reset", + "thanks": "Thanks", + "share": "Share", + "signup": "Sign Up", + "login": "Log in", + "logout": "Log out", + "email": "Email Address", + "password": "Password", + "width": "Width", + "height": "Height", + "nick": "Name", + "role": "Role", + "members": "Members", + "actions": "Actions", + "or": "or", + "you": "you", + "via": "via", + "by": "by", + "zero": "Zero", + "page": "Page", + "new": "New", + "copy": "Copy", + "home": "Home", + "owner": "Owner", + "space": "Space", + "second": "Second", + "not_found": "Not Found.", + "untitled_space": "Untitled Space", + "untitled_folder": "Untitled Folder", + "untitled": "untitled", + "sure": "Are you sure?", + "specify": "Please Specify", + "confirm": "Please Confirm", + "signup_google": "Sign In with Google", + "error_unknown_email": "This email/password combination is unknown. Try login with Google.", + "error_password_confirmation": "The entered passwords don't match.", + "error_domain_blocked": "Your domain is blocked.", + "error_user_email_already_used": "This email address is already in use.", + "support": "Spacedeck Support", + "offline": "Offline. Click for more.", + "error": "Sorry, but something went wrong. Please contact support@spacedeck.com", + "welcome": "Welcome", + "claim": "Your digital Whiteboard.", + "trynow": "Try now.", + "about": "About us", + "terms": "Terms", + "contact": "Contact", + "privacy": "Privacy", + "business_adress": "Business Adress", + "post_adress": "Post Adress", + "phone": "Phone", + "ceo": "Managing Director", + "name": "Name", + "confirm_subject": "Spacedeck Email Confirmation", + "confirm_body": "Thank you for signing up at Spacedeck.\nPlease click on the following link to confirm your email address.\n", + "confirm_action": "Confirm Now", + "team_invite_membership_subject": "Team Invitation for %s", + "team_invite_membership_body": "You have been invited to %s on Spacedeck. Please click on the following link to accept the invitation.", + "team_invite_user_body": "You have been invited to %s on Spacedeck.\nYour temporary password is \"%s\".\nPlease click on the following link to accept the invitation.", + "team_invite_admin_body": "%s was invited for your team: %s. The temporary password is \"%s\".", + "team_invite_membership_acction": "Accept", + "team_new_member_subject": "New Team Member for %s signed up", + "team_new_member_body": "%s just joined Team %s on Spacedeck.", + "space_invite_membership_subject": "%s invited you to a Space %s ", + "space_invite_membership_body": "You have been invited by %s to join a Space %s on Spacedeck. Please click on the following link to accept the invitation.", + "space_invite_membership_action": "Accept", + "folder_invite_membership_subject": "Space", + "folder_invite_membership_body": "You have been invited to a Team on Spacedeck. Please click on the following link to accept the invitation.", + "folder_invite_membership_acction": "Accept", + "login_google": "Login With Google", + "save_changes": "Save Changes", + "upgrade": "Upgrade", + "upgrade_now": "Upgrade Now", + "create_space": "Create Space", + "create_folder": "Create Folder", + "email_unconfirmed": "Email Unconfirmed", + "confirmation_sent": "Email Sent", + "folder_filter": "Filter", + "sort_by": "Sort by", + "last_modified": "Last Modified", + "last_opened": "Last Opened", + "title": "Title", + "edit_team": "Edit Team", + "edit_account": "Edit Account", + "log_out": "Log Out", + "no_spaces_yet": "Welcome! You can create Spaces and Folders here using the buttons in the top left corner.", + "new_folder_title": "New title for folder", + "folder_settings": "Folder Settings", + "upload_cover_image": "Upload Cover Image", + "spacedeck_pro_ad_folders": "With Spacedeck Pro, you can organize an unlimited amount of Spaces in Folders and manage access controls for each Folder. Would you like to learn more about Pro features?", + "spacedeck_pro_ad_versions": "With Spacedeck Pro, you can save unlimited versions of each Space to track your progress or keep snapshots safe. Would you like to learn more about Pro features?", + "spacedeck_pro_ad_pdf": "With Spacedeck Pro, you can export your Spaces as crisp PDFs for archiving, mailing around, or printing. Do you want to learn more about Pro features?", + "spacedeck_pro_ad_zip": "With Spacedeck Pro, you can export the contents of a Space as a ZIP package. Do you want to learn more about Pro features?", + "spacedeck_pro_ad_colors": "With Spacedeck Pro, you can mix your own colors using a professional color picker.", + "profile_caption": "Profile", + "upload_avatar": "Upload Avatar", + "uploading_avatar": "Uploading Avatar…", + "avatar_dimensions": "Recommended dimensions: 200×200 pixels.", + "profile_name": "Name", + "profile_email": "Email Address", + "send_again": "Send Again", + "confirmation_sent_long": "Email confirmation link sent. Please check your inbox.", + "confirmation_sent_another": "Another confirmation link sent.", + "confirmation_sent_dialog_text": "We sent you an email explaining how to confirm your email address.", + "payment_caption": "Payment", + "language_caption": "Language", + "notifications_caption": "Notifications", + "notifications_option_chat": "Inform me via email about new comments", + "notifications_option_spaces": "Send me a daily digest of what happened in my Spaces and Folders", + "password_caption": "Password", + "current_password": "Current Password", + "new_password": "New Password", + "verify_password": "Verify Password", + "change_password": "Change Password", + "reset_password": "Reset Password", + "terminate_caption": "Delete Account", + "terminate_warning": "If you delete your account, all Spaces, Folders and Messages including all content you and other people created in your Spaces will be destroyed.", + "terminate_warning2": "This cannot be undone.", + "terminate_reason": "Message", + "terminate_reason_caption": "Help us improve by sharing your reasons for cancelling.", + "terminate_terminate": "Terminate", + "space_blank1": "Welcome to a fresh new Space!", + "space_blank2": "Drop files, paste links", + "space_blank3": "or use the tools below", + "space_blank4": "to fill this Space with content.", + "draft": "Draft", + "publish": "Publish", + "published": "Published", + "save_version": "Save Version", + "version_saved": "Version Saved", + "post": "Post Message", + "chat_invite_cta1": "Collaboration is fun!", + "chat_invite_cta2": "Why not ", + "chat_invite_cta3": "invite some people", + "chat_invite_cta4": "to work with you?", + "chat_message_placeholder": "Write your message…", + "view": "View", + "edit": "Edit", + "present": "Present", + "chat": "Chat", + "meta": "Meta", + "tool_search": "Search", + "tool_upload": "Upload", + "tool_text": "Text", + "tool_shape": "Shape", + "tool_zones": "Zones", + "tool_canvas": "Canvas", + "search_media": "Search media…", + "type_here": "Type here", + "text_formats": "Formats", + "format_p": "Paragraph", + "format_bullets": "Bullet List", + "format_numbers": "Numbered List", + "format_h1": "Headline 1", + "format_h2": "Headline 2", + "format_h3": "Headline 3", + "font_size": "Font Size", + "line_height": "Line Height", + "tool_align": "Align", + "tool_styles": "Styles", + "tool_bullets": "Bullets", + "tool_numbers": "Numbers", + "color_fill": "Fill", + "color_stroke": "Stroke", + "color_text": "Text", + "tool_type": "Type", + "tool_box": "Box", + "tool_link": "Link", + "tool_layout": "Layout", + "tool_options": "Options", + "tool_stroke": "Stroke", + "tool_delete": "Delete", + "tool_lock": "Lock", + "tool_copy": "Copy", + "stack": "Stack", + "tool_circle": "Circle", + "tool_hexagon": "Hexagon", + "tool_square": "Square", + "tool_diamond": "Diamond", + "tool_bubble": "Bubble", + "tool_cloud": "Cloud", + "tool_burst": "Burst", + "tool_star": "Star", + "tool_heart": "Heart", + "tool_scribble": "Scribble", + "tool_line": "Line", + "tool_arrow": "Arrow", + "search_media_placeholder": "Search web media…", + "add_zone": "New Zone", + "palette": "Palette", + "picker": "Picker", + "background_image_caption": "Image", + "background_color_caption": "Color", + "upload_background_caption": "Click to upload a background image", + "upload_background": "Upload Background", + "access_caption": "Access", + "versions_caption": "Versions", + "info_caption": "Info", + "mode_private": "Private: Only members can view or edit", + "mode_public": "Public: Anyone with the link can view", + "invite_collaborators": "Invite Collaborators", + "revoke_access": "Revoke Access", + "invite": "Send Invitations", + "invitee_email_address": "Email address of new member", + "optional_message": "Optional message", + "role_viewer": "Viewer", + "role_editor": "Editor", + "role_admin": "Admin", + "new_space_title": "New title for Space", + "team": "Team", + "search": "Search", + "search_no_results": "search_no_results", + "search_clear": "search_clear", + "rename": "Rename", + "mobile": "mobile", + "image": "image", + "tool_filter": "filter", + "canel": "canel", + "invite_membership_action": "invite_membership_action", + "viewer": "viewer", + "editor": "editor", + "admin": "admin", + "logging_in": "logging in", + "password_confirmation": "Password Confirmation", + "confirm_again": "We sent you an email explaining how to confirm your email address.", + "confirmed": "Your Account was confirmed successfully. Thank you.", + "signing_up": "Signing up", + "password_check_inbox": "Please check your inbox", + "new_space": "New Space", + "tool_more": "More", + "what_is_your_name": "Welcome to %s! Please choose a username.", + "lang": "en", + "landing_title": "Your Whiteboard on the Web.", + "landing_claim": "Spacedeck lets you easily combine all kinds of media on virtual whiteboards: Text notes, photos, web links, even videos and audio recordings. ", + "landing_example": "People use Spacedeck to organize their ideas, in teams to see whole projects at a glance, or in schools and universities for richer, connected learning experiences.", + "spaces": "My Spaces", + "access_editor_link": "Instant Edit Link", + "access_editor_link_desc": "Give this link to anyone who should be able to instantly edit this Space, no account required: ", + "access_anonymous_edit_blocking": "Anonymous Editors may only change their own items", + "access_current_members": "Current Members", + "access_new_members": "Invite New Members", + "access_no_members": "Members of this Space will show up here.", + "comments": "comments", + "landing_customers": "Trusted by Thousands.", + "landing_features_title": "A Breeze To Use.", + "landing_features_text": "The new Spacedeck 5 has a streamlined, beautiful user interface that makes your work easier and more fun than ever before – while giving you even more powerful features:", + "landing_features_1": "Drag & drop images, videos and audio from your computer or the web", + "landing_features_2": "Write and format text with full control over fonts, colors and style", + "landing_features_3": "Draw, annotate and highlight with included graphical shapes", + "landing_features_4": "Turn your board into a zooming presentation", + "landing_features_5": "Collaborate and chat in realtime with teammates, students or friends.", + "landing_features_6": "Share Spaces on the web or via email", + "landing_features_7": "Export your work as printable PDF or ZIP", + "landing_pricing": "Incredibly Affordable.", + "landing_pricing_lite": "Free/Personal Use", + "landing_pricing_lite_text": "The basic, well-rounded version for collecting pictures and keeping notes.", + "landing_pricing_pro_features_list": "
              • Unlimited Spaces
              • Folder Structures
              • PDF and ZIP Export
              • No Watermarks
              • Custom Backgrounds
              • Activity History
              • 20 GB Storage
                • ", + "landing_pricing_pro": "€4,90/User/Mo.
                  or 49,90/User/Year", + "landing_pricing_pro_text": "Turbocharged with all the power you expect.", + "landing_pricing_pro_features": "Turbocharged with all the power you expect.", + "welcome_subject": "Welcome to Spacedeck", + "welcome_body": "Hello!\nThank you for signing up at Spacedeck.
                  We hope you will enjoy working in Spaces.
                  Remember, your account includes unlimited collaborators. Feel free to share your Spaces with friends and colleagues all over the world.", + "invite_emails": "Email addresses (Comma separated)", + "history_recently_updated": "Recently Updated", + "history_recently_empty": "Nothing has happened yet.", + "parent_folder": "parent_folder", + "created_by": "Created by", + "last_updated": "Last updated", + "feedback_sent": "Thanks for your feedback!", + "role_member": "Member", + "team_invite_membership_action": "Accept invitation", + "space_message_subject": "New Message in Space %s", + "space_message_body": "%s wrote in %s: \n", + "pro_ad_history_headline": "When you upgrade to Spacedeck Pro, you will see a history of recent updates across all your (shared) Spaces here.", + "password_reset_subject": "Reset Password for Spacedeck", + "password_reset_body": "You requested a reset of your Spacedeck password.\nPlease click on the following link to set a new password.", + "password_reset_action": "Reset Now", + "was_offline": "The connection to Spacedeck was interrupted. If you have unsaved work, please keep this browser tab open until the connection is re-established, then touch the unsaved objects again.", + "subscription_failed_user_subject": "Problem with your Spacedeck Payment", + "subscription_failed_user_body": "Unfortunately, we could not process your Payment-method. You can easly create a new payment method including PayPal in your account settings.", + "subscription_failed_team_subject": "Problem with your Spacedeck Payment", + "subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.", + "team_name": "Team Name", + "subdomain": "Subdomain", + "team_adresses": "Email adresses", + "add": "add", + "invited": "invited", + "duplicate_destination": "Into which folder do you want to duplicate this Space?", + "duplicate_confirm": "Duplicate %s into %s?", + "duplicate_success": "%s was duplicated into %s.", + "goto_space": "Go to Space %s", + "goto_folder": "Go to Folder %s", + "stay_here": "Stay here", + "sharing": "sharing", + "list": "Export List", + "link": "link", + "download_space": "Download Space", + "type": "Type", + "download": "download", + "Previous Zone": "Previous Zone", + "Next Zone": "Next Zone", + "promote": "promote", + "demote": "demote" +} +window.locales.de.translation = +{ + "lang": "de", + "ok": "OK", + "cancel": "Abbrechen", + "close": "Schließen", + "open": "Öffnen", + "folder": "Ordner", + "duplicate": "Duplizieren", + "save": "Speichern", + "saved": "Gespeichert", + "created": "Erstellt", + "delete": "Löschen", + "remove": "Entfernen", + "set": "Übernehmen", + "reset": "Zurücksetzen", + "thanks": "Danke", + "share": "Teilen", + "signup": "Registrieren", + "login": "Anmelden", + "logout": "Abmelden", + "email": "E-Mail-Adresse", + "password": "Passwort", + "width": "Breite", + "height": "Höhe", + "nick": "Benutzername", + "role": "Rolle", + "members": "Mitglieder", + "actions": "Aktionen", + "or": "oder", + "you": "du", + "via": "via", + "by": "von", + "new": "Neu", + "zero": "Null", + "page": "Seite", + "copy": "Kopie", + "home": "Übersicht", + "owner": "Besitzer", + "space": "Space", + "second": "Sekunde", + "not_found": "Nicht Gefunden.", + "untitled_space": "Unbenannter Space", + "untitled_folder": "Unbenannter Order", + "untitled": "Unbenannter", + "sure": "Bist du sicher?", + "specify": "Bitte spezifiziere", + "confirm": "Bitte bestätige", + "signup_google": "Mit Google anmelden", + "error_unknown_email": "Unbekannte Kombination von Email und Passwort. Oder versuche dich mit Google anzumelden.", + "error_password_confirmation": "Die beiden Passwörter stimmen nicht überein.", + "error_domain_blocked": "Diese Domain ist gesperrt.", + "error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.", + "support": "Spacedeck-Support", + "offline": "Verbindungsverlust. Mehr Infos hier.", + "error": "Entschuldigung, etwas ist schiefgegangen. Bitte kontaktiere support@spacedeck.com", + "welcome": "Willkommen", + "claim": "Dein digitales Whiteboard.", + "trynow": "Jetzt probieren.", + "about": "Über uns", + "terms": "AGBs", + "contact": "Kontakt", + "privacy": "Privatsphäre", + "post_adress": "Postadresse", + "phone": "Phone", + "business_address": "business_address", + "ceo": "Geschäftsführer", + "business_adress": "business_adress", + "title": "Titel", + "name": "Name", + "confirm_subject": "E-Mail Bestätigung für Spacedeck", + "confirm_body": "Danke, dass du dich bei Spacedeck angemeldet hast.\nBitte klicke auf den folgenden Link, um deine E-Mail Adresse zu bestätigen.\n ", + "confirm_action": "E-Mail Bestätigen", + "team_invite_membership_subject": "Einladung zu %s auf Spacedeck", + "team_invite_membership_body": "Du wurdest zu %s auf Spacedeck eingeladen. \n Bitte klicke auf den folgenden Link, um die Einladung anzunehmen.", + "team_invite_user_body": "Du wurdest zu %s auf Spacedeck eingeladen. Dein temporäres Passwort ist \"%s\".\n Bitte klicke auf den folgenden Link, um die Einladung anzunehmen.", + "team_invite_admin_body": " %s wurde zu %s auf Spacedeck eingeladen. Das temporäres Passwort ist \"%s\".", + "team_invite_membership_action": "Annehmen", + "team_new_member_subject": "Neues Team Mitglied", + "team_new_member_body": "%s hat gerade seine Einladung zum Team %s angenommen.", + "space_invite_membership_subject": "Einladung von %s in Space %s", + "space_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.", + "space_invite_membership_action": "Annehmen", + "folder_invite_membership_subject": "Einladung von %s in Ordner %s", + "folder_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.", + "folder_invite_membership_action": "Accept", + "upgrade": "Upgrade", + "upgrade_now": "Jetzt Upgraden", + "create_space": "Space Erstellen", + "create_folder": "Ordner Erstellen", + "email_unconfirmed": "Email Unbestätigt", + "confirmation_sent": "Email Versandt", + "folder_filter": "Filter", + "sort_by": "Reihenfolge", + "last_modified": "Zuletzt Geändert", + "last_opened": "Zuletzt Geöffnet", + "edit_team": "Team Verwalten", + "edit_account": "Konto Bearbeiten", + "log_out": "Abmelden", + "no_spaces_yet": "Du hast noch keine Spaces erstellt.", + "new_folder_title": "Neuer Titel für Ordner", + "folder_settings": "Ordner-Einstellungen", + "upload_cover_image": "Ordnerbild Hochladen", + "spacedeck_pro_ad_folders": "Mit Spacedeck Pro kannst du beliebig viele Spaces in Ordnerstrukturen organisieren und für jeden Ordner Zugriffsrechte regeln. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_versions": "Mit Spacedeck Pro kannst du beliebig viele Versionen deines Spaces festhalten und später die Entwicklungsgeschichte nachvollziehen. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_pdf": "Mit Spacedeck Pro kannst du Spaces und sogar ganze Ordner als druckreife PDFs exportieren oder per Mail versenden. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_zip": "Mit Spacedeck Pro kannst du die Inhalte deiner Spaces jederzeit als ZIP-Paket exportieren. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_colors": "Spacedeck Pro enthält einen Profi-Farbmischer, mit dem du deine eigenen Farben mischen kannst.", + "profile_caption": "Profil", + "upload_avatar": "Profilbild Hochladen", + "uploading_avatar": "Profilbild wird hochgeladen…", + "avatar_dimensions": "Bestes Format: 200×200 Pixel.", + "profile_name": "Name", + "profile_email": "Email-Adresse", + "send_again": "Erneut Senden", + "confirmation_sent_long": "Email-Bestätigungslink versandt. Bitte überprüfe deine Mails.", + "confirmation_sent_another": "Wir haben eine weiteren Bestätigungslink versandt.", + "confirmation_sent_dialog_text": "Wir haben dir eine Email geschickt, die erklärt, wie das mit der Bestätigung läuft.", + "payment_caption": "Bezahlung", + "language_caption": "Sprache", + "notifications_caption": "Emails", + "notifications_option_chat": "Haltet mich über neue Kommentare auf dem Laufenden.", + "notifications_option_spaces": "Schickt mir täglich eine Zusammenfassung über Änderungen an meinen Spaces und Ordnern.", + "password_caption": "Passwort", + "current_password": "Altes Passwort", + "new_password": "Neues Passwort", + "verify_password": "Zur Sicherheit nochmal", + "change_password": "Passwort Ändern", + "reset_password": "Passwort Zurücksetzen", + "terminate_caption": "Kündigen", + "terminate_warning": "Wenn du kündigst, werden all deine Spaces, Ordner und Nachrichten und alle ihre Inhalte gelöscht.", + "terminate_warning2": "Das kann man nicht rückgängig machen.", + "terminate_reason": "Kündigungsgrund", + "terminate_reason_caption": "Wenn du uns mitteilst, was dich gestört hat, hilft uns das dabei ein besseres Produkt zu machen.", + "terminate_terminate": "Wirklich Kündigen", + "space_blank1": "Dies ist dein brandneuer, leerer Space!", + "space_blank2": "Wirf Dateien rein, paste Web-Links", + "space_blank3": "oder nutz die Werkzeuge da unten.", + "space_blank4": "Sei kreativ und tob dich aus!", + "draft": "Entwurf", + "publish": "Veröffentlichen", + "published": "Veröffentlicht", + "save_version": "Version Speichern", + "version_saved": "Version Gespeichert", + "post": "Abschicken", + "chat_invite_cta1": "Zusammen arbeiten macht Spaß!", + "chat_invite_cta2": "Warum ", + "chat_invite_cta3": "lädst du nicht ein paar Leute ein", + "chat_invite_cta4": "mit denen du dann zusammen arbeiten kannst?", + "chat_message_placeholder": "Schreib hier deine Nachricht…", + "view": "Ansicht", + "edit": "Bearb.", + "present": "Präse.", + "chat": "Chat", + "meta": "Teilen", + "tool_search": "Suche", + "tool_upload": "Upload", + "tool_text": "Text", + "tool_shape": "Grafik", + "tool_zones": "Zonen", + "tool_canvas": "Wand", + "search_media": "Medien im Web suchen…", + "type_here": "Hier was eingeben", + "text_formats": "Formate", + "format_p": "Absatz", + "format_bullets": "Bullet-Liste", + "format_numbers": "Nummerierte Liste", + "format_h1": "Überschrift 1", + "format_h2": "Überschrift 2", + "format_h3": "Überschrift 3", + "font_size": "Schriftgröße", + "line_height": "Zeilenhöhe", + "tool_align": "Bund", + "tool_styles": "Stil", + "tool_bullets": "Bullets", + "tool_numbers": "Zahlen", + "color_fill": "Füllung", + "color_stroke": "Strich", + "color_text": "Text", + "tool_type": "Typo", + "tool_box": "Box", + "tool_link": "Link", + "tool_layout": "Layout", + "tool_options": "Mehr", + "tool_stroke": "Strich", + "tool_delete": "Löschen", + "tool_lock": "Sperren", + "tool_copy": "Kopie", + "stack": "Anordnung", + "tool_circle": "Kreis", + "tool_hexagon": "Sechseck", + "tool_square": "Quadrat", + "tool_diamond": "Diamant", + "tool_bubble": "Blase", + "tool_cloud": "Wolke", + "tool_burst": "Burst", + "tool_star": "Stern", + "tool_heart": "Herz", + "tool_scribble": "Kritzeln", + "tool_line": "Linie", + "tool_arrow": "Pfeil", + "search_media_placeholder": "Online-Medien suchen…", + "add_zone": "Neue Zone", + "palette": "Palette", + "picker": "Mischen", + "background_image_caption": "Bild", + "background_color_caption": "Farbe", + "upload_background_caption": "Klicke hier, um ein Hintergrundbild hochzuladen.", + "upload_background": "Hintergrund Hochladen", + "access_caption": "Zugriff", + "versions_caption": "Versionen", + "info_caption": "Info", + "mode_private": "Privat: Nur Mitglieder können zugreifen", + "mode_public": "Öffentlich: Jede(r) mit Kenntnis des Links darf reinschauen", + "invite_collaborators": "Mitarbeiter Einladen", + "invitee_email_address": "Email-Adresse des neuen Mitglieds", + "optional_message": "Optionale Nachricht", + "revoke_access": "Zugriff Entfernen", + "invite": "Einladen", + "role_viewer": "Betrachter", + "role_editor": "Bearbeiter", + "role_admin": "Admin", + "new_space_title": "Neuer Titel für Space", + "logging_in": "logging_in", + "password_confirmation": "Passwort Wiederholung", + "confirm_again": "In deinem Postfach solltest du eine Bestätigungsmail finden. Bitte klicke auf den Link darin.", + "confirmed": "E-Mail Adresse wurde erfolgreich bestätigt. Danke!", + "password_check_inbox": "password_check_inbox", + "viewer": "Zuschauer", + "editor": "Bearbeiter", + "admin": "Admin", + "mobile": "Mobil", + "image": "Bild", + "tool_filter": "Filter", + "team": "Team", + "search": "Suche", + "search_no_results": "Keine Resultate", + "search_clear": "Zurücksetzen", + "rename": "Umbennen", + "login_google": "Mit Google anmelden", + "save_changes": "Änderungen speichern", + "what_is_your_name": "Willkommen bei %s ! Bitte wähle einen Benutzernamen.", + "landing_title": "Dein Online-Whiteboard.", + "landing_claim": "Mit Spacedeck kannst du multimedial auf virtuellen Whiteboards im Internet zusammenarbeiten: Kombiniere Texte, Fotos, Websites oder sogar Videos und Sounds. ", + "landing_example": "Spacedeck ist ideal, um Ideen zu visualisieren, in kreativen Teams Projekte zu überblicken oder um den Unterricht in Schulen und Universitäten interaktiv zu gestalten.", + "spaces": "Meine Spaces", + "access_editor_link": "Sofort-Mitmachen-Link", + "access_editor_link_desc": "Mit diesem Link kann man sogar ohne Spacedeck-Account sofort mitarbeiten. Praktisch!", + "access_anonymous_edit_blocking": "Anonyme Mitarbeiter dürfen keine Daten anderer anonymer Mitarbeiter ändern.", + "access_current_members": "Aktuelle Mitarbeiter", + "access_new_members": "Neue Mitarbeiter einladen", + "landing_customers": "Tausende Anwender weltweit vertrauen uns.", + "landing_features_title": "Schneller zum Ergebnis.", + "landing_features_text": "Spacedeck 5 hat eine brandneue Benutzeroberfläche, die das Arbeiten einfacher und intuitiver und macht - gleichzeitig aber auch mehr mächtige Werkzeuge bereitstellt.", + "landing_features_1": "Drag & Drop: Bilder-, Video- und Ton-Dateien direkt vom Desktop oder von anderen Webseiten in Spaces ziehen", + "landing_features_2": "Textnotizen mit allen Möglichkeiten bei Schriftart, Farbe und Stil", + "landing_features_3": "Zeichne und Markiere freihändig oder mit fertigen Formen", + "landing_features_4": "Verwandle dein Whiteboard in eine zoombare Präsentation", + "landing_features_5": "Arbeite in Echtzeit mit deinen Kollegen, Schülern oder Freunden zusammen", + "landing_features_6": "Teile deine Whiteboards per Link oder per E-Mail", + "landing_features_7": "Exportiere deine Arbeit als PDF- oder ZIP-Datei", + "landing_pricing": "Unfassbar günstig.", + "landing_pricing_lite": "Private Nutzung", + "landing_pricing_lite_text": "Basisvariante, ausreichend um multimedial zu arbeiten.", + "landing_pricing_pro_features_list": "
                  • Unbegrenzte Spaces
                  • Hierarchische Ordnerstruktur
                  • PDF und ZIP Export
                  • Keine Wasserzeichen
                  • Eigene Hintergründe
                  • Liste von Aktivitäten
                  • 20 GB Speicherplatz
                    • ", + "landing_pricing_pro": "€4,90/Anwender/Mo.
                      oder €49,90/Anwender/Jahr", + "landing_pricing_pro_text": "Alle Features um professionell zu arbeiten.", + "welcome_subject": "Willkommen bei Spacedeck", + "welcome_body": "Danke, dass du dich bei Spacedeck angemeldet hast.
                      Wir hoffen, du wirst viel Spaß mit Spacedeck haben.
                      Vergiss nicht, dass du mit unbegrenzt vielen Kollegen und Freunden kostenlos zusammen arbeiten kannst. ", + "parent_folder": "Übergeordneter Ordner", + "access_no_members": "Noch keine Mitglieder", + "invite_emails": "E-Mail Adressen", + "created_by": "Erstellt von", + "last_updated": "Zuletzt aktualisiert", + "comments": "Kommentare", + "history_recently_updated": "Aktuelles", + "history_recently_empty": "Noch nichts passiert.", + "signing_up": "Registierung läuft", + "feedback_sent": "Danke für dein Feedback!", + "role_member": "Mitglied", + "space_message_subject": "Neue Nachricht im Space %s", + "space_message_body": "%s schrieb in %s: \n", + "password_reset_subject": "Neues Passwort für Spacedeck", + "password_reset_body": "Du möchtest das Passwort für deinen Spacedeck Account zurücksetzen?\nBitte klicke dafür auf den folgenden Link:", + "password_reset_action": "Jetzt Passwort neu setzen", + "pro_ad_history_headline": "Nach einem Upgrade zu Spacedeck Pro kannst du hier einen Überblick über alle aktuellen Aktivitäten in Spaces bekommen.", + "was_offline": "Die Verbindung wurde unterbrochen. Wir empfehlen, alle geänderten Objekte erneut zu selektieren, um sie zu speichern.", + "subscription_failed_user_subject": "Zahlung fehlgeschlagen", + "subscription_failed_user_body": "Unfortunately, we could not process your payment method. You can easly create a new payment method including PayPal in your account settings.", + "subscription_failed_team_subject": "Zahlung fehlgeschlagen", + "subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.", + "add": "hinzufügen", + "team_name": "Team-Name", + "subdomain": "Subdomain", + "invited": "eingeladen", + "team_adresses": "E-Mail Adressen", + "duplicate_destination": "In welchen Ordner möchtest du den Space duplizieren?", + "duplicate_confirm": "%s nach %s duplizieren?", + "duplicate_success": "%s wurde in %s dupliziert.", + "goto_space": "Gehe zu %s", + "goto_folder": "Gehe zu Ordner %s", + "stay_here": "Hier bleiben", + "sharing": "sharing", + "list": "Liste", + "download_space": "Space Herunterladen", + "duplicate_destination_folder": "Zielordner für Duplikat", + "type": "Typ", + "promote": "Befördern", + "demote": "Zurückstufen", + "Previous Zone": "Vorherige Zone", + "Next Zone": "Nächste Zone" +} + +window.locales.fr.translation = +{ + "lang": "fr", + "ok": "OK", + "cancel": "Annuler", + "close": "Fermer", + "open": "Ouvrir", + "folder": "Dossier", + "save": "Enregistrer", + "saved": "Enregistrée", + "created": "établi", + "duplicate": "Dupliquer", + "delete": "Supprimer", + "remove": "Enlever", + "set": "Définir", + "reset": "Rédéfinir", + "thanks": "Merci", + "share": "Partager", + "signup": "S'inscrire", + "login": "Se connecter", + "logout": "Se déconnecter", + "email": "Adresse email", + "password": "Mot de passe", + "width": "Largeur", + "height": "Hauteur", + "nick": "Nom", + "role": "Rôle", + "members": "Membres", + "actions": "Actions", + "or": "ou", + "you": "vous", + "via": "par", + "by": "par", + "zero": "Zero", + "page": "Page", + "new": "Neuf", + "copy": "Copier", + "owner": "Possesseur", + "home": "Accueil", + "space": "Espace", + "second": "Seconde", + "not_found": "Pas trouvé.", + "untitled": "sans titre", + "untitled_space": "Espace sans titre", + "untitled_folder": "Dossier sans titre", + "sure": "Êtes-vous sûr?", + "specify": "Veuillez préciser:", + "confirm": "Veuillez confirmer", + "signup_google": "S'inscrire avec Google", + "error_unknown_email": "Combinaison inconnue de l'email et mot de passe. Ou essayer de signer avec Google.", + "error_password_confirmation": "Les deux mots de passe ne correspondent pas.", + "error_domain_blocked": "Ce domaine a été désactivé.", + "error_user_email_already_used": "Cette adresse email est déjà enregistré.", + "support": "Aide Spacedeck", + "offline": "Désolé , mais les serveurs Spacedeck ne peuvent pas être atteint pour le moment. Plus d' informations ici.", + "error": "Désolé, une erreur s'est produite. Veuillez contacter support@spacedeck.com", + "welcome": "Bienvenue", + "claim": "Le tableau blanc partagé pour tout le monde", + "trynow": "Essayez-le gratuitement", + "about": "de nous", + "terms": "termes", + "contact": "contact", + "privacy": "sphère privée", + "business_adress": "Siège social", + "post_adress": "Adresse courrier", + "phone": "téléphone", + "ceo": "Gestionnaire", + "name": "name", + "confirm_subject": "Confirmation de l'email Spacedeck", + "confirm_body": "Merci pour votre inscription à Spacedeck.\nSil vous plaît cliquez sur le lien suivant pour confirmer votre adresse e-mail.", + "confirm_action": "Confirmer", + "team_invite_membership_subject": "Team Invitation for %s", + "team_invite_membership_body": "You have been invited to %s on Spacedeck.\nPlease click on the following link to accept the invitation.", + "team_invite_user_body": "You have been invited to %s on Spacedeck.\nYour temporary password is \"%s\".\nPlease click on the following link to accept the invitation.", + "team_invite_admin_body": "%s was invited for your team: %s. The temporary password is \"%s\".", + "team_invite_membership_acction": "Accept", + "team_new_member_subject": "New Team Member", + "team_new_member_body": "%s just joined Team %s on Spacedeck.", + "invite_emails": "Entrer les adresses email (séparées pas des virgules)", + "optional_message": "Message personnel (facultatif)", + "space_invite_membership_subject": "Invitation Espace par %s: %s", + "space_invite_membership_body": "Vous avez été invité par %s à Espace \"%s\"", + "space_invite_membership_action": "Accepter L'invitation", + "folder_invite_membership_subject": "Space", + "folder_invite_membership_body": "You have been invited to a Team on Spacedeck. Please click on the following link to accept the invitation.", + "folder_invite_membership_acction": "Accept", + "login_google": "S'inscrire avec Google", + "save_changes": "Enregistrer", + "upgrade": "Upgrade", + "upgrade_now": "Mise à niveau", + "create_space": "Créer un espace", + "create_folder": "Créer un dossier", + "email_unconfirmed": "Email non confirmée", + "confirmation_sent": "L'email est envoyé.", + "folder_filter": "Filtre", + "sort_by": "Ordre", + "last_modified": "Dernière modification", + "last_opened": "Dernière ouverture", + "title": "Titre", + "edit_team": "Modifier l'équipe", + "edit_account": "Modifier le compte", + "log_out": "Déconnecter", + "no_spaces_yet": "Vous ne avez pas encore créé d'espaces.", + "new_folder_title": "Nouveau titre pour le dossier", + "folder_settings": "Paramètres du dossier", + "upload_cover_image": "Charger image de couverture", + "spacedeck_pro_ad_folders": "Avec Spacedeck Pro, vous pouvez organiser un nombre illimité de espaces dans les dossiers et gérer les contrôles d'accès pour chaque dossier. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_versions": "Avec Spacedeck Pro, vous pouvez enregistrer des versions illimitées de chaque espace pour suivre vos progrès ou de conserver des instantanés sécurité. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_pdf": "Avec Spacedeck Pro, vous pouvez exporter vos espaces et même des dossiers entiers belles PDF pour l'archivage, de diffusion, ou autour de l'impression. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_zip": "Avec Spacedeck Pro, vous pouvez exporter le contenu d'un espace comme un paquet ZIP. Voulez-vous en savoir plus sur les fonctionnalités Pro?", + "spacedeck_pro_ad_colors": "Avec Spacedeck Pro, vous pouvez mélanger vos propres couleurs en utilisant un sélecteur de couleur professionnelle.", + "profile_caption": "Profil", + "upload_avatar": "Télécharger l'image profil", + "uploading_avatar": "L'image de profil est téléchargée…", + "avatar_dimensions": "Format suggéré: 200×200 pixels.", + "profile_name": "Name", + "profile_email": "Email", + "send_again": "Renvoyer", + "confirmation_sent_long": "Lien de confirmation email envoyé. Se il vous plaît vérifier votre courrier.", + "confirmation_sent_another": "Nous avons envoyé un autre lien de confirmation.", + "confirmation_sent_dialog_text": "Nous vous avons envoyé un email expliquant comment confirmer votre adresse email.", + "payment_caption": "Paiement", + "language_caption": "Langue", + "notifications_caption": "Emails", + "notifications_option_chat": "Envoyez-moi les nouveaux commentaires par email.", + "notifications_option_spaces": "Envoyez-moi un résumé quotidien des modifications à mes espaces.", + "password_caption": "Mot de passe", + "current_password": "Ancien mot de passe", + "new_password": "Nouveau mot de passe", + "verify_password": "Répéter mot de passe", + "change_password": "Enregistrer", + "reset_password": "Mot de passe oublié?", + "terminate_caption": "Supprimer le compte", + "terminate_warning": "En supprimant votre compte, vos messages, espaces, dossiers et tout leur contenu seront effacés. Cette action ne peut être annulée.", + "terminate_warning2": "Cela ne peut pas être annulée.", + "terminate_reason": "Problèmes rencontrés", + "terminate_reason_caption": "Aidez-nous à améliorer le produit en précisant les raisons de la suppression de votre compte.", + "terminate_terminate": "Supprimer le compte définitivement?", + "space_blank1": "Ceci est votre nouvel espace.", + "space_blank2": "Déposez des fichiers, collez des liens web", + "space_blank3": "ou utilisez les outils.", + "space_blank4": "Soyez créatifs!", + "draft": "Conception", + "publish": "Publier", + "published": "Publié", + "save_version": "Enregistrer une version", + "version_saved": "Version enregistrée.", + "post": "Envoyer", + "chat_invite_cta1": "Travailler ensemble est amusant!", + "chat_invite_cta2": "Pourquoi ", + "chat_invite_cta3": "ne pas vous invitez quelques collaborateurs?", + "chat_invite_cta4": "", + "chat_message_placeholder": "Votre message ici…", + "view": "Vue", + "edit": "Éditer", + "present": "Prés.", + "chat": "Chat", + "meta": "Meta", + "tool_search": "Chercher", + "tool_upload": "Charger", + "tool_text": "Texte", + "tool_shape": "Dessin", + "tool_zones": "Zones", + "tool_canvas": "Mur", + "search_media": "Chercher le web pour les médias…", + "type_here": "Entrez quelque chose ici", + "text_formats": "Formats", + "format_p": "Paragraphe", + "format_bullets": "Liste à puces", + "format_numbers": "Liste numérotée", + "format_h1": "Titre 1", + "format_h2": "Titre 2", + "format_h3": "Titre 3", + "font_size": "Taille de la police", + "line_height": "Hauteur de ligne", + "tool_align": "Align", + "tool_styles": "Style", + "tool_bullets": "Puces", + "tool_numbers": "Numéros", + "color_fill": "Fond", + "color_stroke": "Ligne", + "color_text": "Text", + "tool_type": "Typo.", + "tool_box": "Box", + "tool_link": "Lien", + "tool_layout": "Layout", + "tool_options": "Plus", + "tool_stroke": "Ligne", + "tool_delete": "Effacer", + "tool_lock": "Bloquer", + "tool_copy": "Copie", + "stack": "Empiler", + "tool_circle": "Cercle", + "tool_hexagon": "Hexagone", + "tool_square": "Carré", + "tool_diamond": "Diamant", + "tool_bubble": "Bulle", + "tool_cloud": "Nuage", + "tool_burst": "Éclat", + "tool_star": "Étoile", + "tool_heart": "Cœur", + "tool_scribble": "Crayon", + "tool_line": "Ligne", + "tool_arrow": "Flèche", + "search_media_placeholder": "Chercher le web pour les médias…", + "add_zone": "Ajouter Zone", + "palette": "Palette", + "picker": "Mélange", + "background_image_caption": "Image", + "background_color_caption": "Couleur", + "upload_background_caption": "Cliquez ici pour télécharger une image de fond.", + "upload_background": "Télécharger", + "access_caption": "Accès", + "versions_caption": "Versions", + "info_caption": "Info", + "mode_private": "Privé", + "mode_public": "Public", + "invite_collaborators": "Inviter les collaborateurs", + "revoke_access": "Révoquer l'accès", + "invite": "Inviter", + "role_viewer": "Spectateur", + "role_editor": "Éditeur", + "role_admin": "Administrateur", + "new_space_title": "Nouveau titre pour l'espace", + "invitee_email_address": "Adresse e-mail de invitee", + "viewer": "Spectateur", + "editor": "Éditeur", + "admin": "Administrateur", + "mobile": "Mobile", + "image": "Image", + "tool_filter": "Filter", + "team": "Team", + "search": "Recherche", + "search_no_results": "Aucun résultat trouvé", + "search_clear": "Supprimer", + "rename": "Renommer", + "logging_in": "Connexion", + "password_confirmation": "Confirmation du mot de passe", + "confirm_again": "Veuillez consulter votre boîte pour confirmer votre email.", + "confirmed": "Adresse email confirmée avec succès. merci!", + "password_check_inbox": "password_check_inbox", + "what_is_your_name": "Bonjour! Choisir un nom d'utilisateur s'il vous plaît.", + "landing_title": "Le tableau blanc partagé pour tout le monde.", + "landing_claim": "Le tableau blanc partagé pour tout le monde.", + "landing_example": "Que vous soyez étudiant, enseignant ou chercheur: Avec Spacedeck il est facile pour vous de créer, de gérer et de partager des cours ou le travail en classe. Développez vos théories visuellement. Organisez des notes de recherche, web, images, audio et vidéo.", + "spaces": "Espaces", + "access_editor_link": "Lien instantané.", + "access_editor_link_desc": "Donnez ce lien à tous ceux que vous voulez inviter rapidement. Ils n’ont pas besoin créer un compte Spacedeck.", + "access_anonymous_edit_blocking": "Ces invités ne peuvent modifier que les éléments qu’ils ont eux-même créé.", + "access_current_members": "Membres actuels", + "comments": "Commentaires", + "access_no_members": "No members yet. You can invite some below.", + "access_new_members": "Inviter de nouveaux membres.", + "landing_customers": "Approuvé par des milliers.", + "landing_features_title": "Un jeu d'enfant.", + "landing_features_text": "Le tout nouveau Spacedeck 5 vous permet de travailler bien plus facilement grâce à sa magnifique interface simplifiée.", + "landing_features_1": "Glissez & déposez images, vidéos et audios de votre ordinateur ou du web", + "landing_features_2": "Ecrivez directement sur l'espace et choisissez les polices de caractère, couleurs et styles", + "landing_features_3": "Dessinez, annotez et surlignez grâce aux formes graphiques intégrées", + "landing_features_4": "Transformez votre espace en une présentation dynamique", + "landing_features_5": "Collaborez et discutez en temps réel avec vos collègues, élèves et amis", + "landing_features_6": "Partagez vos espaces sur le web ou par email", + "landing_features_7": "Exportez votre espace en PDF pour l'imprimer", + "landing_pricing": "Incroyablement abordable.", + "landing_pricing_lite": "Usage personnel", + "landing_pricing_lite_text": "La version de base, bien arrondi pour recueillir des images et de garder des notes.", + "landing_pricing_pro_features_list": "
                      • Unlimited Spaces
                      • Exporter PDF, ZIP
                      • No Watermarks
                      • Image de fonds
                      • Activity History
                      • 20 Go de stockage
                        • ", + "landing_pricing_pro": "€4,90/User/Mo.
                          €49,90/User/Year", + "landing_pricing_pro_text": "Avec toute la puissance que vous attendez.", + "landing_pricing_pro_features": "Avec toute la puissance que vous attendez.", + "welcome_subject": "Bienvenue sur Spacedeck", + "welcome_body": "Merci pour votre inscription à Spacedeck.\nNous espérons que vous aurez plaisir à travailler dans les Espaces.
                          Rappelez-vous que votre compte comprend un nombre illimité de collaborateurs.
                          N''hésitez pas à partager vos espaces avec des amis et collègues du monde entier.", + "parent_folder": "Dossier origine", + "created_by": "Créé par", + "last_updated": "Mis à jour", + "history_recently_updated": "Nouvelles", + "history_recently_empty": "Rien ne se passe", + "signing_up": "Signing Up", + "feedback_sent": "Merci pour votre commentaire!", + "space_message_subject": "A posté sur %s", + "space_message_body": "%s a commenté dans %s:\n", + "role_member": "role_member", + "password_reset_subject": "Réinitialiser le Mot de passe pour Spacedeck", + "password_reset_body": "Salut!

                          Vous avez demandé la réinitialisation de votre Mot de passe.
                          Veuillez cliquer sur le lien suivant pour définir un nouveau Mot de passe.
                          ", + "password_reset_action": "Définir un nouveau Mot de passe", + "was_offline": "The connection to Spacedeck was interrupted. If you have unsaved work, please keep this browser tab open until the connection is re-established, then touch the unsaved objects again.", + "subscription_failed_user_subject": "Problem with your Spacedeck Payment", + "subscription_failed_user_body": "Unfortunately, we could not process your Payment-method. You can easly create a new payment method including PayPal in your account settings.", + "subscription_failed_team_subject": "Problem with your Spacedeck Payment", + "subscription_failed_team_body": "Unfortunately, we could not process your Payment-method for your Team-Account. Please fix your payment method asap.", + "pro_ad_history_headline": "Après une mise à niveau vous pouvez obtenir un aperçu de toutes les activités actuelles dans les espaces ici.", + "add": "ajouter", + "team_name": "Nom de l'équipe", + "subdomain": "sous-domaine", + "invited": "invité", + "team_adresses": "E-mail adresse", + "duplicate_destination": "Sélectionnez le dossier de destination", + "duplicate_confirm": "Dupliquer %s dans %s?", + "duplicate_success": "%s a été dupliqué dans %s.", + "goto_space": "Aller à l'espace %s", + "goto_folder": "Aller au dossier %s", + "stay_here": "Reste ici", + "download_space": "télécharger un espace", + "type": "Type", + "Previous Zone": "Zone précédent", + "Next Zone": "Zone suivante", + "list": "liste", + "promote": "promouvoir", + "demote": "rétrograder" +} + diff --git a/public/javascripts/lodash.compat.js b/public/javascripts/lodash.compat.js new file mode 100644 index 0000000..23798ba --- /dev/null +++ b/public/javascripts/lodash.compat.js @@ -0,0 +1,7157 @@ +/** + * @license + * Lo-Dash 2.4.1 (Custom Build) + * Build: `lodash -o ./dist/lodash.compat.js` + * Copyright 2012-2013 The Dojo Foundation + * Based on Underscore.js 1.5.2 + * Copyright 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors + * Available under MIT license + */ +;(function() { + + /** Used as a safe reference for `undefined` in pre ES5 environments */ + var undefined; + + /** Used to pool arrays and objects used internally */ + var arrayPool = [], + objectPool = []; + + /** Used to generate unique IDs */ + var idCounter = 0; + + /** Used internally to indicate various things */ + var indicatorObject = {}; + + /** Used to prefix keys to avoid issues with `__proto__` and properties on `Object.prototype` */ + var keyPrefix = +new Date + ''; + + /** Used as the size when optimizations are enabled for large arrays */ + var largeArraySize = 75; + + /** Used as the max size of the `arrayPool` and `objectPool` */ + var maxPoolSize = 40; + + /** Used to detect and test whitespace */ + var whitespace = ( + // whitespace + ' \t\x0B\f\xA0\ufeff' + + + // line terminators + '\n\r\u2028\u2029' + + + // unicode category "Zs" space separators + '\u1680\u180e\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000' + ); + + /** Used to match empty string literals in compiled template source */ + var reEmptyStringLeading = /\b__p \+= '';/g, + reEmptyStringMiddle = /\b(__p \+=) '' \+/g, + reEmptyStringTrailing = /(__e\(.*?\)|\b__t\)) \+\n'';/g; + + /** + * Used to match ES6 template delimiters + * http://people.mozilla.org/~jorendorff/es6-draft.html#sec-literals-string-literals + */ + var reEsTemplate = /\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g; + + /** Used to match regexp flags from their coerced string values */ + var reFlags = /\w*$/; + + /** Used to detected named functions */ + var reFuncName = /^\s*function[ \n\r\t]+\w/; + + /** Used to match "interpolate" template delimiters */ + var reInterpolate = /<%=([\s\S]+?)%>/g; + + /** Used to match leading whitespace and zeros to be removed */ + var reLeadingSpacesAndZeros = RegExp('^[' + whitespace + ']*0+(?=.$)'); + + /** Used to ensure capturing order of template delimiters */ + var reNoMatch = /($^)/; + + /** Used to detect functions containing a `this` reference */ + var reThis = /\bthis\b/; + + /** Used to match unescaped characters in compiled string literals */ + var reUnescapedString = /['\n\r\t\u2028\u2029\\]/g; + + /** Used to assign default `context` object properties */ + var contextProps = [ + 'Array', 'Boolean', 'Date', 'Error', 'Function', 'Math', 'Number', 'Object', + 'RegExp', 'String', '_', 'attachEvent', 'clearTimeout', 'isFinite', 'isNaN', + 'parseInt', 'setTimeout' + ]; + + /** Used to fix the JScript [[DontEnum]] bug */ + var shadowedProps = [ + 'constructor', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', + 'toLocaleString', 'toString', 'valueOf' + ]; + + /** Used to make template sourceURLs easier to identify */ + var templateCounter = 0; + + /** `Object#toString` result shortcuts */ + var argsClass = '[object Arguments]', + arrayClass = '[object Array]', + boolClass = '[object Boolean]', + dateClass = '[object Date]', + errorClass = '[object Error]', + funcClass = '[object Function]', + numberClass = '[object Number]', + objectClass = '[object Object]', + regexpClass = '[object RegExp]', + stringClass = '[object String]'; + + /** Used to identify object classifications that `_.clone` supports */ + var cloneableClasses = {}; + cloneableClasses[funcClass] = false; + cloneableClasses[argsClass] = cloneableClasses[arrayClass] = + cloneableClasses[boolClass] = cloneableClasses[dateClass] = + cloneableClasses[numberClass] = cloneableClasses[objectClass] = + cloneableClasses[regexpClass] = cloneableClasses[stringClass] = true; + + /** Used as an internal `_.debounce` options object */ + var debounceOptions = { + 'leading': false, + 'maxWait': 0, + 'trailing': false + }; + + /** Used as the property descriptor for `__bindData__` */ + var descriptor = { + 'configurable': false, + 'enumerable': false, + 'value': null, + 'writable': false + }; + + /** Used as the data object for `iteratorTemplate` */ + var iteratorData = { + 'args': '', + 'array': null, + 'bottom': '', + 'firstArg': '', + 'init': '', + 'keys': null, + 'loop': '', + 'shadowedProps': null, + 'support': null, + 'top': '', + 'useHas': false + }; + + /** Used to determine if values are of the language type Object */ + var objectTypes = { + 'boolean': false, + 'function': true, + 'object': true, + 'number': false, + 'string': false, + 'undefined': false + }; + + /** Used to escape characters for inclusion in compiled string literals */ + var stringEscapes = { + '\\': '\\', + "'": "'", + '\n': 'n', + '\r': 'r', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + /** Used as a reference to the global object */ + var root = (objectTypes[typeof window] && window) || this; + + /** Detect free variable `exports` */ + var freeExports = objectTypes[typeof exports] && exports && !exports.nodeType && exports; + + /** Detect free variable `module` */ + var freeModule = objectTypes[typeof module] && module && !module.nodeType && module; + + /** Detect the popular CommonJS extension `module.exports` */ + var moduleExports = freeModule && freeModule.exports === freeExports && freeExports; + + /** Detect free variable `global` from Node.js or Browserified code and use it as `root` */ + var freeGlobal = objectTypes[typeof global] && global; + if (freeGlobal && (freeGlobal.global === freeGlobal || freeGlobal.window === freeGlobal)) { + root = freeGlobal; + } + + /*--------------------------------------------------------------------------*/ + + /** + * The base implementation of `_.indexOf` without support for binary searches + * or `fromIndex` constraints. + * + * @private + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @param {number} [fromIndex=0] The index to search from. + * @returns {number} Returns the index of the matched value or `-1`. + */ + function baseIndexOf(array, value, fromIndex) { + var index = (fromIndex || 0) - 1, + length = array ? array.length : 0; + + while (++index < length) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * An implementation of `_.contains` for cache objects that mimics the return + * signature of `_.indexOf` by returning `0` if the value is found, else `-1`. + * + * @private + * @param {Object} cache The cache object to inspect. + * @param {*} value The value to search for. + * @returns {number} Returns `0` if `value` is found, else `-1`. + */ + function cacheIndexOf(cache, value) { + var type = typeof value; + cache = cache.cache; + + if (type == 'boolean' || value == null) { + return cache[value] ? 0 : -1; + } + if (type != 'number' && type != 'string') { + type = 'object'; + } + var key = type == 'number' ? value : keyPrefix + value; + cache = (cache = cache[type]) && cache[key]; + + return type == 'object' + ? (cache && baseIndexOf(cache, value) > -1 ? 0 : -1) + : (cache ? 0 : -1); + } + + /** + * Adds a given value to the corresponding cache object. + * + * @private + * @param {*} value The value to add to the cache. + */ + function cachePush(value) { + var cache = this.cache, + type = typeof value; + + if (type == 'boolean' || value == null) { + cache[value] = true; + } else { + if (type != 'number' && type != 'string') { + type = 'object'; + } + var key = type == 'number' ? value : keyPrefix + value, + typeCache = cache[type] || (cache[type] = {}); + + if (type == 'object') { + (typeCache[key] || (typeCache[key] = [])).push(value); + } else { + typeCache[key] = true; + } + } + } + + /** + * Used by `_.max` and `_.min` as the default callback when a given + * collection is a string value. + * + * @private + * @param {string} value The character to inspect. + * @returns {number} Returns the code unit of given character. + */ + function charAtCallback(value) { + return value.charCodeAt(0); + } + + /** + * Used by `sortBy` to compare transformed `collection` elements, stable sorting + * them in ascending order. + * + * @private + * @param {Object} a The object to compare to `b`. + * @param {Object} b The object to compare to `a`. + * @returns {number} Returns the sort order indicator of `1` or `-1`. + */ + function compareAscending(a, b) { + var ac = a.criteria, + bc = b.criteria, + index = -1, + length = ac.length; + + while (++index < length) { + var value = ac[index], + other = bc[index]; + + if (value !== other) { + if (value > other || typeof value == 'undefined') { + return 1; + } + if (value < other || typeof other == 'undefined') { + return -1; + } + } + } + // Fixes an `Array#sort` bug in the JS engine embedded in Adobe applications + // that causes it, under certain circumstances, to return the same value for + // `a` and `b`. See https://github.com/jashkenas/underscore/pull/1247 + // + // This also ensures a stable sort in V8 and other engines. + // See http://code.google.com/p/v8/issues/detail?id=90 + return a.index - b.index; + } + + /** + * Creates a cache object to optimize linear searches of large arrays. + * + * @private + * @param {Array} [array=[]] The array to search. + * @returns {null|Object} Returns the cache object or `null` if caching should not be used. + */ + function createCache(array) { + var index = -1, + length = array.length, + first = array[0], + mid = array[(length / 2) | 0], + last = array[length - 1]; + + if (first && typeof first == 'object' && + mid && typeof mid == 'object' && last && typeof last == 'object') { + return false; + } + var cache = getObject(); + cache['false'] = cache['null'] = cache['true'] = cache['undefined'] = false; + + var result = getObject(); + result.array = array; + result.cache = cache; + result.push = cachePush; + + while (++index < length) { + result.push(array[index]); + } + return result; + } + + /** + * Used by `template` to escape characters for inclusion in compiled + * string literals. + * + * @private + * @param {string} match The matched character to escape. + * @returns {string} Returns the escaped character. + */ + function escapeStringChar(match) { + return '\\' + stringEscapes[match]; + } + + /** + * Gets an array from the array pool or creates a new one if the pool is empty. + * + * @private + * @returns {Array} The array from the pool. + */ + function getArray() { + return arrayPool.pop() || []; + } + + /** + * Gets an object from the object pool or creates a new one if the pool is empty. + * + * @private + * @returns {Object} The object from the pool. + */ + function getObject() { + return objectPool.pop() || { + 'array': null, + 'cache': null, + 'criteria': null, + 'false': false, + 'index': 0, + 'null': false, + 'number': null, + 'object': null, + 'push': null, + 'string': null, + 'true': false, + 'undefined': false, + 'value': null + }; + } + + /** + * Checks if `value` is a DOM node in IE < 9. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a DOM node, else `false`. + */ + function isNode(value) { + // IE < 9 presents DOM nodes as `Object` objects except they have `toString` + // methods that are `typeof` "string" and still can coerce nodes to strings + return typeof value.toString != 'function' && typeof (value + '') == 'string'; + } + + /** + * Releases the given array back to the array pool. + * + * @private + * @param {Array} [array] The array to release. + */ + function releaseArray(array) { + array.length = 0; + if (arrayPool.length < maxPoolSize) { + arrayPool.push(array); + } + } + + /** + * Releases the given object back to the object pool. + * + * @private + * @param {Object} [object] The object to release. + */ + function releaseObject(object) { + var cache = object.cache; + if (cache) { + releaseObject(cache); + } + object.array = object.cache = object.criteria = object.object = object.number = object.string = object.value = null; + if (objectPool.length < maxPoolSize) { + objectPool.push(object); + } + } + + /** + * Slices the `collection` from the `start` index up to, but not including, + * the `end` index. + * + * Note: This function is used instead of `Array#slice` to support node lists + * in IE < 9 and to ensure dense arrays are returned. + * + * @private + * @param {Array|Object|string} collection The collection to slice. + * @param {number} start The start index. + * @param {number} end The end index. + * @returns {Array} Returns the new array. + */ + function slice(array, start, end) { + start || (start = 0); + if (typeof end == 'undefined') { + end = array ? array.length : 0; + } + var index = -1, + length = end - start || 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = array[start + index]; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Create a new `lodash` function using the given context object. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Object} [context=root] The context object. + * @returns {Function} Returns the `lodash` function. + */ + function runInContext(context) { + // Avoid issues with some ES3 environments that attempt to use values, named + // after built-in constructors like `Object`, for the creation of literals. + // ES5 clears this up by stating that literals must use built-in constructors. + // See http://es5.github.io/#x11.1.5. + context = context ? _.defaults(root.Object(), context, _.pick(root, contextProps)) : root; + + /** Native constructor references */ + var Array = context.Array, + Boolean = context.Boolean, + Date = context.Date, + Error = context.Error, + Function = context.Function, + Math = context.Math, + Number = context.Number, + Object = context.Object, + RegExp = context.RegExp, + String = context.String, + TypeError = context.TypeError; + + /** + * Used for `Array` method references. + * + * Normally `Array.prototype` would suffice, however, using an array literal + * avoids issues in Narwhal. + */ + var arrayRef = []; + + /** Used for native method references */ + var errorProto = Error.prototype, + objectProto = Object.prototype, + stringProto = String.prototype; + + /** Used to restore the original `_` reference in `noConflict` */ + var oldDash = context._; + + /** Used to resolve the internal [[Class]] of values */ + var toString = objectProto.toString; + + /** Used to detect if a method is native */ + var reNative = RegExp('^' + + String(toString) + .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + .replace(/toString| for [^\]]+/g, '.*?') + '$' + ); + + /** Native method shortcuts */ + var ceil = Math.ceil, + clearTimeout = context.clearTimeout, + floor = Math.floor, + fnToString = Function.prototype.toString, + getPrototypeOf = isNative(getPrototypeOf = Object.getPrototypeOf) && getPrototypeOf, + hasOwnProperty = objectProto.hasOwnProperty, + push = arrayRef.push, + propertyIsEnumerable = objectProto.propertyIsEnumerable, + setTimeout = context.setTimeout, + splice = arrayRef.splice, + unshift = arrayRef.unshift; + + /** Used to set meta data on functions */ + var defineProperty = (function() { + // IE 8 only accepts DOM elements + try { + var o = {}, + func = isNative(func = Object.defineProperty) && func, + result = func(o, o, o) && func; + } catch(e) { } + return result; + }()); + + /* Native method shortcuts for methods with the same name as other `lodash` methods */ + var nativeCreate = isNative(nativeCreate = Object.create) && nativeCreate, + nativeIsArray = isNative(nativeIsArray = Array.isArray) && nativeIsArray, + nativeIsFinite = context.isFinite, + nativeIsNaN = context.isNaN, + nativeKeys = isNative(nativeKeys = Object.keys) && nativeKeys, + nativeMax = Math.max, + nativeMin = Math.min, + nativeParseInt = context.parseInt, + nativeRandom = Math.random; + + /** Used to lookup a built-in constructor by [[Class]] */ + var ctorByClass = {}; + ctorByClass[arrayClass] = Array; + ctorByClass[boolClass] = Boolean; + ctorByClass[dateClass] = Date; + ctorByClass[funcClass] = Function; + ctorByClass[objectClass] = Object; + ctorByClass[numberClass] = Number; + ctorByClass[regexpClass] = RegExp; + ctorByClass[stringClass] = String; + + /** Used to avoid iterating non-enumerable properties in IE < 9 */ + var nonEnumProps = {}; + nonEnumProps[arrayClass] = nonEnumProps[dateClass] = nonEnumProps[numberClass] = { 'constructor': true, 'toLocaleString': true, 'toString': true, 'valueOf': true }; + nonEnumProps[boolClass] = nonEnumProps[stringClass] = { 'constructor': true, 'toString': true, 'valueOf': true }; + nonEnumProps[errorClass] = nonEnumProps[funcClass] = nonEnumProps[regexpClass] = { 'constructor': true, 'toString': true }; + nonEnumProps[objectClass] = { 'constructor': true }; + + (function() { + var length = shadowedProps.length; + while (length--) { + var key = shadowedProps[length]; + for (var className in nonEnumProps) { + if (hasOwnProperty.call(nonEnumProps, className) && !hasOwnProperty.call(nonEnumProps[className], key)) { + nonEnumProps[className][key] = false; + } + } + } + }()); + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a `lodash` object which wraps the given value to enable intuitive + * method chaining. + * + * In addition to Lo-Dash methods, wrappers also have the following `Array` methods: + * `concat`, `join`, `pop`, `push`, `reverse`, `shift`, `slice`, `sort`, `splice`, + * and `unshift` + * + * Chaining is supported in custom builds as long as the `value` method is + * implicitly or explicitly included in the build. + * + * The chainable wrapper functions are: + * `after`, `assign`, `bind`, `bindAll`, `bindKey`, `chain`, `compact`, + * `compose`, `concat`, `countBy`, `create`, `createCallback`, `curry`, + * `debounce`, `defaults`, `defer`, `delay`, `difference`, `filter`, `flatten`, + * `forEach`, `forEachRight`, `forIn`, `forInRight`, `forOwn`, `forOwnRight`, + * `functions`, `groupBy`, `indexBy`, `initial`, `intersection`, `invert`, + * `invoke`, `keys`, `map`, `max`, `memoize`, `merge`, `min`, `object`, `omit`, + * `once`, `pairs`, `partial`, `partialRight`, `pick`, `pluck`, `pull`, `push`, + * `range`, `reject`, `remove`, `rest`, `reverse`, `shuffle`, `slice`, `sort`, + * `sortBy`, `splice`, `tap`, `throttle`, `times`, `toArray`, `transform`, + * `union`, `uniq`, `unshift`, `unzip`, `values`, `where`, `without`, `wrap`, + * and `zip` + * + * The non-chainable wrapper functions are: + * `clone`, `cloneDeep`, `contains`, `escape`, `every`, `find`, `findIndex`, + * `findKey`, `findLast`, `findLastIndex`, `findLastKey`, `has`, `identity`, + * `indexOf`, `isArguments`, `isArray`, `isBoolean`, `isDate`, `isElement`, + * `isEmpty`, `isEqual`, `isFinite`, `isFunction`, `isNaN`, `isNull`, `isNumber`, + * `isObject`, `isPlainObject`, `isRegExp`, `isString`, `isUndefined`, `join`, + * `lastIndexOf`, `mixin`, `noConflict`, `parseInt`, `pop`, `random`, `reduce`, + * `reduceRight`, `result`, `shift`, `size`, `some`, `sortedIndex`, `runInContext`, + * `template`, `unescape`, `uniqueId`, and `value` + * + * The wrapper functions `first` and `last` return wrapped values when `n` is + * provided, otherwise they return unwrapped values. + * + * Explicit chaining can be enabled by using the `_.chain` method. + * + * @name _ + * @constructor + * @category Chaining + * @param {*} value The value to wrap in a `lodash` instance. + * @returns {Object} Returns a `lodash` instance. + * @example + * + * var wrapped = _([1, 2, 3]); + * + * // returns an unwrapped value + * wrapped.reduce(function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * // returns a wrapped value + * var squares = wrapped.map(function(num) { + * return num * num; + * }); + * + * _.isArray(squares); + * // => false + * + * _.isArray(squares.value()); + * // => true + */ + function lodash(value) { + // don't wrap if already wrapped, even if wrapped by a different `lodash` constructor + return (value && typeof value == 'object' && !isArray(value) && hasOwnProperty.call(value, '__wrapped__')) + ? value + : new lodashWrapper(value); + } + + /** + * A fast path for creating `lodash` wrapper objects. + * + * @private + * @param {*} value The value to wrap in a `lodash` instance. + * @param {boolean} chainAll A flag to enable chaining for all methods + * @returns {Object} Returns a `lodash` instance. + */ + function lodashWrapper(value, chainAll) { + this.__chain__ = !!chainAll; + this.__wrapped__ = value; + } + // ensure `new lodashWrapper` is an instance of `lodash` + lodashWrapper.prototype = lodash.prototype; + + /** + * An object used to flag environments features. + * + * @static + * @memberOf _ + * @type Object + */ + var support = lodash.support = {}; + + (function() { + var ctor = function() { this.x = 1; }, + object = { '0': 1, 'length': 1 }, + props = []; + + ctor.prototype = { 'valueOf': 1, 'y': 1 }; + for (var key in new ctor) { props.push(key); } + for (key in arguments) { } + + /** + * Detect if an `arguments` object's [[Class]] is resolvable (all but Firefox < 4, IE < 9). + * + * @memberOf _.support + * @type boolean + */ + support.argsClass = toString.call(arguments) == argsClass; + + /** + * Detect if `arguments` objects are `Object` objects (all but Narwhal and Opera < 10.5). + * + * @memberOf _.support + * @type boolean + */ + support.argsObject = arguments.constructor == Object && !(arguments instanceof Array); + + /** + * Detect if `name` or `message` properties of `Error.prototype` are + * enumerable by default. (IE < 9, Safari < 5.1) + * + * @memberOf _.support + * @type boolean + */ + support.enumErrorProps = propertyIsEnumerable.call(errorProto, 'message') || propertyIsEnumerable.call(errorProto, 'name'); + + /** + * Detect if `prototype` properties are enumerable by default. + * + * Firefox < 3.6, Opera > 9.50 - Opera < 11.60, and Safari < 5.1 + * (if the prototype or a property on the prototype has been set) + * incorrectly sets a function's `prototype` property [[Enumerable]] + * value to `true`. + * + * @memberOf _.support + * @type boolean + */ + support.enumPrototypes = propertyIsEnumerable.call(ctor, 'prototype'); + + /** + * Detect if functions can be decompiled by `Function#toString` + * (all but PS3 and older Opera mobile browsers & avoided in Windows 8 apps). + * + * @memberOf _.support + * @type boolean + */ + support.funcDecomp = !isNative(context.WinRTError) && reThis.test(runInContext); + + /** + * Detect if `Function#name` is supported (all but IE). + * + * @memberOf _.support + * @type boolean + */ + support.funcNames = typeof Function.name == 'string'; + + /** + * Detect if `arguments` object indexes are non-enumerable + * (Firefox < 4, IE < 9, PhantomJS, Safari < 5.1). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumArgs = key != 0; + + /** + * Detect if properties shadowing those on `Object.prototype` are non-enumerable. + * + * In IE < 9 an objects own properties, shadowing non-enumerable ones, are + * made non-enumerable as well (a.k.a the JScript [[DontEnum]] bug). + * + * @memberOf _.support + * @type boolean + */ + support.nonEnumShadows = !/valueOf/.test(props); + + /** + * Detect if own properties are iterated after inherited properties (all but IE < 9). + * + * @memberOf _.support + * @type boolean + */ + support.ownLast = props[0] != 'x'; + + /** + * Detect if `Array#shift` and `Array#splice` augment array-like objects correctly. + * + * Firefox < 10, IE compatibility mode, and IE < 9 have buggy Array `shift()` + * and `splice()` functions that fail to remove the last element, `value[0]`, + * of array-like objects even though the `length` property is set to `0`. + * The `shift()` method is buggy in IE 8 compatibility mode, while `splice()` + * is buggy regardless of mode in IE < 9 and buggy in compatibility mode in IE 9. + * + * @memberOf _.support + * @type boolean + */ + support.spliceObjects = (arrayRef.splice.call(object, 0, 1), !object[0]); + + /** + * Detect lack of support for accessing string characters by index. + * + * IE < 8 can't access characters by index and IE 8 can only access + * characters by index on string literals. + * + * @memberOf _.support + * @type boolean + */ + support.unindexedChars = ('x'[0] + Object('x')[0]) != 'xx'; + + /** + * Detect if a DOM node's [[Class]] is resolvable (all but IE < 9) + * and that the JS engine errors when attempting to coerce an object to + * a string without a `toString` function. + * + * @memberOf _.support + * @type boolean + */ + try { + support.nodeClass = !(toString.call(document) == objectClass && !({ 'toString': 0 } + '')); + } catch(e) { + support.nodeClass = true; + } + }(1)); + + /** + * By default, the template delimiters used by Lo-Dash are similar to those in + * embedded Ruby (ERB). Change the following template settings to use alternative + * delimiters. + * + * @static + * @memberOf _ + * @type Object + */ + lodash.templateSettings = { + + /** + * Used to detect `data` property values to be HTML-escaped. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'escape': /<%-([\s\S]+?)%>/g, + + /** + * Used to detect code to be evaluated. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'evaluate': /<%([\s\S]+?)%>/g, + + /** + * Used to detect `data` property values to inject. + * + * @memberOf _.templateSettings + * @type RegExp + */ + 'interpolate': reInterpolate, + + /** + * Used to reference the data object in the template text. + * + * @memberOf _.templateSettings + * @type string + */ + 'variable': '', + + /** + * Used to import variables into the compiled template. + * + * @memberOf _.templateSettings + * @type Object + */ + 'imports': { + + /** + * A reference to the `lodash` function. + * + * @memberOf _.templateSettings.imports + * @type Function + */ + '_': lodash + } + }; + + /*--------------------------------------------------------------------------*/ + + /** + * The template used to create iterator functions. + * + * @private + * @param {Object} data The data object used to populate the text. + * @returns {string} Returns the interpolated text. + */ + var iteratorTemplate = function(obj) { + + var __p = 'var index, iterable = ' + + (obj.firstArg) + + ', result = ' + + (obj.init) + + ';\nif (!iterable) return result;\n' + + (obj.top) + + ';'; + if (obj.array) { + __p += '\nvar length = iterable.length; index = -1;\nif (' + + (obj.array) + + ') { '; + if (support.unindexedChars) { + __p += '\n if (isString(iterable)) {\n iterable = iterable.split(\'\')\n } '; + } + __p += '\n while (++index < length) {\n ' + + (obj.loop) + + ';\n }\n}\nelse { '; + } else if (support.nonEnumArgs) { + __p += '\n var length = iterable.length; index = -1;\n if (length && isArguments(iterable)) {\n while (++index < length) {\n index += \'\';\n ' + + (obj.loop) + + ';\n }\n } else { '; + } + + if (support.enumPrototypes) { + __p += '\n var skipProto = typeof iterable == \'function\';\n '; + } + + if (support.enumErrorProps) { + __p += '\n var skipErrorProps = iterable === errorProto || iterable instanceof Error;\n '; + } + + var conditions = []; if (support.enumPrototypes) { conditions.push('!(skipProto && index == "prototype")'); } if (support.enumErrorProps) { conditions.push('!(skipErrorProps && (index == "message" || index == "name"))'); } + + if (obj.useHas && obj.keys) { + __p += '\n var ownIndex = -1,\n ownProps = objectTypes[typeof iterable] && keys(iterable),\n length = ownProps ? ownProps.length : 0;\n\n while (++ownIndex < length) {\n index = ownProps[ownIndex];\n'; + if (conditions.length) { + __p += ' if (' + + (conditions.join(' && ')) + + ') {\n '; + } + __p += + (obj.loop) + + '; '; + if (conditions.length) { + __p += '\n }'; + } + __p += '\n } '; + } else { + __p += '\n for (index in iterable) {\n'; + if (obj.useHas) { conditions.push("hasOwnProperty.call(iterable, index)"); } if (conditions.length) { + __p += ' if (' + + (conditions.join(' && ')) + + ') {\n '; + } + __p += + (obj.loop) + + '; '; + if (conditions.length) { + __p += '\n }'; + } + __p += '\n } '; + if (support.nonEnumShadows) { + __p += '\n\n if (iterable !== objectProto) {\n var ctor = iterable.constructor,\n isProto = iterable === (ctor && ctor.prototype),\n className = iterable === stringProto ? stringClass : iterable === errorProto ? errorClass : toString.call(iterable),\n nonEnum = nonEnumProps[className];\n '; + for (k = 0; k < 7; k++) { + __p += '\n index = \'' + + (obj.shadowedProps[k]) + + '\';\n if ((!(isProto && nonEnum[index]) && hasOwnProperty.call(iterable, index))'; + if (!obj.useHas) { + __p += ' || (!nonEnum[index] && iterable[index] !== objectProto[index])'; + } + __p += ') {\n ' + + (obj.loop) + + ';\n } '; + } + __p += '\n } '; + } + + } + + if (obj.array || support.nonEnumArgs) { + __p += '\n}'; + } + __p += + (obj.bottom) + + ';\nreturn result'; + + return __p + }; + + /*--------------------------------------------------------------------------*/ + + /** + * The base implementation of `_.bind` that creates the bound function and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new bound function. + */ + function baseBind(bindData) { + var func = bindData[0], + partialArgs = bindData[2], + thisArg = bindData[4]; + + function bound() { + // `Function#bind` spec + // http://es5.github.io/#x15.3.4.5 + if (partialArgs) { + // avoid `arguments` object deoptimizations by using `slice` instead + // of `Array.prototype.slice.call` and not assigning `arguments` to a + // variable as a ternary expression + var args = slice(partialArgs); + push.apply(args, arguments); + } + // mimic the constructor's `return` behavior + // http://es5.github.io/#x13.2.2 + if (this instanceof bound) { + // ensure `new bound` is an instance of `func` + var thisBinding = baseCreate(func.prototype), + result = func.apply(thisBinding, args || arguments); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisArg, args || arguments); + } + setBindData(bound, bindData); + return bound; + } + + /** + * The base implementation of `_.clone` without argument juggling or support + * for `thisArg` binding. + * + * @private + * @param {*} value The value to clone. + * @param {boolean} [isDeep=false] Specify a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates clones with source counterparts. + * @returns {*} Returns the cloned value. + */ + function baseClone(value, isDeep, callback, stackA, stackB) { + if (callback) { + var result = callback(value); + if (typeof result != 'undefined') { + return result; + } + } + // inspect [[Class]] + var isObj = isObject(value); + if (isObj) { + var className = toString.call(value); + if (!cloneableClasses[className] || (!support.nodeClass && isNode(value))) { + return value; + } + var ctor = ctorByClass[className]; + switch (className) { + case boolClass: + case dateClass: + return new ctor(+value); + + case numberClass: + case stringClass: + return new ctor(value); + + case regexpClass: + result = ctor(value.source, reFlags.exec(value)); + result.lastIndex = value.lastIndex; + return result; + } + } else { + return value; + } + var isArr = isArray(value); + if (isDeep) { + // check for circular references and return corresponding clone + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == value) { + return stackB[length]; + } + } + result = isArr ? ctor(value.length) : {}; + } + else { + result = isArr ? slice(value) : assign({}, value); + } + // add array properties assigned by `RegExp#exec` + if (isArr) { + if (hasOwnProperty.call(value, 'index')) { + result.index = value.index; + } + if (hasOwnProperty.call(value, 'input')) { + result.input = value.input; + } + } + // exit for shallow clone + if (!isDeep) { + return result; + } + // add the source value to the stack of traversed objects + // and associate it with its clone + stackA.push(value); + stackB.push(result); + + // recursively populate clone (susceptible to call stack limits) + (isArr ? baseEach : forOwn)(value, function(objValue, key) { + result[key] = baseClone(objValue, isDeep, callback, stackA, stackB); + }); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * The base implementation of `_.create` without support for assigning + * properties to the created object. + * + * @private + * @param {Object} prototype The object to inherit from. + * @returns {Object} Returns the new object. + */ + function baseCreate(prototype, properties) { + return isObject(prototype) ? nativeCreate(prototype) : {}; + } + // fallback for browsers without `Object.create` + if (!nativeCreate) { + baseCreate = (function() { + function Object() {} + return function(prototype) { + if (isObject(prototype)) { + Object.prototype = prototype; + var result = new Object; + Object.prototype = null; + } + return result || context.Object(); + }; + }()); + } + + /** + * The base implementation of `_.createCallback` without support for creating + * "_.pluck" or "_.where" style callbacks. + * + * @private + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + */ + function baseCreateCallback(func, thisArg, argCount) { + if (typeof func != 'function') { + return identity; + } + // exit early for no `thisArg` or already bound by `Function#bind` + if (typeof thisArg == 'undefined' || !('prototype' in func)) { + return func; + } + var bindData = func.__bindData__; + if (typeof bindData == 'undefined') { + if (support.funcNames) { + bindData = !func.name; + } + bindData = bindData || !support.funcDecomp; + if (!bindData) { + var source = fnToString.call(func); + if (!support.funcNames) { + bindData = !reFuncName.test(source); + } + if (!bindData) { + // checks if `func` references the `this` keyword and stores the result + bindData = reThis.test(source); + setBindData(func, bindData); + } + } + } + // exit early if there are no `this` references or `func` is bound + if (bindData === false || (bindData !== true && bindData[1] & 1)) { + return func; + } + switch (argCount) { + case 1: return function(value) { + return func.call(thisArg, value); + }; + case 2: return function(a, b) { + return func.call(thisArg, a, b); + }; + case 3: return function(value, index, collection) { + return func.call(thisArg, value, index, collection); + }; + case 4: return function(accumulator, value, index, collection) { + return func.call(thisArg, accumulator, value, index, collection); + }; + } + return bind(func, thisArg); + } + + /** + * The base implementation of `createWrapper` that creates the wrapper and + * sets its meta data. + * + * @private + * @param {Array} bindData The bind data array. + * @returns {Function} Returns the new function. + */ + function baseCreateWrapper(bindData) { + var func = bindData[0], + bitmask = bindData[1], + partialArgs = bindData[2], + partialRightArgs = bindData[3], + thisArg = bindData[4], + arity = bindData[5]; + + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + key = func; + + function bound() { + var thisBinding = isBind ? thisArg : this; + if (partialArgs) { + var args = slice(partialArgs); + push.apply(args, arguments); + } + if (partialRightArgs || isCurry) { + args || (args = slice(arguments)); + if (partialRightArgs) { + push.apply(args, partialRightArgs); + } + if (isCurry && args.length < arity) { + bitmask |= 16 & ~32; + return baseCreateWrapper([func, (isCurryBound ? bitmask : bitmask & ~3), args, null, thisArg, arity]); + } + } + args || (args = arguments); + if (isBindKey) { + func = thisBinding[key]; + } + if (this instanceof bound) { + thisBinding = baseCreate(func.prototype); + var result = func.apply(thisBinding, args); + return isObject(result) ? result : thisBinding; + } + return func.apply(thisBinding, args); + } + setBindData(bound, bindData); + return bound; + } + + /** + * The base implementation of `_.difference` that accepts a single array + * of values to exclude. + * + * @private + * @param {Array} array The array to process. + * @param {Array} [values] The array of values to exclude. + * @returns {Array} Returns a new array of filtered values. + */ + function baseDifference(array, values) { + var index = -1, + indexOf = getIndexOf(), + length = array ? array.length : 0, + isLarge = length >= largeArraySize && indexOf === baseIndexOf, + result = []; + + if (isLarge) { + var cache = createCache(values); + if (cache) { + indexOf = cacheIndexOf; + values = cache; + } else { + isLarge = false; + } + } + while (++index < length) { + var value = array[index]; + if (indexOf(values, value) < 0) { + result.push(value); + } + } + if (isLarge) { + releaseObject(values); + } + return result; + } + + /** + * The base implementation of `_.flatten` without support for callback + * shorthands or `thisArg` binding. + * + * @private + * @param {Array} array The array to flatten. + * @param {boolean} [isShallow=false] A flag to restrict flattening to a single level. + * @param {boolean} [isStrict=false] A flag to restrict flattening to arrays and `arguments` objects. + * @param {number} [fromIndex=0] The index to start from. + * @returns {Array} Returns a new flattened array. + */ + function baseFlatten(array, isShallow, isStrict, fromIndex) { + var index = (fromIndex || 0) - 1, + length = array ? array.length : 0, + result = []; + + while (++index < length) { + var value = array[index]; + + if (value && typeof value == 'object' && typeof value.length == 'number' + && (isArray(value) || isArguments(value))) { + // recursively flatten arrays (susceptible to call stack limits) + if (!isShallow) { + value = baseFlatten(value, isShallow, isStrict); + } + var valIndex = -1, + valLength = value.length, + resIndex = result.length; + + result.length += valLength; + while (++valIndex < valLength) { + result[resIndex++] = value[valIndex]; + } + } else if (!isStrict) { + result.push(value); + } + } + return result; + } + + /** + * The base implementation of `_.isEqual`, without support for `thisArg` binding, + * that allows partial "_.where" style comparisons. + * + * @private + * @param {*} a The value to compare. + * @param {*} b The other value to compare. + * @param {Function} [callback] The function to customize comparing values. + * @param {Function} [isWhere=false] A flag to indicate performing partial comparisons. + * @param {Array} [stackA=[]] Tracks traversed `a` objects. + * @param {Array} [stackB=[]] Tracks traversed `b` objects. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + */ + function baseIsEqual(a, b, callback, isWhere, stackA, stackB) { + // used to indicate that when comparing objects, `a` has at least the properties of `b` + if (callback) { + var result = callback(a, b); + if (typeof result != 'undefined') { + return !!result; + } + } + // exit early for identical values + if (a === b) { + // treat `+0` vs. `-0` as not equal + return a !== 0 || (1 / a == 1 / b); + } + var type = typeof a, + otherType = typeof b; + + // exit early for unlike primitive values + if (a === a && + !(a && objectTypes[type]) && + !(b && objectTypes[otherType])) { + return false; + } + // exit early for `null` and `undefined` avoiding ES3's Function#call behavior + // http://es5.github.io/#x15.3.4.4 + if (a == null || b == null) { + return a === b; + } + // compare [[Class]] names + var className = toString.call(a), + otherClass = toString.call(b); + + if (className == argsClass) { + className = objectClass; + } + if (otherClass == argsClass) { + otherClass = objectClass; + } + if (className != otherClass) { + return false; + } + switch (className) { + case boolClass: + case dateClass: + // coerce dates and booleans to numbers, dates to milliseconds and booleans + // to `1` or `0` treating invalid dates coerced to `NaN` as not equal + return +a == +b; + + case numberClass: + // treat `NaN` vs. `NaN` as equal + return (a != +a) + ? b != +b + // but treat `+0` vs. `-0` as not equal + : (a == 0 ? (1 / a == 1 / b) : a == +b); + + case regexpClass: + case stringClass: + // coerce regexes to strings (http://es5.github.io/#x15.10.6.4) + // treat string primitives and their corresponding object instances as equal + return a == String(b); + } + var isArr = className == arrayClass; + if (!isArr) { + // unwrap any `lodash` wrapped values + var aWrapped = hasOwnProperty.call(a, '__wrapped__'), + bWrapped = hasOwnProperty.call(b, '__wrapped__'); + + if (aWrapped || bWrapped) { + return baseIsEqual(aWrapped ? a.__wrapped__ : a, bWrapped ? b.__wrapped__ : b, callback, isWhere, stackA, stackB); + } + // exit for functions and DOM nodes + if (className != objectClass || (!support.nodeClass && (isNode(a) || isNode(b)))) { + return false; + } + // in older versions of Opera, `arguments` objects have `Array` constructors + var ctorA = !support.argsObject && isArguments(a) ? Object : a.constructor, + ctorB = !support.argsObject && isArguments(b) ? Object : b.constructor; + + // non `Object` object instances with different constructors are not equal + if (ctorA != ctorB && + !(isFunction(ctorA) && ctorA instanceof ctorA && isFunction(ctorB) && ctorB instanceof ctorB) && + ('constructor' in a && 'constructor' in b) + ) { + return false; + } + } + // assume cyclic structures are equal + // the algorithm for detecting cyclic structures is adapted from ES 5.1 + // section 15.12.3, abstract operation `JO` (http://es5.github.io/#x15.12.3) + var initedStack = !stackA; + stackA || (stackA = getArray()); + stackB || (stackB = getArray()); + + var length = stackA.length; + while (length--) { + if (stackA[length] == a) { + return stackB[length] == b; + } + } + var size = 0; + result = true; + + // add `a` and `b` to the stack of traversed objects + stackA.push(a); + stackB.push(b); + + // recursively compare objects and arrays (susceptible to call stack limits) + if (isArr) { + // compare lengths to determine if a deep comparison is necessary + length = a.length; + size = b.length; + result = size == length; + + if (result || isWhere) { + // deep compare the contents, ignoring non-numeric properties + while (size--) { + var index = length, + value = b[size]; + + if (isWhere) { + while (index--) { + if ((result = baseIsEqual(a[index], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } else if (!(result = baseIsEqual(a[size], value, callback, isWhere, stackA, stackB))) { + break; + } + } + } + } + else { + // deep compare objects using `forIn`, instead of `forOwn`, to avoid `Object.keys` + // which, in this case, is more costly + forIn(b, function(value, key, b) { + if (hasOwnProperty.call(b, key)) { + // count the number of properties. + size++; + // deep compare each property value. + return (result = hasOwnProperty.call(a, key) && baseIsEqual(a[key], value, callback, isWhere, stackA, stackB)); + } + }); + + if (result && !isWhere) { + // ensure both objects have the same number of properties + forIn(a, function(value, key, a) { + if (hasOwnProperty.call(a, key)) { + // `size` will be `-1` if `a` has more properties than `b` + return (result = --size > -1); + } + }); + } + } + stackA.pop(); + stackB.pop(); + + if (initedStack) { + releaseArray(stackA); + releaseArray(stackB); + } + return result; + } + + /** + * The base implementation of `_.merge` without argument juggling or support + * for `thisArg` binding. + * + * @private + * @param {Object} object The destination object. + * @param {Object} source The source object. + * @param {Function} [callback] The function to customize merging properties. + * @param {Array} [stackA=[]] Tracks traversed source objects. + * @param {Array} [stackB=[]] Associates values with source counterparts. + */ + function baseMerge(object, source, callback, stackA, stackB) { + (isArray(source) ? forEach : forOwn)(source, function(source, key) { + var found, + isArr, + result = source, + value = object[key]; + + if (source && ((isArr = isArray(source)) || isPlainObject(source))) { + // avoid merging previously merged cyclic sources + var stackLength = stackA.length; + while (stackLength--) { + if ((found = stackA[stackLength] == source)) { + value = stackB[stackLength]; + break; + } + } + if (!found) { + var isShallow; + if (callback) { + result = callback(value, source); + if ((isShallow = typeof result != 'undefined')) { + value = result; + } + } + if (!isShallow) { + value = isArr + ? (isArray(value) ? value : []) + : (isPlainObject(value) ? value : {}); + } + // add `source` and associated `value` to the stack of traversed objects + stackA.push(source); + stackB.push(value); + + // recursively merge objects and arrays (susceptible to call stack limits) + if (!isShallow) { + baseMerge(value, source, callback, stackA, stackB); + } + } + } + else { + if (callback) { + result = callback(value, source); + if (typeof result == 'undefined') { + result = source; + } + } + if (typeof result != 'undefined') { + value = result; + } + } + object[key] = value; + }); + } + + /** + * The base implementation of `_.random` without argument juggling or support + * for returning floating-point numbers. + * + * @private + * @param {number} min The minimum possible value. + * @param {number} max The maximum possible value. + * @returns {number} Returns a random number. + */ + function baseRandom(min, max) { + return min + floor(nativeRandom() * (max - min + 1)); + } + + /** + * The base implementation of `_.uniq` without support for callback shorthands + * or `thisArg` binding. + * + * @private + * @param {Array} array The array to process. + * @param {boolean} [isSorted=false] A flag to indicate that `array` is sorted. + * @param {Function} [callback] The function called per iteration. + * @returns {Array} Returns a duplicate-value-free array. + */ + function baseUniq(array, isSorted, callback) { + var index = -1, + indexOf = getIndexOf(), + length = array ? array.length : 0, + result = []; + + var isLarge = !isSorted && length >= largeArraySize && indexOf === baseIndexOf, + seen = (callback || isLarge) ? getArray() : result; + + if (isLarge) { + var cache = createCache(seen); + indexOf = cacheIndexOf; + seen = cache; + } + while (++index < length) { + var value = array[index], + computed = callback ? callback(value, index, array) : value; + + if (isSorted + ? !index || seen[seen.length - 1] !== computed + : indexOf(seen, computed) < 0 + ) { + if (callback || isLarge) { + seen.push(computed); + } + result.push(value); + } + } + if (isLarge) { + releaseArray(seen.array); + releaseObject(seen); + } else if (callback) { + releaseArray(seen); + } + return result; + } + + /** + * Creates a function that aggregates a collection, creating an object composed + * of keys generated from the results of running each element of the collection + * through a callback. The given `setter` function sets the keys and values + * of the composed object. + * + * @private + * @param {Function} setter The setter function. + * @returns {Function} Returns the new aggregator function. + */ + function createAggregator(setter) { + return function(collection, callback, thisArg) { + var result = {}; + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + setter(result, value, callback(value, index, collection), collection); + } + } else { + baseEach(collection, function(value, key, collection) { + setter(result, value, callback(value, key, collection), collection); + }); + } + return result; + }; + } + + /** + * Creates a function that, when called, either curries or invokes `func` + * with an optional `this` binding and partially applied arguments. + * + * @private + * @param {Function|string} func The function or method name to reference. + * @param {number} bitmask The bitmask of method flags to compose. + * The bitmask may be composed of the following flags: + * 1 - `_.bind` + * 2 - `_.bindKey` + * 4 - `_.curry` + * 8 - `_.curry` (bound) + * 16 - `_.partial` + * 32 - `_.partialRight` + * @param {Array} [partialArgs] An array of arguments to prepend to those + * provided to the new function. + * @param {Array} [partialRightArgs] An array of arguments to append to those + * provided to the new function. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {number} [arity] The arity of `func`. + * @returns {Function} Returns the new function. + */ + function createWrapper(func, bitmask, partialArgs, partialRightArgs, thisArg, arity) { + var isBind = bitmask & 1, + isBindKey = bitmask & 2, + isCurry = bitmask & 4, + isCurryBound = bitmask & 8, + isPartial = bitmask & 16, + isPartialRight = bitmask & 32; + + if (!isBindKey && !isFunction(func)) { + throw new TypeError; + } + if (isPartial && !partialArgs.length) { + bitmask &= ~16; + isPartial = partialArgs = false; + } + if (isPartialRight && !partialRightArgs.length) { + bitmask &= ~32; + isPartialRight = partialRightArgs = false; + } + var bindData = func && func.__bindData__; + if (bindData && bindData !== true) { + // clone `bindData` + bindData = slice(bindData); + if (bindData[2]) { + bindData[2] = slice(bindData[2]); + } + if (bindData[3]) { + bindData[3] = slice(bindData[3]); + } + // set `thisBinding` is not previously bound + if (isBind && !(bindData[1] & 1)) { + bindData[4] = thisArg; + } + // set if previously bound but not currently (subsequent curried functions) + if (!isBind && bindData[1] & 1) { + bitmask |= 8; + } + // set curried arity if not yet set + if (isCurry && !(bindData[1] & 4)) { + bindData[5] = arity; + } + // append partial left arguments + if (isPartial) { + push.apply(bindData[2] || (bindData[2] = []), partialArgs); + } + // append partial right arguments + if (isPartialRight) { + unshift.apply(bindData[3] || (bindData[3] = []), partialRightArgs); + } + // merge flags + bindData[1] |= bitmask; + return createWrapper.apply(null, bindData); + } + // fast path for `_.bind` + var creater = (bitmask == 1 || bitmask === 17) ? baseBind : baseCreateWrapper; + return creater([func, bitmask, partialArgs, partialRightArgs, thisArg, arity]); + } + + /** + * Creates compiled iteration functions. + * + * @private + * @param {...Object} [options] The compile options object(s). + * @param {string} [options.array] Code to determine if the iterable is an array or array-like. + * @param {boolean} [options.useHas] Specify using `hasOwnProperty` checks in the object loop. + * @param {Function} [options.keys] A reference to `_.keys` for use in own property iteration. + * @param {string} [options.args] A comma separated string of iteration function arguments. + * @param {string} [options.top] Code to execute before the iteration branches. + * @param {string} [options.loop] Code to execute in the object loop. + * @param {string} [options.bottom] Code to execute after the iteration branches. + * @returns {Function} Returns the compiled function. + */ + function createIterator() { + // data properties + iteratorData.shadowedProps = shadowedProps; + + // iterator options + iteratorData.array = iteratorData.bottom = iteratorData.loop = iteratorData.top = ''; + iteratorData.init = 'iterable'; + iteratorData.useHas = true; + + // merge options into a template data object + for (var object, index = 0; object = arguments[index]; index++) { + for (var key in object) { + iteratorData[key] = object[key]; + } + } + var args = iteratorData.args; + iteratorData.firstArg = /^[^,]+/.exec(args)[0]; + + // create the function factory + var factory = Function( + 'baseCreateCallback, errorClass, errorProto, hasOwnProperty, ' + + 'indicatorObject, isArguments, isArray, isString, keys, objectProto, ' + + 'objectTypes, nonEnumProps, stringClass, stringProto, toString', + 'return function(' + args + ') {\n' + iteratorTemplate(iteratorData) + '\n}' + ); + + // return the compiled function + return factory( + baseCreateCallback, errorClass, errorProto, hasOwnProperty, + indicatorObject, isArguments, isArray, isString, iteratorData.keys, objectProto, + objectTypes, nonEnumProps, stringClass, stringProto, toString + ); + } + + /** + * Used by `escape` to convert characters to HTML entities. + * + * @private + * @param {string} match The matched character to escape. + * @returns {string} Returns the escaped character. + */ + function escapeHtmlChar(match) { + return htmlEscapes[match]; + } + + /** + * Gets the appropriate "indexOf" function. If the `_.indexOf` method is + * customized, this method returns the custom method, otherwise it returns + * the `baseIndexOf` function. + * + * @private + * @returns {Function} Returns the "indexOf" function. + */ + function getIndexOf() { + var result = (result = lodash.indexOf) === indexOf ? baseIndexOf : result; + return result; + } + + /** + * Checks if `value` is a native function. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a native function, else `false`. + */ + function isNative(value) { + return typeof value == 'function' && reNative.test(value); + } + + /** + * Sets `this` binding data on a given function. + * + * @private + * @param {Function} func The function to set data on. + * @param {Array} value The data array to set. + */ + var setBindData = !defineProperty ? noop : function(func, value) { + descriptor.value = value; + defineProperty(func, '__bindData__', descriptor); + }; + + /** + * A fallback implementation of `isPlainObject` which checks if a given value + * is an object created by the `Object` constructor, assuming objects created + * by the `Object` constructor have no inherited enumerable properties and that + * there are no `Object.prototype` extensions. + * + * @private + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + */ + function shimIsPlainObject(value) { + var ctor, + result; + + // avoid non Object objects, `arguments` objects, and DOM elements + if (!(value && toString.call(value) == objectClass) || + (ctor = value.constructor, isFunction(ctor) && !(ctor instanceof ctor)) || + (!support.argsClass && isArguments(value)) || + (!support.nodeClass && isNode(value))) { + return false; + } + // IE < 9 iterates inherited properties before own properties. If the first + // iterated property is an object's own property then there are no inherited + // enumerable properties. + if (support.ownLast) { + forIn(value, function(value, key, object) { + result = hasOwnProperty.call(object, key); + return false; + }); + return result !== false; + } + // In most environments an object's own properties are iterated before + // its inherited properties. If the last iterated property is an object's + // own property then there are no inherited enumerable properties. + forIn(value, function(value, key) { + result = key; + }); + return typeof result == 'undefined' || hasOwnProperty.call(value, result); + } + + /** + * Used by `unescape` to convert HTML entities to characters. + * + * @private + * @param {string} match The matched character to unescape. + * @returns {string} Returns the unescaped character. + */ + function unescapeHtmlChar(match) { + return htmlUnescapes[match]; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Checks if `value` is an `arguments` object. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an `arguments` object, else `false`. + * @example + * + * (function() { return _.isArguments(arguments); })(1, 2, 3); + * // => true + * + * _.isArguments([1, 2, 3]); + * // => false + */ + function isArguments(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + toString.call(value) == argsClass || false; + } + // fallback for browsers that can't detect `arguments` objects by [[Class]] + if (!support.argsClass) { + isArguments = function(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + hasOwnProperty.call(value, 'callee') && !propertyIsEnumerable.call(value, 'callee') || false; + }; + } + + /** + * Checks if `value` is an array. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an array, else `false`. + * @example + * + * (function() { return _.isArray(arguments); })(); + * // => false + * + * _.isArray([1, 2, 3]); + * // => true + */ + var isArray = nativeIsArray || function(value) { + return value && typeof value == 'object' && typeof value.length == 'number' && + toString.call(value) == arrayClass || false; + }; + + /** + * A fallback implementation of `Object.keys` which produces an array of the + * given object's own enumerable property names. + * + * @private + * @type Function + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. + */ + var shimKeys = createIterator({ + 'args': 'object', + 'init': '[]', + 'top': 'if (!(objectTypes[typeof object])) return result', + 'loop': 'result.push(index)' + }); + + /** + * Creates an array composed of the own enumerable property names of an object. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names. + * @example + * + * _.keys({ 'one': 1, 'two': 2, 'three': 3 }); + * // => ['one', 'two', 'three'] (property order is not guaranteed across environments) + */ + var keys = !nativeKeys ? shimKeys : function(object) { + if (!isObject(object)) { + return []; + } + if ((support.enumPrototypes && typeof object == 'function') || + (support.nonEnumArgs && object.length && isArguments(object))) { + return shimKeys(object); + } + return nativeKeys(object); + }; + + /** Reusable iterator options shared by `each`, `forIn`, and `forOwn` */ + var eachIteratorOptions = { + 'args': 'collection, callback, thisArg', + 'top': "callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3)", + 'array': "typeof length == 'number'", + 'keys': keys, + 'loop': 'if (callback(iterable[index], index, collection) === false) return result' + }; + + /** Reusable iterator options for `assign` and `defaults` */ + var defaultsIteratorOptions = { + 'args': 'object, source, guard', + 'top': + 'var args = arguments,\n' + + ' argsIndex = 0,\n' + + " argsLength = typeof guard == 'number' ? 2 : args.length;\n" + + 'while (++argsIndex < argsLength) {\n' + + ' iterable = args[argsIndex];\n' + + ' if (iterable && objectTypes[typeof iterable]) {', + 'keys': keys, + 'loop': "if (typeof result[index] == 'undefined') result[index] = iterable[index]", + 'bottom': ' }\n}' + }; + + /** Reusable iterator options for `forIn` and `forOwn` */ + var forOwnIteratorOptions = { + 'top': 'if (!objectTypes[typeof iterable]) return result;\n' + eachIteratorOptions.top, + 'array': false + }; + + /** + * Used to convert characters to HTML entities: + * + * Though the `>` character is escaped for symmetry, characters like `>` and `/` + * don't require escaping in HTML and have no special meaning unless they're part + * of a tag or an unquoted attribute value. + * http://mathiasbynens.be/notes/ambiguous-ampersands (under "semi-related fun fact") + */ + var htmlEscapes = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + + /** Used to convert HTML entities to characters */ + var htmlUnescapes = invert(htmlEscapes); + + /** Used to match HTML entities and HTML characters */ + var reEscapedHtml = RegExp('(' + keys(htmlUnescapes).join('|') + ')', 'g'), + reUnescapedHtml = RegExp('[' + keys(htmlEscapes).join('') + ']', 'g'); + + /** + * A function compiled to iterate `arguments` objects, arrays, objects, and + * strings consistenly across environments, executing the callback for each + * element in the collection. The callback is bound to `thisArg` and invoked + * with three arguments; (value, index|key, collection). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @private + * @type Function + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + */ + var baseEach = createIterator(eachIteratorOptions); + + /*--------------------------------------------------------------------------*/ + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object. Subsequent sources will overwrite property assignments of previous + * sources. If a callback is provided it will be executed to produce the + * assigned values. The callback is bound to `thisArg` and invoked with two + * arguments; (objectValue, sourceValue). + * + * @static + * @memberOf _ + * @type Function + * @alias extend + * @category Objects + * @param {Object} object The destination object. + * @param {...Object} [source] The source objects. + * @param {Function} [callback] The function to customize assigning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the destination object. + * @example + * + * _.assign({ 'name': 'fred' }, { 'employer': 'slate' }); + * // => { 'name': 'fred', 'employer': 'slate' } + * + * var defaults = _.partialRight(_.assign, function(a, b) { + * return typeof a == 'undefined' ? b : a; + * }); + * + * var object = { 'name': 'barney' }; + * defaults(object, { 'name': 'fred', 'employer': 'slate' }); + * // => { 'name': 'barney', 'employer': 'slate' } + */ + var assign = createIterator(defaultsIteratorOptions, { + 'top': + defaultsIteratorOptions.top.replace(';', + ';\n' + + "if (argsLength > 3 && typeof args[argsLength - 2] == 'function') {\n" + + ' var callback = baseCreateCallback(args[--argsLength - 1], args[argsLength--], 2);\n' + + "} else if (argsLength > 2 && typeof args[argsLength - 1] == 'function') {\n" + + ' callback = args[--argsLength];\n' + + '}' + ), + 'loop': 'result[index] = callback ? callback(result[index], iterable[index]) : iterable[index]' + }); + + /** + * Creates a clone of `value`. If `isDeep` is `true` nested objects will also + * be cloned, otherwise they will be assigned by reference. If a callback + * is provided it will be executed to produce the cloned values. If the + * callback returns `undefined` cloning will be handled by the method instead. + * The callback is bound to `thisArg` and invoked with one argument; (value). + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to clone. + * @param {boolean} [isDeep=false] Specify a deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the cloned value. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * var shallow = _.clone(characters); + * shallow[0] === characters[0]; + * // => true + * + * var deep = _.clone(characters, true); + * deep[0] === characters[0]; + * // => false + * + * _.mixin({ + * 'clone': _.partialRight(_.clone, function(value) { + * return _.isElement(value) ? value.cloneNode(false) : undefined; + * }) + * }); + * + * var clone = _.clone(document.body); + * clone.childNodes.length; + * // => 0 + */ + function clone(value, isDeep, callback, thisArg) { + // allows working with "Collections" methods without using their `index` + // and `collection` arguments for `isDeep` and `callback` + if (typeof isDeep != 'boolean' && isDeep != null) { + thisArg = callback; + callback = isDeep; + isDeep = false; + } + return baseClone(value, isDeep, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); + } + + /** + * Creates a deep clone of `value`. If a callback is provided it will be + * executed to produce the cloned values. If the callback returns `undefined` + * cloning will be handled by the method instead. The callback is bound to + * `thisArg` and invoked with one argument; (value). + * + * Note: This method is loosely based on the structured clone algorithm. Functions + * and DOM nodes are **not** cloned. The enumerable properties of `arguments` objects and + * objects created by constructors other than `Object` are cloned to plain `Object` objects. + * See http://www.w3.org/TR/html5/infrastructure.html#internal-structured-cloning-algorithm. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to deep clone. + * @param {Function} [callback] The function to customize cloning values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the deep cloned value. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * var deep = _.cloneDeep(characters); + * deep[0] === characters[0]; + * // => false + * + * var view = { + * 'label': 'docs', + * 'node': element + * }; + * + * var clone = _.cloneDeep(view, function(value) { + * return _.isElement(value) ? value.cloneNode(true) : undefined; + * }); + * + * clone.node == view.node; + * // => false + */ + function cloneDeep(value, callback, thisArg) { + return baseClone(value, true, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 1)); + } + + /** + * Creates an object that inherits from the given `prototype` object. If a + * `properties` object is provided its own enumerable properties are assigned + * to the created object. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} prototype The object to inherit from. + * @param {Object} [properties] The properties to assign to the object. + * @returns {Object} Returns the new object. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * function Circle() { + * Shape.call(this); + * } + * + * Circle.prototype = _.create(Shape.prototype, { 'constructor': Circle }); + * + * var circle = new Circle; + * circle instanceof Circle; + * // => true + * + * circle instanceof Shape; + * // => true + */ + function create(prototype, properties) { + var result = baseCreate(prototype); + return properties ? assign(result, properties) : result; + } + + /** + * Assigns own enumerable properties of source object(s) to the destination + * object for all destination properties that resolve to `undefined`. Once a + * property is set, additional defaults of the same property will be ignored. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The destination object. + * @param {...Object} [source] The source objects. + * @param- {Object} [guard] Allows working with `_.reduce` without using its + * `key` and `object` arguments as sources. + * @returns {Object} Returns the destination object. + * @example + * + * var object = { 'name': 'barney' }; + * _.defaults(object, { 'name': 'fred', 'employer': 'slate' }); + * // => { 'name': 'barney', 'employer': 'slate' } + */ + var defaults = createIterator(defaultsIteratorOptions); + + /** + * This method is like `_.findIndex` except that it returns the key of the + * first element that passes the callback check, instead of the element itself. + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to search. + * @param {Function|Object|string} [callback=identity] The function called per + * iteration. If a property name or object is provided it will be used to + * create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {string|undefined} Returns the key of the found element, else `undefined`. + * @example + * + * var characters = { + * 'barney': { 'age': 36, 'blocked': false }, + * 'fred': { 'age': 40, 'blocked': true }, + * 'pebbles': { 'age': 1, 'blocked': false } + * }; + * + * _.findKey(characters, function(chr) { + * return chr.age < 40; + * }); + * // => 'barney' (property order is not guaranteed across environments) + * + * // using "_.where" callback shorthand + * _.findKey(characters, { 'age': 1 }); + * // => 'pebbles' + * + * // using "_.pluck" callback shorthand + * _.findKey(characters, 'blocked'); + * // => 'fred' + */ + function findKey(object, callback, thisArg) { + var result; + callback = lodash.createCallback(callback, thisArg, 3); + forOwn(object, function(value, key, object) { + if (callback(value, key, object)) { + result = key; + return false; + } + }); + return result; + } + + /** + * This method is like `_.findKey` except that it iterates over elements + * of a `collection` in the opposite order. + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to search. + * @param {Function|Object|string} [callback=identity] The function called per + * iteration. If a property name or object is provided it will be used to + * create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {string|undefined} Returns the key of the found element, else `undefined`. + * @example + * + * var characters = { + * 'barney': { 'age': 36, 'blocked': true }, + * 'fred': { 'age': 40, 'blocked': false }, + * 'pebbles': { 'age': 1, 'blocked': true } + * }; + * + * _.findLastKey(characters, function(chr) { + * return chr.age < 40; + * }); + * // => returns `pebbles`, assuming `_.findKey` returns `barney` + * + * // using "_.where" callback shorthand + * _.findLastKey(characters, { 'age': 40 }); + * // => 'fred' + * + * // using "_.pluck" callback shorthand + * _.findLastKey(characters, 'blocked'); + * // => 'pebbles' + */ + function findLastKey(object, callback, thisArg) { + var result; + callback = lodash.createCallback(callback, thisArg, 3); + forOwnRight(object, function(value, key, object) { + if (callback(value, key, object)) { + result = key; + return false; + } + }); + return result; + } + + /** + * Iterates over own and inherited enumerable properties of an object, + * executing the callback for each property. The callback is bound to `thisArg` + * and invoked with three arguments; (value, key, object). Callbacks may exit + * iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * Shape.prototype.move = function(x, y) { + * this.x += x; + * this.y += y; + * }; + * + * _.forIn(new Shape, function(value, key) { + * console.log(key); + * }); + * // => logs 'x', 'y', and 'move' (property order is not guaranteed across environments) + */ + var forIn = createIterator(eachIteratorOptions, forOwnIteratorOptions, { + 'useHas': false + }); + + /** + * This method is like `_.forIn` except that it iterates over elements + * of a `collection` in the opposite order. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * Shape.prototype.move = function(x, y) { + * this.x += x; + * this.y += y; + * }; + * + * _.forInRight(new Shape, function(value, key) { + * console.log(key); + * }); + * // => logs 'move', 'y', and 'x' assuming `_.forIn ` logs 'x', 'y', and 'move' + */ + function forInRight(object, callback, thisArg) { + var pairs = []; + + forIn(object, function(value, key) { + pairs.push(key, value); + }); + + var length = pairs.length; + callback = baseCreateCallback(callback, thisArg, 3); + while (length--) { + if (callback(pairs[length--], pairs[length], object) === false) { + break; + } + } + return object; + } + + /** + * Iterates over own enumerable properties of an object, executing the callback + * for each property. The callback is bound to `thisArg` and invoked with three + * arguments; (value, key, object). Callbacks may exit iteration early by + * explicitly returning `false`. + * + * @static + * @memberOf _ + * @type Function + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * _.forOwn({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { + * console.log(key); + * }); + * // => logs '0', '1', and 'length' (property order is not guaranteed across environments) + */ + var forOwn = createIterator(eachIteratorOptions, forOwnIteratorOptions); + + /** + * This method is like `_.forOwn` except that it iterates over elements + * of a `collection` in the opposite order. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns `object`. + * @example + * + * _.forOwnRight({ '0': 'zero', '1': 'one', 'length': 2 }, function(num, key) { + * console.log(key); + * }); + * // => logs 'length', '1', and '0' assuming `_.forOwn` logs '0', '1', and 'length' + */ + function forOwnRight(object, callback, thisArg) { + var props = keys(object), + length = props.length; + + callback = baseCreateCallback(callback, thisArg, 3); + while (length--) { + var key = props[length]; + if (callback(object[key], key, object) === false) { + break; + } + } + return object; + } + + /** + * Creates a sorted array of property names of all enumerable properties, + * own and inherited, of `object` that have function values. + * + * @static + * @memberOf _ + * @alias methods + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property names that have function values. + * @example + * + * _.functions(_); + * // => ['all', 'any', 'bind', 'bindAll', 'clone', 'compact', 'compose', ...] + */ + function functions(object) { + var result = []; + forIn(object, function(value, key) { + if (isFunction(value)) { + result.push(key); + } + }); + return result.sort(); + } + + /** + * Checks if the specified property name exists as a direct property of `object`, + * instead of an inherited property. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @param {string} key The name of the property to check. + * @returns {boolean} Returns `true` if key is a direct property, else `false`. + * @example + * + * _.has({ 'a': 1, 'b': 2, 'c': 3 }, 'b'); + * // => true + */ + function has(object, key) { + return object ? hasOwnProperty.call(object, key) : false; + } + + /** + * Creates an object composed of the inverted keys and values of the given object. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to invert. + * @returns {Object} Returns the created inverted object. + * @example + * + * _.invert({ 'first': 'fred', 'second': 'barney' }); + * // => { 'fred': 'first', 'barney': 'second' } + */ + function invert(object) { + var index = -1, + props = keys(object), + length = props.length, + result = {}; + + while (++index < length) { + var key = props[index]; + result[object[key]] = key; + } + return result; + } + + /** + * Checks if `value` is a boolean value. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a boolean value, else `false`. + * @example + * + * _.isBoolean(null); + * // => false + */ + function isBoolean(value) { + return value === true || value === false || + value && typeof value == 'object' && toString.call(value) == boolClass || false; + } + + /** + * Checks if `value` is a date. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a date, else `false`. + * @example + * + * _.isDate(new Date); + * // => true + */ + function isDate(value) { + return value && typeof value == 'object' && toString.call(value) == dateClass || false; + } + + /** + * Checks if `value` is a DOM element. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a DOM element, else `false`. + * @example + * + * _.isElement(document.body); + * // => true + */ + function isElement(value) { + return value && value.nodeType === 1 || false; + } + + /** + * Checks if `value` is empty. Arrays, strings, or `arguments` objects with a + * length of `0` and objects with no own enumerable properties are considered + * "empty". + * + * @static + * @memberOf _ + * @category Objects + * @param {Array|Object|string} value The value to inspect. + * @returns {boolean} Returns `true` if the `value` is empty, else `false`. + * @example + * + * _.isEmpty([1, 2, 3]); + * // => false + * + * _.isEmpty({}); + * // => true + * + * _.isEmpty(''); + * // => true + */ + function isEmpty(value) { + var result = true; + if (!value) { + return result; + } + var className = toString.call(value), + length = value.length; + + if ((className == arrayClass || className == stringClass || + (support.argsClass ? className == argsClass : isArguments(value))) || + (className == objectClass && typeof length == 'number' && isFunction(value.splice))) { + return !length; + } + forOwn(value, function() { + return (result = false); + }); + return result; + } + + /** + * Performs a deep comparison between two values to determine if they are + * equivalent to each other. If a callback is provided it will be executed + * to compare values. If the callback returns `undefined` comparisons will + * be handled by the method instead. The callback is bound to `thisArg` and + * invoked with two arguments; (a, b). + * + * @static + * @memberOf _ + * @category Objects + * @param {*} a The value to compare. + * @param {*} b The other value to compare. + * @param {Function} [callback] The function to customize comparing values. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {boolean} Returns `true` if the values are equivalent, else `false`. + * @example + * + * var object = { 'name': 'fred' }; + * var copy = { 'name': 'fred' }; + * + * object == copy; + * // => false + * + * _.isEqual(object, copy); + * // => true + * + * var words = ['hello', 'goodbye']; + * var otherWords = ['hi', 'goodbye']; + * + * _.isEqual(words, otherWords, function(a, b) { + * var reGreet = /^(?:hello|hi)$/i, + * aGreet = _.isString(a) && reGreet.test(a), + * bGreet = _.isString(b) && reGreet.test(b); + * + * return (aGreet || bGreet) ? (aGreet == bGreet) : undefined; + * }); + * // => true + */ + function isEqual(a, b, callback, thisArg) { + return baseIsEqual(a, b, typeof callback == 'function' && baseCreateCallback(callback, thisArg, 2)); + } + + /** + * Checks if `value` is, or can be coerced to, a finite number. + * + * Note: This is not the same as native `isFinite` which will return true for + * booleans and empty strings. See http://es5.github.io/#x15.1.2.5. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is finite, else `false`. + * @example + * + * _.isFinite(-101); + * // => true + * + * _.isFinite('10'); + * // => true + * + * _.isFinite(true); + * // => false + * + * _.isFinite(''); + * // => false + * + * _.isFinite(Infinity); + * // => false + */ + function isFinite(value) { + return nativeIsFinite(value) && !nativeIsNaN(parseFloat(value)); + } + + /** + * Checks if `value` is a function. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a function, else `false`. + * @example + * + * _.isFunction(_); + * // => true + */ + function isFunction(value) { + return typeof value == 'function'; + } + // fallback for older versions of Chrome and Safari + if (isFunction(/x/)) { + isFunction = function(value) { + return typeof value == 'function' && toString.call(value) == funcClass; + }; + } + + /** + * Checks if `value` is the language type of Object. + * (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`) + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is an object, else `false`. + * @example + * + * _.isObject({}); + * // => true + * + * _.isObject([1, 2, 3]); + * // => true + * + * _.isObject(1); + * // => false + */ + function isObject(value) { + // check if the value is the ECMAScript language type of Object + // http://es5.github.io/#x8 + // and avoid a V8 bug + // http://code.google.com/p/v8/issues/detail?id=2291 + return !!(value && objectTypes[typeof value]); + } + + /** + * Checks if `value` is `NaN`. + * + * Note: This is not the same as native `isNaN` which will return `true` for + * `undefined` and other non-numeric values. See http://es5.github.io/#x15.1.2.4. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `NaN`, else `false`. + * @example + * + * _.isNaN(NaN); + * // => true + * + * _.isNaN(new Number(NaN)); + * // => true + * + * isNaN(undefined); + * // => true + * + * _.isNaN(undefined); + * // => false + */ + function isNaN(value) { + // `NaN` as a primitive is the only value that is not equal to itself + // (perform the [[Class]] check first to avoid errors with some host objects in IE) + return isNumber(value) && value != +value; + } + + /** + * Checks if `value` is `null`. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `null`, else `false`. + * @example + * + * _.isNull(null); + * // => true + * + * _.isNull(undefined); + * // => false + */ + function isNull(value) { + return value === null; + } + + /** + * Checks if `value` is a number. + * + * Note: `NaN` is considered a number. See http://es5.github.io/#x8.5. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a number, else `false`. + * @example + * + * _.isNumber(8.4 * 5); + * // => true + */ + function isNumber(value) { + return typeof value == 'number' || + value && typeof value == 'object' && toString.call(value) == numberClass || false; + } + + /** + * Checks if `value` is an object created by the `Object` constructor. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if `value` is a plain object, else `false`. + * @example + * + * function Shape() { + * this.x = 0; + * this.y = 0; + * } + * + * _.isPlainObject(new Shape); + * // => false + * + * _.isPlainObject([1, 2, 3]); + * // => false + * + * _.isPlainObject({ 'x': 0, 'y': 0 }); + * // => true + */ + var isPlainObject = !getPrototypeOf ? shimIsPlainObject : function(value) { + if (!(value && toString.call(value) == objectClass) || (!support.argsClass && isArguments(value))) { + return false; + } + var valueOf = value.valueOf, + objProto = isNative(valueOf) && (objProto = getPrototypeOf(valueOf)) && getPrototypeOf(objProto); + + return objProto + ? (value == objProto || getPrototypeOf(value) == objProto) + : shimIsPlainObject(value); + }; + + /** + * Checks if `value` is a regular expression. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a regular expression, else `false`. + * @example + * + * _.isRegExp(/fred/); + * // => true + */ + function isRegExp(value) { + return value && objectTypes[typeof value] && toString.call(value) == regexpClass || false; + } + + /** + * Checks if `value` is a string. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is a string, else `false`. + * @example + * + * _.isString('fred'); + * // => true + */ + function isString(value) { + return typeof value == 'string' || + value && typeof value == 'object' && toString.call(value) == stringClass || false; + } + + /** + * Checks if `value` is `undefined`. + * + * @static + * @memberOf _ + * @category Objects + * @param {*} value The value to check. + * @returns {boolean} Returns `true` if the `value` is `undefined`, else `false`. + * @example + * + * _.isUndefined(void 0); + * // => true + */ + function isUndefined(value) { + return typeof value == 'undefined'; + } + + /** + * Creates an object with the same keys as `object` and values generated by + * running each own enumerable property of `object` through the callback. + * The callback is bound to `thisArg` and invoked with three arguments; + * (value, key, object). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new object with values of the results of each `callback` execution. + * @example + * + * _.mapValues({ 'a': 1, 'b': 2, 'c': 3} , function(num) { return num * 3; }); + * // => { 'a': 3, 'b': 6, 'c': 9 } + * + * var characters = { + * 'fred': { 'name': 'fred', 'age': 40 }, + * 'pebbles': { 'name': 'pebbles', 'age': 1 } + * }; + * + * // using "_.pluck" callback shorthand + * _.mapValues(characters, 'age'); + * // => { 'fred': 40, 'pebbles': 1 } + */ + function mapValues(object, callback, thisArg) { + var result = {}; + callback = lodash.createCallback(callback, thisArg, 3); + + forOwn(object, function(value, key, object) { + result[key] = callback(value, key, object); + }); + return result; + } + + /** + * Recursively merges own enumerable properties of the source object(s), that + * don't resolve to `undefined` into the destination object. Subsequent sources + * will overwrite property assignments of previous sources. If a callback is + * provided it will be executed to produce the merged values of the destination + * and source properties. If the callback returns `undefined` merging will + * be handled by the method instead. The callback is bound to `thisArg` and + * invoked with two arguments; (objectValue, sourceValue). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The destination object. + * @param {...Object} [source] The source objects. + * @param {Function} [callback] The function to customize merging properties. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the destination object. + * @example + * + * var names = { + * 'characters': [ + * { 'name': 'barney' }, + * { 'name': 'fred' } + * ] + * }; + * + * var ages = { + * 'characters': [ + * { 'age': 36 }, + * { 'age': 40 } + * ] + * }; + * + * _.merge(names, ages); + * // => { 'characters': [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] } + * + * var food = { + * 'fruits': ['apple'], + * 'vegetables': ['beet'] + * }; + * + * var otherFood = { + * 'fruits': ['banana'], + * 'vegetables': ['carrot'] + * }; + * + * _.merge(food, otherFood, function(a, b) { + * return _.isArray(a) ? a.concat(b) : undefined; + * }); + * // => { 'fruits': ['apple', 'banana'], 'vegetables': ['beet', 'carrot] } + */ + function merge(object) { + var args = arguments, + length = 2; + + if (!isObject(object)) { + return object; + } + // allows working with `_.reduce` and `_.reduceRight` without using + // their `index` and `collection` arguments + if (typeof args[2] != 'number') { + length = args.length; + } + if (length > 3 && typeof args[length - 2] == 'function') { + var callback = baseCreateCallback(args[--length - 1], args[length--], 2); + } else if (length > 2 && typeof args[length - 1] == 'function') { + callback = args[--length]; + } + var sources = slice(arguments, 1, length), + index = -1, + stackA = getArray(), + stackB = getArray(); + + while (++index < length) { + baseMerge(object, sources[index], callback, stackA, stackB); + } + releaseArray(stackA); + releaseArray(stackB); + return object; + } + + /** + * Creates a shallow clone of `object` excluding the specified properties. + * Property names may be specified as individual arguments or as arrays of + * property names. If a callback is provided it will be executed for each + * property of `object` omitting the properties the callback returns truey + * for. The callback is bound to `thisArg` and invoked with three arguments; + * (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The source object. + * @param {Function|...string|string[]} [callback] The properties to omit or the + * function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns an object without the omitted properties. + * @example + * + * _.omit({ 'name': 'fred', 'age': 40 }, 'age'); + * // => { 'name': 'fred' } + * + * _.omit({ 'name': 'fred', 'age': 40 }, function(value) { + * return typeof value == 'number'; + * }); + * // => { 'name': 'fred' } + */ + function omit(object, callback, thisArg) { + var result = {}; + if (typeof callback != 'function') { + var props = []; + forIn(object, function(value, key) { + props.push(key); + }); + props = baseDifference(props, baseFlatten(arguments, true, false, 1)); + + var index = -1, + length = props.length; + + while (++index < length) { + var key = props[index]; + result[key] = object[key]; + } + } else { + callback = lodash.createCallback(callback, thisArg, 3); + forIn(object, function(value, key, object) { + if (!callback(value, key, object)) { + result[key] = value; + } + }); + } + return result; + } + + /** + * Creates a two dimensional array of an object's key-value pairs, + * i.e. `[[key1, value1], [key2, value2]]`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns new array of key-value pairs. + * @example + * + * _.pairs({ 'barney': 36, 'fred': 40 }); + * // => [['barney', 36], ['fred', 40]] (property order is not guaranteed across environments) + */ + function pairs(object) { + var index = -1, + props = keys(object), + length = props.length, + result = Array(length); + + while (++index < length) { + var key = props[index]; + result[index] = [key, object[key]]; + } + return result; + } + + /** + * Creates a shallow clone of `object` composed of the specified properties. + * Property names may be specified as individual arguments or as arrays of + * property names. If a callback is provided it will be executed for each + * property of `object` picking the properties the callback returns truey + * for. The callback is bound to `thisArg` and invoked with three arguments; + * (value, key, object). + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The source object. + * @param {Function|...string|string[]} [callback] The function called per + * iteration or property names to pick, specified as individual property + * names or arrays of property names. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns an object composed of the picked properties. + * @example + * + * _.pick({ 'name': 'fred', '_userid': 'fred1' }, 'name'); + * // => { 'name': 'fred' } + * + * _.pick({ 'name': 'fred', '_userid': 'fred1' }, function(value, key) { + * return key.charAt(0) != '_'; + * }); + * // => { 'name': 'fred' } + */ + function pick(object, callback, thisArg) { + var result = {}; + if (typeof callback != 'function') { + var index = -1, + props = baseFlatten(arguments, true, false, 1), + length = isObject(object) ? props.length : 0; + + while (++index < length) { + var key = props[index]; + if (key in object) { + result[key] = object[key]; + } + } + } else { + callback = lodash.createCallback(callback, thisArg, 3); + forIn(object, function(value, key, object) { + if (callback(value, key, object)) { + result[key] = value; + } + }); + } + return result; + } + + /** + * An alternative to `_.reduce` this method transforms `object` to a new + * `accumulator` object which is the result of running each of its own + * enumerable properties through a callback, with each callback execution + * potentially mutating the `accumulator` object. The callback is bound to + * `thisArg` and invoked with four arguments; (accumulator, value, key, object). + * Callbacks may exit iteration early by explicitly returning `false`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Array|Object} object The object to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [accumulator] The custom accumulator value. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the accumulated value. + * @example + * + * var squares = _.transform([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], function(result, num) { + * num *= num; + * if (num % 2) { + * return result.push(num) < 3; + * } + * }); + * // => [1, 9, 25] + * + * var mapped = _.transform({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { + * result[key] = num * 3; + * }); + * // => { 'a': 3, 'b': 6, 'c': 9 } + */ + function transform(object, callback, accumulator, thisArg) { + var isArr = isArray(object); + if (accumulator == null) { + if (isArr) { + accumulator = []; + } else { + var ctor = object && object.constructor, + proto = ctor && ctor.prototype; + + accumulator = baseCreate(proto); + } + } + if (callback) { + callback = lodash.createCallback(callback, thisArg, 4); + (isArr ? baseEach : forOwn)(object, function(value, index, object) { + return callback(accumulator, value, index, object); + }); + } + return accumulator; + } + + /** + * Creates an array composed of the own enumerable property values of `object`. + * + * @static + * @memberOf _ + * @category Objects + * @param {Object} object The object to inspect. + * @returns {Array} Returns an array of property values. + * @example + * + * _.values({ 'one': 1, 'two': 2, 'three': 3 }); + * // => [1, 2, 3] (property order is not guaranteed across environments) + */ + function values(object) { + var index = -1, + props = keys(object), + length = props.length, + result = Array(length); + + while (++index < length) { + result[index] = object[props[index]]; + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates an array of elements from the specified indexes, or keys, of the + * `collection`. Indexes may be specified as individual arguments or as arrays + * of indexes. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {...(number|number[]|string|string[])} [index] The indexes of `collection` + * to retrieve, specified as individual indexes or arrays of indexes. + * @returns {Array} Returns a new array of elements corresponding to the + * provided indexes. + * @example + * + * _.at(['a', 'b', 'c', 'd', 'e'], [0, 2, 4]); + * // => ['a', 'c', 'e'] + * + * _.at(['fred', 'barney', 'pebbles'], 0, 2); + * // => ['fred', 'pebbles'] + */ + function at(collection) { + var args = arguments, + index = -1, + props = baseFlatten(args, true, false, 1), + length = (args[2] && args[2][args[1]] === collection) ? 1 : props.length, + result = Array(length); + + if (support.unindexedChars && isString(collection)) { + collection = collection.split(''); + } + while(++index < length) { + result[index] = collection[props[index]]; + } + return result; + } + + /** + * Checks if a given value is present in a collection using strict equality + * for comparisons, i.e. `===`. If `fromIndex` is negative, it is used as the + * offset from the end of the collection. + * + * @static + * @memberOf _ + * @alias include + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {*} target The value to check for. + * @param {number} [fromIndex=0] The index to search from. + * @returns {boolean} Returns `true` if the `target` element is found, else `false`. + * @example + * + * _.contains([1, 2, 3], 1); + * // => true + * + * _.contains([1, 2, 3], 1, 2); + * // => false + * + * _.contains({ 'name': 'fred', 'age': 40 }, 'fred'); + * // => true + * + * _.contains('pebbles', 'eb'); + * // => true + */ + function contains(collection, target, fromIndex) { + var index = -1, + indexOf = getIndexOf(), + length = collection ? collection.length : 0, + result = false; + + fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex) || 0; + if (isArray(collection)) { + result = indexOf(collection, target, fromIndex) > -1; + } else if (typeof length == 'number') { + result = (isString(collection) ? collection.indexOf(target, fromIndex) : indexOf(collection, target, fromIndex)) > -1; + } else { + baseEach(collection, function(value) { + if (++index >= fromIndex) { + return !(result = value === target); + } + }); + } + return result; + } + + /** + * Creates an object composed of keys generated from the results of running + * each element of `collection` through the callback. The corresponding value + * of each key is the number of times the key was returned by the callback. + * The callback is bound to `thisArg` and invoked with three arguments; + * (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.countBy([4.3, 6.1, 6.4], function(num) { return Math.floor(num); }); + * // => { '4': 1, '6': 2 } + * + * _.countBy([4.3, 6.1, 6.4], function(num) { return this.floor(num); }, Math); + * // => { '4': 1, '6': 2 } + * + * _.countBy(['one', 'two', 'three'], 'length'); + * // => { '3': 2, '5': 1 } + */ + var countBy = createAggregator(function(result, value, key) { + (hasOwnProperty.call(result, key) ? result[key]++ : result[key] = 1); + }); + + /** + * Checks if the given callback returns truey value for **all** elements of + * a collection. The callback is bound to `thisArg` and invoked with three + * arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias all + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {boolean} Returns `true` if all elements passed the callback check, + * else `false`. + * @example + * + * _.every([true, 1, null, 'yes']); + * // => false + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.every(characters, 'age'); + * // => true + * + * // using "_.where" callback shorthand + * _.every(characters, { 'age': 36 }); + * // => false + */ + function every(collection, callback, thisArg) { + var result = true; + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + if (!(result = !!callback(collection[index], index, collection))) { + break; + } + } + } else { + baseEach(collection, function(value, index, collection) { + return (result = !!callback(value, index, collection)); + }); + } + return result; + } + + /** + * Iterates over elements of a collection, returning an array of all elements + * the callback returns truey for. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias select + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of elements that passed the callback check. + * @example + * + * var evens = _.filter([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); + * // => [2, 4, 6] + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.filter(characters, 'blocked'); + * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] + * + * // using "_.where" callback shorthand + * _.filter(characters, { 'age': 36 }); + * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] + */ + function filter(collection, callback, thisArg) { + var result = []; + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + result.push(value); + } + } + } else { + baseEach(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result.push(value); + } + }); + } + return result; + } + + /** + * Iterates over elements of a collection, returning the first element that + * the callback returns truey for. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias detect, findWhere + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the found element, else `undefined`. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true }, + * { 'name': 'pebbles', 'age': 1, 'blocked': false } + * ]; + * + * _.find(characters, function(chr) { + * return chr.age < 40; + * }); + * // => { 'name': 'barney', 'age': 36, 'blocked': false } + * + * // using "_.where" callback shorthand + * _.find(characters, { 'age': 1 }); + * // => { 'name': 'pebbles', 'age': 1, 'blocked': false } + * + * // using "_.pluck" callback shorthand + * _.find(characters, 'blocked'); + * // => { 'name': 'fred', 'age': 40, 'blocked': true } + */ + function find(collection, callback, thisArg) { + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (callback(value, index, collection)) { + return value; + } + } + } else { + var result; + baseEach(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result = value; + return false; + } + }); + return result; + } + } + + /** + * This method is like `_.find` except that it iterates over elements + * of a `collection` from right to left. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the found element, else `undefined`. + * @example + * + * _.findLast([1, 2, 3, 4], function(num) { + * return num % 2 == 1; + * }); + * // => 3 + */ + function findLast(collection, callback, thisArg) { + var result; + callback = lodash.createCallback(callback, thisArg, 3); + forEachRight(collection, function(value, index, collection) { + if (callback(value, index, collection)) { + result = value; + return false; + } + }); + return result; + } + + /** + * Iterates over elements of a collection, executing the callback for each + * element. The callback is bound to `thisArg` and invoked with three arguments; + * (value, index|key, collection). Callbacks may exit iteration early by + * explicitly returning `false`. + * + * Note: As with other "Collections" methods, objects with a `length` property + * are iterated like arrays. To avoid this behavior `_.forIn` or `_.forOwn` + * may be used for object iteration. + * + * @static + * @memberOf _ + * @alias each + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + * @example + * + * _([1, 2, 3]).forEach(function(num) { console.log(num); }).join(','); + * // => logs each number and returns '1,2,3' + * + * _.forEach({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { console.log(num); }); + * // => logs each number and returns the object (property order is not guaranteed across environments) + */ + function forEach(collection, callback, thisArg) { + if (callback && typeof thisArg == 'undefined' && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + if (callback(collection[index], index, collection) === false) { + break; + } + } + } else { + baseEach(collection, callback, thisArg); + } + return collection; + } + + /** + * This method is like `_.forEach` except that it iterates over elements + * of a `collection` from right to left. + * + * @static + * @memberOf _ + * @alias eachRight + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array|Object|string} Returns `collection`. + * @example + * + * _([1, 2, 3]).forEachRight(function(num) { console.log(num); }).join(','); + * // => logs each number from right to left and returns '3,2,1' + */ + function forEachRight(collection, callback, thisArg) { + var iterable = collection, + length = collection ? collection.length : 0; + + callback = callback && typeof thisArg == 'undefined' ? callback : baseCreateCallback(callback, thisArg, 3); + if (isArray(collection)) { + while (length--) { + if (callback(collection[length], length, collection) === false) { + break; + } + } + } else { + if (typeof length != 'number') { + var props = keys(collection); + length = props.length; + } else if (support.unindexedChars && isString(collection)) { + iterable = collection.split(''); + } + baseEach(collection, function(value, key, collection) { + key = props ? props[--length] : --length; + return callback(iterable[key], key, collection); + }); + } + return collection; + } + + /** + * Creates an object composed of keys generated from the results of running + * each element of a collection through the callback. The corresponding value + * of each key is an array of the elements responsible for generating the key. + * The callback is bound to `thisArg` and invoked with three arguments; + * (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false` + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * _.groupBy([4.2, 6.1, 6.4], function(num) { return Math.floor(num); }); + * // => { '4': [4.2], '6': [6.1, 6.4] } + * + * _.groupBy([4.2, 6.1, 6.4], function(num) { return this.floor(num); }, Math); + * // => { '4': [4.2], '6': [6.1, 6.4] } + * + * // using "_.pluck" callback shorthand + * _.groupBy(['one', 'two', 'three'], 'length'); + * // => { '3': ['one', 'two'], '5': ['three'] } + */ + var groupBy = createAggregator(function(result, value, key) { + (hasOwnProperty.call(result, key) ? result[key] : result[key] = []).push(value); + }); + + /** + * Creates an object composed of keys generated from the results of running + * each element of the collection through the given callback. The corresponding + * value of each key is the last element responsible for generating the key. + * The callback is bound to `thisArg` and invoked with three arguments; + * (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Object} Returns the composed aggregate object. + * @example + * + * var keys = [ + * { 'dir': 'left', 'code': 97 }, + * { 'dir': 'right', 'code': 100 } + * ]; + * + * _.indexBy(keys, 'dir'); + * // => { 'left': { 'dir': 'left', 'code': 97 }, 'right': { 'dir': 'right', 'code': 100 } } + * + * _.indexBy(keys, function(key) { return String.fromCharCode(key.code); }); + * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } + * + * _.indexBy(characters, function(key) { this.fromCharCode(key.code); }, String); + * // => { 'a': { 'dir': 'left', 'code': 97 }, 'd': { 'dir': 'right', 'code': 100 } } + */ + var indexBy = createAggregator(function(result, value, key) { + result[key] = value; + }); + + /** + * Invokes the method named by `methodName` on each element in the `collection` + * returning an array of the results of each invoked method. Additional arguments + * will be provided to each invoked method. If `methodName` is a function it + * will be invoked for, and `this` bound to, each element in the `collection`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|string} methodName The name of the method to invoke or + * the function invoked per iteration. + * @param {...*} [arg] Arguments to invoke the method with. + * @returns {Array} Returns a new array of the results of each invoked method. + * @example + * + * _.invoke([[5, 1, 7], [3, 2, 1]], 'sort'); + * // => [[1, 5, 7], [1, 2, 3]] + * + * _.invoke([123, 456], String.prototype.split, ''); + * // => [['1', '2', '3'], ['4', '5', '6']] + */ + function invoke(collection, methodName) { + var args = slice(arguments, 2), + index = -1, + isFunc = typeof methodName == 'function', + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + forEach(collection, function(value) { + result[++index] = (isFunc ? methodName : value[methodName]).apply(value, args); + }); + return result; + } + + /** + * Creates an array of values by running each element in the collection + * through the callback. The callback is bound to `thisArg` and invoked with + * three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias collect + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of the results of each `callback` execution. + * @example + * + * _.map([1, 2, 3], function(num) { return num * 3; }); + * // => [3, 6, 9] + * + * _.map({ 'one': 1, 'two': 2, 'three': 3 }, function(num) { return num * 3; }); + * // => [3, 6, 9] (property order is not guaranteed across environments) + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.map(characters, 'name'); + * // => ['barney', 'fred'] + */ + function map(collection, callback, thisArg) { + var index = -1, + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + callback = lodash.createCallback(callback, thisArg, 3); + if (isArray(collection)) { + while (++index < length) { + result[index] = callback(collection[index], index, collection); + } + } else { + baseEach(collection, function(value, key, collection) { + result[++index] = callback(value, key, collection); + }); + } + return result; + } + + /** + * Retrieves the maximum value of a collection. If the collection is empty or + * falsey `-Infinity` is returned. If a callback is provided it will be executed + * for each value in the collection to generate the criterion by which the value + * is ranked. The callback is bound to `thisArg` and invoked with three + * arguments; (value, index, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the maximum value. + * @example + * + * _.max([4, 2, 8, 6]); + * // => 8 + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * _.max(characters, function(chr) { return chr.age; }); + * // => { 'name': 'fred', 'age': 40 }; + * + * // using "_.pluck" callback shorthand + * _.max(characters, 'age'); + * // => { 'name': 'fred', 'age': 40 }; + */ + function max(collection, callback, thisArg) { + var computed = -Infinity, + result = computed; + + // allows working with functions like `_.map` without using + // their `index` argument as a callback + if (typeof callback != 'function' && thisArg && thisArg[callback] === collection) { + callback = null; + } + if (callback == null && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (value > result) { + result = value; + } + } + } else { + callback = (callback == null && isString(collection)) + ? charAtCallback + : lodash.createCallback(callback, thisArg, 3); + + baseEach(collection, function(value, index, collection) { + var current = callback(value, index, collection); + if (current > computed) { + computed = current; + result = value; + } + }); + } + return result; + } + + /** + * Retrieves the minimum value of a collection. If the collection is empty or + * falsey `Infinity` is returned. If a callback is provided it will be executed + * for each value in the collection to generate the criterion by which the value + * is ranked. The callback is bound to `thisArg` and invoked with three + * arguments; (value, index, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the minimum value. + * @example + * + * _.min([4, 2, 8, 6]); + * // => 2 + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * _.min(characters, function(chr) { return chr.age; }); + * // => { 'name': 'barney', 'age': 36 }; + * + * // using "_.pluck" callback shorthand + * _.min(characters, 'age'); + * // => { 'name': 'barney', 'age': 36 }; + */ + function min(collection, callback, thisArg) { + var computed = Infinity, + result = computed; + + // allows working with functions like `_.map` without using + // their `index` argument as a callback + if (typeof callback != 'function' && thisArg && thisArg[callback] === collection) { + callback = null; + } + if (callback == null && isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + var value = collection[index]; + if (value < result) { + result = value; + } + } + } else { + callback = (callback == null && isString(collection)) + ? charAtCallback + : lodash.createCallback(callback, thisArg, 3); + + baseEach(collection, function(value, index, collection) { + var current = callback(value, index, collection); + if (current < computed) { + computed = current; + result = value; + } + }); + } + return result; + } + + /** + * Retrieves the value of a specified property from all elements in the collection. + * + * @static + * @memberOf _ + * @type Function + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {string} property The name of the property to pluck. + * @returns {Array} Returns a new array of property values. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * _.pluck(characters, 'name'); + * // => ['barney', 'fred'] + */ + var pluck = map; + + /** + * Reduces a collection to a value which is the accumulated result of running + * each element in the collection through the callback, where each successive + * callback execution consumes the return value of the previous execution. If + * `accumulator` is not provided the first element of the collection will be + * used as the initial `accumulator` value. The callback is bound to `thisArg` + * and invoked with four arguments; (accumulator, value, index|key, collection). + * + * @static + * @memberOf _ + * @alias foldl, inject + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [accumulator] Initial value of the accumulator. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the accumulated value. + * @example + * + * var sum = _.reduce([1, 2, 3], function(sum, num) { + * return sum + num; + * }); + * // => 6 + * + * var mapped = _.reduce({ 'a': 1, 'b': 2, 'c': 3 }, function(result, num, key) { + * result[key] = num * 3; + * return result; + * }, {}); + * // => { 'a': 3, 'b': 6, 'c': 9 } + */ + function reduce(collection, callback, accumulator, thisArg) { + var noaccum = arguments.length < 3; + callback = lodash.createCallback(callback, thisArg, 4); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + if (noaccum) { + accumulator = collection[++index]; + } + while (++index < length) { + accumulator = callback(accumulator, collection[index], index, collection); + } + } else { + baseEach(collection, function(value, index, collection) { + accumulator = noaccum + ? (noaccum = false, value) + : callback(accumulator, value, index, collection) + }); + } + return accumulator; + } + + /** + * This method is like `_.reduce` except that it iterates over elements + * of a `collection` from right to left. + * + * @static + * @memberOf _ + * @alias foldr + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function} [callback=identity] The function called per iteration. + * @param {*} [accumulator] Initial value of the accumulator. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the accumulated value. + * @example + * + * var list = [[0, 1], [2, 3], [4, 5]]; + * var flat = _.reduceRight(list, function(a, b) { return a.concat(b); }, []); + * // => [4, 5, 2, 3, 0, 1] + */ + function reduceRight(collection, callback, accumulator, thisArg) { + var noaccum = arguments.length < 3; + callback = lodash.createCallback(callback, thisArg, 4); + forEachRight(collection, function(value, index, collection) { + accumulator = noaccum + ? (noaccum = false, value) + : callback(accumulator, value, index, collection); + }); + return accumulator; + } + + /** + * The opposite of `_.filter` this method returns the elements of a + * collection that the callback does **not** return truey for. + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of elements that failed the callback check. + * @example + * + * var odds = _.reject([1, 2, 3, 4, 5, 6], function(num) { return num % 2 == 0; }); + * // => [1, 3, 5] + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.reject(characters, 'blocked'); + * // => [{ 'name': 'barney', 'age': 36, 'blocked': false }] + * + * // using "_.where" callback shorthand + * _.reject(characters, { 'age': 36 }); + * // => [{ 'name': 'fred', 'age': 40, 'blocked': true }] + */ + function reject(collection, callback, thisArg) { + callback = lodash.createCallback(callback, thisArg, 3); + return filter(collection, function(value, index, collection) { + return !callback(value, index, collection); + }); + } + + /** + * Retrieves a random element or `n` random elements from a collection. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to sample. + * @param {number} [n] The number of elements to sample. + * @param- {Object} [guard] Allows working with functions like `_.map` + * without using their `index` arguments as `n`. + * @returns {Array} Returns the random sample(s) of `collection`. + * @example + * + * _.sample([1, 2, 3, 4]); + * // => 2 + * + * _.sample([1, 2, 3, 4], 2); + * // => [3, 1] + */ + function sample(collection, n, guard) { + if (collection && typeof collection.length != 'number') { + collection = values(collection); + } else if (support.unindexedChars && isString(collection)) { + collection = collection.split(''); + } + if (n == null || guard) { + return collection ? collection[baseRandom(0, collection.length - 1)] : undefined; + } + var result = shuffle(collection); + result.length = nativeMin(nativeMax(0, n), result.length); + return result; + } + + /** + * Creates an array of shuffled values, using a version of the Fisher-Yates + * shuffle. See http://en.wikipedia.org/wiki/Fisher-Yates_shuffle. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to shuffle. + * @returns {Array} Returns a new shuffled collection. + * @example + * + * _.shuffle([1, 2, 3, 4, 5, 6]); + * // => [4, 1, 6, 3, 5, 2] + */ + function shuffle(collection) { + var index = -1, + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + forEach(collection, function(value) { + var rand = baseRandom(0, ++index); + result[index] = result[rand]; + result[rand] = value; + }); + return result; + } + + /** + * Gets the size of the `collection` by returning `collection.length` for arrays + * and array-like objects or the number of own enumerable properties for objects. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to inspect. + * @returns {number} Returns `collection.length` or number of own enumerable properties. + * @example + * + * _.size([1, 2]); + * // => 2 + * + * _.size({ 'one': 1, 'two': 2, 'three': 3 }); + * // => 3 + * + * _.size('pebbles'); + * // => 7 + */ + function size(collection) { + var length = collection ? collection.length : 0; + return typeof length == 'number' ? length : keys(collection).length; + } + + /** + * Checks if the callback returns a truey value for **any** element of a + * collection. The function returns as soon as it finds a passing value and + * does not iterate over the entire collection. The callback is bound to + * `thisArg` and invoked with three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias any + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {boolean} Returns `true` if any element passed the callback check, + * else `false`. + * @example + * + * _.some([null, 0, 'yes', false], Boolean); + * // => true + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true } + * ]; + * + * // using "_.pluck" callback shorthand + * _.some(characters, 'blocked'); + * // => true + * + * // using "_.where" callback shorthand + * _.some(characters, { 'age': 1 }); + * // => false + */ + function some(collection, callback, thisArg) { + var result; + callback = lodash.createCallback(callback, thisArg, 3); + + if (isArray(collection)) { + var index = -1, + length = collection.length; + + while (++index < length) { + if ((result = callback(collection[index], index, collection))) { + break; + } + } + } else { + baseEach(collection, function(value, index, collection) { + return !(result = callback(value, index, collection)); + }); + } + return !!result; + } + + /** + * Creates an array of elements, sorted in ascending order by the results of + * running each element in a collection through the callback. This method + * performs a stable sort, that is, it will preserve the original sort order + * of equal elements. The callback is bound to `thisArg` and invoked with + * three arguments; (value, index|key, collection). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an array of property names is provided for `callback` the collection + * will be sorted by each property value. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Array|Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of sorted elements. + * @example + * + * _.sortBy([1, 2, 3], function(num) { return Math.sin(num); }); + * // => [3, 1, 2] + * + * _.sortBy([1, 2, 3], function(num) { return this.sin(num); }, Math); + * // => [3, 1, 2] + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 }, + * { 'name': 'barney', 'age': 26 }, + * { 'name': 'fred', 'age': 30 } + * ]; + * + * // using "_.pluck" callback shorthand + * _.map(_.sortBy(characters, 'age'), _.values); + * // => [['barney', 26], ['fred', 30], ['barney', 36], ['fred', 40]] + * + * // sorting by multiple properties + * _.map(_.sortBy(characters, ['name', 'age']), _.values); + * // = > [['barney', 26], ['barney', 36], ['fred', 30], ['fred', 40]] + */ + function sortBy(collection, callback, thisArg) { + var index = -1, + isArr = isArray(callback), + length = collection ? collection.length : 0, + result = Array(typeof length == 'number' ? length : 0); + + if (!isArr) { + callback = lodash.createCallback(callback, thisArg, 3); + } + forEach(collection, function(value, key, collection) { + var object = result[++index] = getObject(); + if (isArr) { + object.criteria = map(callback, function(key) { return value[key]; }); + } else { + (object.criteria = getArray())[0] = callback(value, key, collection); + } + object.index = index; + object.value = value; + }); + + length = result.length; + result.sort(compareAscending); + while (length--) { + var object = result[length]; + result[length] = object.value; + if (!isArr) { + releaseArray(object.criteria); + } + releaseObject(object); + } + return result; + } + + /** + * Converts the `collection` to an array. + * + * @static + * @memberOf _ + * @category Collections + * @param {Array|Object|string} collection The collection to convert. + * @returns {Array} Returns the new converted array. + * @example + * + * (function() { return _.toArray(arguments).slice(1); })(1, 2, 3, 4); + * // => [2, 3, 4] + */ + function toArray(collection) { + if (collection && typeof collection.length == 'number') { + return (support.unindexedChars && isString(collection)) + ? collection.split('') + : slice(collection); + } + return values(collection); + } + + /** + * Performs a deep comparison of each element in a `collection` to the given + * `properties` object, returning an array of all elements that have equivalent + * property values. + * + * @static + * @memberOf _ + * @type Function + * @category Collections + * @param {Array|Object|string} collection The collection to iterate over. + * @param {Object} props The object of property values to filter by. + * @returns {Array} Returns a new array of elements that have the given properties. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'pets': ['hoppy'] }, + * { 'name': 'fred', 'age': 40, 'pets': ['baby puss', 'dino'] } + * ]; + * + * _.where(characters, { 'age': 36 }); + * // => [{ 'name': 'barney', 'age': 36, 'pets': ['hoppy'] }] + * + * _.where(characters, { 'pets': ['dino'] }); + * // => [{ 'name': 'fred', 'age': 40, 'pets': ['baby puss', 'dino'] }] + */ + var where = filter; + + /*--------------------------------------------------------------------------*/ + + /** + * Creates an array with all falsey values removed. The values `false`, `null`, + * `0`, `""`, `undefined`, and `NaN` are all falsey. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to compact. + * @returns {Array} Returns a new array of filtered values. + * @example + * + * _.compact([0, 1, false, 2, '', 3]); + * // => [1, 2, 3] + */ + function compact(array) { + var index = -1, + length = array ? array.length : 0, + result = []; + + while (++index < length) { + var value = array[index]; + if (value) { + result.push(value); + } + } + return result; + } + + /** + * Creates an array excluding all values of the provided arrays using strict + * equality for comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to process. + * @param {...Array} [values] The arrays of values to exclude. + * @returns {Array} Returns a new array of filtered values. + * @example + * + * _.difference([1, 2, 3, 4, 5], [5, 2, 10]); + * // => [1, 3, 4] + */ + function difference(array) { + return baseDifference(array, baseFlatten(arguments, true, true, 1)); + } + + /** + * This method is like `_.find` except that it returns the index of the first + * element that passes the callback check, instead of the element itself. + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': false }, + * { 'name': 'fred', 'age': 40, 'blocked': true }, + * { 'name': 'pebbles', 'age': 1, 'blocked': false } + * ]; + * + * _.findIndex(characters, function(chr) { + * return chr.age < 20; + * }); + * // => 2 + * + * // using "_.where" callback shorthand + * _.findIndex(characters, { 'age': 36 }); + * // => 0 + * + * // using "_.pluck" callback shorthand + * _.findIndex(characters, 'blocked'); + * // => 1 + */ + function findIndex(array, callback, thisArg) { + var index = -1, + length = array ? array.length : 0; + + callback = lodash.createCallback(callback, thisArg, 3); + while (++index < length) { + if (callback(array[index], index, array)) { + return index; + } + } + return -1; + } + + /** + * This method is like `_.findIndex` except that it iterates over elements + * of a `collection` from right to left. + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {number} Returns the index of the found element, else `-1`. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36, 'blocked': true }, + * { 'name': 'fred', 'age': 40, 'blocked': false }, + * { 'name': 'pebbles', 'age': 1, 'blocked': true } + * ]; + * + * _.findLastIndex(characters, function(chr) { + * return chr.age > 30; + * }); + * // => 1 + * + * // using "_.where" callback shorthand + * _.findLastIndex(characters, { 'age': 36 }); + * // => 0 + * + * // using "_.pluck" callback shorthand + * _.findLastIndex(characters, 'blocked'); + * // => 2 + */ + function findLastIndex(array, callback, thisArg) { + var length = array ? array.length : 0; + callback = lodash.createCallback(callback, thisArg, 3); + while (length--) { + if (callback(array[length], length, array)) { + return length; + } + } + return -1; + } + + /** + * Gets the first element or first `n` elements of an array. If a callback + * is provided elements at the beginning of the array are returned as long + * as the callback returns truey. The callback is bound to `thisArg` and + * invoked with three arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias head, take + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|number|string} [callback] The function called + * per element or the number of elements to return. If a property name or + * object is provided it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the first element(s) of `array`. + * @example + * + * _.first([1, 2, 3]); + * // => 1 + * + * _.first([1, 2, 3], 2); + * // => [1, 2] + * + * _.first([1, 2, 3], function(num) { + * return num < 3; + * }); + * // => [1, 2] + * + * var characters = [ + * { 'name': 'barney', 'blocked': true, 'employer': 'slate' }, + * { 'name': 'fred', 'blocked': false, 'employer': 'slate' }, + * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.first(characters, 'blocked'); + * // => [{ 'name': 'barney', 'blocked': true, 'employer': 'slate' }] + * + * // using "_.where" callback shorthand + * _.pluck(_.first(characters, { 'employer': 'slate' }), 'name'); + * // => ['barney', 'fred'] + */ + function first(array, callback, thisArg) { + var n = 0, + length = array ? array.length : 0; + + if (typeof callback != 'number' && callback != null) { + var index = -1; + callback = lodash.createCallback(callback, thisArg, 3); + while (++index < length && callback(array[index], index, array)) { + n++; + } + } else { + n = callback; + if (n == null || thisArg) { + return array ? array[0] : undefined; + } + } + return slice(array, 0, nativeMin(nativeMax(0, n), length)); + } + + /** + * Flattens a nested array (the nesting can be to any depth). If `isShallow` + * is truey, the array will only be flattened a single level. If a callback + * is provided each element of the array is passed through the callback before + * flattening. The callback is bound to `thisArg` and invoked with three + * arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to flatten. + * @param {boolean} [isShallow=false] A flag to restrict flattening to a single level. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new flattened array. + * @example + * + * _.flatten([1, [2], [3, [[4]]]]); + * // => [1, 2, 3, 4]; + * + * _.flatten([1, [2], [3, [[4]]]], true); + * // => [1, 2, 3, [[4]]]; + * + * var characters = [ + * { 'name': 'barney', 'age': 30, 'pets': ['hoppy'] }, + * { 'name': 'fred', 'age': 40, 'pets': ['baby puss', 'dino'] } + * ]; + * + * // using "_.pluck" callback shorthand + * _.flatten(characters, 'pets'); + * // => ['hoppy', 'baby puss', 'dino'] + */ + function flatten(array, isShallow, callback, thisArg) { + // juggle arguments + if (typeof isShallow != 'boolean' && isShallow != null) { + thisArg = callback; + callback = (typeof isShallow != 'function' && thisArg && thisArg[isShallow] === array) ? null : isShallow; + isShallow = false; + } + if (callback != null) { + array = map(array, callback, thisArg); + } + return baseFlatten(array, isShallow); + } + + /** + * Gets the index at which the first occurrence of `value` is found using + * strict equality for comparisons, i.e. `===`. If the array is already sorted + * providing `true` for `fromIndex` will run a faster binary search. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @param {boolean|number} [fromIndex=0] The index to search from or `true` + * to perform a binary search on a sorted array. + * @returns {number} Returns the index of the matched value or `-1`. + * @example + * + * _.indexOf([1, 2, 3, 1, 2, 3], 2); + * // => 1 + * + * _.indexOf([1, 2, 3, 1, 2, 3], 2, 3); + * // => 4 + * + * _.indexOf([1, 1, 2, 2, 3, 3], 2, true); + * // => 2 + */ + function indexOf(array, value, fromIndex) { + if (typeof fromIndex == 'number') { + var length = array ? array.length : 0; + fromIndex = (fromIndex < 0 ? nativeMax(0, length + fromIndex) : fromIndex || 0); + } else if (fromIndex) { + var index = sortedIndex(array, value); + return array[index] === value ? index : -1; + } + return baseIndexOf(array, value, fromIndex); + } + + /** + * Gets all but the last element or last `n` elements of an array. If a + * callback is provided elements at the end of the array are excluded from + * the result as long as the callback returns truey. The callback is bound + * to `thisArg` and invoked with three arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|number|string} [callback=1] The function called + * per element or the number of elements to exclude. If a property name or + * object is provided it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a slice of `array`. + * @example + * + * _.initial([1, 2, 3]); + * // => [1, 2] + * + * _.initial([1, 2, 3], 2); + * // => [1] + * + * _.initial([1, 2, 3], function(num) { + * return num > 1; + * }); + * // => [1] + * + * var characters = [ + * { 'name': 'barney', 'blocked': false, 'employer': 'slate' }, + * { 'name': 'fred', 'blocked': true, 'employer': 'slate' }, + * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.initial(characters, 'blocked'); + * // => [{ 'name': 'barney', 'blocked': false, 'employer': 'slate' }] + * + * // using "_.where" callback shorthand + * _.pluck(_.initial(characters, { 'employer': 'na' }), 'name'); + * // => ['barney', 'fred'] + */ + function initial(array, callback, thisArg) { + var n = 0, + length = array ? array.length : 0; + + if (typeof callback != 'number' && callback != null) { + var index = length; + callback = lodash.createCallback(callback, thisArg, 3); + while (index-- && callback(array[index], index, array)) { + n++; + } + } else { + n = (callback == null || thisArg) ? 1 : callback || n; + } + return slice(array, 0, nativeMin(nativeMax(0, length - n), length)); + } + + /** + * Creates an array of unique values present in all provided arrays using + * strict equality for comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {...Array} [array] The arrays to inspect. + * @returns {Array} Returns an array of shared values. + * @example + * + * _.intersection([1, 2, 3], [5, 2, 1, 4], [2, 1]); + * // => [1, 2] + */ + function intersection() { + var args = [], + argsIndex = -1, + argsLength = arguments.length, + caches = getArray(), + indexOf = getIndexOf(), + trustIndexOf = indexOf === baseIndexOf, + seen = getArray(); + + while (++argsIndex < argsLength) { + var value = arguments[argsIndex]; + if (isArray(value) || isArguments(value)) { + args.push(value); + caches.push(trustIndexOf && value.length >= largeArraySize && + createCache(argsIndex ? args[argsIndex] : seen)); + } + } + var array = args[0], + index = -1, + length = array ? array.length : 0, + result = []; + + outer: + while (++index < length) { + var cache = caches[0]; + value = array[index]; + + if ((cache ? cacheIndexOf(cache, value) : indexOf(seen, value)) < 0) { + argsIndex = argsLength; + (cache || seen).push(value); + while (--argsIndex) { + cache = caches[argsIndex]; + if ((cache ? cacheIndexOf(cache, value) : indexOf(args[argsIndex], value)) < 0) { + continue outer; + } + } + result.push(value); + } + } + while (argsLength--) { + cache = caches[argsLength]; + if (cache) { + releaseObject(cache); + } + } + releaseArray(caches); + releaseArray(seen); + return result; + } + + /** + * Gets the last element or last `n` elements of an array. If a callback is + * provided elements at the end of the array are returned as long as the + * callback returns truey. The callback is bound to `thisArg` and invoked + * with three arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|number|string} [callback] The function called + * per element or the number of elements to return. If a property name or + * object is provided it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {*} Returns the last element(s) of `array`. + * @example + * + * _.last([1, 2, 3]); + * // => 3 + * + * _.last([1, 2, 3], 2); + * // => [2, 3] + * + * _.last([1, 2, 3], function(num) { + * return num > 1; + * }); + * // => [2, 3] + * + * var characters = [ + * { 'name': 'barney', 'blocked': false, 'employer': 'slate' }, + * { 'name': 'fred', 'blocked': true, 'employer': 'slate' }, + * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.pluck(_.last(characters, 'blocked'), 'name'); + * // => ['fred', 'pebbles'] + * + * // using "_.where" callback shorthand + * _.last(characters, { 'employer': 'na' }); + * // => [{ 'name': 'pebbles', 'blocked': true, 'employer': 'na' }] + */ + function last(array, callback, thisArg) { + var n = 0, + length = array ? array.length : 0; + + if (typeof callback != 'number' && callback != null) { + var index = length; + callback = lodash.createCallback(callback, thisArg, 3); + while (index-- && callback(array[index], index, array)) { + n++; + } + } else { + n = callback; + if (n == null || thisArg) { + return array ? array[length - 1] : undefined; + } + } + return slice(array, nativeMax(0, length - n)); + } + + /** + * Gets the index at which the last occurrence of `value` is found using strict + * equality for comparisons, i.e. `===`. If `fromIndex` is negative, it is used + * as the offset from the end of the collection. + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to search. + * @param {*} value The value to search for. + * @param {number} [fromIndex=array.length-1] The index to search from. + * @returns {number} Returns the index of the matched value or `-1`. + * @example + * + * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2); + * // => 4 + * + * _.lastIndexOf([1, 2, 3, 1, 2, 3], 2, 3); + * // => 1 + */ + function lastIndexOf(array, value, fromIndex) { + var index = array ? array.length : 0; + if (typeof fromIndex == 'number') { + index = (fromIndex < 0 ? nativeMax(0, index + fromIndex) : nativeMin(fromIndex, index - 1)) + 1; + } + while (index--) { + if (array[index] === value) { + return index; + } + } + return -1; + } + + /** + * Removes all provided values from the given array using strict equality for + * comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to modify. + * @param {...*} [value] The values to remove. + * @returns {Array} Returns `array`. + * @example + * + * var array = [1, 2, 3, 1, 2, 3]; + * _.pull(array, 2, 3); + * console.log(array); + * // => [1, 1] + */ + function pull(array) { + var args = arguments, + argsIndex = 0, + argsLength = args.length, + length = array ? array.length : 0; + + while (++argsIndex < argsLength) { + var index = -1, + value = args[argsIndex]; + while (++index < length) { + if (array[index] === value) { + splice.call(array, index--, 1); + length--; + } + } + } + return array; + } + + /** + * Creates an array of numbers (positive and/or negative) progressing from + * `start` up to but not including `end`. If `start` is less than `stop` a + * zero-length range is created unless a negative `step` is specified. + * + * @static + * @memberOf _ + * @category Arrays + * @param {number} [start=0] The start of the range. + * @param {number} end The end of the range. + * @param {number} [step=1] The value to increment or decrement by. + * @returns {Array} Returns a new range array. + * @example + * + * _.range(4); + * // => [0, 1, 2, 3] + * + * _.range(1, 5); + * // => [1, 2, 3, 4] + * + * _.range(0, 20, 5); + * // => [0, 5, 10, 15] + * + * _.range(0, -4, -1); + * // => [0, -1, -2, -3] + * + * _.range(1, 4, 0); + * // => [1, 1, 1] + * + * _.range(0); + * // => [] + */ + function range(start, end, step) { + start = +start || 0; + step = typeof step == 'number' ? step : (+step || 1); + + if (end == null) { + end = start; + start = 0; + } + // use `Array(length)` so engines like Chakra and V8 avoid slower modes + // http://youtu.be/XAqIpGU8ZZk#t=17m25s + var index = -1, + length = nativeMax(0, ceil((end - start) / (step || 1))), + result = Array(length); + + while (++index < length) { + result[index] = start; + start += step; + } + return result; + } + + /** + * Removes all elements from an array that the callback returns truey for + * and returns an array of removed elements. The callback is bound to `thisArg` + * and invoked with three arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to modify. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a new array of removed elements. + * @example + * + * var array = [1, 2, 3, 4, 5, 6]; + * var evens = _.remove(array, function(num) { return num % 2 == 0; }); + * + * console.log(array); + * // => [1, 3, 5] + * + * console.log(evens); + * // => [2, 4, 6] + */ + function remove(array, callback, thisArg) { + var index = -1, + length = array ? array.length : 0, + result = []; + + callback = lodash.createCallback(callback, thisArg, 3); + while (++index < length) { + var value = array[index]; + if (callback(value, index, array)) { + result.push(value); + splice.call(array, index--, 1); + length--; + } + } + return result; + } + + /** + * The opposite of `_.initial` this method gets all but the first element or + * first `n` elements of an array. If a callback function is provided elements + * at the beginning of the array are excluded from the result as long as the + * callback returns truey. The callback is bound to `thisArg` and invoked + * with three arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias drop, tail + * @category Arrays + * @param {Array} array The array to query. + * @param {Function|Object|number|string} [callback=1] The function called + * per element or the number of elements to exclude. If a property name or + * object is provided it will be used to create a "_.pluck" or "_.where" + * style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a slice of `array`. + * @example + * + * _.rest([1, 2, 3]); + * // => [2, 3] + * + * _.rest([1, 2, 3], 2); + * // => [3] + * + * _.rest([1, 2, 3], function(num) { + * return num < 3; + * }); + * // => [3] + * + * var characters = [ + * { 'name': 'barney', 'blocked': true, 'employer': 'slate' }, + * { 'name': 'fred', 'blocked': false, 'employer': 'slate' }, + * { 'name': 'pebbles', 'blocked': true, 'employer': 'na' } + * ]; + * + * // using "_.pluck" callback shorthand + * _.pluck(_.rest(characters, 'blocked'), 'name'); + * // => ['fred', 'pebbles'] + * + * // using "_.where" callback shorthand + * _.rest(characters, { 'employer': 'slate' }); + * // => [{ 'name': 'pebbles', 'blocked': true, 'employer': 'na' }] + */ + function rest(array, callback, thisArg) { + if (typeof callback != 'number' && callback != null) { + var n = 0, + index = -1, + length = array ? array.length : 0; + + callback = lodash.createCallback(callback, thisArg, 3); + while (++index < length && callback(array[index], index, array)) { + n++; + } + } else { + n = (callback == null || thisArg) ? 1 : nativeMax(0, callback); + } + return slice(array, n); + } + + /** + * Uses a binary search to determine the smallest index at which a value + * should be inserted into a given sorted array in order to maintain the sort + * order of the array. If a callback is provided it will be executed for + * `value` and each element of `array` to compute their sort ranking. The + * callback is bound to `thisArg` and invoked with one argument; (value). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to inspect. + * @param {*} value The value to evaluate. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {number} Returns the index at which `value` should be inserted + * into `array`. + * @example + * + * _.sortedIndex([20, 30, 50], 40); + * // => 2 + * + * // using "_.pluck" callback shorthand + * _.sortedIndex([{ 'x': 20 }, { 'x': 30 }, { 'x': 50 }], { 'x': 40 }, 'x'); + * // => 2 + * + * var dict = { + * 'wordToNumber': { 'twenty': 20, 'thirty': 30, 'fourty': 40, 'fifty': 50 } + * }; + * + * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { + * return dict.wordToNumber[word]; + * }); + * // => 2 + * + * _.sortedIndex(['twenty', 'thirty', 'fifty'], 'fourty', function(word) { + * return this.wordToNumber[word]; + * }, dict); + * // => 2 + */ + function sortedIndex(array, value, callback, thisArg) { + var low = 0, + high = array ? array.length : low; + + // explicitly reference `identity` for better inlining in Firefox + callback = callback ? lodash.createCallback(callback, thisArg, 1) : identity; + value = callback(value); + + while (low < high) { + var mid = (low + high) >>> 1; + (callback(array[mid]) < value) + ? low = mid + 1 + : high = mid; + } + return low; + } + + /** + * Creates an array of unique values, in order, of the provided arrays using + * strict equality for comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {...Array} [array] The arrays to inspect. + * @returns {Array} Returns an array of combined values. + * @example + * + * _.union([1, 2, 3], [5, 2, 1, 4], [2, 1]); + * // => [1, 2, 3, 5, 4] + */ + function union() { + return baseUniq(baseFlatten(arguments, true, true)); + } + + /** + * Creates a duplicate-value-free version of an array using strict equality + * for comparisons, i.e. `===`. If the array is sorted, providing + * `true` for `isSorted` will use a faster algorithm. If a callback is provided + * each element of `array` is passed through the callback before uniqueness + * is computed. The callback is bound to `thisArg` and invoked with three + * arguments; (value, index, array). + * + * If a property name is provided for `callback` the created "_.pluck" style + * callback will return the property value of the given element. + * + * If an object is provided for `callback` the created "_.where" style callback + * will return `true` for elements that have the properties of the given object, + * else `false`. + * + * @static + * @memberOf _ + * @alias unique + * @category Arrays + * @param {Array} array The array to process. + * @param {boolean} [isSorted=false] A flag to indicate that `array` is sorted. + * @param {Function|Object|string} [callback=identity] The function called + * per iteration. If a property name or object is provided it will be used + * to create a "_.pluck" or "_.where" style callback, respectively. + * @param {*} [thisArg] The `this` binding of `callback`. + * @returns {Array} Returns a duplicate-value-free array. + * @example + * + * _.uniq([1, 2, 1, 3, 1]); + * // => [1, 2, 3] + * + * _.uniq([1, 1, 2, 2, 3], true); + * // => [1, 2, 3] + * + * _.uniq(['A', 'b', 'C', 'a', 'B', 'c'], function(letter) { return letter.toLowerCase(); }); + * // => ['A', 'b', 'C'] + * + * _.uniq([1, 2.5, 3, 1.5, 2, 3.5], function(num) { return this.floor(num); }, Math); + * // => [1, 2.5, 3] + * + * // using "_.pluck" callback shorthand + * _.uniq([{ 'x': 1 }, { 'x': 2 }, { 'x': 1 }], 'x'); + * // => [{ 'x': 1 }, { 'x': 2 }] + */ + function uniq(array, isSorted, callback, thisArg) { + // juggle arguments + if (typeof isSorted != 'boolean' && isSorted != null) { + thisArg = callback; + callback = (typeof isSorted != 'function' && thisArg && thisArg[isSorted] === array) ? null : isSorted; + isSorted = false; + } + if (callback != null) { + callback = lodash.createCallback(callback, thisArg, 3); + } + return baseUniq(array, isSorted, callback); + } + + /** + * Creates an array excluding all provided values using strict equality for + * comparisons, i.e. `===`. + * + * @static + * @memberOf _ + * @category Arrays + * @param {Array} array The array to filter. + * @param {...*} [value] The values to exclude. + * @returns {Array} Returns a new array of filtered values. + * @example + * + * _.without([1, 2, 1, 0, 3, 1, 4], 0, 1); + * // => [2, 3, 4] + */ + function without(array) { + return baseDifference(array, slice(arguments, 1)); + } + + /** + * Creates an array that is the symmetric difference of the provided arrays. + * See http://en.wikipedia.org/wiki/Symmetric_difference. + * + * @static + * @memberOf _ + * @category Arrays + * @param {...Array} [array] The arrays to inspect. + * @returns {Array} Returns an array of values. + * @example + * + * _.xor([1, 2, 3], [5, 2, 1, 4]); + * // => [3, 5, 4] + * + * _.xor([1, 2, 5], [2, 3, 5], [3, 4, 5]); + * // => [1, 4, 5] + */ + function xor() { + var index = -1, + length = arguments.length; + + while (++index < length) { + var array = arguments[index]; + if (isArray(array) || isArguments(array)) { + var result = result + ? baseUniq(baseDifference(result, array).concat(baseDifference(array, result))) + : array; + } + } + return result || []; + } + + /** + * Creates an array of grouped elements, the first of which contains the first + * elements of the given arrays, the second of which contains the second + * elements of the given arrays, and so on. + * + * @static + * @memberOf _ + * @alias unzip + * @category Arrays + * @param {...Array} [array] Arrays to process. + * @returns {Array} Returns a new array of grouped elements. + * @example + * + * _.zip(['fred', 'barney'], [30, 40], [true, false]); + * // => [['fred', 30, true], ['barney', 40, false]] + */ + function zip() { + var array = arguments.length > 1 ? arguments : arguments[0], + index = -1, + length = array ? max(pluck(array, 'length')) : 0, + result = Array(length < 0 ? 0 : length); + + while (++index < length) { + result[index] = pluck(array, index); + } + return result; + } + + /** + * Creates an object composed from arrays of `keys` and `values`. Provide + * either a single two dimensional array, i.e. `[[key1, value1], [key2, value2]]` + * or two arrays, one of `keys` and one of corresponding `values`. + * + * @static + * @memberOf _ + * @alias object + * @category Arrays + * @param {Array} keys The array of keys. + * @param {Array} [values=[]] The array of values. + * @returns {Object} Returns an object composed of the given keys and + * corresponding values. + * @example + * + * _.zipObject(['fred', 'barney'], [30, 40]); + * // => { 'fred': 30, 'barney': 40 } + */ + function zipObject(keys, values) { + var index = -1, + length = keys ? keys.length : 0, + result = {}; + + if (!values && length && !isArray(keys[0])) { + values = []; + } + while (++index < length) { + var key = keys[index]; + if (values) { + result[key] = values[index]; + } else if (key) { + result[key[0]] = key[1]; + } + } + return result; + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a function that executes `func`, with the `this` binding and + * arguments of the created function, only after being called `n` times. + * + * @static + * @memberOf _ + * @category Functions + * @param {number} n The number of times the function must be called before + * `func` is executed. + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var saves = ['profile', 'settings']; + * + * var done = _.after(saves.length, function() { + * console.log('Done saving!'); + * }); + * + * _.forEach(saves, function(type) { + * asyncSave({ 'type': type, 'complete': done }); + * }); + * // => logs 'Done saving!', after all saves have completed + */ + function after(n, func) { + if (!isFunction(func)) { + throw new TypeError; + } + return function() { + if (--n < 1) { + return func.apply(this, arguments); + } + }; + } + + /** + * Creates a function that, when called, invokes `func` with the `this` + * binding of `thisArg` and prepends any additional `bind` arguments to those + * provided to the bound function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to bind. + * @param {*} [thisArg] The `this` binding of `func`. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var func = function(greeting) { + * return greeting + ' ' + this.name; + * }; + * + * func = _.bind(func, { 'name': 'fred' }, 'hi'); + * func(); + * // => 'hi fred' + */ + function bind(func, thisArg) { + return arguments.length > 2 + ? createWrapper(func, 17, slice(arguments, 2), null, thisArg) + : createWrapper(func, 1, null, null, thisArg); + } + + /** + * Binds methods of an object to the object itself, overwriting the existing + * method. Method names may be specified as individual arguments or as arrays + * of method names. If no method names are provided all the function properties + * of `object` will be bound. + * + * @static + * @memberOf _ + * @category Functions + * @param {Object} object The object to bind and assign the bound methods to. + * @param {...string} [methodName] The object method names to + * bind, specified as individual method names or arrays of method names. + * @returns {Object} Returns `object`. + * @example + * + * var view = { + * 'label': 'docs', + * 'onClick': function() { console.log('clicked ' + this.label); } + * }; + * + * _.bindAll(view); + * jQuery('#docs').on('click', view.onClick); + * // => logs 'clicked docs', when the button is clicked + */ + function bindAll(object) { + var funcs = arguments.length > 1 ? baseFlatten(arguments, true, false, 1) : functions(object), + index = -1, + length = funcs.length; + + while (++index < length) { + var key = funcs[index]; + object[key] = createWrapper(object[key], 1, null, null, object); + } + return object; + } + + /** + * Creates a function that, when called, invokes the method at `object[key]` + * and prepends any additional `bindKey` arguments to those provided to the bound + * function. This method differs from `_.bind` by allowing bound functions to + * reference methods that will be redefined or don't yet exist. + * See http://michaux.ca/articles/lazy-function-definition-pattern. + * + * @static + * @memberOf _ + * @category Functions + * @param {Object} object The object the method belongs to. + * @param {string} key The key of the method. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new bound function. + * @example + * + * var object = { + * 'name': 'fred', + * 'greet': function(greeting) { + * return greeting + ' ' + this.name; + * } + * }; + * + * var func = _.bindKey(object, 'greet', 'hi'); + * func(); + * // => 'hi fred' + * + * object.greet = function(greeting) { + * return greeting + 'ya ' + this.name + '!'; + * }; + * + * func(); + * // => 'hiya fred!' + */ + function bindKey(object, key) { + return arguments.length > 2 + ? createWrapper(key, 19, slice(arguments, 2), null, object) + : createWrapper(key, 3, null, null, object); + } + + /** + * Creates a function that is the composition of the provided functions, + * where each function consumes the return value of the function that follows. + * For example, composing the functions `f()`, `g()`, and `h()` produces `f(g(h()))`. + * Each function is executed with the `this` binding of the composed function. + * + * @static + * @memberOf _ + * @category Functions + * @param {...Function} [func] Functions to compose. + * @returns {Function} Returns the new composed function. + * @example + * + * var realNameMap = { + * 'pebbles': 'penelope' + * }; + * + * var format = function(name) { + * name = realNameMap[name.toLowerCase()] || name; + * return name.charAt(0).toUpperCase() + name.slice(1).toLowerCase(); + * }; + * + * var greet = function(formatted) { + * return 'Hiya ' + formatted + '!'; + * }; + * + * var welcome = _.compose(greet, format); + * welcome('pebbles'); + * // => 'Hiya Penelope!' + */ + function compose() { + var funcs = arguments, + length = funcs.length; + + while (length--) { + if (!isFunction(funcs[length])) { + throw new TypeError; + } + } + return function() { + var args = arguments, + length = funcs.length; + + while (length--) { + args = [funcs[length].apply(this, args)]; + } + return args[0]; + }; + } + + /** + * Creates a function which accepts one or more arguments of `func` that when + * invoked either executes `func` returning its result, if all `func` arguments + * have been provided, or returns a function that accepts one or more of the + * remaining `func` arguments, and so on. The arity of `func` can be specified + * if `func.length` is not sufficient. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to curry. + * @param {number} [arity=func.length] The arity of `func`. + * @returns {Function} Returns the new curried function. + * @example + * + * var curried = _.curry(function(a, b, c) { + * console.log(a + b + c); + * }); + * + * curried(1)(2)(3); + * // => 6 + * + * curried(1, 2)(3); + * // => 6 + * + * curried(1, 2, 3); + * // => 6 + */ + function curry(func, arity) { + arity = typeof arity == 'number' ? arity : (+arity || func.length); + return createWrapper(func, 4, null, null, null, arity); + } + + /** + * Creates a function that will delay the execution of `func` until after + * `wait` milliseconds have elapsed since the last time it was invoked. + * Provide an options object to indicate that `func` should be invoked on + * the leading and/or trailing edge of the `wait` timeout. Subsequent calls + * to the debounced function will return the result of the last `func` call. + * + * Note: If `leading` and `trailing` options are `true` `func` will be called + * on the trailing edge of the timeout only if the the debounced function is + * invoked more than once during the `wait` timeout. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to debounce. + * @param {number} wait The number of milliseconds to delay. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=false] Specify execution on the leading edge of the timeout. + * @param {number} [options.maxWait] The maximum time `func` is allowed to be delayed before it's called. + * @param {boolean} [options.trailing=true] Specify execution on the trailing edge of the timeout. + * @returns {Function} Returns the new debounced function. + * @example + * + * // avoid costly calculations while the window size is in flux + * var lazyLayout = _.debounce(calculateLayout, 150); + * jQuery(window).on('resize', lazyLayout); + * + * // execute `sendMail` when the click event is fired, debouncing subsequent calls + * jQuery('#postbox').on('click', _.debounce(sendMail, 300, { + * 'leading': true, + * 'trailing': false + * }); + * + * // ensure `batchLog` is executed once after 1 second of debounced calls + * var source = new EventSource('/stream'); + * source.addEventListener('message', _.debounce(batchLog, 250, { + * 'maxWait': 1000 + * }, false); + */ + function debounce(func, wait, options) { + var args, + maxTimeoutId, + result, + stamp, + thisArg, + timeoutId, + trailingCall, + lastCalled = 0, + maxWait = false, + trailing = true; + + if (!isFunction(func)) { + throw new TypeError; + } + wait = nativeMax(0, wait) || 0; + if (options === true) { + var leading = true; + trailing = false; + } else if (isObject(options)) { + leading = options.leading; + maxWait = 'maxWait' in options && (nativeMax(wait, options.maxWait) || 0); + trailing = 'trailing' in options ? options.trailing : trailing; + } + var delayed = function() { + var remaining = wait - (now() - stamp); + if (remaining <= 0) { + if (maxTimeoutId) { + clearTimeout(maxTimeoutId); + } + var isCalled = trailingCall; + maxTimeoutId = timeoutId = trailingCall = undefined; + if (isCalled) { + lastCalled = now(); + result = func.apply(thisArg, args); + if (!timeoutId && !maxTimeoutId) { + args = thisArg = null; + } + } + } else { + timeoutId = setTimeout(delayed, remaining); + } + }; + + var maxDelayed = function() { + if (timeoutId) { + clearTimeout(timeoutId); + } + maxTimeoutId = timeoutId = trailingCall = undefined; + if (trailing || (maxWait !== wait)) { + lastCalled = now(); + result = func.apply(thisArg, args); + if (!timeoutId && !maxTimeoutId) { + args = thisArg = null; + } + } + }; + + return function() { + args = arguments; + stamp = now(); + thisArg = this; + trailingCall = trailing && (timeoutId || !leading); + + if (maxWait === false) { + var leadingCall = leading && !timeoutId; + } else { + if (!maxTimeoutId && !leading) { + lastCalled = stamp; + } + var remaining = maxWait - (stamp - lastCalled), + isCalled = remaining <= 0; + + if (isCalled) { + if (maxTimeoutId) { + maxTimeoutId = clearTimeout(maxTimeoutId); + } + lastCalled = stamp; + result = func.apply(thisArg, args); + } + else if (!maxTimeoutId) { + maxTimeoutId = setTimeout(maxDelayed, remaining); + } + } + if (isCalled && timeoutId) { + timeoutId = clearTimeout(timeoutId); + } + else if (!timeoutId && wait !== maxWait) { + timeoutId = setTimeout(delayed, wait); + } + if (leadingCall) { + isCalled = true; + result = func.apply(thisArg, args); + } + if (isCalled && !timeoutId && !maxTimeoutId) { + args = thisArg = null; + } + return result; + }; + } + + /** + * Defers executing the `func` function until the current call stack has cleared. + * Additional arguments will be provided to `func` when it is invoked. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to defer. + * @param {...*} [arg] Arguments to invoke the function with. + * @returns {number} Returns the timer id. + * @example + * + * _.defer(function(text) { console.log(text); }, 'deferred'); + * // logs 'deferred' after one or more milliseconds + */ + function defer(func) { + if (!isFunction(func)) { + throw new TypeError; + } + var args = slice(arguments, 1); + return setTimeout(function() { func.apply(undefined, args); }, 1); + } + + /** + * Executes the `func` function after `wait` milliseconds. Additional arguments + * will be provided to `func` when it is invoked. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to delay. + * @param {number} wait The number of milliseconds to delay execution. + * @param {...*} [arg] Arguments to invoke the function with. + * @returns {number} Returns the timer id. + * @example + * + * _.delay(function(text) { console.log(text); }, 1000, 'later'); + * // => logs 'later' after one second + */ + function delay(func, wait) { + if (!isFunction(func)) { + throw new TypeError; + } + var args = slice(arguments, 2); + return setTimeout(function() { func.apply(undefined, args); }, wait); + } + + /** + * Creates a function that memoizes the result of `func`. If `resolver` is + * provided it will be used to determine the cache key for storing the result + * based on the arguments provided to the memoized function. By default, the + * first argument provided to the memoized function is used as the cache key. + * The `func` is executed with the `this` binding of the memoized function. + * The result cache is exposed as the `cache` property on the memoized function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to have its output memoized. + * @param {Function} [resolver] A function used to resolve the cache key. + * @returns {Function} Returns the new memoizing function. + * @example + * + * var fibonacci = _.memoize(function(n) { + * return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); + * }); + * + * fibonacci(9) + * // => 34 + * + * var data = { + * 'fred': { 'name': 'fred', 'age': 40 }, + * 'pebbles': { 'name': 'pebbles', 'age': 1 } + * }; + * + * // modifying the result cache + * var get = _.memoize(function(name) { return data[name]; }, _.identity); + * get('pebbles'); + * // => { 'name': 'pebbles', 'age': 1 } + * + * get.cache.pebbles.name = 'penelope'; + * get('pebbles'); + * // => { 'name': 'penelope', 'age': 1 } + */ + function memoize(func, resolver) { + if (!isFunction(func)) { + throw new TypeError; + } + var memoized = function() { + var cache = memoized.cache, + key = resolver ? resolver.apply(this, arguments) : keyPrefix + arguments[0]; + + return hasOwnProperty.call(cache, key) + ? cache[key] + : (cache[key] = func.apply(this, arguments)); + } + memoized.cache = {}; + return memoized; + } + + /** + * Creates a function that is restricted to execute `func` once. Repeat calls to + * the function will return the value of the first call. The `func` is executed + * with the `this` binding of the created function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to restrict. + * @returns {Function} Returns the new restricted function. + * @example + * + * var initialize = _.once(createApplication); + * initialize(); + * initialize(); + * // `initialize` executes `createApplication` once + */ + function once(func) { + var ran, + result; + + if (!isFunction(func)) { + throw new TypeError; + } + return function() { + if (ran) { + return result; + } + ran = true; + result = func.apply(this, arguments); + + // clear the `func` variable so the function may be garbage collected + func = null; + return result; + }; + } + + /** + * Creates a function that, when called, invokes `func` with any additional + * `partial` arguments prepended to those provided to the new function. This + * method is similar to `_.bind` except it does **not** alter the `this` binding. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * var greet = function(greeting, name) { return greeting + ' ' + name; }; + * var hi = _.partial(greet, 'hi'); + * hi('fred'); + * // => 'hi fred' + */ + function partial(func) { + return createWrapper(func, 16, slice(arguments, 1)); + } + + /** + * This method is like `_.partial` except that `partial` arguments are + * appended to those provided to the new function. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to partially apply arguments to. + * @param {...*} [arg] Arguments to be partially applied. + * @returns {Function} Returns the new partially applied function. + * @example + * + * var defaultsDeep = _.partialRight(_.merge, _.defaults); + * + * var options = { + * 'variable': 'data', + * 'imports': { 'jq': $ } + * }; + * + * defaultsDeep(options, _.templateSettings); + * + * options.variable + * // => 'data' + * + * options.imports + * // => { '_': _, 'jq': $ } + */ + function partialRight(func) { + return createWrapper(func, 32, null, slice(arguments, 1)); + } + + /** + * Creates a function that, when executed, will only call the `func` function + * at most once per every `wait` milliseconds. Provide an options object to + * indicate that `func` should be invoked on the leading and/or trailing edge + * of the `wait` timeout. Subsequent calls to the throttled function will + * return the result of the last `func` call. + * + * Note: If `leading` and `trailing` options are `true` `func` will be called + * on the trailing edge of the timeout only if the the throttled function is + * invoked more than once during the `wait` timeout. + * + * @static + * @memberOf _ + * @category Functions + * @param {Function} func The function to throttle. + * @param {number} wait The number of milliseconds to throttle executions to. + * @param {Object} [options] The options object. + * @param {boolean} [options.leading=true] Specify execution on the leading edge of the timeout. + * @param {boolean} [options.trailing=true] Specify execution on the trailing edge of the timeout. + * @returns {Function} Returns the new throttled function. + * @example + * + * // avoid excessively updating the position while scrolling + * var throttled = _.throttle(updatePosition, 100); + * jQuery(window).on('scroll', throttled); + * + * // execute `renewToken` when the click event is fired, but not more than once every 5 minutes + * jQuery('.interactive').on('click', _.throttle(renewToken, 300000, { + * 'trailing': false + * })); + */ + function throttle(func, wait, options) { + var leading = true, + trailing = true; + + if (!isFunction(func)) { + throw new TypeError; + } + if (options === false) { + leading = false; + } else if (isObject(options)) { + leading = 'leading' in options ? options.leading : leading; + trailing = 'trailing' in options ? options.trailing : trailing; + } + debounceOptions.leading = leading; + debounceOptions.maxWait = wait; + debounceOptions.trailing = trailing; + + return debounce(func, wait, debounceOptions); + } + + /** + * Creates a function that provides `value` to the wrapper function as its + * first argument. Additional arguments provided to the function are appended + * to those provided to the wrapper function. The wrapper is executed with + * the `this` binding of the created function. + * + * @static + * @memberOf _ + * @category Functions + * @param {*} value The value to wrap. + * @param {Function} wrapper The wrapper function. + * @returns {Function} Returns the new function. + * @example + * + * var p = _.wrap(_.escape, function(func, text) { + * return '

                          ' + func(text) + '

                          '; + * }); + * + * p('Fred, Wilma, & Pebbles'); + * // => '

                          Fred, Wilma, & Pebbles

                          ' + */ + function wrap(value, wrapper) { + return createWrapper(wrapper, 16, [value]); + } + + /*--------------------------------------------------------------------------*/ + + /** + * Creates a function that returns `value`. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} value The value to return from the new function. + * @returns {Function} Returns the new function. + * @example + * + * var object = { 'name': 'fred' }; + * var getter = _.constant(object); + * getter() === object; + * // => true + */ + function constant(value) { + return function() { + return value; + }; + } + + /** + * Produces a callback bound to an optional `thisArg`. If `func` is a property + * name the created callback will return the property value for a given element. + * If `func` is an object the created callback will return `true` for elements + * that contain the equivalent object properties, otherwise it will return `false`. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} [func=identity] The value to convert to a callback. + * @param {*} [thisArg] The `this` binding of the created callback. + * @param {number} [argCount] The number of arguments the callback accepts. + * @returns {Function} Returns a callback function. + * @example + * + * var characters = [ + * { 'name': 'barney', 'age': 36 }, + * { 'name': 'fred', 'age': 40 } + * ]; + * + * // wrap to create custom callback shorthands + * _.createCallback = _.wrap(_.createCallback, function(func, callback, thisArg) { + * var match = /^(.+?)__([gl]t)(.+)$/.exec(callback); + * return !match ? func(callback, thisArg) : function(object) { + * return match[2] == 'gt' ? object[match[1]] > match[3] : object[match[1]] < match[3]; + * }; + * }); + * + * _.filter(characters, 'age__gt38'); + * // => [{ 'name': 'fred', 'age': 40 }] + */ + function createCallback(func, thisArg, argCount) { + var type = typeof func; + if (func == null || type == 'function') { + return baseCreateCallback(func, thisArg, argCount); + } + // handle "_.pluck" style callback shorthands + if (type != 'object') { + return property(func); + } + var props = keys(func), + key = props[0], + a = func[key]; + + // handle "_.where" style callback shorthands + if (props.length == 1 && a === a && !isObject(a)) { + // fast path the common case of providing an object with a single + // property containing a primitive value + return function(object) { + var b = object[key]; + return a === b && (a !== 0 || (1 / a == 1 / b)); + }; + } + return function(object) { + var length = props.length, + result = false; + + while (length--) { + if (!(result = baseIsEqual(object[props[length]], func[props[length]], null, true))) { + break; + } + } + return result; + }; + } + + /** + * Converts the characters `&`, `<`, `>`, `"`, and `'` in `string` to their + * corresponding HTML entities. + * + * @static + * @memberOf _ + * @category Utilities + * @param {string} string The string to escape. + * @returns {string} Returns the escaped string. + * @example + * + * _.escape('Fred, Wilma, & Pebbles'); + * // => 'Fred, Wilma, & Pebbles' + */ + function escape(string) { + return string == null ? '' : String(string).replace(reUnescapedHtml, escapeHtmlChar); + } + + /** + * This method returns the first argument provided to it. + * + * @static + * @memberOf _ + * @category Utilities + * @param {*} value Any value. + * @returns {*} Returns `value`. + * @example + * + * var object = { 'name': 'fred' }; + * _.identity(object) === object; + * // => true + */ + function identity(value) { + return value; + } + + /** + * Adds function properties of a source object to the destination object. + * If `object` is a function methods will be added to its prototype as well. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Function|Object} [object=lodash] object The destination object. + * @param {Object} source The object of functions to add. + * @param {Object} [options] The options object. + * @param {boolean} [options.chain=true] Specify whether the functions added are chainable. + * @example + * + * function capitalize(string) { + * return string.charAt(0).toUpperCase() + string.slice(1).toLowerCase(); + * } + * + * _.mixin({ 'capitalize': capitalize }); + * _.capitalize('fred'); + * // => 'Fred' + * + * _('fred').capitalize().value(); + * // => 'Fred' + * + * _.mixin({ 'capitalize': capitalize }, { 'chain': false }); + * _('fred').capitalize(); + * // => 'Fred' + */ + function mixin(object, source, options) { + var chain = true, + methodNames = source && functions(source); + + if (!source || (!options && !methodNames.length)) { + if (options == null) { + options = source; + } + ctor = lodashWrapper; + source = object; + object = lodash; + methodNames = functions(source); + } + if (options === false) { + chain = false; + } else if (isObject(options) && 'chain' in options) { + chain = options.chain; + } + var ctor = object, + isFunc = isFunction(ctor); + + forEach(methodNames, function(methodName) { + var func = object[methodName] = source[methodName]; + if (isFunc) { + ctor.prototype[methodName] = function() { + var chainAll = this.__chain__, + value = this.__wrapped__, + args = [value]; + + push.apply(args, arguments); + var result = func.apply(object, args); + if (chain || chainAll) { + if (value === result && isObject(result)) { + return this; + } + result = new ctor(result); + result.__chain__ = chainAll; + } + return result; + }; + } + }); + } + + /** + * Reverts the '_' variable to its previous value and returns a reference to + * the `lodash` function. + * + * @static + * @memberOf _ + * @category Utilities + * @returns {Function} Returns the `lodash` function. + * @example + * + * var lodash = _.noConflict(); + */ + function noConflict() { + context._ = oldDash; + return this; + } + + /** + * A no-operation function. + * + * @static + * @memberOf _ + * @category Utilities + * @example + * + * var object = { 'name': 'fred' }; + * _.noop(object) === undefined; + * // => true + */ + function noop() { + // no operation performed + } + + /** + * Gets the number of milliseconds that have elapsed since the Unix epoch + * (1 January 1970 00:00:00 UTC). + * + * @static + * @memberOf _ + * @category Utilities + * @example + * + * var stamp = _.now(); + * _.defer(function() { console.log(_.now() - stamp); }); + * // => logs the number of milliseconds it took for the deferred function to be called + */ + var now = isNative(now = Date.now) && now || function() { + return new Date().getTime(); + }; + + /** + * Converts the given value into an integer of the specified radix. + * If `radix` is `undefined` or `0` a `radix` of `10` is used unless the + * `value` is a hexadecimal, in which case a `radix` of `16` is used. + * + * Note: This method avoids differences in native ES3 and ES5 `parseInt` + * implementations. See http://es5.github.io/#E. + * + * @static + * @memberOf _ + * @category Utilities + * @param {string} value The value to parse. + * @param {number} [radix] The radix used to interpret the value to parse. + * @returns {number} Returns the new integer value. + * @example + * + * _.parseInt('08'); + * // => 8 + */ + var parseInt = nativeParseInt(whitespace + '08') == 8 ? nativeParseInt : function(value, radix) { + // Firefox < 21 and Opera < 15 follow the ES3 specified implementation of `parseInt` + return nativeParseInt(isString(value) ? value.replace(reLeadingSpacesAndZeros, '') : value, radix || 0); + }; + + /** + * Creates a "_.pluck" style function, which returns the `key` value of a + * given object. + * + * @static + * @memberOf _ + * @category Utilities + * @param {string} key The name of the property to retrieve. + * @returns {Function} Returns the new function. + * @example + * + * var characters = [ + * { 'name': 'fred', 'age': 40 }, + * { 'name': 'barney', 'age': 36 } + * ]; + * + * var getName = _.property('name'); + * + * _.map(characters, getName); + * // => ['barney', 'fred'] + * + * _.sortBy(characters, getName); + * // => [{ 'name': 'barney', 'age': 36 }, { 'name': 'fred', 'age': 40 }] + */ + function property(key) { + return function(object) { + return object[key]; + }; + } + + /** + * Produces a random number between `min` and `max` (inclusive). If only one + * argument is provided a number between `0` and the given number will be + * returned. If `floating` is truey or either `min` or `max` are floats a + * floating-point number will be returned instead of an integer. + * + * @static + * @memberOf _ + * @category Utilities + * @param {number} [min=0] The minimum possible value. + * @param {number} [max=1] The maximum possible value. + * @param {boolean} [floating=false] Specify returning a floating-point number. + * @returns {number} Returns a random number. + * @example + * + * _.random(0, 5); + * // => an integer between 0 and 5 + * + * _.random(5); + * // => also an integer between 0 and 5 + * + * _.random(5, true); + * // => a floating-point number between 0 and 5 + * + * _.random(1.2, 5.2); + * // => a floating-point number between 1.2 and 5.2 + */ + function random(min, max, floating) { + var noMin = min == null, + noMax = max == null; + + if (floating == null) { + if (typeof min == 'boolean' && noMax) { + floating = min; + min = 1; + } + else if (!noMax && typeof max == 'boolean') { + floating = max; + noMax = true; + } + } + if (noMin && noMax) { + max = 1; + } + min = +min || 0; + if (noMax) { + max = min; + min = 0; + } else { + max = +max || 0; + } + if (floating || min % 1 || max % 1) { + var rand = nativeRandom(); + return nativeMin(min + (rand * (max - min + parseFloat('1e-' + ((rand +'').length - 1)))), max); + } + return baseRandom(min, max); + } + + /** + * Resolves the value of property `key` on `object`. If `key` is a function + * it will be invoked with the `this` binding of `object` and its result returned, + * else the property value is returned. If `object` is falsey then `undefined` + * is returned. + * + * @static + * @memberOf _ + * @category Utilities + * @param {Object} object The object to inspect. + * @param {string} key The name of the property to resolve. + * @returns {*} Returns the resolved value. + * @example + * + * var object = { + * 'cheese': 'crumpets', + * 'stuff': function() { + * return 'nonsense'; + * } + * }; + * + * _.result(object, 'cheese'); + * // => 'crumpets' + * + * _.result(object, 'stuff'); + * // => 'nonsense' + */ + function result(object, key) { + if (object) { + var value = object[key]; + return isFunction(value) ? object[key]() : value; + } + } + + /** + * A micro-templating method that handles arbitrary delimiters, preserves + * whitespace, and correctly escapes quotes within interpolated code. + * + * Note: In the development build, `_.template` utilizes sourceURLs for easier + * debugging. See http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/#toc-sourceurl + * + * For more information on precompiling templates see: + * http://lodash.com/custom-builds + * + * For more information on Chrome extension sandboxes see: + * http://developer.chrome.com/stable/extensions/sandboxingEval.html + * + * @static + * @memberOf _ + * @category Utilities + * @param {string} text The template text. + * @param {Object} data The data object used to populate the text. + * @param {Object} [options] The options object. + * @param {RegExp} [options.escape] The "escape" delimiter. + * @param {RegExp} [options.evaluate] The "evaluate" delimiter. + * @param {Object} [options.imports] An object to import into the template as local variables. + * @param {RegExp} [options.interpolate] The "interpolate" delimiter. + * @param {string} [sourceURL] The sourceURL of the template's compiled source. + * @param {string} [variable] The data object variable name. + * @returns {Function|string} Returns a compiled function when no `data` object + * is given, else it returns the interpolated text. + * @example + * + * // using the "interpolate" delimiter to create a compiled template + * var compiled = _.template('hello <%= name %>'); + * compiled({ 'name': 'fred' }); + * // => 'hello fred' + * + * // using the "escape" delimiter to escape HTML in data property values + * _.template('<%- value %>', { 'value': ' + + + + + + +
                          +
                          + +
                          + +
                          + {% if !user %} + + {% if (locale != "de") %}Deutsch{% endif %} + {% if (locale != "en") %}English{% endif %} + {% if (locale != "fr") %}Français{% endif %} + + + [[__("login")]] + [[__("signup")]] + {% else %} + [[__("spaces")]] + [[__("logout")]] + + {% endif %} + +
                          +
                          + + {% block content %}{% endblock %} + + + + diff --git a/views/not_found.html b/views/not_found.html new file mode 100644 index 0000000..aae8463 --- /dev/null +++ b/views/not_found.html @@ -0,0 +1,11 @@ +{% extends 'layouts/outer.html' %} + +{% block title %}[[ __("not_found") ]]{% endblock %} + +{% block content %} + +
                          +

                          [[__("not_found")]]

                          +
                          + +{% endblock %} diff --git a/views/partials/account.html b/views/partials/account.html new file mode 100644 index 0000000..ff70cfc --- /dev/null +++ b/views/partials/account.html @@ -0,0 +1,169 @@ +
                          + + + +
                          +
                          [[__("profile_caption")]]
                          +
                          [[__("language_caption")]]
                          +
                          [[__("notifications_caption")]]
                          +
                          [[__("password_caption")]]
                          +
                          [[__("terminate_caption")]]
                          +
                          + +
                          +
                          +
                          +
                          +
                          + + + + + + + +
                          + +
                          +
                          + + +

                          [[__("avatar_dimensions")]]

                          +
                          +
                          +
                          +
                          + +
                          + +
                          + + +
                          + +
                          + + + + + +
                          + +
                          +

                          [[__("confirmation_sent_long")]]

                          + + [[__("send_again")]] + +

                          + [[__("confirmation_sent_another")]] +

                          +
                          +
                          +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + + + + +
                          + +
                          +
                          +

                          [[__("terminate_warning")]]

                          +

                          [[__("terminate_warning2")]]

                          +
                          + +
                          +
                          + + +
                          + +
                          + + +

                          [[__("terminate_reason_caption")]]

                          +
                          +
                          {{account_remove_error}}
                          +
                          + + + +
                          +
                          +
                          diff --git a/views/partials/folders.html b/views/partials/folders.html new file mode 100644 index 0000000..79eaceb --- /dev/null +++ b/views/partials/folders.html @@ -0,0 +1,201 @@ +
                          +
                          + + + + + +
                          + +
                          + + + + + + + + +
                          + +
                          + +
                          +
                          + + +
                          + +
                          + +
                          + + + {{item.name}} + / + + + + {{active_folder.name}} + + + + + +
                          + [[ __('home') ]] +
                          + +
                          + +
                          +
                          +

                          [[ __('no_spaces_yet') ]]

                          +
                          +
                          + + +
                          +
                          +

                          "{{folder_spaces_filter}}"
                          [[ __('search_no_results') ]]

                          + +
                          +
                          +
                          +
                          + + + + + +
                          + {{item.name}} + +
                          + + {{item.updated_at | date 'YYYY-MM-DD'}} + + + + + + {{item.creator.nickname}} + + + +
                          + +
                          +
                          +
                          diff --git a/views/partials/login.html b/views/partials/login.html new file mode 100644 index 0000000..782295d --- /dev/null +++ b/views/partials/login.html @@ -0,0 +1,140 @@ +
                          +
                          + +
                          + +
                          + + {% if (locale != "de") %}Deutsch{% endif %} + {% if (locale != "en") %}English{% endif %} + {% if (locale != "fr") %}Français{% endif %} + + + [[__("login")]] + [[__("signup")]] +
                          +
                          + +
                          + +
                          +
                          +
                          + + [[__("login_google")]] + +

                          [[__("login")]]

                          + +
                          +
                          + +
                          +
                          + +
                          +
                          + + + +
                          {{login_error}}
                          + + +
                          +
                          +
                          + +
                          +
                          +
                          + [[__("login_google")]] + +

                          [[__("signup")]]

                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          +
                          + +
                          By signing up you agree to our TOS and Privacy Policy.
                          +
                          + + + +
                          {{signup_error}}
                          + + [[__("login")]] +
                          +
                          +
                          + +
                          +
                          +
                          +

                          Password Recovery

                          +
                          +
                          + +
                          +
                          +
                          {{password_reset_error}}
                          + +
                          +
                          +
                          +

                          [[__("password_confirmation")]]

                          + [[__("password_check_inbox")]] +
                          +
                          + +
                          +
                          +
                          +

                          Password Recovery

                          + +
                          +
                          + +
                          + +
                          + +
                          +
                          + +
                          {{password_reset_confirm_error}}
                          + +
                          +
                          +
                          + +
                          diff --git a/views/partials/meta-folder.html b/views/partials/meta-folder.html new file mode 100644 index 0000000..9cf40b7 --- /dev/null +++ b/views/partials/meta-folder.html @@ -0,0 +1,55 @@ + diff --git a/views/partials/meta.html b/views/partials/meta.html new file mode 100644 index 0000000..8ed1a6f --- /dev/null +++ b/views/partials/meta.html @@ -0,0 +1,71 @@ + diff --git a/views/partials/modal/access.html b/views/partials/modal/access.html new file mode 100644 index 0000000..f394cdb --- /dev/null +++ b/views/partials/modal/access.html @@ -0,0 +1,124 @@ + diff --git a/views/partials/modal/create-space.html b/views/partials/modal/create-space.html new file mode 100644 index 0000000..28397cb --- /dev/null +++ b/views/partials/modal/create-space.html @@ -0,0 +1,35 @@ + diff --git a/views/partials/modal/folder-settings.html b/views/partials/modal/folder-settings.html new file mode 100644 index 0000000..d9cad76 --- /dev/null +++ b/views/partials/modal/folder-settings.html @@ -0,0 +1,43 @@ + diff --git a/views/partials/modal/login.html b/views/partials/modal/login.html new file mode 100644 index 0000000..bebac28 --- /dev/null +++ b/views/partials/modal/login.html @@ -0,0 +1,84 @@ + diff --git a/views/partials/modal/pdfoptions.html b/views/partials/modal/pdfoptions.html new file mode 100644 index 0000000..409f46b --- /dev/null +++ b/views/partials/modal/pdfoptions.html @@ -0,0 +1,36 @@ + diff --git a/views/partials/modal/space-share.html b/views/partials/modal/space-share.html new file mode 100644 index 0000000..a9f5911 --- /dev/null +++ b/views/partials/modal/space-share.html @@ -0,0 +1,24 @@ + diff --git a/views/partials/modal/support.html b/views/partials/modal/support.html new file mode 100644 index 0000000..8f8306b --- /dev/null +++ b/views/partials/modal/support.html @@ -0,0 +1,31 @@ + diff --git a/views/partials/share-dialog.html b/views/partials/share-dialog.html new file mode 100644 index 0000000..156c610 --- /dev/null +++ b/views/partials/share-dialog.html @@ -0,0 +1,3 @@ +
                          +

                          Share this with others

                          +
                          diff --git a/views/partials/space-isolated.html b/views/partials/space-isolated.html new file mode 100644 index 0000000..a7b8efb --- /dev/null +++ b/views/partials/space-isolated.html @@ -0,0 +1,135 @@ +
                          + +
                          + +
                          + +
                          + +
                          + +
                          +
                          +
                          +
                          {{{a.description}}}
                          +
                          {{{a.description|urls_to_links}}}
                          +
                          +
                          +
                          + + +
                          +
                          {{{a.view.vector_svg}}}
                          +
                          + + +
                          +
                          {{{a.view.vector_svg}}}
                          +
                          +
                          +
                          {{{a.description}}}
                          +
                          +
                          +
                          + + +
                          + +
                          + + +
                          + {{a.title}} +
                          +
                          {{a.description}}
                          + + + + +
                          + + +
                          + + +
                          +
                          {{a.description}}
                          +
                          + + +
                          + + + +
                          +
                          +
                          +
                          +
                          + +
                          +
                          + + + + + + + +
                          + + + + + + {{a.view.filename}} + + {{a.player_view.current_time_string}} / + {{a.player_view.total_time_string}} + +
                          + +
                          +
                          {{a.description}}
                          +
                          + + +
                          +
                          +
                          {{{a.description}}}
                          +
                          +
                          + + + + + +
                          + + {{a.view.filename}} +
                          +
                          + +
                          +
                          + + +
                          + +
                          diff --git a/views/partials/space.html b/views/partials/space.html new file mode 100644 index 0000000..208173e --- /dev/null +++ b/views/partials/space.html @@ -0,0 +1,493 @@ + + + +
                          +
                          + +
                          + + + + + + + + + + + + {{active_space.name}} + + {{active_space.name}} + + + + + +
                          +
                          + + [[__("offline")]] +
                          + +
                          + + + + + +
                          + +
                          + +
                          + + + + + +
                          + + + +
                          + +
                          +
                          + +{% include "./tool/toolbar-elements.html" %} +{% include "./tool/toolbar-object.html" %} + +
                          +
                          +
                          +
                          +
                          +
                          +
                          +

                          Click anywhere to add content.
                          + You can also drop images, sounds and video
                          + or use copy and paste.

                          +
                          +
                          +
                          +
                          + +
                          +
                          +
                          +
                          +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + + +
                          + + + + + + +
                          +
                          +
                          +
                          {{{a.description}}}
                          +
                          {{{a.description|urls_to_links}}}
                          +
                          +
                          + + + {{a.view.link_caption}} + + +
                          + +
                          + + +
                          +
                          {{{a.view.vector_svg}}}
                          +
                          + + +
                          +
                          {{{a.view.vector_svg}}}
                          +
                          +
                          +
                          +
                          {{{a.description|urls_to_links}}}
                          +
                          +
                          + + {{a.view.link_caption}} + + +
                          + +
                          + + +
                          + +
                          + + +
                          + {{a.title}} +
                          +
                          +
                          {{a.description}}
                          + + + + + + + {{a.view.link_caption}} + +
                          + + +
                          + + +
                          +
                          + + + + + + + +
                          + + + + + +
                          +
                          +
                          {{a.description}}
                          +
                          + + +
                          + + + +
                          +
                          +
                          +
                          +
                          + +
                          +
                          + + + + + + + +
                          + + + + + + {{a.view.filename}} + + {{a.player_view.current_time_string}} + / {{a.player_view.total_time_string}} + + + + + + + + + + + + + +
                          + +
                          +
                          {{a.description}}
                          +
                          + + +
                          +
                          +
                          +
                          {{{a.description|urls_to_links}}}
                          +
                          +
                          + + +
                          + {{{a.view.oembed_html}}} + +
                          + + +
                          + + {{a.view.filename}} +
                          +
                          + +
                          +
                          + + +
                          + +
                          + {{selection_metrics.h}} + {{selection_metrics.w}} +
                          + +
                          + {{selection_metrics.h}} +
                          + +
                          + {{selection_metrics.h}} + {{selection_metrics.w}} +
                          + +
                          + {{selection_metrics.w}} +
                          + +
                          + {{selection_metrics.h}} + {{selection_metrics.w}} +
                          + +
                          + {{selection_metrics.h}} +
                          + +
                          + {{selection_metrics.h}} + {{selection_metrics.w}} +
                          + +
                          + {{selection_metrics.w}} +
                          + +
                          {{selection_metrics.h}}
                          +
                          {{selection_metrics.h}}
                          +
                          {{selection_metrics.w}}
                          +
                          {{selection_metrics.w}}
                          + +
                          + +
                          +
                          + + +
                          {{c.name}}
                          + +
                          + + +
                          + +
                          +
                          +
                          +
                          + +
                          + +
                          + + + +
                          + +
                          diff --git a/views/partials/team.html b/views/partials/team.html new file mode 100644 index 0000000..c8d9977 --- /dev/null +++ b/views/partials/team.html @@ -0,0 +1,91 @@ +
                          + + + +

                          Spacedeck Team Management

                          + +
                          + You are not in a team yet. Please upgrade first. +
                          + +
                          +
                          +

                          [[__("team_name")]]

                          +
                          + + + + [[__("save")]] + + +
                          +
                          + +
                          +

                          [[__("subdomain")]]

                          +
                          + + + + [[__("save")]] + + +
                          +
                          + +
                          +

                          Members

                          + +

                          + New members will get an invitation email. After the invitation was used, the member is active. The number of active members in your team will affect your monthly charge. +

                          + +
                          + + + + [[__("add")]] + ✓ [[__("invited")]] + + +
                          + + + + + + + + + + + + + + + + + + + + + + + + + +
                          [[__("email")]] [[__("name")]] [[__("role")]]
                          + {{u.nickname}} + + [[__("role_admin")]] + [[__("role_member")]] + + [[__("remove")]] + [[__("promote")]] + [[__("demote")]] +
                          +
                          +
                          +
                          diff --git a/views/partials/tool/audio.html b/views/partials/tool/audio.html new file mode 100644 index 0000000..d37e3d0 --- /dev/null +++ b/views/partials/tool/audio.html @@ -0,0 +1,16 @@ +

                          audio

                          + +
                          + +
                          + +

                          Click to Upload
                          or drag file(s) here

                          +
                          +
                          + +
                          + +
                          diff --git a/views/partials/tool/background.html b/views/partials/tool/background.html new file mode 100644 index 0000000..dfe73be --- /dev/null +++ b/views/partials/tool/background.html @@ -0,0 +1,112 @@ + +
                          +
                          +
                          [[__("background_image_caption")]]
                          +
                          [[__("background_color_caption")]]
                          +
                          +
                          + +
                          +
                          +
                          +
                          +
                          + +
                          + +
                          + +
                          + +
                          +
                          + +
                          + +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + + +
                          +
                          + + +
                          +
                          +
                          + +
                          +
                          +
                          + +
                          +
                          +
                          + + + +
                          +
                          +
                          + +
                          +
                          +
                          + +
                          + +

                          [[__("upload_background_caption")]]

                          +
                          + +
                          +
                          + + +
                          +
                          +
                          + +
                          diff --git a/views/partials/tool/canvas.html b/views/partials/tool/canvas.html new file mode 100644 index 0000000..1a5223d --- /dev/null +++ b/views/partials/tool/canvas.html @@ -0,0 +1,28 @@ +

                          Padding

                          + +
                          +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          +
                          \ No newline at end of file diff --git a/views/partials/tool/color.html b/views/partials/tool/color.html new file mode 100644 index 0000000..a4ee018 --- /dev/null +++ b/views/partials/tool/color.html @@ -0,0 +1,152 @@ +
                          +
                          +
                          +
                          + +
                          + +
                          + +
                          + +
                          +
                          + +
                          + +
                          +
                          +
                          + + +
                          +
                          + + +
                          +
                          + + +
                          +
                          + + +
                          +
                          +
                          + + +
                          + +
                          +
                          + +
                          +
                          +
                          + + + + + +
                          +
                          +
                          + + + + px +
                          + +
                          + + + + px +
                          +
                          +
                          + + +
                          +
                          +
                          + + + + + px +
                          + +
                          + + + + + +
                          + + +
                          +
                          diff --git a/views/partials/tool/crop.html b/views/partials/tool/crop.html new file mode 100644 index 0000000..62e9f5c --- /dev/null +++ b/views/partials/tool/crop.html @@ -0,0 +1,30 @@ +

                          Crop

                          (Not yet implemented)

                          + +
                          +
                          +
                          + +
                          + 50 + +
                          +
                          +
                          + +
                          +
                          + +
                          + + + +
                          +
                          +
                          +
                          \ No newline at end of file diff --git a/views/partials/tool/embed.html b/views/partials/tool/embed.html new file mode 100644 index 0000000..3951c6d --- /dev/null +++ b/views/partials/tool/embed.html @@ -0,0 +1,15 @@ +

                          Embed Space

                          + +
                          +
                          +

                          + Copy and paste this HTML code into your blog or website: +

                          +
                          +
                          +
                          +
                          + +
                          +
                          +
                          \ No newline at end of file diff --git a/views/partials/tool/filter.html b/views/partials/tool/filter.html new file mode 100644 index 0000000..341d1ff --- /dev/null +++ b/views/partials/tool/filter.html @@ -0,0 +1,73 @@ +

                          Filters

                          + +
                          +
                          + +
                          + +
                          + {{active_style.brightness}} + +
                          +
                          +
                          + +
                          + +
                          + +
                          + {{active_style.contrast}} + +
                          +
                          +
                          + +
                          + +
                          + +
                          + {{active_style.saturation}} + +
                          +
                          +
                          + +
                          + +
                          + +
                          + {{active_style.opacity}} + +
                          +
                          +
                          + +
                          + +
                          + +
                          + {{active_style.hue}} + +
                          +
                          +
                          + +
                          + +
                          + +
                          + {{active_style.blur}} + +
                          +
                          +
                          +
                          + +
                          + +
                          diff --git a/views/partials/tool/grid.html b/views/partials/tool/grid.html new file mode 100644 index 0000000..011e237 --- /dev/null +++ b/views/partials/tool/grid.html @@ -0,0 +1,29 @@ +
                          +
                          +
                          + + + + px +
                          +
                          + +
                          +
                          + + + + + +
                          +
                          + +
                          + +
                          + +
                          \ No newline at end of file diff --git a/views/partials/tool/image.html b/views/partials/tool/image.html new file mode 100644 index 0000000..474078e --- /dev/null +++ b/views/partials/tool/image.html @@ -0,0 +1,15 @@ +

                          Image

                          + +
                          +
                          + +

                          Click to Upload
                          or drag file(s) anywhere.

                          +
                          +
                          + +
                          + +
                          diff --git a/views/partials/tool/layout.html b/views/partials/tool/layout.html new file mode 100644 index 0000000..1982127 --- /dev/null +++ b/views/partials/tool/layout.html @@ -0,0 +1,65 @@ +
                          +
                          +
                          + + + + + + +
                          +
                          + + + + + + + +
                          + + + +
                          +
                          diff --git a/views/partials/tool/link.html b/views/partials/tool/link.html new file mode 100644 index 0000000..b96f64f --- /dev/null +++ b/views/partials/tool/link.html @@ -0,0 +1,13 @@ +
                          +
                          +
                          + +
                          + + +
                          +
                          diff --git a/views/partials/tool/metrics.html b/views/partials/tool/metrics.html new file mode 100644 index 0000000..a8f3cf4 --- /dev/null +++ b/views/partials/tool/metrics.html @@ -0,0 +1,88 @@ +

                          Metrics

                          + +
                          + + +
                          +
                          +
                          +
                          + + + + + × + +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          + + +
                          \ No newline at end of file diff --git a/views/partials/tool/object-options.html b/views/partials/tool/object-options.html new file mode 100644 index 0000000..170ee57 --- /dev/null +++ b/views/partials/tool/object-options.html @@ -0,0 +1,37 @@ +
                          +
                          + + + + + + + + + + + + + + + +
                          +
                          diff --git a/views/partials/tool/object.html b/views/partials/tool/object.html new file mode 100644 index 0000000..be0cd5a --- /dev/null +++ b/views/partials/tool/object.html @@ -0,0 +1,24 @@ +
                          +

                          Selection

                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          +
                          diff --git a/views/partials/tool/pattern.html b/views/partials/tool/pattern.html new file mode 100644 index 0000000..3edc547 --- /dev/null +++ b/views/partials/tool/pattern.html @@ -0,0 +1,48 @@ +

                          Pattern

                          + +
                          +
                          + + +
                          + +
                          + + +
                          + + + + + + + + + +
                          +
                          + +
                          +
                          + +
                          + 50 + +
                          +
                          +
                          + +
                          \ No newline at end of file diff --git a/views/partials/tool/pick-mobile.html b/views/partials/tool/pick-mobile.html new file mode 100644 index 0000000..8ad2c97 --- /dev/null +++ b/views/partials/tool/pick-mobile.html @@ -0,0 +1,12 @@ +
                          +

                          Mobile Upload

                          + + + +

                          + Install the Spacedeck App on your phone and scan this QR code to upload photos, sound, video or text. +

                          +

                          + Access Code: {{active_space.edit_hash}} +

                          +
                          diff --git a/views/partials/tool/search.html b/views/partials/tool/search.html new file mode 100644 index 0000000..0f7390c --- /dev/null +++ b/views/partials/tool/search.html @@ -0,0 +1,70 @@ + diff --git a/views/partials/tool/selection.html b/views/partials/tool/selection.html new file mode 100644 index 0000000..14ee476 --- /dev/null +++ b/views/partials/tool/selection.html @@ -0,0 +1,31 @@ +
                          +

                          Selection

                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          + +
                          +
                          diff --git a/views/partials/tool/shapes.html b/views/partials/tool/shapes.html new file mode 100644 index 0000000..6171887 --- /dev/null +++ b/views/partials/tool/shapes.html @@ -0,0 +1,69 @@ +

                          [[__("tool_shape")]]

                          + +
                          +
                          + + + + + + + + + + + + + + + + + + + + + +
                          +
                          + + diff --git a/views/partials/tool/share.html b/views/partials/tool/share.html new file mode 100644 index 0000000..777e75c --- /dev/null +++ b/views/partials/tool/share.html @@ -0,0 +1,19 @@ + + +
                          + + + facebook + + + + twitter + + +
                          +

                          Share via Link

                          + {{share_base_url+active_space._id}} +
                          diff --git a/views/partials/tool/size.html b/views/partials/tool/size.html new file mode 100644 index 0000000..edf473b --- /dev/null +++ b/views/partials/tool/size.html @@ -0,0 +1,110 @@ +

                          Size

                          + +
                          + +
                          +
                          + + + + + +
                          +
                          + +
                          + + + + + +
                          + +
                          +
                          +
                          +
                          + + + + + × + +
                          +
                          +
                          +
                          + + + + +
                          +
                          +
                          +
                          + + + + + + + +
                          \ No newline at end of file diff --git a/views/partials/tool/stroke.html b/views/partials/tool/stroke.html new file mode 100644 index 0000000..5551b2b --- /dev/null +++ b/views/partials/tool/stroke.html @@ -0,0 +1,68 @@ +

                          Stroke

                          + +
                          +
                          +
                          + + + px +
                          +
                          + +
                          +
                          + + + + px +
                          +
                          + +
                          +
                          + +
                          + + + +
                          +
                          +
                          + + +
                          + +
                          + +
                          + + + diff --git a/views/partials/tool/text-align.html b/views/partials/tool/text-align.html new file mode 100644 index 0000000..0b6ae17 --- /dev/null +++ b/views/partials/tool/text-align.html @@ -0,0 +1,28 @@ +
                          +
                          + + + + + + + + + + +
                          +
                          diff --git a/views/partials/tool/text-columns.html b/views/partials/tool/text-columns.html new file mode 100644 index 0000000..9729c7b --- /dev/null +++ b/views/partials/tool/text-columns.html @@ -0,0 +1,25 @@ +

                          Columns

                          + +
                          +
                          +
                          + + + + + +
                          +
                          + +
                          +
                          + + + + px +
                          +
                          +
                          + diff --git a/views/partials/tool/text-digits.html b/views/partials/tool/text-digits.html new file mode 100644 index 0000000..127963a --- /dev/null +++ b/views/partials/tool/text-digits.html @@ -0,0 +1,13 @@ +
                          + +
                          +
                            +
                          • + {{font}} +
                          • +
                          +
                          +
                          diff --git a/views/partials/tool/text-formats.html b/views/partials/tool/text-formats.html new file mode 100644 index 0000000..63e8085 --- /dev/null +++ b/views/partials/tool/text-formats.html @@ -0,0 +1,20 @@ +

                          [[__("text_formats")]]

                          + +
                            +
                          • [[__("format_p")]]
                          • +
                          • [[__("format_bullets")]]
                          • +
                          • [[__("format_numbers")]]
                          • +
                          • [[__("format_h1")]]
                          • +
                          • [[__("format_h2")]]
                          • +
                          • [[__("format_h3")]]
                          • +
                          + + diff --git a/views/partials/tool/text-styles.html b/views/partials/tool/text-styles.html new file mode 100644 index 0000000..e88f10a --- /dev/null +++ b/views/partials/tool/text-styles.html @@ -0,0 +1,29 @@ + +
                          +
                          + + + + + + + +
                          +
                          diff --git a/views/partials/tool/textbox.html b/views/partials/tool/textbox.html new file mode 100644 index 0000000..07d9885 --- /dev/null +++ b/views/partials/tool/textbox.html @@ -0,0 +1,101 @@ +

                          Textframe

                          + +
                          +
                          + +
                          + + + +
                          +
                          + +
                          +
                          + + + + px +
                          +
                          + + +
                          + +
                          + + + + + + + +
                          \ No newline at end of file diff --git a/views/partials/tool/toolbar-elements.html b/views/partials/tool/toolbar-elements.html new file mode 100644 index 0000000..c0c0a2f --- /dev/null +++ b/views/partials/tool/toolbar-elements.html @@ -0,0 +1,110 @@ +
                          + +
                          + + + + + + + + + + + + + + + + + + + + + + +
                          +
                          diff --git a/views/partials/tool/toolbar-object-options.html b/views/partials/tool/toolbar-object-options.html new file mode 100644 index 0000000..6598a33 --- /dev/null +++ b/views/partials/tool/toolbar-object-options.html @@ -0,0 +1,46 @@ +
                          + +
                          +
                          +
                          + +
                          + +
                          + +
                          + +
                          + +
                          +
                          + +
                          + +
                          + + +
                          + +
                          + +
                          +
                          diff --git a/views/partials/tool/toolbar-object.html b/views/partials/tool/toolbar-object.html new file mode 100644 index 0000000..132f71d --- /dev/null +++ b/views/partials/tool/toolbar-object.html @@ -0,0 +1,130 @@ +
                          + +
                          + +
                          + +
                          + + + + + + + + + + + + + + + + + +
                          +
                          diff --git a/views/partials/tool/toolbar-social.html b/views/partials/tool/toolbar-social.html new file mode 100644 index 0000000..8aa9015 --- /dev/null +++ b/views/partials/tool/toolbar-social.html @@ -0,0 +1,29 @@ +
                          +
                          + + + + + +
                          +
                          diff --git a/views/partials/tool/toolbar-text.html b/views/partials/tool/toolbar-text.html new file mode 100644 index 0000000..5c552c6 --- /dev/null +++ b/views/partials/tool/toolbar-text.html @@ -0,0 +1,72 @@ +
                          +
                          + + + + + + + + + + + + +
                          + +
                          + + +
                          + +
                          + +
                          +
                          diff --git a/views/partials/tool/video.html b/views/partials/tool/video.html new file mode 100644 index 0000000..ef340ec --- /dev/null +++ b/views/partials/tool/video.html @@ -0,0 +1,15 @@ +

                          Video

                          + +
                          +
                          + +

                          Click to Upload
                          or drag file(s) anywhere.

                          +
                          +
                          + +
                          + +
                          diff --git a/views/partials/tool/zones.html b/views/partials/tool/zones.html new file mode 100644 index 0000000..8bfec42 --- /dev/null +++ b/views/partials/tool/zones.html @@ -0,0 +1,17 @@ +

                          [[__("tool_zones")]]

                          + +
                          +
                          +

                          + Turn your Space into a zooming presentation by placing some Zones and switch through them when presenting. +

                          + + +
                          + +
                          + + + +
                          +
                          diff --git a/views/public/contact.html b/views/public/contact.html new file mode 100644 index 0000000..7e4ebe2 --- /dev/null +++ b/views/public/contact.html @@ -0,0 +1,10 @@ +{% extends '../layouts/outer.html' %} + +{% block title %}[[ __("contact") ]]{% endblock %} + +{% block content %} +
                          + +
                          + +{% endblock %} diff --git a/views/public/privacy.html b/views/public/privacy.html new file mode 100644 index 0000000..5c6a614 --- /dev/null +++ b/views/public/privacy.html @@ -0,0 +1,9 @@ +{% extends '../layouts/outer.html' %} +{% block title %}[[ __("privacy") ]]{% endblock %} + +{% block content %} + +
                          + +
                          +{% endblock %} diff --git a/views/public/terms.html b/views/public/terms.html new file mode 100644 index 0000000..42ac18f --- /dev/null +++ b/views/public/terms.html @@ -0,0 +1,8 @@ +{% extends '../layouts/outer.html' %} + +{% block title %}[[ __("terms") ]]{% endblock %} + +{% block content %} +
                          +
                          +{% endblock %} diff --git a/views/space_list.html b/views/space_list.html new file mode 100644 index 0000000..8d2d7e0 --- /dev/null +++ b/views/space_list.html @@ -0,0 +1,20 @@ + + + +

                          [[ __("folder") ]]: [[space.name]]

                          + + + + + + + {% for s in subspaces %} + + + + + + {% endfor %} +
                          [[__("created")]][[__("name")]][[__("link")]]
                          [[ s.created_at | date('d.m.Y H:i') ]][[ s.name ]][[ s.ae_link ]]
                          + + diff --git a/views/spacedeck.html b/views/spacedeck.html new file mode 100644 index 0000000..54144dd --- /dev/null +++ b/views/spacedeck.html @@ -0,0 +1,110 @@ + + + + + Spacedeck Open + + + + + + + + + + + + + + + {% if process.env.NODE_ENV == "production" %} + + {% else %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% endif %} + + + + + + {% include "./partials/login.html" %} + {% include "./partials/space.html" %} + {% include "./partials/folders.html" %} + {% include "./partials/team.html" %} + {% include "./partials/account.html" %} + {% include "./partials/meta.html" %} + {% include "./partials/meta-folder.html" %} + + {% include "./partials/modal/access.html" %} + {% include "./partials/modal/folder-settings.html" %} + {% include "./partials/modal/support.html" %} + {% include "./partials/modal/login.html" %} + {% include "./partials/modal/pdfoptions.html" %} + + + + +