From d40c8e270ce9cc71dc6149e23e50cdd857622b08 Mon Sep 17 00:00:00 2001 From: Ajurna Date: Fri, 23 Apr 2021 18:21:25 +0100 Subject: [PATCH] Rewrite of Comic lists so that tehy have thumbnails. (#26) --- .env.example | 2 + .gitignore | 1 + LICENSE | 427 +++ cbreader/settings/base.py | 6 + cbreader/settings/nginx.conf | 3 + cbreader/urls.py | 12 +- comic/management/commands/scan_comics.py | 19 +- comic/migrations/0022_comicbook_thumbnail.py | 19 + comic/migrations/0023_directory_thumbnail.py | 19 + comic/migrations/0024_auto_20210422_0855.py | 29 + comic/models.py | 132 +- comic/templates/base.html | 1 + comic/templates/comic/comic_list.html | 250 +- comic/tests/test_models.py | 47 +- comic/urls.py | 5 +- comic/util.py | 87 +- comic/views.py | 83 +- docker-compose.yml | 2 + placehoder.xcf | Bin 0 -> 9991 bytes poetry.lock | 92 +- pyproject.toml | 2 + requirements.txt | 45 + static/img/placeholder.png | Bin 0 -> 7701 bytes static/js/isotope.pkgd.js | 3563 ++++++++++++++++++ static/js/isotope.pkgd.min.js | 12 + 25 files changed, 4588 insertions(+), 270 deletions(-) create mode 100644 LICENSE create mode 100644 comic/migrations/0022_comicbook_thumbnail.py create mode 100644 comic/migrations/0023_directory_thumbnail.py create mode 100644 comic/migrations/0024_auto_20210422_0855.py create mode 100644 placehoder.xcf create mode 100644 static/img/placeholder.png create mode 100644 static/js/isotope.pkgd.js create mode 100644 static/js/isotope.pkgd.min.js diff --git a/.env.example b/.env.example index 4729bc8..0a15f30 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,8 @@ COMIC_BOOK_VOLUME = '/path/to/comic/folder' STATIC_ROOT = '/static' +MEDIA_ROOT = './media' + # This expects the office winrar unrar command line tool for windows or linux. # Will work without setting if it is in the path # UNRAR_TOOL = 'unrar.exe' diff --git a/.gitignore b/.gitignore index 4267685..3f3a59e 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,7 @@ target/ *.pyc __pycache__/ local_settings.py +media .env db.sqlite3 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3b7b82d --- /dev/null +++ b/LICENSE @@ -0,0 +1,427 @@ +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/cbreader/settings/base.py b/cbreader/settings/base.py index c45de04..a7a073f 100644 --- a/cbreader/settings/base.py +++ b/cbreader/settings/base.py @@ -39,6 +39,7 @@ INSTALLED_APPS = ( "comic", "comic_auth", 'django_extensions', + 'imagekit', ) MIDDLEWARE = [ @@ -108,6 +109,11 @@ STATICFILES_DIRS = [ STATIC_ROOT = os.getenv('STATIC_ROOT', None) + +MEDIA_ROOT = os.getenv('MEDIA_ROOT', None) + +MEDIA_URL = '/media/' + LOGIN_REDIRECT_URL = "/comic/" LOGIN_URL = "/login/" diff --git a/cbreader/settings/nginx.conf b/cbreader/settings/nginx.conf index 54590d0..a81126c 100644 --- a/cbreader/settings/nginx.conf +++ b/cbreader/settings/nginx.conf @@ -16,5 +16,8 @@ server { location /static/ { alias /static/; } + location /media/ { + alias /media/; + } } \ No newline at end of file diff --git a/cbreader/urls.py b/cbreader/urls.py index 542b836..4f94512 100644 --- a/cbreader/urls.py +++ b/cbreader/urls.py @@ -13,10 +13,10 @@ Including another URLconf 1. Add an import: from blog import urls as blog_urls 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) """ -from django.conf.urls import include, url -from django.contrib import admin from django.conf import settings - +from django.conf.urls import include, url +from django.conf.urls.static import static +from django.contrib import admin import comic.views import comic_auth.views @@ -28,7 +28,7 @@ urlpatterns = [ url(r"^setup/", comic.views.initial_setup), url(r"^comic/", include("comic.urls")), url(r"^admin/", admin.site.urls), - # url(r'^silk/', include('silk.urls', namespace='silk')) + ] -if settings.SILK_ENABLED: - urlpatterns += [url(r'^silk/', include('silk.urls', namespace='silk'))] +if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/comic/management/commands/scan_comics.py b/comic/management/commands/scan_comics.py index 4d5f7cb..070038c 100644 --- a/comic/management/commands/scan_comics.py +++ b/comic/management/commands/scan_comics.py @@ -1,13 +1,11 @@ -import os -from os.path import isdir from pathlib import Path +from PIL import UnidentifiedImageError +from django.conf import settings +from django.core.management.base import BaseCommand from loguru import logger -from django.core.management.base import BaseCommand -from django.conf import settings - -from comic.models import ComicBook, Directory, ComicStatus +from comic.models import ComicBook, Directory class Command(BaseCommand): @@ -15,6 +13,7 @@ class Command(BaseCommand): def __init__(self): super().__init__() + self.OUTPUT = False def add_arguments(self, parser): parser.add_argument( @@ -91,4 +90,10 @@ class Command(BaseCommand): book.version = 1 book.save() except ComicBook.DoesNotExist: - ComicBook.process_comic_book(file, directory) + book = ComicBook.process_comic_book(file, directory) + try: + book.generate_thumbnail() + except UnidentifiedImageError: + book.generate_thumbnail(1) + except: + pass diff --git a/comic/migrations/0022_comicbook_thumbnail.py b/comic/migrations/0022_comicbook_thumbnail.py new file mode 100644 index 0000000..ff85559 --- /dev/null +++ b/comic/migrations/0022_comicbook_thumbnail.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2 on 2021-04-21 11:13 + +from django.db import migrations +import imagekit.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('comic', '0021_delete_setting'), + ] + + operations = [ + migrations.AddField( + model_name='comicbook', + name='thumbnail', + field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'), + ), + ] diff --git a/comic/migrations/0023_directory_thumbnail.py b/comic/migrations/0023_directory_thumbnail.py new file mode 100644 index 0000000..c0088c2 --- /dev/null +++ b/comic/migrations/0023_directory_thumbnail.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2 on 2021-04-21 17:44 + +from django.db import migrations +import imagekit.models.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('comic', '0022_comicbook_thumbnail'), + ] + + operations = [ + migrations.AddField( + model_name='directory', + name='thumbnail', + field=imagekit.models.fields.ProcessedImageField(null=True, upload_to='thumbs'), + ), + ] diff --git a/comic/migrations/0024_auto_20210422_0855.py b/comic/migrations/0024_auto_20210422_0855.py new file mode 100644 index 0000000..14fcc63 --- /dev/null +++ b/comic/migrations/0024_auto_20210422_0855.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2 on 2021-04-22 07:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('comic', '0023_directory_thumbnail'), + ] + + operations = [ + migrations.AddField( + model_name='comicbook', + name='thumbnail_index', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='directory', + name='thumbnail_index', + field=models.PositiveIntegerField(default=0), + ), + migrations.AddField( + model_name='directory', + name='thumbnail_issue', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='directory_thumbnail_issue', to='comic.comicbook'), + ), + ] diff --git a/comic/models.py b/comic/models.py index ec483a7..aef6877 100644 --- a/comic/models.py +++ b/comic/models.py @@ -8,15 +8,18 @@ from os import listdir from pathlib import Path from typing import Optional, List, Union, Tuple +import PyPDF4 +import PyPDF4.utils +import rarfile +from PIL import Image from django.conf import settings from django.contrib.auth.models import User +from django.core.files.uploadedfile import InMemoryUploadedFile from django.db import models from django.db.transaction import atomic from django.utils.http import urlsafe_base64_encode -import PyPDF4 -import PyPDF4.utils - -import rarfile +from imagekit.models import ProcessedImageField +from imagekit.processors import ResizeToFill from comic.errors import NotCompatibleArchive @@ -28,6 +31,15 @@ class Directory(models.Model): name = models.CharField(max_length=100) parent = models.ForeignKey("Directory", null=True, blank=True, on_delete=models.CASCADE) selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True) + thumbnail = ProcessedImageField(upload_to='thumbs', + processors=[ResizeToFill(200, 300)], + format='JPEG', + options={'quality': 60}, + null=True) + thumbnail_issue = models.ForeignKey("ComicBook", null=True, + on_delete=models.SET_NULL, + related_name='directory_thumbnail_issue') + thumbnail_index = models.PositiveIntegerField(default=0) class Meta: ordering = ['name'] @@ -35,6 +47,39 @@ class Directory(models.Model): def __str__(self): return "Directory: {0}; {1}".format(self.name, self.parent) + def mark_read(self, user): + books = ComicBook.objects.filter(directory=self) + for book in books: + book.mark_read(user) + + def mark_unread(self, user): + books = ComicBook.objects.filter(directory=self) + for book in books: + book.mark_unread(user) + + def get_thumbnail_url(self): + if self.thumbnail: + return self.thumbnail.url + else: + self.generate_thumbnail() + return self.thumbnail.url + + def generate_thumbnail(self): + book = ComicBook.objects.filter(directory=self).order_by('file_name').first() + if not book: + return + img, content_type = book.get_image(0) + pil_data = Image.open(img) + self.thumbnail = InMemoryUploadedFile( + img, + None, + f'{self.name}.jpg', + content_type, + pil_data.tell(), + None + ) + self.save() + @property def path(self) -> Path: return self.get_path() @@ -63,6 +108,10 @@ class Directory(models.Model): self.parent.get_path_objects(p) return p + @property + def url_safe_selector(self): + return urlsafe_base64_encode(self.selector.bytes) + class ComicBook(models.Model): file_name = models.TextField() @@ -70,12 +119,33 @@ class ComicBook(models.Model): directory = models.ForeignKey(Directory, blank=True, null=True, on_delete=models.CASCADE) selector = models.UUIDField(unique=True, default=uuid.uuid4, db_index=True) version = models.IntegerField(default=1) + thumbnail = ProcessedImageField(upload_to='thumbs', + processors=[ResizeToFill(200, 300)], + format='JPEG', + options={'quality': 60}, + null=True) + thumbnail_index = models.PositiveIntegerField(default=0) def __str__(self): return self.file_name + def mark_read(self, user: User): + status, _ = ComicStatus.objects.get_or_create(comic=self, user=user) + status.mark_read() + + def mark_unread(self, user: User): + status, _ = ComicStatus.objects.get_or_create(comic=self, user=user) + status.mark_unread() + + def mark_previous(self, user): + books = ComicBook.objects.filter(directory=self.directory).order_by('file_name') + for book in books: + if book == self: + break + book.mark_read(user) + @property - def selector_string(self): + def url_safe_selector(self): return urlsafe_base64_encode(self.selector.bytes) def get_pdf(self): @@ -99,6 +169,26 @@ class ComicBook(models.Model): out = (archive.open(page_obj.page_file_name), page_obj.content_type) return out + def get_thumbnail_url(self): + if self.thumbnail: + return self.thumbnail.url + else: + self.generate_thumbnail() + return self.thumbnail.url + + def generate_thumbnail(self, page_index: int = 0): + img, content_type = self.get_image(page_index) + pil_data = Image.open(img) + self.thumbnail = InMemoryUploadedFile( + img, + None, + f'{self.file_name}.jpg', + content_type, + pil_data.tell(), + None + ) + self.save() + def is_last_page(self, page): if (self.page_count - 1) == page: return True @@ -209,22 +299,15 @@ class ComicBook(models.Model): def __str__(self): return self.name - @property - def pages(self): - return [cp for cp in ComicPage.objects.filter(Comic=self).order_by("index")] - - def page_name(self, index): - return ComicPage.objects.get(Comic=self, index=index).page_file_name - @staticmethod - def process_comic_book(comic_file_name: Path, directory: "Directory" = False) -> Union["ComicBook", Path]: + def process_comic_book(comic_file_path: Path, directory: "Directory" = False) -> Union["ComicBook", Path]: """ - :type comic_file_name: str + :type comic_file_path: str :type directory: Directory """ try: - book = ComicBook.objects.get(file_name=comic_file_name, version=0) + book = ComicBook.objects.get(file_name=comic_file_path.name, version=0) book.directory = directory book.version = 1 book.save() @@ -232,12 +315,12 @@ class ComicBook(models.Model): except ComicBook.DoesNotExist: pass - book = ComicBook(file_name=comic_file_name, directory=directory if directory else None) + book = ComicBook(file_name=comic_file_path.name, directory=directory if directory else None) book.save() try: archive, archive_type = book.get_archive() except NotCompatibleArchive: - return comic_file_name + return comic_file_path if archive_type == 'archive': book.verify_pages() @@ -340,9 +423,18 @@ class ComicStatus(models.Model): unread = models.BooleanField(default=True) finished = models.BooleanField(default=False) - @property - def read(self): - return self.last_read_page + def mark_read(self): + page_count = ComicPage.objects.filter(Comic=self.comic).count() + self.unread = False + self.finished = True + self.last_read_page = page_count - 1 + self.save() + + def mark_unread(self): + self.unread = True + self.finished = False + self.last_read_page = 0 + self.save() def __str__(self): return self.__repr__() diff --git a/comic/templates/base.html b/comic/templates/base.html index 7d431c7..6b22f41 100644 --- a/comic/templates/base.html +++ b/comic/templates/base.html @@ -69,6 +69,7 @@ + {% block script %} {% endblock %} diff --git a/comic/templates/comic/comic_list.html b/comic/templates/comic/comic_list.html index 0e50756..d750f80 100644 --- a/comic/templates/comic/comic_list.html +++ b/comic/templates/comic/comic_list.html @@ -1,121 +1,165 @@ {% extends "base.html" %} +{% load static %} {% block title %}{{ title }}{% endblock %} {% block content %} +
-
- {% csrf_token %} - - - - - - - - - - - - - - - - - - -

Comics

mark selected issues as: - -
File/FolderStatus
loading data
-
+
+
+ +
+ + + +
+ + +
+
+
+
+
+
+
+ {% for file in files %} +
+
+ {% if file.item_type == 'Directory' %} + + {% elif file.item_type == 'ComicBook' %} + + {% endif %} + + {% if file.obj.thumbnail %} + {{ file.name }} + {% else %} + {% if file.item_type == 'Directory' %} + {{ file.name }} + {% elif file.item_type == 'ComicBook' %} + {{ file.name }} + {% endif %} + {% endif %} + +
+
+ {% if file.item_type == 'Directory' %} + + {% elif file.item_type == 'ComicBook' %} + + {% endif %} + {{ file.name }} + +
+

+

+
{{ file.percent }}%
+
+

+
+ + +
+ + +
+ +
+
+
+
+ {% endfor %} +
{% endblock %} {% block script %} {% endblock %} \ No newline at end of file diff --git a/comic/tests/test_models.py b/comic/tests/test_models.py index a30a04c..e06d176 100644 --- a/comic/tests/test_models.py +++ b/comic/tests/test_models.py @@ -103,43 +103,23 @@ class ComicBookTests(TestCase): folders = generate_directory(user) dir1 = folders[0] self.assertEqual(dir1.name, "test_folder") - self.assertEqual(dir1.type, "directory") - self.assertEqual("fa-folder-open", dir1.icon) - d = Directory.objects.get(name="test_folder", parent__isnull=True) - location = "/comic/{0}/".format(urlsafe_base64_encode(d.selector.bytes)) - self.assertEqual(dir1.location, location) - self.assertEqual(dir1.label, '
Empty
') + self.assertEqual(dir1.item_type, "Directory") dir2 = folders[1] self.assertEqual(dir2.name, "test1.rar") - self.assertEqual(dir2.type, "book") - self.assertEqual("fa-book", dir2.icon) - c = ComicBook.objects.get(file_name="test1.rar", directory__isnull=True) - location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes)) - self.assertEqual(dir2.location, location) - self.assertEqual(dir2.label, '
Unread
') + self.assertEqual(dir2.item_type, "ComicBook") dir3 = folders[2] self.assertEqual(dir3.name, "test2.rar") - self.assertEqual(dir3.type, "book") - self.assertEqual("fa-book", dir3.icon) - c = ComicBook.objects.get(file_name="test2.rar", directory__isnull=True) - location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes)) - self.assertEqual(dir3.location, location) - self.assertEqual(dir3.label, '
3/4
') + self.assertEqual(dir2.item_type, "ComicBook") dir4 = folders[3] self.assertEqual(dir4.name, "test3.rar") - self.assertEqual(dir4.type, "book") - self.assertEqual("fa-book", dir3.icon) - c = ComicBook.objects.get(file_name="test3.rar", directory__isnull=True) - location = "/comic/read/{0}/".format(urlsafe_base64_encode(c.selector.bytes)) - self.assertEqual(dir4.location, location) - self.assertEqual(dir4.label, '
Unread
') + self.assertEqual(dir4.item_type, "ComicBook") def test_pages(self): book = ComicBook.objects.get(file_name="test1.rar") - pages = book.pages + pages = [cp for cp in ComicPage.objects.filter(Comic=book).order_by("index")] self.assertEqual(pages[0].page_file_name, "img1.jpg") self.assertEqual(pages[0].index, 0) self.assertEqual(pages[1].page_file_name, "img2.png") @@ -149,9 +129,6 @@ class ComicBookTests(TestCase): self.assertEqual(pages[3].page_file_name, "img4.bmp") self.assertEqual(pages[3].index, 3) - def test_page_name(self): - book = ComicBook.objects.get(file_name="test1.rar") - self.assertEqual(book.page_name(0), "img1.jpg") def test_comic_list(self): c = Client() @@ -166,18 +143,6 @@ class ComicBookTests(TestCase): response = c.get(f"/comic/{urlsafe_base64_encode(directory.selector.bytes)}/") self.assertEqual(response.status_code, 200) - def test_comic_list_json(self): - c = Client() - response = c.post("/comic/list_json/") - self.assertEqual(response.status_code, 302) - - c.login(username="test", password="test") - response = c.post("/comic/list_json/") - self.assertEqual(response.status_code, 200) - directory = Directory.objects.first() - response = c.post(f"/comic/list_json/{urlsafe_base64_encode(directory.selector.bytes)}/") - self.assertEqual(response.status_code, 200) - def test_recent_comics(self): c = Client() response = c.get("/comic/recent/") @@ -240,7 +205,7 @@ class ComicBookTests(TestCase): response = c.get("/comic/edit/") self.assertEqual(response.status_code, 405) - req_data = {"comic_list_length": 10, "func": "unread", "selected": book.selector_string} + req_data = {"comic_list_length": 10, "func": "unread", "selected": book.url_safe_selector} response = c.post("/comic/edit/", req_data) self.assertEqual(response.status_code, 200) diff --git a/comic/urls.py b/comic/urls.py index 43b3f84..a854bfd 100644 --- a/comic/urls.py +++ b/comic/urls.py @@ -10,14 +10,15 @@ urlpatterns = [ path("settings/users/add/", views.user_add_page, name="add_users"), path("account/", views.account_page, name="account"), path("read//", views.read_comic, name="read_comic"), + path("read//thumb", views.comic_thumbnail, name="comic_thumbnail"), path("set_page///", views.set_read_page, name="set_read_page"), path("read///img", views.get_image, name="get_image"), path("read//pdf", views.get_pdf, name="get_pdf"), - path("list_json/", views.comic_list_json, name="comic_list_json1"), - path("list_json//", views.comic_list_json, name="comic_list_json2"), path("recent/", views.recent_comics, name="recent_comics"), path("recent/json/", views.recent_comics_json, name="recent_comics_json"), path("edit/", views.comic_edit, name="comic_edit"), path("feed//", feeds.RecentComics()), path("/", views.comic_list, name="comic_list"), + path("/thumb", views.directory_thumbnail, name="directory_thumbnail"), + path("action////", views.perform_action, name="perform_action") ] diff --git a/comic/util.py b/comic/util.py index fb98c7c..4ef1e5b 100644 --- a/comic/util.py +++ b/comic/util.py @@ -1,6 +1,7 @@ from collections import OrderedDict -from os import listdir +from dataclasses import dataclass from pathlib import Path +from typing import Union from django.conf import settings from django.db.models import Count, Q, F @@ -78,44 +79,30 @@ def generate_breadcrumbs_from_menu(paths): return output +@dataclass class DirFile: - def __init__(self): - self.name = "" - self.icon = "" - self.location = "" - self.label = "" - self.type = "" - self.selector = "" + obj: Union[Directory, ComicBook] + name: str = '' + item_type: str = '' + percent: int = 0 + selector: str = '' - def __str__(self): - return self.name - - def populate_directory(self, directory, user): - self.name = directory.name - self.icon = "fa-folder-open" - self.selector = urlsafe_base64_encode(directory.selector.bytes) - self.location = "/comic/{0}/".format(self.selector) - self.label = generate_dir_status(directory.total, directory.total_read) - self.type = "directory" - - def populate_comic(self, comic, user): - if type(comic) == str: - self.icon = "fa-exclamation-circle" - self.name = comic - self.selector = "0" - self.location = "/" - self.label = '
Error
' - self.type = "book" - else: - self.icon = "fa-book" - self.name = comic.file_name - self.selector = urlsafe_base64_encode(comic.selector.bytes) - self.location = "/comic/read/{0}/".format(self.selector) - self.label = generate_label(comic) - self.type = "book" - - def __repr__(self): - return f"" + def __post_init__(self): + self.item_type = type(self.obj).__name__ + if hasattr(self.obj, 'total') and hasattr(self.obj, 'total_read'): + # because pages count from zero. + total_adjustment = 1 + if isinstance(self.obj, Directory): + total_adjustment = 0 + try: + self.percent = int((self.obj.total_read / (self.obj.total - total_adjustment)) * 100) + except ZeroDivisionError: + self.percent = 0 + self.selector = self.obj.url_safe_selector + if isinstance(self.obj, Directory): + self.name = self.obj.name + elif isinstance(self.obj, ComicBook): + self.name = self.obj.file_name def generate_directory(user, directory=False): @@ -127,11 +114,9 @@ def generate_directory(user, directory=False): files = [] if directory: dir_path = Path(base_dir, directory.path) - # ordered_dir_list = sorted(dir_path.glob('*')) dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, directory.path, x).is_dir()] else: dir_path = base_dir - # ordered_dir_list = base_dir.glob('*') dir_list = [x for x in sorted(dir_path.glob('*')) if Path(base_dir, x).is_dir()] file_list = [x for x in sorted(dir_path.glob('*')) if x.is_file()] if directory: @@ -154,23 +139,19 @@ def generate_directory(user, directory=False): ComicStatus.objects.bulk_create(new_status) file_list_obj = file_list_obj.annotate( - total_pages=Count('comicpage', distinct=True), - last_read_page=F('comicstatus__last_read_page'), + total=Count('comicpage', distinct=True), + total_read=F('comicstatus__last_read_page'), finished=F('comicstatus__finished'), unread=F('comicstatus__unread'), user=F('comicstatus__user') ).filter(Q(user__isnull=True) | Q(user=user.id)) for directory_obj in dir_list_obj: - df = DirFile() - df.populate_directory(directory_obj, user) - files.append(df) + files.append(DirFile(directory_obj)) dir_list.remove(Path(dir_path, directory_obj.name)) for file_obj in file_list_obj: - df = DirFile() - df.populate_comic(file_obj, user) - files.append(df) + files.append(DirFile(file_obj)) file_list.remove(Path(dir_path, file_obj.file_name)) for directory_name in dir_list: @@ -181,18 +162,14 @@ def generate_directory(user, directory=False): directory_obj.save() directory_obj.total = 0 directory_obj.total_read = 0 - df = DirFile() - df.populate_directory(directory_obj, user) - files.append(df) + files.append(DirFile(directory_obj)) for file_name in file_list: if file_name.suffix.lower() in [".rar", ".zip", ".cbr", ".cbz", ".pdf"]: - book = ComicBook.process_comic_book(file_name.name, directory) - df = DirFile() - df.populate_comic(book, user) - files.append(df) + book = ComicBook.process_comic_book(file_name, directory) + files.append(DirFile(book)) files.sort(key=lambda x: x.name) - files.sort(key=lambda x: x.type, reverse=True) + files.sort(key=lambda x: x.item_type, reverse=True) return files diff --git a/comic/views.py b/comic/views.py index ad0663c..d64ba50 100644 --- a/comic/views.py +++ b/comic/views.py @@ -1,9 +1,11 @@ import json import uuid +from PIL import Image from django.contrib.auth import authenticate, login from django.contrib.auth.decorators import login_required, user_passes_test from django.contrib.auth.models import User +from django.core.files.uploadedfile import InMemoryUploadedFile from django.db.models import Max, Count, F from django.db.transaction import atomic from django.http import HttpResponse, FileResponse @@ -31,60 +33,55 @@ from .util import ( def comic_list(request, directory_selector=False): if User.objects.all().count() == 0: return redirect("/comic/settings/") - # try: - # base_dir = Setting.objects.get(name="BASE_DIR").value - # except Setting.DoesNotExist: - # return redirect("/comic/settings/") - # if not path.isdir(base_dir): - # return redirect("/comic/settings/") + directory = None if directory_selector: selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector)) directory = Directory.objects.get(selector=selector) - else: - directory = False if directory: title = generate_title_from_path(directory.path) breadcrumbs = generate_breadcrumbs_from_path(directory) - json_url = "/comic/list_json/{0}/".format(directory_selector) else: title = generate_title_from_path("Home") breadcrumbs = generate_breadcrumbs_from_path() - json_url = "/comic/list_json/" + + files = generate_directory(request.user, directory) return render( request, "comic/comic_list.html", - {"breadcrumbs": breadcrumbs, "menu": Menu(request.user, "Browse"), "title": title, "json_url": json_url}, + { + "breadcrumbs": breadcrumbs, + "menu": Menu(request.user, "Browse"), + "title": title, + "files": files, + "selector": directory_selector if directory_selector else 'None' + }, ) @login_required -@require_POST -def comic_list_json(request, directory_selector=False): - icon_str = '' - if directory_selector: - directory_selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector)) - directory = Directory.objects.get(selector=directory_selector) - else: - directory = False - files = generate_directory(request.user, directory) - response_data = dict() - response_data["data"] = [] - for file in files: - response_data["data"].append( - { - "blank": "", - "selector": file.selector, - "type": file.type, - "icon": icon_str.format(file.icon), - "name": file.name, - "label": file.label, - "url": file.location, - } - ) - return HttpResponse(json.dumps(response_data), content_type="application/json") +def perform_action(request, operation, item_type, selector): + if operation not in ['mark_read', 'mark_unread', 'mark_previous']: + return HttpResponse(400) + elif operation == 'mark_previous' and item_type == 'Directory': + return HttpResponse(422) + try: + selector_uuid = uuid.UUID(bytes=urlsafe_base64_decode(selector)) + except ValueError: + if item_type == 'Directory': + for book in ComicBook.objects.filter(directory__isnull=True): + getattr(book, operation)(request.user) + return HttpResponse(204) + if item_type == 'ComicBook': + book = get_object_or_404(ComicBook, selector=selector_uuid) + getattr(book, operation)(request.user) + return HttpResponse(204) + elif item_type == 'Directory': + directory = get_object_or_404(Directory, selector=selector_uuid) + getattr(directory, operation)(request.user) + return HttpResponse(204) @login_required @@ -328,6 +325,22 @@ def get_image(_, comic_selector, page): return FileResponse(img, content_type=content) +@xframe_options_sameorigin +@login_required +def comic_thumbnail(_, comic_selector): + selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector)) + book = ComicBook.objects.get(selector=selector) + return redirect(book.get_thumbnail_url()) + + +@xframe_options_sameorigin +@login_required +def directory_thumbnail(_, directory_selector): + selector = uuid.UUID(bytes=urlsafe_base64_decode(directory_selector)) + folder = Directory.objects.get(selector=selector) + return redirect(folder.get_thumbnail_url()) + + @login_required def get_pdf(_, comic_selector): selector = uuid.UUID(bytes=urlsafe_base64_decode(comic_selector)) diff --git a/docker-compose.yml b/docker-compose.yml index b153068..b3d1cf4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,6 +14,7 @@ services: volumes: - ${COMIC_BOOK_VOLUME}:/data - static_files:/static + - media_files:/media command: /bin/bash entrypoint.sh cbwebreader-cron: @@ -42,6 +43,7 @@ services: image: nginx volumes: - static_files:/static + - media_files:/media - ./cbreader/settings/nginx.conf:/etc/nginx/conf.d/default.conf ports: - 1337:80 diff --git a/placehoder.xcf b/placehoder.xcf new file mode 100644 index 0000000000000000000000000000000000000000..afa30d570cb28999c17a71576ab7482e2fa3d881 GIT binary patch literal 9991 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.2.2" argon2 = ["argon2-cffi (>=19.1.0)"] bcrypt = ["bcrypt"] +[[package]] +name = "django-appconf" +version = "1.0.4" +description = "A helper class for handling configuration defaults of packaged apps gracefully." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +django = "*" + [[package]] name = "django-bootstrap4" version = "2.3.1" @@ -122,6 +133,23 @@ python-versions = ">=3.6" [package.dependencies] Django = ">=2.2" +[[package]] +name = "django-imagekit" +version = "4.0.2" +description = "Automated image processing for Django models." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +django-appconf = ">=0.5" +pilkit = ">=0.2.0" +six = "*" + +[package.extras] +async = ["django-celery (>=3.0)"] +async_rq = ["django-rq (>=0.6.0)"] + [[package]] name = "django-recaptcha2" version = "1.4.1" @@ -251,6 +279,22 @@ category = "main" optional = false python-versions = ">=3.5" +[[package]] +name = "pilkit" +version = "2.0" +description = "A collection of utilities and processors for the Python Imaging Libary." +category = "main" +optional = false +python-versions = "*" + +[[package]] +name = "pillow" +version = "8.2.0" +description = "Python Imaging Library (Fork)" +category = "main" +optional = false +python-versions = ">=3.6" + [[package]] name = "psycopg2" version = "2.8.6" @@ -414,7 +458,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"] [metadata] lock-version = "1.1" python-versions = "^3.8" -content-hash = "4b1fa38b242472a4006be4f77c969c122881924594ff66f0f74e7a7eded4f362" +content-hash = "05469ef9d59ad0de19cda78ddb8511654d50ac61d14eca9ca2ebac3605a3973c" [metadata.files] asgiref = [ @@ -504,6 +548,10 @@ django = [ {file = "Django-3.2-py3-none-any.whl", hash = "sha256:0604e84c4fb698a5e53e5857b5aea945b2f19a18f25f10b8748dbdf935788927"}, {file = "Django-3.2.tar.gz", hash = "sha256:21f0f9643722675976004eb683c55d33c05486f94506672df3d6a141546f389d"}, ] +django-appconf = [ + {file = "django-appconf-1.0.4.tar.gz", hash = "sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380"}, + {file = "django_appconf-1.0.4-py2.py3-none-any.whl", hash = "sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06"}, +] django-bootstrap4 = [ {file = "django-bootstrap4-2.3.1.tar.gz", hash = "sha256:2c199020ac38866cdf8d1c5561ce7468116b9685b455a29843c0225ef8568879"}, {file = "django_bootstrap4-2.3.1-py3-none-any.whl", hash = "sha256:b68f073b647b20ec7894a252a0ca4e06b7b8dafdbad995cb0cdc783d0bb4629d"}, @@ -512,6 +560,10 @@ django-extensions = [ {file = "django-extensions-3.1.3.tar.gz", hash = "sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0"}, {file = "django_extensions-3.1.3-py3-none-any.whl", hash = "sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3"}, ] +django-imagekit = [ + {file = "django-imagekit-4.0.2.tar.gz", hash = "sha256:6ec0afb77cdf52cd453c9fc2c10ef350d111edfd6ce53c5977aa8a0e22cee00c"}, + {file = "django_imagekit-4.0.2-py2.py3-none-any.whl", hash = "sha256:304c3379f6a5cac387e47ace11195a603ad3cb01e3e951b45489824d25b00359"}, +] django-recaptcha2 = [ {file = "django-recaptcha2-1.4.1.tar.gz", hash = "sha256:c0b43851b05c6bf6ebb5ecc890c13ccedacd9bb33d64b4291c74dd6fcbc89366"}, {file = "django_recaptcha2-1.4.1-py3-none-any.whl", hash = "sha256:9ea90db0cec502741be1066c09ec1b8e02a73162a319a042e78e67c4605087af"}, @@ -626,6 +678,44 @@ mysqlclient = [ {file = "mysqlclient-2.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6"}, {file = "mysqlclient-2.0.3.tar.gz", hash = "sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432"}, ] +pilkit = [ + {file = "pilkit-2.0.tar.gz", hash = "sha256:ddb30c2f0198a147e56b151476c3bb9fe045fbfd5b0a0fa2a3148dba62d1559f"}, +] +pillow = [ + {file = "Pillow-8.2.0-cp36-cp36m-macosx_10_10_x86_64.whl", hash = "sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9"}, + {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b"}, + {file = "Pillow-8.2.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b"}, + {file = "Pillow-8.2.0-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9"}, + {file = "Pillow-8.2.0-cp36-cp36m-win32.whl", hash = "sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727"}, + {file = "Pillow-8.2.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f"}, + {file = "Pillow-8.2.0-cp37-cp37m-macosx_10_10_x86_64.whl", hash = "sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d"}, + {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a"}, + {file = "Pillow-8.2.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9"}, + {file = "Pillow-8.2.0-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388"}, + {file = "Pillow-8.2.0-cp37-cp37m-win32.whl", hash = "sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5"}, + {file = "Pillow-8.2.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2"}, + {file = "Pillow-8.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4"}, + {file = "Pillow-8.2.0-cp38-cp38-manylinux1_i686.whl", hash = "sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812"}, + {file = "Pillow-8.2.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178"}, + {file = "Pillow-8.2.0-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb"}, + {file = "Pillow-8.2.0-cp38-cp38-win32.whl", hash = "sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232"}, + {file = "Pillow-8.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797"}, + {file = "Pillow-8.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5"}, + {file = "Pillow-8.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484"}, + {file = "Pillow-8.2.0-cp39-cp39-manylinux1_i686.whl", hash = "sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602"}, + {file = "Pillow-8.2.0-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2"}, + {file = "Pillow-8.2.0-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef"}, + {file = "Pillow-8.2.0-cp39-cp39-win32.whl", hash = "sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713"}, + {file = "Pillow-8.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c"}, + {file = "Pillow-8.2.0-pp36-pypy36_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9"}, + {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_i686.whl", hash = "sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9"}, + {file = "Pillow-8.2.0-pp36-pypy36_pp73-manylinux2010_x86_64.whl", hash = "sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_i686.whl", hash = "sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-manylinux2010_x86_64.whl", hash = "sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120"}, + {file = "Pillow-8.2.0-pp37-pypy37_pp73-win32.whl", hash = "sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e"}, + {file = "Pillow-8.2.0.tar.gz", hash = "sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1"}, +] psycopg2 = [ {file = "psycopg2-2.8.6-cp27-cp27m-win32.whl", hash = "sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725"}, {file = "psycopg2-2.8.6-cp27-cp27m-win_amd64.whl", hash = "sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5"}, diff --git a/pyproject.toml b/pyproject.toml index 0572fb0..7140089 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,6 +24,8 @@ psycopg2 = "^2.8.6" rarfile = "^4.0" coverage = "^5.5" django-extensions = "^3.1.3" +Pillow = "^8.2.0" +django-imagekit = "^4.0.2" [tool.poetry.dev-dependencies] mypy = "^0.812" diff --git a/requirements.txt b/requirements.txt index 76bda85..cc0810f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -73,9 +73,18 @@ coverage==5.5; (python_version >= "2.7" and python_full_version < "3.0.0") or (p dj-database-url==0.5.0 \ --hash=sha256:4aeaeb1f573c74835b0686a2b46b85990571159ffc21aa57ecd4d1e1cb334163 \ --hash=sha256:851785365761ebe4994a921b433062309eb882fedd318e1b0fcecc607ed02da9 +django-appconf==1.0.4 \ + --hash=sha256:be58deb54a43d77d2e1621fe59f787681376d3cd0b8bd8e4758ef6c3a6453380 \ + --hash=sha256:1b1d0e1069c843ebe8ae5aa48ec52403b1440402b320c3e3a206a0907e97bb06 django-bootstrap4==2.3.1; python_version >= "3.6" and python_version < "4.0" \ --hash=sha256:2c199020ac38866cdf8d1c5561ce7468116b9685b455a29843c0225ef8568879 \ --hash=sha256:b68f073b647b20ec7894a252a0ca4e06b7b8dafdbad995cb0cdc783d0bb4629d +django-extensions==3.1.3; python_version >= "3.6" \ + --hash=sha256:5f0fea7bf131ca303090352577a9e7f8bfbf5489bd9d9c8aea9401db28db34a0 \ + --hash=sha256:50de8977794a66a91575dd40f87d5053608f679561731845edbd325ceeb387e3 +django-imagekit==4.0.2 \ + --hash=sha256:6ec0afb77cdf52cd453c9fc2c10ef350d111edfd6ce53c5977aa8a0e22cee00c \ + --hash=sha256:304c3379f6a5cac387e47ace11195a603ad3cb01e3e951b45489824d25b00359 django-recaptcha2==1.4.1 \ --hash=sha256:c0b43851b05c6bf6ebb5ecc890c13ccedacd9bb33d64b4291c74dd6fcbc89366 \ --hash=sha256:9ea90db0cec502741be1066c09ec1b8e02a73162a319a042e78e67c4605087af @@ -156,6 +165,42 @@ mysqlclient==2.0.3; python_version >= "3.5" \ --hash=sha256:71c4b330cf2313bbda0307fc858cc9055e64493ba9bf28454d25cf8b3ee8d7f5 \ --hash=sha256:fc575093cf81b6605bed84653e48b277318b880dc9becf42dd47fa11ffd3e2b6 \ --hash=sha256:f6ebea7c008f155baeefe16c56cd3ee6239f7a5a9ae42396c2f1860f08a7c432 +pilkit==2.0 \ + --hash=sha256:ddb30c2f0198a147e56b151476c3bb9fe045fbfd5b0a0fa2a3148dba62d1559f +pillow==8.2.0; python_version >= "3.6" \ + --hash=sha256:dc38f57d8f20f06dd7c3161c59ca2c86893632623f33a42d592f097b00f720a9 \ + --hash=sha256:a013cbe25d20c2e0c4e85a9daf438f85121a4d0344ddc76e33fd7e3965d9af4b \ + --hash=sha256:8bb1e155a74e1bfbacd84555ea62fa21c58e0b4e7e6b20e4447b8d07990ac78b \ + --hash=sha256:c5236606e8570542ed424849f7852a0ff0bce2c4c8d0ba05cc202a5a9c97dee9 \ + --hash=sha256:12e5e7471f9b637762453da74e390e56cc43e486a88289995c1f4c1dc0bfe727 \ + --hash=sha256:5afe6b237a0b81bd54b53f835a153770802f164c5570bab5e005aad693dab87f \ + --hash=sha256:cb7a09e173903541fa888ba010c345893cd9fc1b5891aaf060f6ca77b6a3722d \ + --hash=sha256:0d19d70ee7c2ba97631bae1e7d4725cdb2ecf238178096e8c82ee481e189168a \ + --hash=sha256:083781abd261bdabf090ad07bb69f8f5599943ddb539d64497ed021b2a67e5a9 \ + --hash=sha256:c6b39294464b03457f9064e98c124e09008b35a62e3189d3513e5148611c9388 \ + --hash=sha256:01425106e4e8cee195a411f729cff2a7d61813b0b11737c12bd5991f5f14bcd5 \ + --hash=sha256:3b570f84a6161cf8865c4e08adf629441f56e32f180f7aa4ccbd2e0a5a02cba2 \ + --hash=sha256:031a6c88c77d08aab84fecc05c3cde8414cd6f8406f4d2b16fed1e97634cc8a4 \ + --hash=sha256:66cc56579fd91f517290ab02c51e3a80f581aba45fd924fcdee01fa06e635812 \ + --hash=sha256:6c32cc3145928c4305d142ebec682419a6c0a8ce9e33db900027ddca1ec39178 \ + --hash=sha256:624b977355cde8b065f6d51b98497d6cd5fbdd4f36405f7a8790e3376125e2bb \ + --hash=sha256:5cbf3e3b1014dddc45496e8cf38b9f099c95a326275885199f427825c6522232 \ + --hash=sha256:463822e2f0d81459e113372a168f2ff59723e78528f91f0bd25680ac185cf797 \ + --hash=sha256:95d5ef984eff897850f3a83883363da64aae1000e79cb3c321915468e8c6add5 \ + --hash=sha256:b91c36492a4bbb1ee855b7d16fe51379e5f96b85692dc8210831fbb24c43e484 \ + --hash=sha256:d68cb92c408261f806b15923834203f024110a2e2872ecb0bd2a110f89d3c602 \ + --hash=sha256:f217c3954ce5fd88303fc0c317af55d5e0204106d86dea17eb8205700d47dec2 \ + --hash=sha256:5b70110acb39f3aff6b74cf09bb4169b167e2660dabc304c1e25b6555fa781ef \ + --hash=sha256:a7d5e9fad90eff8f6f6106d3b98b553a88b6f976e51fce287192a5d2d5363713 \ + --hash=sha256:238c197fc275b475e87c1453b05b467d2d02c2915fdfdd4af126145ff2e4610c \ + --hash=sha256:0e04d61f0064b545b989126197930807c86bcbd4534d39168f4aa5fda39bb8f9 \ + --hash=sha256:63728564c1410d99e6d1ae8e3b810fe012bc440952168af0a2877e8ff5ab96b9 \ + --hash=sha256:c03c07ed32c5324939b19e36ae5f75c660c81461e312a41aea30acdd46f93a7c \ + --hash=sha256:4d98abdd6b1e3bf1a1cbb14c3895226816e666749ac040c4e2554231068c639b \ + --hash=sha256:aac00e4bc94d1b7813fe882c28990c1bc2f9d0e1aa765a5f2b516e8a6a16a9e4 \ + --hash=sha256:22fd0f42ad15dfdde6c581347eaa4adb9a6fc4b865f90b23378aa7914895e120 \ + --hash=sha256:e98eca29a05913e82177b3ba3d198b1728e164869c613d76d0de4bde6768a50e \ + --hash=sha256:a787ab10d7bb5494e5f76536ac460741788f1fbce851068d73a87ca7c35fc3e1 psycopg2==2.8.6; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") \ --hash=sha256:068115e13c70dc5982dfc00c5d70437fe37c014c808acce119b5448361c03725 \ --hash=sha256:d160744652e81c80627a909a0e808f3c6653a40af435744de037e3172cf277f5 \ diff --git a/static/img/placeholder.png b/static/img/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..33dd8d5f6f5aba4ecc0d05209c21e28927819cbb GIT binary patch literal 7701 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 $().plugin('option', {...}) + if ( !PluginClass.prototype.option ) { + // option setter + PluginClass.prototype.option = function( opts ) { + // bail out if not an object + if ( !$.isPlainObject( opts ) ){ + return; + } + this.options = $.extend( true, this.options, opts ); + }; + } + + // make jQuery plugin + $.fn[ namespace ] = function( arg0 /*, arg1 */ ) { + if ( typeof arg0 == 'string' ) { + // method call $().plugin( 'methodName', { options } ) + // shift arguments by 1 + var args = arraySlice.call( arguments, 1 ); + return methodCall( this, arg0, args ); + } + // just $().plugin({ options }) + plainCall( this, arg0 ); + return this; + }; + + // $().plugin('methodName') + function methodCall( $elems, methodName, args ) { + var returnValue; + var pluginMethodStr = '$().' + namespace + '("' + methodName + '")'; + + $elems.each( function( i, elem ) { + // get instance + var instance = $.data( elem, namespace ); + if ( !instance ) { + logError( namespace + ' not initialized. Cannot call methods, i.e. ' + + pluginMethodStr ); + return; + } + + var method = instance[ methodName ]; + if ( !method || methodName.charAt(0) == '_' ) { + logError( pluginMethodStr + ' is not a valid method' ); + return; + } + + // apply method, get return value + var value = method.apply( instance, args ); + // set return value if value is returned, use only first value + returnValue = returnValue === undefined ? value : returnValue; + }); + + return returnValue !== undefined ? returnValue : $elems; + } + + function plainCall( $elems, options ) { + $elems.each( function( i, elem ) { + var instance = $.data( elem, namespace ); + if ( instance ) { + // set options & init + instance.option( options ); + instance._init(); + } else { + // initialize new instance + instance = new PluginClass( elem, options ); + $.data( elem, namespace, instance ); + } + }); + } + + updateJQuery( $ ); + +} + +// ----- updateJQuery ----- // + +// set $.bridget for v1 backwards compatibility +function updateJQuery( $ ) { + if ( !$ || ( $ && $.bridget ) ) { + return; + } + $.bridget = jQueryBridget; +} + +updateJQuery( jQuery || window.jQuery ); + +// ----- ----- // + +return jQueryBridget; + +})); + +/** + * EvEmitter v1.1.0 + * Lil' event emitter + * MIT License + */ + +/* jshint unused: true, undef: true, strict: true */ + +( function( global, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, window */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'ev-emitter/ev-emitter',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory(); + } else { + // Browser globals + global.EvEmitter = factory(); + } + +}( typeof window != 'undefined' ? window : this, function() { + + + +function EvEmitter() {} + +var proto = EvEmitter.prototype; + +proto.on = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // set events hash + var events = this._events = this._events || {}; + // set listeners array + var listeners = events[ eventName ] = events[ eventName ] || []; + // only add once + if ( listeners.indexOf( listener ) == -1 ) { + listeners.push( listener ); + } + + return this; +}; + +proto.once = function( eventName, listener ) { + if ( !eventName || !listener ) { + return; + } + // add event + this.on( eventName, listener ); + // set once flag + // set onceEvents hash + var onceEvents = this._onceEvents = this._onceEvents || {}; + // set onceListeners object + var onceListeners = onceEvents[ eventName ] = onceEvents[ eventName ] || {}; + // set flag + onceListeners[ listener ] = true; + + return this; +}; + +proto.off = function( eventName, listener ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + var index = listeners.indexOf( listener ); + if ( index != -1 ) { + listeners.splice( index, 1 ); + } + + return this; +}; + +proto.emitEvent = function( eventName, args ) { + var listeners = this._events && this._events[ eventName ]; + if ( !listeners || !listeners.length ) { + return; + } + // copy over to avoid interference if .off() in listener + listeners = listeners.slice(0); + args = args || []; + // once stuff + var onceListeners = this._onceEvents && this._onceEvents[ eventName ]; + + for ( var i=0; i < listeners.length; i++ ) { + var listener = listeners[i] + var isOnce = onceListeners && onceListeners[ listener ]; + if ( isOnce ) { + // remove listener + // remove before trigger to prevent recursion + this.off( eventName, listener ); + // unset once flag + delete onceListeners[ listener ]; + } + // trigger listener + listener.apply( this, args ); + } + + return this; +}; + +proto.allOff = function() { + delete this._events; + delete this._onceEvents; +}; + +return EvEmitter; + +})); + +/*! + * getSize v2.0.3 + * measure size of elements + * MIT license + */ + +/* jshint browser: true, strict: true, undef: true, unused: true */ +/* globals console: false */ + +( function( window, factory ) { + /* jshint strict: false */ /* globals define, module */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'get-size/get-size',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.getSize = factory(); + } + +})( window, function factory() { +'use strict'; + +// -------------------------- helpers -------------------------- // + +// get a number from a string, not a percentage +function getStyleSize( value ) { + var num = parseFloat( value ); + // not a percent like '100%', and a number + var isValid = value.indexOf('%') == -1 && !isNaN( num ); + return isValid && num; +} + +function noop() {} + +var logError = typeof console == 'undefined' ? noop : + function( message ) { + console.error( message ); + }; + +// -------------------------- measurements -------------------------- // + +var measurements = [ + 'paddingLeft', + 'paddingRight', + 'paddingTop', + 'paddingBottom', + 'marginLeft', + 'marginRight', + 'marginTop', + 'marginBottom', + 'borderLeftWidth', + 'borderRightWidth', + 'borderTopWidth', + 'borderBottomWidth' +]; + +var measurementsLength = measurements.length; + +function getZeroSize() { + var size = { + width: 0, + height: 0, + innerWidth: 0, + innerHeight: 0, + outerWidth: 0, + outerHeight: 0 + }; + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + size[ measurement ] = 0; + } + return size; +} + +// -------------------------- getStyle -------------------------- // + +/** + * getStyle, get style of element, check for Firefox bug + * https://bugzilla.mozilla.org/show_bug.cgi?id=548397 + */ +function getStyle( elem ) { + var style = getComputedStyle( elem ); + if ( !style ) { + logError( 'Style returned ' + style + + '. Are you running this code in a hidden iframe on Firefox? ' + + 'See https://bit.ly/getsizebug1' ); + } + return style; +} + +// -------------------------- setup -------------------------- // + +var isSetup = false; + +var isBoxSizeOuter; + +/** + * setup + * check isBoxSizerOuter + * do on first getSize() rather than on page load for Firefox bug + */ +function setup() { + // setup once + if ( isSetup ) { + return; + } + isSetup = true; + + // -------------------------- box sizing -------------------------- // + + /** + * Chrome & Safari measure the outer-width on style.width on border-box elems + * IE11 & Firefox<29 measures the inner-width + */ + var div = document.createElement('div'); + div.style.width = '200px'; + div.style.padding = '1px 2px 3px 4px'; + div.style.borderStyle = 'solid'; + div.style.borderWidth = '1px 2px 3px 4px'; + div.style.boxSizing = 'border-box'; + + var body = document.body || document.documentElement; + body.appendChild( div ); + var style = getStyle( div ); + // round value for browser zoom. desandro/masonry#928 + isBoxSizeOuter = Math.round( getStyleSize( style.width ) ) == 200; + getSize.isBoxSizeOuter = isBoxSizeOuter; + + body.removeChild( div ); +} + +// -------------------------- getSize -------------------------- // + +function getSize( elem ) { + setup(); + + // use querySeletor if elem is string + if ( typeof elem == 'string' ) { + elem = document.querySelector( elem ); + } + + // do not proceed on non-objects + if ( !elem || typeof elem != 'object' || !elem.nodeType ) { + return; + } + + var style = getStyle( elem ); + + // if hidden, everything is 0 + if ( style.display == 'none' ) { + return getZeroSize(); + } + + var size = {}; + size.width = elem.offsetWidth; + size.height = elem.offsetHeight; + + var isBorderBox = size.isBorderBox = style.boxSizing == 'border-box'; + + // get all measurements + for ( var i=0; i < measurementsLength; i++ ) { + var measurement = measurements[i]; + var value = style[ measurement ]; + var num = parseFloat( value ); + // any 'auto', 'medium' value will be 0 + size[ measurement ] = !isNaN( num ) ? num : 0; + } + + var paddingWidth = size.paddingLeft + size.paddingRight; + var paddingHeight = size.paddingTop + size.paddingBottom; + var marginWidth = size.marginLeft + size.marginRight; + var marginHeight = size.marginTop + size.marginBottom; + var borderWidth = size.borderLeftWidth + size.borderRightWidth; + var borderHeight = size.borderTopWidth + size.borderBottomWidth; + + var isBorderBoxSizeOuter = isBorderBox && isBoxSizeOuter; + + // overwrite width and height if we can get it from style + var styleWidth = getStyleSize( style.width ); + if ( styleWidth !== false ) { + size.width = styleWidth + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingWidth + borderWidth ); + } + + var styleHeight = getStyleSize( style.height ); + if ( styleHeight !== false ) { + size.height = styleHeight + + // add padding and border unless it's already including it + ( isBorderBoxSizeOuter ? 0 : paddingHeight + borderHeight ); + } + + size.innerWidth = size.width - ( paddingWidth + borderWidth ); + size.innerHeight = size.height - ( paddingHeight + borderHeight ); + + size.outerWidth = size.width + marginWidth; + size.outerHeight = size.height + marginHeight; + + return size; +} + +return getSize; + +}); + +/** + * matchesSelector v2.0.2 + * matchesSelector( element, '.selector' ) + * MIT license + */ + +/*jshint browser: true, strict: true, undef: true, unused: true */ + +( function( window, factory ) { + /*global define: false, module: false */ + 'use strict'; + // universal module definition + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'desandro-matches-selector/matches-selector',factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory(); + } else { + // browser global + window.matchesSelector = factory(); + } + +}( window, function factory() { + 'use strict'; + + var matchesMethod = ( function() { + var ElemProto = window.Element.prototype; + // check for the standard method name first + if ( ElemProto.matches ) { + return 'matches'; + } + // check un-prefixed + if ( ElemProto.matchesSelector ) { + return 'matchesSelector'; + } + // check vendor prefixes + var prefixes = [ 'webkit', 'moz', 'ms', 'o' ]; + + for ( var i=0; i < prefixes.length; i++ ) { + var prefix = prefixes[i]; + var method = prefix + 'MatchesSelector'; + if ( ElemProto[ method ] ) { + return method; + } + } + })(); + + return function matchesSelector( elem, selector ) { + return elem[ matchesMethod ]( selector ); + }; + +})); + +/** + * Fizzy UI utils v2.0.7 + * MIT license + */ + +/*jshint browser: true, undef: true, unused: true, strict: true */ + +( function( window, factory ) { + // universal module definition + /*jshint strict: false */ /*globals define, module, require */ + + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'fizzy-ui-utils/utils',[ + 'desandro-matches-selector/matches-selector' + ], function( matchesSelector ) { + return factory( window, matchesSelector ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('desandro-matches-selector') + ); + } else { + // browser global + window.fizzyUIUtils = factory( + window, + window.matchesSelector + ); + } + +}( window, function factory( window, matchesSelector ) { + + + +var utils = {}; + +// ----- extend ----- // + +// extends objects +utils.extend = function( a, b ) { + for ( var prop in b ) { + a[ prop ] = b[ prop ]; + } + return a; +}; + +// ----- modulo ----- // + +utils.modulo = function( num, div ) { + return ( ( num % div ) + div ) % div; +}; + +// ----- makeArray ----- // + +var arraySlice = Array.prototype.slice; + +// turn element or nodeList into an array +utils.makeArray = function( obj ) { + if ( Array.isArray( obj ) ) { + // use object if already an array + return obj; + } + // return empty array if undefined or null. #6 + if ( obj === null || obj === undefined ) { + return []; + } + + var isArrayLike = typeof obj == 'object' && typeof obj.length == 'number'; + if ( isArrayLike ) { + // convert nodeList to array + return arraySlice.call( obj ); + } + + // array of single index + return [ obj ]; +}; + +// ----- removeFrom ----- // + +utils.removeFrom = function( ary, obj ) { + var index = ary.indexOf( obj ); + if ( index != -1 ) { + ary.splice( index, 1 ); + } +}; + +// ----- getParent ----- // + +utils.getParent = function( elem, selector ) { + while ( elem.parentNode && elem != document.body ) { + elem = elem.parentNode; + if ( matchesSelector( elem, selector ) ) { + return elem; + } + } +}; + +// ----- getQueryElement ----- // + +// use element as selector string +utils.getQueryElement = function( elem ) { + if ( typeof elem == 'string' ) { + return document.querySelector( elem ); + } + return elem; +}; + +// ----- handleEvent ----- // + +// enable .ontype to trigger from .addEventListener( elem, 'type' ) +utils.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +// ----- filterFindElements ----- // + +utils.filterFindElements = function( elems, selector ) { + // make array of elems + elems = utils.makeArray( elems ); + var ffElems = []; + + elems.forEach( function( elem ) { + // check that elem is an actual element + if ( !( elem instanceof HTMLElement ) ) { + return; + } + // add elem if no selector + if ( !selector ) { + ffElems.push( elem ); + return; + } + // filter & find items if we have a selector + // filter + if ( matchesSelector( elem, selector ) ) { + ffElems.push( elem ); + } + // find children + var childElems = elem.querySelectorAll( selector ); + // concat childElems to filterFound array + for ( var i=0; i < childElems.length; i++ ) { + ffElems.push( childElems[i] ); + } + }); + + return ffElems; +}; + +// ----- debounceMethod ----- // + +utils.debounceMethod = function( _class, methodName, threshold ) { + threshold = threshold || 100; + // original method + var method = _class.prototype[ methodName ]; + var timeoutName = methodName + 'Timeout'; + + _class.prototype[ methodName ] = function() { + var timeout = this[ timeoutName ]; + clearTimeout( timeout ); + + var args = arguments; + var _this = this; + this[ timeoutName ] = setTimeout( function() { + method.apply( _this, args ); + delete _this[ timeoutName ]; + }, threshold ); + }; +}; + +// ----- docReady ----- // + +utils.docReady = function( callback ) { + var readyState = document.readyState; + if ( readyState == 'complete' || readyState == 'interactive' ) { + // do async to allow for other scripts to run. metafizzy/flickity#441 + setTimeout( callback ); + } else { + document.addEventListener( 'DOMContentLoaded', callback ); + } +}; + +// ----- htmlInit ----- // + +// http://jamesroberts.name/blog/2010/02/22/string-functions-for-javascript-trim-to-camel-case-to-dashed-and-to-underscore/ +utils.toDashed = function( str ) { + return str.replace( /(.)([A-Z])/g, function( match, $1, $2 ) { + return $1 + '-' + $2; + }).toLowerCase(); +}; + +var console = window.console; +/** + * allow user to initialize classes via [data-namespace] or .js-namespace class + * htmlInit( Widget, 'widgetName' ) + * options are parsed from data-namespace-options + */ +utils.htmlInit = function( WidgetClass, namespace ) { + utils.docReady( function() { + var dashedNamespace = utils.toDashed( namespace ); + var dataAttr = 'data-' + dashedNamespace; + var dataAttrElems = document.querySelectorAll( '[' + dataAttr + ']' ); + var jsDashElems = document.querySelectorAll( '.js-' + dashedNamespace ); + var elems = utils.makeArray( dataAttrElems ) + .concat( utils.makeArray( jsDashElems ) ); + var dataOptionsAttr = dataAttr + '-options'; + var jQuery = window.jQuery; + + elems.forEach( function( elem ) { + var attr = elem.getAttribute( dataAttr ) || + elem.getAttribute( dataOptionsAttr ); + var options; + try { + options = attr && JSON.parse( attr ); + } catch ( error ) { + // log error, do not initialize + if ( console ) { + console.error( 'Error parsing ' + dataAttr + ' on ' + elem.className + + ': ' + error ); + } + return; + } + // initialize + var instance = new WidgetClass( elem, options ); + // make available via $().data('namespace') + if ( jQuery ) { + jQuery.data( elem, namespace, instance ); + } + }); + + }); +}; + +// ----- ----- // + +return utils; + +})); + +/** + * Outlayer Item + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'outlayer/item',[ + 'ev-emitter/ev-emitter', + 'get-size/get-size' + ], + factory + ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory( + require('ev-emitter'), + require('get-size') + ); + } else { + // browser global + window.Outlayer = {}; + window.Outlayer.Item = factory( + window.EvEmitter, + window.getSize + ); + } + +}( window, function factory( EvEmitter, getSize ) { +'use strict'; + +// ----- helpers ----- // + +function isEmptyObj( obj ) { + for ( var prop in obj ) { + return false; + } + prop = null; + return true; +} + +// -------------------------- CSS3 support -------------------------- // + + +var docElemStyle = document.documentElement.style; + +var transitionProperty = typeof docElemStyle.transition == 'string' ? + 'transition' : 'WebkitTransition'; +var transformProperty = typeof docElemStyle.transform == 'string' ? + 'transform' : 'WebkitTransform'; + +var transitionEndEvent = { + WebkitTransition: 'webkitTransitionEnd', + transition: 'transitionend' +}[ transitionProperty ]; + +// cache all vendor properties that could have vendor prefix +var vendorProperties = { + transform: transformProperty, + transition: transitionProperty, + transitionDuration: transitionProperty + 'Duration', + transitionProperty: transitionProperty + 'Property', + transitionDelay: transitionProperty + 'Delay' +}; + +// -------------------------- Item -------------------------- // + +function Item( element, layout ) { + if ( !element ) { + return; + } + + this.element = element; + // parent layout class, i.e. Masonry, Isotope, or Packery + this.layout = layout; + this.position = { + x: 0, + y: 0 + }; + + this._create(); +} + +// inherit EvEmitter +var proto = Item.prototype = Object.create( EvEmitter.prototype ); +proto.constructor = Item; + +proto._create = function() { + // transition objects + this._transn = { + ingProperties: {}, + clean: {}, + onEnd: {} + }; + + this.css({ + position: 'absolute' + }); +}; + +// trigger specified handler for event type +proto.handleEvent = function( event ) { + var method = 'on' + event.type; + if ( this[ method ] ) { + this[ method ]( event ); + } +}; + +proto.getSize = function() { + this.size = getSize( this.element ); +}; + +/** + * apply CSS styles to element + * @param {Object} style + */ +proto.css = function( style ) { + var elemStyle = this.element.style; + + for ( var prop in style ) { + // use vendor property if available + var supportedProp = vendorProperties[ prop ] || prop; + elemStyle[ supportedProp ] = style[ prop ]; + } +}; + + // measure position, and sets it +proto.getPosition = function() { + var style = getComputedStyle( this.element ); + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + var xValue = style[ isOriginLeft ? 'left' : 'right' ]; + var yValue = style[ isOriginTop ? 'top' : 'bottom' ]; + var x = parseFloat( xValue ); + var y = parseFloat( yValue ); + // convert percent to pixels + var layoutSize = this.layout.size; + if ( xValue.indexOf('%') != -1 ) { + x = ( x / 100 ) * layoutSize.width; + } + if ( yValue.indexOf('%') != -1 ) { + y = ( y / 100 ) * layoutSize.height; + } + // clean up 'auto' or other non-integer values + x = isNaN( x ) ? 0 : x; + y = isNaN( y ) ? 0 : y; + // remove padding from measurement + x -= isOriginLeft ? layoutSize.paddingLeft : layoutSize.paddingRight; + y -= isOriginTop ? layoutSize.paddingTop : layoutSize.paddingBottom; + + this.position.x = x; + this.position.y = y; +}; + +// set settled position, apply padding +proto.layoutPosition = function() { + var layoutSize = this.layout.size; + var style = {}; + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + + // x + var xPadding = isOriginLeft ? 'paddingLeft' : 'paddingRight'; + var xProperty = isOriginLeft ? 'left' : 'right'; + var xResetProperty = isOriginLeft ? 'right' : 'left'; + + var x = this.position.x + layoutSize[ xPadding ]; + // set in percentage or pixels + style[ xProperty ] = this.getXValue( x ); + // reset other property + style[ xResetProperty ] = ''; + + // y + var yPadding = isOriginTop ? 'paddingTop' : 'paddingBottom'; + var yProperty = isOriginTop ? 'top' : 'bottom'; + var yResetProperty = isOriginTop ? 'bottom' : 'top'; + + var y = this.position.y + layoutSize[ yPadding ]; + // set in percentage or pixels + style[ yProperty ] = this.getYValue( y ); + // reset other property + style[ yResetProperty ] = ''; + + this.css( style ); + this.emitEvent( 'layout', [ this ] ); +}; + +proto.getXValue = function( x ) { + var isHorizontal = this.layout._getOption('horizontal'); + return this.layout.options.percentPosition && !isHorizontal ? + ( ( x / this.layout.size.width ) * 100 ) + '%' : x + 'px'; +}; + +proto.getYValue = function( y ) { + var isHorizontal = this.layout._getOption('horizontal'); + return this.layout.options.percentPosition && isHorizontal ? + ( ( y / this.layout.size.height ) * 100 ) + '%' : y + 'px'; +}; + +proto._transitionTo = function( x, y ) { + this.getPosition(); + // get current x & y from top/left + var curX = this.position.x; + var curY = this.position.y; + + var didNotMove = x == this.position.x && y == this.position.y; + + // save end position + this.setPosition( x, y ); + + // if did not move and not transitioning, just go to layout + if ( didNotMove && !this.isTransitioning ) { + this.layoutPosition(); + return; + } + + var transX = x - curX; + var transY = y - curY; + var transitionStyle = {}; + transitionStyle.transform = this.getTranslate( transX, transY ); + + this.transition({ + to: transitionStyle, + onTransitionEnd: { + transform: this.layoutPosition + }, + isCleaning: true + }); +}; + +proto.getTranslate = function( x, y ) { + // flip cooridinates if origin on right or bottom + var isOriginLeft = this.layout._getOption('originLeft'); + var isOriginTop = this.layout._getOption('originTop'); + x = isOriginLeft ? x : -x; + y = isOriginTop ? y : -y; + return 'translate3d(' + x + 'px, ' + y + 'px, 0)'; +}; + +// non transition + transform support +proto.goTo = function( x, y ) { + this.setPosition( x, y ); + this.layoutPosition(); +}; + +proto.moveTo = proto._transitionTo; + +proto.setPosition = function( x, y ) { + this.position.x = parseFloat( x ); + this.position.y = parseFloat( y ); +}; + +// ----- transition ----- // + +/** + * @param {Object} style - CSS + * @param {Function} onTransitionEnd + */ + +// non transition, just trigger callback +proto._nonTransition = function( args ) { + this.css( args.to ); + if ( args.isCleaning ) { + this._removeStyles( args.to ); + } + for ( var prop in args.onTransitionEnd ) { + args.onTransitionEnd[ prop ].call( this ); + } +}; + +/** + * proper transition + * @param {Object} args - arguments + * @param {Object} to - style to transition to + * @param {Object} from - style to start transition from + * @param {Boolean} isCleaning - removes transition styles after transition + * @param {Function} onTransitionEnd - callback + */ +proto.transition = function( args ) { + // redirect to nonTransition if no transition duration + if ( !parseFloat( this.layout.options.transitionDuration ) ) { + this._nonTransition( args ); + return; + } + + var _transition = this._transn; + // keep track of onTransitionEnd callback by css property + for ( var prop in args.onTransitionEnd ) { + _transition.onEnd[ prop ] = args.onTransitionEnd[ prop ]; + } + // keep track of properties that are transitioning + for ( prop in args.to ) { + _transition.ingProperties[ prop ] = true; + // keep track of properties to clean up when transition is done + if ( args.isCleaning ) { + _transition.clean[ prop ] = true; + } + } + + // set from styles + if ( args.from ) { + this.css( args.from ); + // force redraw. http://blog.alexmaccaw.com/css-transitions + var h = this.element.offsetHeight; + // hack for JSHint to hush about unused var + h = null; + } + // enable transition + this.enableTransition( args.to ); + // set styles that are transitioning + this.css( args.to ); + + this.isTransitioning = true; + +}; + +// dash before all cap letters, including first for +// WebkitTransform => -webkit-transform +function toDashedAll( str ) { + return str.replace( /([A-Z])/g, function( $1 ) { + return '-' + $1.toLowerCase(); + }); +} + +var transitionProps = 'opacity,' + toDashedAll( transformProperty ); + +proto.enableTransition = function(/* style */) { + // HACK changing transitionProperty during a transition + // will cause transition to jump + if ( this.isTransitioning ) { + return; + } + + // make `transition: foo, bar, baz` from style object + // HACK un-comment this when enableTransition can work + // while a transition is happening + // var transitionValues = []; + // for ( var prop in style ) { + // // dash-ify camelCased properties like WebkitTransition + // prop = vendorProperties[ prop ] || prop; + // transitionValues.push( toDashedAll( prop ) ); + // } + // munge number to millisecond, to match stagger + var duration = this.layout.options.transitionDuration; + duration = typeof duration == 'number' ? duration + 'ms' : duration; + // enable transition styles + this.css({ + transitionProperty: transitionProps, + transitionDuration: duration, + transitionDelay: this.staggerDelay || 0 + }); + // listen for transition end event + this.element.addEventListener( transitionEndEvent, this, false ); +}; + +// ----- events ----- // + +proto.onwebkitTransitionEnd = function( event ) { + this.ontransitionend( event ); +}; + +proto.onotransitionend = function( event ) { + this.ontransitionend( event ); +}; + +// properties that I munge to make my life easier +var dashedVendorProperties = { + '-webkit-transform': 'transform' +}; + +proto.ontransitionend = function( event ) { + // disregard bubbled events from children + if ( event.target !== this.element ) { + return; + } + var _transition = this._transn; + // get property name of transitioned property, convert to prefix-free + var propertyName = dashedVendorProperties[ event.propertyName ] || event.propertyName; + + // remove property that has completed transitioning + delete _transition.ingProperties[ propertyName ]; + // check if any properties are still transitioning + if ( isEmptyObj( _transition.ingProperties ) ) { + // all properties have completed transitioning + this.disableTransition(); + } + // clean style + if ( propertyName in _transition.clean ) { + // clean up style + this.element.style[ event.propertyName ] = ''; + delete _transition.clean[ propertyName ]; + } + // trigger onTransitionEnd callback + if ( propertyName in _transition.onEnd ) { + var onTransitionEnd = _transition.onEnd[ propertyName ]; + onTransitionEnd.call( this ); + delete _transition.onEnd[ propertyName ]; + } + + this.emitEvent( 'transitionEnd', [ this ] ); +}; + +proto.disableTransition = function() { + this.removeTransitionStyles(); + this.element.removeEventListener( transitionEndEvent, this, false ); + this.isTransitioning = false; +}; + +/** + * removes style property from element + * @param {Object} style +**/ +proto._removeStyles = function( style ) { + // clean up transition styles + var cleanStyle = {}; + for ( var prop in style ) { + cleanStyle[ prop ] = ''; + } + this.css( cleanStyle ); +}; + +var cleanTransitionStyle = { + transitionProperty: '', + transitionDuration: '', + transitionDelay: '' +}; + +proto.removeTransitionStyles = function() { + // remove transition + this.css( cleanTransitionStyle ); +}; + +// ----- stagger ----- // + +proto.stagger = function( delay ) { + delay = isNaN( delay ) ? 0 : delay; + this.staggerDelay = delay + 'ms'; +}; + +// ----- show/hide/remove ----- // + +// remove element from DOM +proto.removeElem = function() { + this.element.parentNode.removeChild( this.element ); + // remove display: none + this.css({ display: '' }); + this.emitEvent( 'remove', [ this ] ); +}; + +proto.remove = function() { + // just remove element if no transition support or no transition + if ( !transitionProperty || !parseFloat( this.layout.options.transitionDuration ) ) { + this.removeElem(); + return; + } + + // start transition + this.once( 'transitionEnd', function() { + this.removeElem(); + }); + this.hide(); +}; + +proto.reveal = function() { + delete this.isHidden; + // remove display: none + this.css({ display: '' }); + + var options = this.layout.options; + + var onTransitionEnd = {}; + var transitionEndProperty = this.getHideRevealTransitionEndProperty('visibleStyle'); + onTransitionEnd[ transitionEndProperty ] = this.onRevealTransitionEnd; + + this.transition({ + from: options.hiddenStyle, + to: options.visibleStyle, + isCleaning: true, + onTransitionEnd: onTransitionEnd + }); +}; + +proto.onRevealTransitionEnd = function() { + // check if still visible + // during transition, item may have been hidden + if ( !this.isHidden ) { + this.emitEvent('reveal'); + } +}; + +/** + * get style property use for hide/reveal transition end + * @param {String} styleProperty - hiddenStyle/visibleStyle + * @returns {String} + */ +proto.getHideRevealTransitionEndProperty = function( styleProperty ) { + var optionStyle = this.layout.options[ styleProperty ]; + // use opacity + if ( optionStyle.opacity ) { + return 'opacity'; + } + // get first property + for ( var prop in optionStyle ) { + return prop; + } +}; + +proto.hide = function() { + // set flag + this.isHidden = true; + // remove display: none + this.css({ display: '' }); + + var options = this.layout.options; + + var onTransitionEnd = {}; + var transitionEndProperty = this.getHideRevealTransitionEndProperty('hiddenStyle'); + onTransitionEnd[ transitionEndProperty ] = this.onHideTransitionEnd; + + this.transition({ + from: options.visibleStyle, + to: options.hiddenStyle, + // keep hidden stuff hidden + isCleaning: true, + onTransitionEnd: onTransitionEnd + }); +}; + +proto.onHideTransitionEnd = function() { + // check if still hidden + // during transition, item may have been un-hidden + if ( this.isHidden ) { + this.css({ display: 'none' }); + this.emitEvent('hide'); + } +}; + +proto.destroy = function() { + this.css({ + position: '', + left: '', + right: '', + top: '', + bottom: '', + transition: '', + transform: '' + }); +}; + +return Item; + +})); + +/*! + * Outlayer v2.1.1 + * the brains and guts of a layout library + * MIT license + */ + +( function( window, factory ) { + 'use strict'; + // universal module definition + /* jshint strict: false */ /* globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD - RequireJS + define( 'outlayer/outlayer',[ + 'ev-emitter/ev-emitter', + 'get-size/get-size', + 'fizzy-ui-utils/utils', + './item' + ], + function( EvEmitter, getSize, utils, Item ) { + return factory( window, EvEmitter, getSize, utils, Item); + } + ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS - Browserify, Webpack + module.exports = factory( + window, + require('ev-emitter'), + require('get-size'), + require('fizzy-ui-utils'), + require('./item') + ); + } else { + // browser global + window.Outlayer = factory( + window, + window.EvEmitter, + window.getSize, + window.fizzyUIUtils, + window.Outlayer.Item + ); + } + +}( window, function factory( window, EvEmitter, getSize, utils, Item ) { +'use strict'; + +// ----- vars ----- // + +var console = window.console; +var jQuery = window.jQuery; +var noop = function() {}; + +// -------------------------- Outlayer -------------------------- // + +// globally unique identifiers +var GUID = 0; +// internal store of all Outlayer intances +var instances = {}; + + +/** + * @param {Element, String} element + * @param {Object} options + * @constructor + */ +function Outlayer( element, options ) { + var queryElement = utils.getQueryElement( element ); + if ( !queryElement ) { + if ( console ) { + console.error( 'Bad element for ' + this.constructor.namespace + + ': ' + ( queryElement || element ) ); + } + return; + } + this.element = queryElement; + // add jQuery + if ( jQuery ) { + this.$element = jQuery( this.element ); + } + + // options + this.options = utils.extend( {}, this.constructor.defaults ); + this.option( options ); + + // add id for Outlayer.getFromElement + var id = ++GUID; + this.element.outlayerGUID = id; // expando + instances[ id ] = this; // associate via id + + // kick it off + this._create(); + + var isInitLayout = this._getOption('initLayout'); + if ( isInitLayout ) { + this.layout(); + } +} + +// settings are for internal use only +Outlayer.namespace = 'outlayer'; +Outlayer.Item = Item; + +// default options +Outlayer.defaults = { + containerStyle: { + position: 'relative' + }, + initLayout: true, + originLeft: true, + originTop: true, + resize: true, + resizeContainer: true, + // item options + transitionDuration: '0.4s', + hiddenStyle: { + opacity: 0, + transform: 'scale(0.001)' + }, + visibleStyle: { + opacity: 1, + transform: 'scale(1)' + } +}; + +var proto = Outlayer.prototype; +// inherit EvEmitter +utils.extend( proto, EvEmitter.prototype ); + +/** + * set options + * @param {Object} opts + */ +proto.option = function( opts ) { + utils.extend( this.options, opts ); +}; + +/** + * get backwards compatible option value, check old name + */ +proto._getOption = function( option ) { + var oldOption = this.constructor.compatOptions[ option ]; + return oldOption && this.options[ oldOption ] !== undefined ? + this.options[ oldOption ] : this.options[ option ]; +}; + +Outlayer.compatOptions = { + // currentName: oldName + initLayout: 'isInitLayout', + horizontal: 'isHorizontal', + layoutInstant: 'isLayoutInstant', + originLeft: 'isOriginLeft', + originTop: 'isOriginTop', + resize: 'isResizeBound', + resizeContainer: 'isResizingContainer' +}; + +proto._create = function() { + // get items from children + this.reloadItems(); + // elements that affect layout, but are not laid out + this.stamps = []; + this.stamp( this.options.stamp ); + // set container style + utils.extend( this.element.style, this.options.containerStyle ); + + // bind resize method + var canBindResize = this._getOption('resize'); + if ( canBindResize ) { + this.bindResize(); + } +}; + +// goes through all children again and gets bricks in proper order +proto.reloadItems = function() { + // collection of item elements + this.items = this._itemize( this.element.children ); +}; + + +/** + * turn elements into Outlayer.Items to be used in layout + * @param {Array or NodeList or HTMLElement} elems + * @returns {Array} items - collection of new Outlayer Items + */ +proto._itemize = function( elems ) { + + var itemElems = this._filterFindItemElements( elems ); + var Item = this.constructor.Item; + + // create new Outlayer Items for collection + var items = []; + for ( var i=0; i < itemElems.length; i++ ) { + var elem = itemElems[i]; + var item = new Item( elem, this ); + items.push( item ); + } + + return items; +}; + +/** + * get item elements to be used in layout + * @param {Array or NodeList or HTMLElement} elems + * @returns {Array} items - item elements + */ +proto._filterFindItemElements = function( elems ) { + return utils.filterFindElements( elems, this.options.itemSelector ); +}; + +/** + * getter method for getting item elements + * @returns {Array} elems - collection of item elements + */ +proto.getItemElements = function() { + return this.items.map( function( item ) { + return item.element; + }); +}; + +// ----- init & layout ----- // + +/** + * lays out all items + */ +proto.layout = function() { + this._resetLayout(); + this._manageStamps(); + + // don't animate first layout + var layoutInstant = this._getOption('layoutInstant'); + var isInstant = layoutInstant !== undefined ? + layoutInstant : !this._isLayoutInited; + this.layoutItems( this.items, isInstant ); + + // flag for initalized + this._isLayoutInited = true; +}; + +// _init is alias for layout +proto._init = proto.layout; + +/** + * logic before any new layout + */ +proto._resetLayout = function() { + this.getSize(); +}; + + +proto.getSize = function() { + this.size = getSize( this.element ); +}; + +/** + * get measurement from option, for columnWidth, rowHeight, gutter + * if option is String -> get element from selector string, & get size of element + * if option is Element -> get size of element + * else use option as a number + * + * @param {String} measurement + * @param {String} size - width or height + * @private + */ +proto._getMeasurement = function( measurement, size ) { + var option = this.options[ measurement ]; + var elem; + if ( !option ) { + // default to 0 + this[ measurement ] = 0; + } else { + // use option as an element + if ( typeof option == 'string' ) { + elem = this.element.querySelector( option ); + } else if ( option instanceof HTMLElement ) { + elem = option; + } + // use size of element, if element + this[ measurement ] = elem ? getSize( elem )[ size ] : option; + } +}; + +/** + * layout a collection of item elements + * @api public + */ +proto.layoutItems = function( items, isInstant ) { + items = this._getItemsForLayout( items ); + + this._layoutItems( items, isInstant ); + + this._postLayout(); +}; + +/** + * get the items to be laid out + * you may want to skip over some items + * @param {Array} items + * @returns {Array} items + */ +proto._getItemsForLayout = function( items ) { + return items.filter( function( item ) { + return !item.isIgnored; + }); +}; + +/** + * layout items + * @param {Array} items + * @param {Boolean} isInstant + */ +proto._layoutItems = function( items, isInstant ) { + this._emitCompleteOnItems( 'layout', items ); + + if ( !items || !items.length ) { + // no items, emit event with empty array + return; + } + + var queue = []; + + items.forEach( function( item ) { + // get x/y object from method + var position = this._getItemLayoutPosition( item ); + // enqueue + position.item = item; + position.isInstant = isInstant || item.isLayoutInstant; + queue.push( position ); + }, this ); + + this._processLayoutQueue( queue ); +}; + +/** + * get item layout position + * @param {Outlayer.Item} item + * @returns {Object} x and y position + */ +proto._getItemLayoutPosition = function( /* item */ ) { + return { + x: 0, + y: 0 + }; +}; + +/** + * iterate over array and position each item + * Reason being - separating this logic prevents 'layout invalidation' + * thx @paul_irish + * @param {Array} queue + */ +proto._processLayoutQueue = function( queue ) { + this.updateStagger(); + queue.forEach( function( obj, i ) { + this._positionItem( obj.item, obj.x, obj.y, obj.isInstant, i ); + }, this ); +}; + +// set stagger from option in milliseconds number +proto.updateStagger = function() { + var stagger = this.options.stagger; + if ( stagger === null || stagger === undefined ) { + this.stagger = 0; + return; + } + this.stagger = getMilliseconds( stagger ); + return this.stagger; +}; + +/** + * Sets position of item in DOM + * @param {Outlayer.Item} item + * @param {Number} x - horizontal position + * @param {Number} y - vertical position + * @param {Boolean} isInstant - disables transitions + */ +proto._positionItem = function( item, x, y, isInstant, i ) { + if ( isInstant ) { + // if not transition, just set CSS + item.goTo( x, y ); + } else { + item.stagger( i * this.stagger ); + item.moveTo( x, y ); + } +}; + +/** + * Any logic you want to do after each layout, + * i.e. size the container + */ +proto._postLayout = function() { + this.resizeContainer(); +}; + +proto.resizeContainer = function() { + var isResizingContainer = this._getOption('resizeContainer'); + if ( !isResizingContainer ) { + return; + } + var size = this._getContainerSize(); + if ( size ) { + this._setContainerMeasure( size.width, true ); + this._setContainerMeasure( size.height, false ); + } +}; + +/** + * Sets width or height of container if returned + * @returns {Object} size + * @param {Number} width + * @param {Number} height + */ +proto._getContainerSize = noop; + +/** + * @param {Number} measure - size of width or height + * @param {Boolean} isWidth + */ +proto._setContainerMeasure = function( measure, isWidth ) { + if ( measure === undefined ) { + return; + } + + var elemSize = this.size; + // add padding and border width if border box + if ( elemSize.isBorderBox ) { + measure += isWidth ? elemSize.paddingLeft + elemSize.paddingRight + + elemSize.borderLeftWidth + elemSize.borderRightWidth : + elemSize.paddingBottom + elemSize.paddingTop + + elemSize.borderTopWidth + elemSize.borderBottomWidth; + } + + measure = Math.max( measure, 0 ); + this.element.style[ isWidth ? 'width' : 'height' ] = measure + 'px'; +}; + +/** + * emit eventComplete on a collection of items events + * @param {String} eventName + * @param {Array} items - Outlayer.Items + */ +proto._emitCompleteOnItems = function( eventName, items ) { + var _this = this; + function onComplete() { + _this.dispatchEvent( eventName + 'Complete', null, [ items ] ); + } + + var count = items.length; + if ( !items || !count ) { + onComplete(); + return; + } + + var doneCount = 0; + function tick() { + doneCount++; + if ( doneCount == count ) { + onComplete(); + } + } + + // bind callback + items.forEach( function( item ) { + item.once( eventName, tick ); + }); +}; + +/** + * emits events via EvEmitter and jQuery events + * @param {String} type - name of event + * @param {Event} event - original event + * @param {Array} args - extra arguments + */ +proto.dispatchEvent = function( type, event, args ) { + // add original event to arguments + var emitArgs = event ? [ event ].concat( args ) : args; + this.emitEvent( type, emitArgs ); + + if ( jQuery ) { + // set this.$element + this.$element = this.$element || jQuery( this.element ); + if ( event ) { + // create jQuery event + var $event = jQuery.Event( event ); + $event.type = type; + this.$element.trigger( $event, args ); + } else { + // just trigger with type if no event available + this.$element.trigger( type, args ); + } + } +}; + +// -------------------------- ignore & stamps -------------------------- // + + +/** + * keep item in collection, but do not lay it out + * ignored items do not get skipped in layout + * @param {Element} elem + */ +proto.ignore = function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + item.isIgnored = true; + } +}; + +/** + * return item to layout collection + * @param {Element} elem + */ +proto.unignore = function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + delete item.isIgnored; + } +}; + +/** + * adds elements to stamps + * @param {NodeList, Array, Element, or String} elems + */ +proto.stamp = function( elems ) { + elems = this._find( elems ); + if ( !elems ) { + return; + } + + this.stamps = this.stamps.concat( elems ); + // ignore + elems.forEach( this.ignore, this ); +}; + +/** + * removes elements to stamps + * @param {NodeList, Array, or Element} elems + */ +proto.unstamp = function( elems ) { + elems = this._find( elems ); + if ( !elems ){ + return; + } + + elems.forEach( function( elem ) { + // filter out removed stamp elements + utils.removeFrom( this.stamps, elem ); + this.unignore( elem ); + }, this ); +}; + +/** + * finds child elements + * @param {NodeList, Array, Element, or String} elems + * @returns {Array} elems + */ +proto._find = function( elems ) { + if ( !elems ) { + return; + } + // if string, use argument as selector string + if ( typeof elems == 'string' ) { + elems = this.element.querySelectorAll( elems ); + } + elems = utils.makeArray( elems ); + return elems; +}; + +proto._manageStamps = function() { + if ( !this.stamps || !this.stamps.length ) { + return; + } + + this._getBoundingRect(); + + this.stamps.forEach( this._manageStamp, this ); +}; + +// update boundingLeft / Top +proto._getBoundingRect = function() { + // get bounding rect for container element + var boundingRect = this.element.getBoundingClientRect(); + var size = this.size; + this._boundingRect = { + left: boundingRect.left + size.paddingLeft + size.borderLeftWidth, + top: boundingRect.top + size.paddingTop + size.borderTopWidth, + right: boundingRect.right - ( size.paddingRight + size.borderRightWidth ), + bottom: boundingRect.bottom - ( size.paddingBottom + size.borderBottomWidth ) + }; +}; + +/** + * @param {Element} stamp +**/ +proto._manageStamp = noop; + +/** + * get x/y position of element relative to container element + * @param {Element} elem + * @returns {Object} offset - has left, top, right, bottom + */ +proto._getElementOffset = function( elem ) { + var boundingRect = elem.getBoundingClientRect(); + var thisRect = this._boundingRect; + var size = getSize( elem ); + var offset = { + left: boundingRect.left - thisRect.left - size.marginLeft, + top: boundingRect.top - thisRect.top - size.marginTop, + right: thisRect.right - boundingRect.right - size.marginRight, + bottom: thisRect.bottom - boundingRect.bottom - size.marginBottom + }; + return offset; +}; + +// -------------------------- resize -------------------------- // + +// enable event handlers for listeners +// i.e. resize -> onresize +proto.handleEvent = utils.handleEvent; + +/** + * Bind layout to window resizing + */ +proto.bindResize = function() { + window.addEventListener( 'resize', this ); + this.isResizeBound = true; +}; + +/** + * Unbind layout to window resizing + */ +proto.unbindResize = function() { + window.removeEventListener( 'resize', this ); + this.isResizeBound = false; +}; + +proto.onresize = function() { + this.resize(); +}; + +utils.debounceMethod( Outlayer, 'onresize', 100 ); + +proto.resize = function() { + // don't trigger if size did not change + // or if resize was unbound. See #9 + if ( !this.isResizeBound || !this.needsResizeLayout() ) { + return; + } + + this.layout(); +}; + +/** + * check if layout is needed post layout + * @returns Boolean + */ +proto.needsResizeLayout = function() { + var size = getSize( this.element ); + // check that this.size and size are there + // IE8 triggers resize on body size change, so they might not be + var hasSizes = this.size && size; + return hasSizes && size.innerWidth !== this.size.innerWidth; +}; + +// -------------------------- methods -------------------------- // + +/** + * add items to Outlayer instance + * @param {Array or NodeList or Element} elems + * @returns {Array} items - Outlayer.Items +**/ +proto.addItems = function( elems ) { + var items = this._itemize( elems ); + // add items to collection + if ( items.length ) { + this.items = this.items.concat( items ); + } + return items; +}; + +/** + * Layout newly-appended item elements + * @param {Array or NodeList or Element} elems + */ +proto.appended = function( elems ) { + var items = this.addItems( elems ); + if ( !items.length ) { + return; + } + // layout and reveal just the new items + this.layoutItems( items, true ); + this.reveal( items ); +}; + +/** + * Layout prepended elements + * @param {Array or NodeList or Element} elems + */ +proto.prepended = function( elems ) { + var items = this._itemize( elems ); + if ( !items.length ) { + return; + } + // add items to beginning of collection + var previousItems = this.items.slice(0); + this.items = items.concat( previousItems ); + // start new layout + this._resetLayout(); + this._manageStamps(); + // layout new stuff without transition + this.layoutItems( items, true ); + this.reveal( items ); + // layout previous items + this.layoutItems( previousItems ); +}; + +/** + * reveal a collection of items + * @param {Array of Outlayer.Items} items + */ +proto.reveal = function( items ) { + this._emitCompleteOnItems( 'reveal', items ); + if ( !items || !items.length ) { + return; + } + var stagger = this.updateStagger(); + items.forEach( function( item, i ) { + item.stagger( i * stagger ); + item.reveal(); + }); +}; + +/** + * hide a collection of items + * @param {Array of Outlayer.Items} items + */ +proto.hide = function( items ) { + this._emitCompleteOnItems( 'hide', items ); + if ( !items || !items.length ) { + return; + } + var stagger = this.updateStagger(); + items.forEach( function( item, i ) { + item.stagger( i * stagger ); + item.hide(); + }); +}; + +/** + * reveal item elements + * @param {Array}, {Element}, {NodeList} items + */ +proto.revealItemElements = function( elems ) { + var items = this.getItems( elems ); + this.reveal( items ); +}; + +/** + * hide item elements + * @param {Array}, {Element}, {NodeList} items + */ +proto.hideItemElements = function( elems ) { + var items = this.getItems( elems ); + this.hide( items ); +}; + +/** + * get Outlayer.Item, given an Element + * @param {Element} elem + * @param {Function} callback + * @returns {Outlayer.Item} item + */ +proto.getItem = function( elem ) { + // loop through items to get the one that matches + for ( var i=0; i < this.items.length; i++ ) { + var item = this.items[i]; + if ( item.element == elem ) { + // return item + return item; + } + } +}; + +/** + * get collection of Outlayer.Items, given Elements + * @param {Array} elems + * @returns {Array} items - Outlayer.Items + */ +proto.getItems = function( elems ) { + elems = utils.makeArray( elems ); + var items = []; + elems.forEach( function( elem ) { + var item = this.getItem( elem ); + if ( item ) { + items.push( item ); + } + }, this ); + + return items; +}; + +/** + * remove element(s) from instance and DOM + * @param {Array or NodeList or Element} elems + */ +proto.remove = function( elems ) { + var removeItems = this.getItems( elems ); + + this._emitCompleteOnItems( 'remove', removeItems ); + + // bail if no items to remove + if ( !removeItems || !removeItems.length ) { + return; + } + + removeItems.forEach( function( item ) { + item.remove(); + // remove item from collection + utils.removeFrom( this.items, item ); + }, this ); +}; + +// ----- destroy ----- // + +// remove and disable Outlayer instance +proto.destroy = function() { + // clean up dynamic styles + var style = this.element.style; + style.height = ''; + style.position = ''; + style.width = ''; + // destroy items + this.items.forEach( function( item ) { + item.destroy(); + }); + + this.unbindResize(); + + var id = this.element.outlayerGUID; + delete instances[ id ]; // remove reference to instance by id + delete this.element.outlayerGUID; + // remove data for jQuery + if ( jQuery ) { + jQuery.removeData( this.element, this.constructor.namespace ); + } + +}; + +// -------------------------- data -------------------------- // + +/** + * get Outlayer instance from element + * @param {Element} elem + * @returns {Outlayer} + */ +Outlayer.data = function( elem ) { + elem = utils.getQueryElement( elem ); + var id = elem && elem.outlayerGUID; + return id && instances[ id ]; +}; + + +// -------------------------- create Outlayer class -------------------------- // + +/** + * create a layout class + * @param {String} namespace + */ +Outlayer.create = function( namespace, options ) { + // sub-class Outlayer + var Layout = subclass( Outlayer ); + // apply new options and compatOptions + Layout.defaults = utils.extend( {}, Outlayer.defaults ); + utils.extend( Layout.defaults, options ); + Layout.compatOptions = utils.extend( {}, Outlayer.compatOptions ); + + Layout.namespace = namespace; + + Layout.data = Outlayer.data; + + // sub-class Item + Layout.Item = subclass( Item ); + + // -------------------------- declarative -------------------------- // + + utils.htmlInit( Layout, namespace ); + + // -------------------------- jQuery bridge -------------------------- // + + // make into jQuery plugin + if ( jQuery && jQuery.bridget ) { + jQuery.bridget( namespace, Layout ); + } + + return Layout; +}; + +function subclass( Parent ) { + function SubClass() { + Parent.apply( this, arguments ); + } + + SubClass.prototype = Object.create( Parent.prototype ); + SubClass.prototype.constructor = SubClass; + + return SubClass; +} + +// ----- helpers ----- // + +// how many milliseconds are in each unit +var msUnits = { + ms: 1, + s: 1000 +}; + +// munge time-like parameter into millisecond number +// '0.4s' -> 40 +function getMilliseconds( time ) { + if ( typeof time == 'number' ) { + return time; + } + var matches = time.match( /(^\d*\.?\d*)(\w*)/ ); + var num = matches && matches[1]; + var unit = matches && matches[2]; + if ( !num.length ) { + return 0; + } + num = parseFloat( num ); + var mult = msUnits[ unit ] || 1; + return num * mult; +} + +// ----- fin ----- // + +// back in global +Outlayer.Item = Item; + +return Outlayer; + +})); + +/** + * Isotope Item +**/ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'isotope-layout/js/item',[ + 'outlayer/outlayer' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('outlayer') + ); + } else { + // browser global + window.Isotope = window.Isotope || {}; + window.Isotope.Item = factory( + window.Outlayer + ); + } + +}( window, function factory( Outlayer ) { +'use strict'; + +// -------------------------- Item -------------------------- // + +// sub-class Outlayer Item +function Item() { + Outlayer.Item.apply( this, arguments ); +} + +var proto = Item.prototype = Object.create( Outlayer.Item.prototype ); + +var _create = proto._create; +proto._create = function() { + // assign id, used for original-order sorting + this.id = this.layout.itemGUID++; + _create.call( this ); + this.sortData = {}; +}; + +proto.updateSortData = function() { + if ( this.isIgnored ) { + return; + } + // default sorters + this.sortData.id = this.id; + // for backward compatibility + this.sortData['original-order'] = this.id; + this.sortData.random = Math.random(); + // go thru getSortData obj and apply the sorters + var getSortData = this.layout.options.getSortData; + var sorters = this.layout._sorters; + for ( var key in getSortData ) { + var sorter = sorters[ key ]; + this.sortData[ key ] = sorter( this.element, this ); + } +}; + +var _destroy = proto.destroy; +proto.destroy = function() { + // call super + _destroy.apply( this, arguments ); + // reset display, #741 + this.css({ + display: '' + }); +}; + +return Item; + +})); + +/** + * Isotope LayoutMode + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'isotope-layout/js/layout-mode',[ + 'get-size/get-size', + 'outlayer/outlayer' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('get-size'), + require('outlayer') + ); + } else { + // browser global + window.Isotope = window.Isotope || {}; + window.Isotope.LayoutMode = factory( + window.getSize, + window.Outlayer + ); + } + +}( window, function factory( getSize, Outlayer ) { + 'use strict'; + + // layout mode class + function LayoutMode( isotope ) { + this.isotope = isotope; + // link properties + if ( isotope ) { + this.options = isotope.options[ this.namespace ]; + this.element = isotope.element; + this.items = isotope.filteredItems; + this.size = isotope.size; + } + } + + var proto = LayoutMode.prototype; + + /** + * some methods should just defer to default Outlayer method + * and reference the Isotope instance as `this` + **/ + var facadeMethods = [ + '_resetLayout', + '_getItemLayoutPosition', + '_manageStamp', + '_getContainerSize', + '_getElementOffset', + 'needsResizeLayout', + '_getOption' + ]; + + facadeMethods.forEach( function( methodName ) { + proto[ methodName ] = function() { + return Outlayer.prototype[ methodName ].apply( this.isotope, arguments ); + }; + }); + + // ----- ----- // + + // for horizontal layout modes, check vertical size + proto.needsVerticalResizeLayout = function() { + // don't trigger if size did not change + var size = getSize( this.isotope.element ); + // check that this.size and size are there + // IE8 triggers resize on body size change, so they might not be + var hasSizes = this.isotope.size && size; + return hasSizes && size.innerHeight != this.isotope.size.innerHeight; + }; + + // ----- measurements ----- // + + proto._getMeasurement = function() { + this.isotope._getMeasurement.apply( this, arguments ); + }; + + proto.getColumnWidth = function() { + this.getSegmentSize( 'column', 'Width' ); + }; + + proto.getRowHeight = function() { + this.getSegmentSize( 'row', 'Height' ); + }; + + /** + * get columnWidth or rowHeight + * segment: 'column' or 'row' + * size 'Width' or 'Height' + **/ + proto.getSegmentSize = function( segment, size ) { + var segmentName = segment + size; + var outerSize = 'outer' + size; + // columnWidth / outerWidth // rowHeight / outerHeight + this._getMeasurement( segmentName, outerSize ); + // got rowHeight or columnWidth, we can chill + if ( this[ segmentName ] ) { + return; + } + // fall back to item of first element + var firstItemSize = this.getFirstItemSize(); + this[ segmentName ] = firstItemSize && firstItemSize[ outerSize ] || + // or size of container + this.isotope.size[ 'inner' + size ]; + }; + + proto.getFirstItemSize = function() { + var firstItem = this.isotope.filteredItems[0]; + return firstItem && firstItem.element && getSize( firstItem.element ); + }; + + // ----- methods that should reference isotope ----- // + + proto.layout = function() { + this.isotope.layout.apply( this.isotope, arguments ); + }; + + proto.getSize = function() { + this.isotope.getSize(); + this.size = this.isotope.size; + }; + + // -------------------------- create -------------------------- // + + LayoutMode.modes = {}; + + LayoutMode.create = function( namespace, options ) { + + function Mode() { + LayoutMode.apply( this, arguments ); + } + + Mode.prototype = Object.create( proto ); + Mode.prototype.constructor = Mode; + + // default options + if ( options ) { + Mode.options = options; + } + + Mode.prototype.namespace = namespace; + // register in Isotope + LayoutMode.modes[ namespace ] = Mode; + + return Mode; + }; + + return LayoutMode; + +})); + +/*! + * Masonry v4.2.1 + * Cascading grid layout library + * https://masonry.desandro.com + * MIT License + * by David DeSandro + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'masonry-layout/masonry',[ + 'outlayer/outlayer', + 'get-size/get-size' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('outlayer'), + require('get-size') + ); + } else { + // browser global + window.Masonry = factory( + window.Outlayer, + window.getSize + ); + } + +}( window, function factory( Outlayer, getSize ) { + + + +// -------------------------- masonryDefinition -------------------------- // + + // create an Outlayer layout class + var Masonry = Outlayer.create('masonry'); + // isFitWidth -> fitWidth + Masonry.compatOptions.fitWidth = 'isFitWidth'; + + var proto = Masonry.prototype; + + proto._resetLayout = function() { + this.getSize(); + this._getMeasurement( 'columnWidth', 'outerWidth' ); + this._getMeasurement( 'gutter', 'outerWidth' ); + this.measureColumns(); + + // reset column Y + this.colYs = []; + for ( var i=0; i < this.cols; i++ ) { + this.colYs.push( 0 ); + } + + this.maxY = 0; + this.horizontalColIndex = 0; + }; + + proto.measureColumns = function() { + this.getContainerWidth(); + // if columnWidth is 0, default to outerWidth of first item + if ( !this.columnWidth ) { + var firstItem = this.items[0]; + var firstItemElem = firstItem && firstItem.element; + // columnWidth fall back to item of first element + this.columnWidth = firstItemElem && getSize( firstItemElem ).outerWidth || + // if first elem has no width, default to size of container + this.containerWidth; + } + + var columnWidth = this.columnWidth += this.gutter; + + // calculate columns + var containerWidth = this.containerWidth + this.gutter; + var cols = containerWidth / columnWidth; + // fix rounding errors, typically with gutters + var excess = columnWidth - containerWidth % columnWidth; + // if overshoot is less than a pixel, round up, otherwise floor it + var mathMethod = excess && excess < 1 ? 'round' : 'floor'; + cols = Math[ mathMethod ]( cols ); + this.cols = Math.max( cols, 1 ); + }; + + proto.getContainerWidth = function() { + // container is parent if fit width + var isFitWidth = this._getOption('fitWidth'); + var container = isFitWidth ? this.element.parentNode : this.element; + // check that this.size and size are there + // IE8 triggers resize on body size change, so they might not be + var size = getSize( container ); + this.containerWidth = size && size.innerWidth; + }; + + proto._getItemLayoutPosition = function( item ) { + item.getSize(); + // how many columns does this brick span + var remainder = item.size.outerWidth % this.columnWidth; + var mathMethod = remainder && remainder < 1 ? 'round' : 'ceil'; + // round if off by 1 pixel, otherwise use ceil + var colSpan = Math[ mathMethod ]( item.size.outerWidth / this.columnWidth ); + colSpan = Math.min( colSpan, this.cols ); + // use horizontal or top column position + var colPosMethod = this.options.horizontalOrder ? + '_getHorizontalColPosition' : '_getTopColPosition'; + var colPosition = this[ colPosMethod ]( colSpan, item ); + // position the brick + var position = { + x: this.columnWidth * colPosition.col, + y: colPosition.y + }; + // apply setHeight to necessary columns + var setHeight = colPosition.y + item.size.outerHeight; + var setMax = colSpan + colPosition.col; + for ( var i = colPosition.col; i < setMax; i++ ) { + this.colYs[i] = setHeight; + } + + return position; + }; + + proto._getTopColPosition = function( colSpan ) { + var colGroup = this._getTopColGroup( colSpan ); + // get the minimum Y value from the columns + var minimumY = Math.min.apply( Math, colGroup ); + + return { + col: colGroup.indexOf( minimumY ), + y: minimumY, + }; + }; + + /** + * @param {Number} colSpan - number of columns the element spans + * @returns {Array} colGroup + */ + proto._getTopColGroup = function( colSpan ) { + if ( colSpan < 2 ) { + // if brick spans only one column, use all the column Ys + return this.colYs; + } + + var colGroup = []; + // how many different places could this brick fit horizontally + var groupCount = this.cols + 1 - colSpan; + // for each group potential horizontal position + for ( var i = 0; i < groupCount; i++ ) { + colGroup[i] = this._getColGroupY( i, colSpan ); + } + return colGroup; + }; + + proto._getColGroupY = function( col, colSpan ) { + if ( colSpan < 2 ) { + return this.colYs[ col ]; + } + // make an array of colY values for that one group + var groupColYs = this.colYs.slice( col, col + colSpan ); + // and get the max value of the array + return Math.max.apply( Math, groupColYs ); + }; + + // get column position based on horizontal index. #873 + proto._getHorizontalColPosition = function( colSpan, item ) { + var col = this.horizontalColIndex % this.cols; + var isOver = colSpan > 1 && col + colSpan > this.cols; + // shift to next row if item can't fit on current row + col = isOver ? 0 : col; + // don't let zero-size items take up space + var hasSize = item.size.outerWidth && item.size.outerHeight; + this.horizontalColIndex = hasSize ? col + colSpan : this.horizontalColIndex; + + return { + col: col, + y: this._getColGroupY( col, colSpan ), + }; + }; + + proto._manageStamp = function( stamp ) { + var stampSize = getSize( stamp ); + var offset = this._getElementOffset( stamp ); + // get the columns that this stamp affects + var isOriginLeft = this._getOption('originLeft'); + var firstX = isOriginLeft ? offset.left : offset.right; + var lastX = firstX + stampSize.outerWidth; + var firstCol = Math.floor( firstX / this.columnWidth ); + firstCol = Math.max( 0, firstCol ); + var lastCol = Math.floor( lastX / this.columnWidth ); + // lastCol should not go over if multiple of columnWidth #425 + lastCol -= lastX % this.columnWidth ? 0 : 1; + lastCol = Math.min( this.cols - 1, lastCol ); + // set colYs to bottom of the stamp + + var isOriginTop = this._getOption('originTop'); + var stampMaxY = ( isOriginTop ? offset.top : offset.bottom ) + + stampSize.outerHeight; + for ( var i = firstCol; i <= lastCol; i++ ) { + this.colYs[i] = Math.max( stampMaxY, this.colYs[i] ); + } + }; + + proto._getContainerSize = function() { + this.maxY = Math.max.apply( Math, this.colYs ); + var size = { + height: this.maxY + }; + + if ( this._getOption('fitWidth') ) { + size.width = this._getContainerFitWidth(); + } + + return size; + }; + + proto._getContainerFitWidth = function() { + var unusedCols = 0; + // count unused columns + var i = this.cols; + while ( --i ) { + if ( this.colYs[i] !== 0 ) { + break; + } + unusedCols++; + } + // fit container to columns that have been used + return ( this.cols - unusedCols ) * this.columnWidth - this.gutter; + }; + + proto.needsResizeLayout = function() { + var previousWidth = this.containerWidth; + this.getContainerWidth(); + return previousWidth != this.containerWidth; + }; + + return Masonry; + +})); + +/*! + * Masonry layout mode + * sub-classes Masonry + * https://masonry.desandro.com + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'isotope-layout/js/layout-modes/masonry',[ + '../layout-mode', + 'masonry-layout/masonry' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('../layout-mode'), + require('masonry-layout') + ); + } else { + // browser global + factory( + window.Isotope.LayoutMode, + window.Masonry + ); + } + +}( window, function factory( LayoutMode, Masonry ) { +'use strict'; + +// -------------------------- masonryDefinition -------------------------- // + + // create an Outlayer layout class + var MasonryMode = LayoutMode.create('masonry'); + + var proto = MasonryMode.prototype; + + var keepModeMethods = { + _getElementOffset: true, + layout: true, + _getMeasurement: true + }; + + // inherit Masonry prototype + for ( var method in Masonry.prototype ) { + // do not inherit mode methods + if ( !keepModeMethods[ method ] ) { + proto[ method ] = Masonry.prototype[ method ]; + } + } + + var measureColumns = proto.measureColumns; + proto.measureColumns = function() { + // set items, used if measuring first item + this.items = this.isotope.filteredItems; + measureColumns.call( this ); + }; + + // point to mode options for fitWidth + var _getOption = proto._getOption; + proto._getOption = function( option ) { + if ( option == 'fitWidth' ) { + return this.options.isFitWidth !== undefined ? + this.options.isFitWidth : this.options.fitWidth; + } + return _getOption.apply( this.isotope, arguments ); + }; + + return MasonryMode; + +})); + +/** + * fitRows layout mode + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'isotope-layout/js/layout-modes/fit-rows',[ + '../layout-mode' + ], + factory ); + } else if ( typeof exports == 'object' ) { + // CommonJS + module.exports = factory( + require('../layout-mode') + ); + } else { + // browser global + factory( + window.Isotope.LayoutMode + ); + } + +}( window, function factory( LayoutMode ) { +'use strict'; + +var FitRows = LayoutMode.create('fitRows'); + +var proto = FitRows.prototype; + +proto._resetLayout = function() { + this.x = 0; + this.y = 0; + this.maxY = 0; + this._getMeasurement( 'gutter', 'outerWidth' ); +}; + +proto._getItemLayoutPosition = function( item ) { + item.getSize(); + + var itemWidth = item.size.outerWidth + this.gutter; + // if this element cannot fit in the current row + var containerWidth = this.isotope.size.innerWidth + this.gutter; + if ( this.x !== 0 && itemWidth + this.x > containerWidth ) { + this.x = 0; + this.y = this.maxY; + } + + var position = { + x: this.x, + y: this.y + }; + + this.maxY = Math.max( this.maxY, this.y + item.size.outerHeight ); + this.x += itemWidth; + + return position; +}; + +proto._getContainerSize = function() { + return { height: this.maxY }; +}; + +return FitRows; + +})); + +/** + * vertical layout mode + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( 'isotope-layout/js/layout-modes/vertical',[ + '../layout-mode' + ], + factory ); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + require('../layout-mode') + ); + } else { + // browser global + factory( + window.Isotope.LayoutMode + ); + } + +}( window, function factory( LayoutMode ) { +'use strict'; + +var Vertical = LayoutMode.create( 'vertical', { + horizontalAlignment: 0 +}); + +var proto = Vertical.prototype; + +proto._resetLayout = function() { + this.y = 0; +}; + +proto._getItemLayoutPosition = function( item ) { + item.getSize(); + var x = ( this.isotope.size.innerWidth - item.size.outerWidth ) * + this.options.horizontalAlignment; + var y = this.y; + this.y += item.size.outerHeight; + return { x: x, y: y }; +}; + +proto._getContainerSize = function() { + return { height: this.y }; +}; + +return Vertical; + +})); + +/*! + * Isotope v3.0.6 + * + * Licensed GPLv3 for open source use + * or Isotope Commercial License for commercial use + * + * https://isotope.metafizzy.co + * Copyright 2010-2018 Metafizzy + */ + +( function( window, factory ) { + // universal module definition + /* jshint strict: false */ /*globals define, module, require */ + if ( typeof define == 'function' && define.amd ) { + // AMD + define( [ + 'outlayer/outlayer', + 'get-size/get-size', + 'desandro-matches-selector/matches-selector', + 'fizzy-ui-utils/utils', + 'isotope-layout/js/item', + 'isotope-layout/js/layout-mode', + // include default layout modes + 'isotope-layout/js/layout-modes/masonry', + 'isotope-layout/js/layout-modes/fit-rows', + 'isotope-layout/js/layout-modes/vertical' + ], + function( Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ) { + return factory( window, Outlayer, getSize, matchesSelector, utils, Item, LayoutMode ); + }); + } else if ( typeof module == 'object' && module.exports ) { + // CommonJS + module.exports = factory( + window, + require('outlayer'), + require('get-size'), + require('desandro-matches-selector'), + require('fizzy-ui-utils'), + require('isotope-layout/js/item'), + require('isotope-layout/js/layout-mode'), + // include default layout modes + require('isotope-layout/js/layout-modes/masonry'), + require('isotope-layout/js/layout-modes/fit-rows'), + require('isotope-layout/js/layout-modes/vertical') + ); + } else { + // browser global + window.Isotope = factory( + window, + window.Outlayer, + window.getSize, + window.matchesSelector, + window.fizzyUIUtils, + window.Isotope.Item, + window.Isotope.LayoutMode + ); + } + +}( window, function factory( window, Outlayer, getSize, matchesSelector, utils, + Item, LayoutMode ) { + + + +// -------------------------- vars -------------------------- // + +var jQuery = window.jQuery; + +// -------------------------- helpers -------------------------- // + +var trim = String.prototype.trim ? + function( str ) { + return str.trim(); + } : + function( str ) { + return str.replace( /^\s+|\s+$/g, '' ); + }; + +// -------------------------- isotopeDefinition -------------------------- // + + // create an Outlayer layout class + var Isotope = Outlayer.create( 'isotope', { + layoutMode: 'masonry', + isJQueryFiltering: true, + sortAscending: true + }); + + Isotope.Item = Item; + Isotope.LayoutMode = LayoutMode; + + var proto = Isotope.prototype; + + proto._create = function() { + this.itemGUID = 0; + // functions that sort items + this._sorters = {}; + this._getSorters(); + // call super + Outlayer.prototype._create.call( this ); + + // create layout modes + this.modes = {}; + // start filteredItems with all items + this.filteredItems = this.items; + // keep of track of sortBys + this.sortHistory = [ 'original-order' ]; + // create from registered layout modes + for ( var name in LayoutMode.modes ) { + this._initLayoutMode( name ); + } + }; + + proto.reloadItems = function() { + // reset item ID counter + this.itemGUID = 0; + // call super + Outlayer.prototype.reloadItems.call( this ); + }; + + proto._itemize = function() { + var items = Outlayer.prototype._itemize.apply( this, arguments ); + // assign ID for original-order + for ( var i=0; i < items.length; i++ ) { + var item = items[i]; + item.id = this.itemGUID++; + } + this._updateItemsSortData( items ); + return items; + }; + + + // -------------------------- layout -------------------------- // + + proto._initLayoutMode = function( name ) { + var Mode = LayoutMode.modes[ name ]; + // set mode options + // HACK extend initial options, back-fill in default options + var initialOpts = this.options[ name ] || {}; + this.options[ name ] = Mode.options ? + utils.extend( Mode.options, initialOpts ) : initialOpts; + // init layout mode instance + this.modes[ name ] = new Mode( this ); + }; + + + proto.layout = function() { + // if first time doing layout, do all magic + if ( !this._isLayoutInited && this._getOption('initLayout') ) { + this.arrange(); + return; + } + this._layout(); + }; + + // private method to be used in layout() & magic() + proto._layout = function() { + // don't animate first layout + var isInstant = this._getIsInstant(); + // layout flow + this._resetLayout(); + this._manageStamps(); + this.layoutItems( this.filteredItems, isInstant ); + + // flag for initalized + this._isLayoutInited = true; + }; + + // filter + sort + layout + proto.arrange = function( opts ) { + // set any options pass + this.option( opts ); + this._getIsInstant(); + // filter, sort, and layout + + // filter + var filtered = this._filter( this.items ); + this.filteredItems = filtered.matches; + + this._bindArrangeComplete(); + + if ( this._isInstant ) { + this._noTransition( this._hideReveal, [ filtered ] ); + } else { + this._hideReveal( filtered ); + } + + this._sort(); + this._layout(); + }; + // alias to _init for main plugin method + proto._init = proto.arrange; + + proto._hideReveal = function( filtered ) { + this.reveal( filtered.needReveal ); + this.hide( filtered.needHide ); + }; + + // HACK + // Don't animate/transition first layout + // Or don't animate/transition other layouts + proto._getIsInstant = function() { + var isLayoutInstant = this._getOption('layoutInstant'); + var isInstant = isLayoutInstant !== undefined ? isLayoutInstant : + !this._isLayoutInited; + this._isInstant = isInstant; + return isInstant; + }; + + // listen for layoutComplete, hideComplete and revealComplete + // to trigger arrangeComplete + proto._bindArrangeComplete = function() { + // listen for 3 events to trigger arrangeComplete + var isLayoutComplete, isHideComplete, isRevealComplete; + var _this = this; + function arrangeParallelCallback() { + if ( isLayoutComplete && isHideComplete && isRevealComplete ) { + _this.dispatchEvent( 'arrangeComplete', null, [ _this.filteredItems ] ); + } + } + this.once( 'layoutComplete', function() { + isLayoutComplete = true; + arrangeParallelCallback(); + }); + this.once( 'hideComplete', function() { + isHideComplete = true; + arrangeParallelCallback(); + }); + this.once( 'revealComplete', function() { + isRevealComplete = true; + arrangeParallelCallback(); + }); + }; + + // -------------------------- filter -------------------------- // + + proto._filter = function( items ) { + var filter = this.options.filter; + filter = filter || '*'; + var matches = []; + var hiddenMatched = []; + var visibleUnmatched = []; + + var test = this._getFilterTest( filter ); + + // test each item + for ( var i=0; i < items.length; i++ ) { + var item = items[i]; + if ( item.isIgnored ) { + continue; + } + // add item to either matched or unmatched group + var isMatched = test( item ); + // item.isFilterMatched = isMatched; + // add to matches if its a match + if ( isMatched ) { + matches.push( item ); + } + // add to additional group if item needs to be hidden or revealed + if ( isMatched && item.isHidden ) { + hiddenMatched.push( item ); + } else if ( !isMatched && !item.isHidden ) { + visibleUnmatched.push( item ); + } + } + + // return collections of items to be manipulated + return { + matches: matches, + needReveal: hiddenMatched, + needHide: visibleUnmatched + }; + }; + + // get a jQuery, function, or a matchesSelector test given the filter + proto._getFilterTest = function( filter ) { + if ( jQuery && this.options.isJQueryFiltering ) { + // use jQuery + return function( item ) { + return jQuery( item.element ).is( filter ); + }; + } + if ( typeof filter == 'function' ) { + // use filter as function + return function( item ) { + return filter( item.element ); + }; + } + // default, use filter as selector string + return function( item ) { + return matchesSelector( item.element, filter ); + }; + }; + + // -------------------------- sorting -------------------------- // + + /** + * @params {Array} elems + * @public + */ + proto.updateSortData = function( elems ) { + // get items + var items; + if ( elems ) { + elems = utils.makeArray( elems ); + items = this.getItems( elems ); + } else { + // update all items if no elems provided + items = this.items; + } + + this._getSorters(); + this._updateItemsSortData( items ); + }; + + proto._getSorters = function() { + var getSortData = this.options.getSortData; + for ( var key in getSortData ) { + var sorter = getSortData[ key ]; + this._sorters[ key ] = mungeSorter( sorter ); + } + }; + + /** + * @params {Array} items - of Isotope.Items + * @private + */ + proto._updateItemsSortData = function( items ) { + // do not update if no items + var len = items && items.length; + + for ( var i=0; len && i < len; i++ ) { + var item = items[i]; + item.updateSortData(); + } + }; + + // ----- munge sorter ----- // + + // encapsulate this, as we just need mungeSorter + // other functions in here are just for munging + var mungeSorter = ( function() { + // add a magic layer to sorters for convienent shorthands + // `.foo-bar` will use the text of .foo-bar querySelector + // `[foo-bar]` will use attribute + // you can also add parser + // `.foo-bar parseInt` will parse that as a number + function mungeSorter( sorter ) { + // if not a string, return function or whatever it is + if ( typeof sorter != 'string' ) { + return sorter; + } + // parse the sorter string + var args = trim( sorter ).split(' '); + var query = args[0]; + // check if query looks like [an-attribute] + var attrMatch = query.match( /^\[(.+)\]$/ ); + var attr = attrMatch && attrMatch[1]; + var getValue = getValueGetter( attr, query ); + // use second argument as a parser + var parser = Isotope.sortDataParsers[ args[1] ]; + // parse the value, if there was a parser + sorter = parser ? function( elem ) { + return elem && parser( getValue( elem ) ); + } : + // otherwise just return value + function( elem ) { + return elem && getValue( elem ); + }; + + return sorter; + } + + // get an attribute getter, or get text of the querySelector + function getValueGetter( attr, query ) { + // if query looks like [foo-bar], get attribute + if ( attr ) { + return function getAttribute( elem ) { + return elem.getAttribute( attr ); + }; + } + + // otherwise, assume its a querySelector, and get its text + return function getChildText( elem ) { + var child = elem.querySelector( query ); + return child && child.textContent; + }; + } + + return mungeSorter; + })(); + + // parsers used in getSortData shortcut strings + Isotope.sortDataParsers = { + 'parseInt': function( val ) { + return parseInt( val, 10 ); + }, + 'parseFloat': function( val ) { + return parseFloat( val ); + } + }; + + // ----- sort method ----- // + + // sort filteredItem order + proto._sort = function() { + if ( !this.options.sortBy ) { + return; + } + // keep track of sortBy History + var sortBys = utils.makeArray( this.options.sortBy ); + if ( !this._getIsSameSortBy( sortBys ) ) { + // concat all sortBy and sortHistory, add to front, oldest goes in last + this.sortHistory = sortBys.concat( this.sortHistory ); + } + // sort magic + var itemSorter = getItemSorter( this.sortHistory, this.options.sortAscending ); + this.filteredItems.sort( itemSorter ); + }; + + // check if sortBys is same as start of sortHistory + proto._getIsSameSortBy = function( sortBys ) { + for ( var i=0; i < sortBys.length; i++ ) { + if ( sortBys[i] != this.sortHistory[i] ) { + return false; + } + } + return true; + }; + + // returns a function used for sorting + function getItemSorter( sortBys, sortAsc ) { + return function sorter( itemA, itemB ) { + // cycle through all sortKeys + for ( var i = 0; i < sortBys.length; i++ ) { + var sortBy = sortBys[i]; + var a = itemA.sortData[ sortBy ]; + var b = itemB.sortData[ sortBy ]; + if ( a > b || a < b ) { + // if sortAsc is an object, use the value given the sortBy key + var isAscending = sortAsc[ sortBy ] !== undefined ? sortAsc[ sortBy ] : sortAsc; + var direction = isAscending ? 1 : -1; + return ( a > b ? 1 : -1 ) * direction; + } + } + return 0; + }; + } + + // -------------------------- methods -------------------------- // + + // get layout mode + proto._mode = function() { + var layoutMode = this.options.layoutMode; + var mode = this.modes[ layoutMode ]; + if ( !mode ) { + // TODO console.error + throw new Error( 'No layout mode: ' + layoutMode ); + } + // HACK sync mode's options + // any options set after init for layout mode need to be synced + mode.options = this.options[ layoutMode ]; + return mode; + }; + + proto._resetLayout = function() { + // trigger original reset layout + Outlayer.prototype._resetLayout.call( this ); + this._mode()._resetLayout(); + }; + + proto._getItemLayoutPosition = function( item ) { + return this._mode()._getItemLayoutPosition( item ); + }; + + proto._manageStamp = function( stamp ) { + this._mode()._manageStamp( stamp ); + }; + + proto._getContainerSize = function() { + return this._mode()._getContainerSize(); + }; + + proto.needsResizeLayout = function() { + return this._mode().needsResizeLayout(); + }; + + // -------------------------- adding & removing -------------------------- // + + // HEADS UP overwrites default Outlayer appended + proto.appended = function( elems ) { + var items = this.addItems( elems ); + if ( !items.length ) { + return; + } + // filter, layout, reveal new items + var filteredItems = this._filterRevealAdded( items ); + // add to filteredItems + this.filteredItems = this.filteredItems.concat( filteredItems ); + }; + + // HEADS UP overwrites default Outlayer prepended + proto.prepended = function( elems ) { + var items = this._itemize( elems ); + if ( !items.length ) { + return; + } + // start new layout + this._resetLayout(); + this._manageStamps(); + // filter, layout, reveal new items + var filteredItems = this._filterRevealAdded( items ); + // layout previous items + this.layoutItems( this.filteredItems ); + // add to items and filteredItems + this.filteredItems = filteredItems.concat( this.filteredItems ); + this.items = items.concat( this.items ); + }; + + proto._filterRevealAdded = function( items ) { + var filtered = this._filter( items ); + this.hide( filtered.needHide ); + // reveal all new items + this.reveal( filtered.matches ); + // layout new items, no transition + this.layoutItems( filtered.matches, true ); + return filtered.matches; + }; + + /** + * Filter, sort, and layout newly-appended item elements + * @param {Array or NodeList or Element} elems + */ + proto.insert = function( elems ) { + var items = this.addItems( elems ); + if ( !items.length ) { + return; + } + // append item elements + var i, item; + var len = items.length; + for ( i=0; i < len; i++ ) { + item = items[i]; + this.element.appendChild( item.element ); + } + // filter new stuff + var filteredInsertItems = this._filter( items ).matches; + // set flag + for ( i=0; i < len; i++ ) { + items[i].isLayoutInstant = true; + } + this.arrange(); + // reset flag + for ( i=0; i < len; i++ ) { + delete items[i].isLayoutInstant; + } + this.reveal( filteredInsertItems ); + }; + + var _remove = proto.remove; + proto.remove = function( elems ) { + elems = utils.makeArray( elems ); + var removeItems = this.getItems( elems ); + // do regular thing + _remove.call( this, elems ); + // bail if no items to remove + var len = removeItems && removeItems.length; + // remove elems from filteredItems + for ( var i=0; len && i < len; i++ ) { + var item = removeItems[i]; + // remove item from collection + utils.removeFrom( this.filteredItems, item ); + } + }; + + proto.shuffle = function() { + // update random sortData + for ( var i=0; i < this.items.length; i++ ) { + var item = this.items[i]; + item.sortData.random = Math.random(); + } + this.options.sortBy = 'random'; + this._sort(); + this._layout(); + }; + + /** + * trigger fn without transition + * kind of hacky to have this in the first place + * @param {Function} fn + * @param {Array} args + * @returns ret + * @private + */ + proto._noTransition = function( fn, args ) { + // save transitionDuration before disabling + var transitionDuration = this.options.transitionDuration; + // disable transition + this.options.transitionDuration = 0; + // do it + var returnValue = fn.apply( this, args ); + // re-enable transition for reveal + this.options.transitionDuration = transitionDuration; + return returnValue; + }; + + // ----- helper methods ----- // + + /** + * getter method for getting filtered item elements + * @returns {Array} elems - collection of item elements + */ + proto.getFilteredItemElements = function() { + return this.filteredItems.map( function( item ) { + return item.element; + }); + }; + + // ----- ----- // + + return Isotope; + +})); + diff --git a/static/js/isotope.pkgd.min.js b/static/js/isotope.pkgd.min.js new file mode 100644 index 0000000..7ca671c --- /dev/null +++ b/static/js/isotope.pkgd.min.js @@ -0,0 +1,12 @@ +/*! + * Isotope PACKAGED v3.0.6 + * + * Licensed GPLv3 for open source use + * or Isotope Commercial License for commercial use + * + * https://isotope.metafizzy.co + * Copyright 2010-2018 Metafizzy + */ + +!function(t,e){"function"==typeof define&&define.amd?define("jquery-bridget/jquery-bridget",["jquery"],function(i){return e(t,i)}):"object"==typeof module&&module.exports?module.exports=e(t,require("jquery")):t.jQueryBridget=e(t,t.jQuery)}(window,function(t,e){"use strict";function i(i,s,a){function u(t,e,o){var n,s="$()."+i+'("'+e+'")';return t.each(function(t,u){var h=a.data(u,i);if(!h)return void r(i+" not initialized. Cannot call methods, i.e. "+s);var d=h[e];if(!d||"_"==e.charAt(0))return void r(s+" is not a valid method");var l=d.apply(h,o);n=void 0===n?l:n}),void 0!==n?n:t}function h(t,e){t.each(function(t,o){var n=a.data(o,i);n?(n.option(e),n._init()):(n=new s(o,e),a.data(o,i,n))})}a=a||e||t.jQuery,a&&(s.prototype.option||(s.prototype.option=function(t){a.isPlainObject(t)&&(this.options=a.extend(!0,this.options,t))}),a.fn[i]=function(t){if("string"==typeof t){var e=n.call(arguments,1);return u(this,t,e)}return h(this,t),this},o(a))}function o(t){!t||t&&t.bridget||(t.bridget=i)}var n=Array.prototype.slice,s=t.console,r="undefined"==typeof s?function(){}:function(t){s.error(t)};return o(e||t.jQuery),i}),function(t,e){"function"==typeof define&&define.amd?define("ev-emitter/ev-emitter",e):"object"==typeof module&&module.exports?module.exports=e():t.EvEmitter=e()}("undefined"!=typeof window?window:this,function(){function t(){}var e=t.prototype;return e.on=function(t,e){if(t&&e){var i=this._events=this._events||{},o=i[t]=i[t]||[];return o.indexOf(e)==-1&&o.push(e),this}},e.once=function(t,e){if(t&&e){this.on(t,e);var i=this._onceEvents=this._onceEvents||{},o=i[t]=i[t]||{};return o[e]=!0,this}},e.off=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){var o=i.indexOf(e);return o!=-1&&i.splice(o,1),this}},e.emitEvent=function(t,e){var i=this._events&&this._events[t];if(i&&i.length){i=i.slice(0),e=e||[];for(var o=this._onceEvents&&this._onceEvents[t],n=0;n