From 53ffe927f344f61c44c0f66ca77a1aedda4e8efe Mon Sep 17 00:00:00 2001 From: Aaron Kimbre Date: Sun, 16 Jan 2022 12:22:00 -0600 Subject: [PATCH] Move Code into repo --- .dockerignore | 5 + .editorconfig | 37 + .gitattributes | 4 + .gitignore | 15 + Dockerfile | 21 + Jenkinsfile | 50 + README.md | 69 + app/__init__.py | 203 + app/accounts.py | 170 + app/bug_reports.py | 115 + app/characters.py | 195 + app/commands.py | 70 + app/forms.py | 173 + app/log.py | 98 + app/luclient.py | 346 + app/mail.py | 85 + app/main.py | 42 + app/models.py | 1010 +++ app/moderation.py | 108 + app/play_keys.py | 150 + app/properties.py | 387 + app/pylddlib.py | 902 +++ app/reports.py | 58 + app/schemas.py | 117 + app/settings.py | 39 + .../js/bootstrap.bundle.min.js | 7 + .../js/bootstrap.bundle.min.js.map | 1 + .../bootstrap-4.2.1/js/jquery-3.3.1.min.js | 2 + app/static/bootstrap-4.2.1/scss/_alert.scss | 51 + app/static/bootstrap-4.2.1/scss/_badge.scss | 53 + .../bootstrap-4.2.1/scss/_breadcrumb.scss | 41 + .../bootstrap-4.2.1/scss/_button-group.scss | 163 + app/static/bootstrap-4.2.1/scss/_buttons.scss | 140 + app/static/bootstrap-4.2.1/scss/_card.scss | 310 + .../bootstrap-4.2.1/scss/_carousel.scss | 198 + app/static/bootstrap-4.2.1/scss/_close.scss | 44 + app/static/bootstrap-4.2.1/scss/_code.scss | 48 + .../bootstrap-4.2.1/scss/_custom-forms.scss | 507 ++ .../bootstrap-4.2.1/scss/_dropdown.scss | 191 + app/static/bootstrap-4.2.1/scss/_forms.scss | 334 + .../bootstrap-4.2.1/scss/_functions.scss | 86 + app/static/bootstrap-4.2.1/scss/_grid.scss | 52 + app/static/bootstrap-4.2.1/scss/_images.scss | 42 + .../bootstrap-4.2.1/scss/_input-group.scss | 193 + .../bootstrap-4.2.1/scss/_jumbotron.scss | 16 + .../bootstrap-4.2.1/scss/_list-group.scss | 121 + app/static/bootstrap-4.2.1/scss/_media.scss | 8 + app/static/bootstrap-4.2.1/scss/_mixins.scss | 41 + app/static/bootstrap-4.2.1/scss/_modal.scss | 186 + app/static/bootstrap-4.2.1/scss/_nav.scss | 120 + app/static/bootstrap-4.2.1/scss/_navbar.scss | 299 + .../bootstrap-4.2.1/scss/_pagination.scss | 78 + app/static/bootstrap-4.2.1/scss/_popover.scss | 183 + app/static/bootstrap-4.2.1/scss/_print.scss | 141 + .../bootstrap-4.2.1/scss/_progress.scss | 34 + app/static/bootstrap-4.2.1/scss/_reboot.scss | 462 ++ app/static/bootstrap-4.2.1/scss/_root.scss | 19 + .../bootstrap-4.2.1/scss/_spinners.scss | 53 + app/static/bootstrap-4.2.1/scss/_tables.scss | 187 + app/static/bootstrap-4.2.1/scss/_toasts.scss | 43 + app/static/bootstrap-4.2.1/scss/_tooltip.scss | 115 + .../bootstrap-4.2.1/scss/_transitions.scss | 22 + app/static/bootstrap-4.2.1/scss/_type.scss | 125 + .../bootstrap-4.2.1/scss/_utilities.scss | 16 + .../bootstrap-4.2.1/scss/_variables.scss | 1091 +++ .../bootstrap-4.2.1/scss/bootstrap-grid.scss | 29 + .../scss/bootstrap-reboot.scss | 12 + .../bootstrap-4.2.1/scss/bootstrap.scss | 44 + .../bootstrap-4.2.1/scss/mixins/_alert.scss | 13 + .../scss/mixins/_background-variant.scss | 21 + .../bootstrap-4.2.1/scss/mixins/_badge.scss | 11 + .../scss/mixins/_border-radius.scss | 35 + .../scss/mixins/_box-shadow.scss | 5 + .../scss/mixins/_breakpoints.scss | 123 + .../bootstrap-4.2.1/scss/mixins/_buttons.scss | 111 + .../bootstrap-4.2.1/scss/mixins/_caret.scss | 62 + .../scss/mixins/_clearfix.scss | 7 + .../bootstrap-4.2.1/scss/mixins/_float.scss | 11 + .../bootstrap-4.2.1/scss/mixins/_forms.scss | 198 + .../scss/mixins/_gradients.scss | 45 + .../scss/mixins/_grid-framework.scss | 66 + .../bootstrap-4.2.1/scss/mixins/_grid.scss | 51 + .../bootstrap-4.2.1/scss/mixins/_hover.scss | 37 + .../bootstrap-4.2.1/scss/mixins/_image.scss | 36 + .../scss/mixins/_list-group.scss | 21 + .../bootstrap-4.2.1/scss/mixins/_lists.scss | 7 + .../scss/mixins/_nav-divider.scss | 10 + .../scss/mixins/_pagination.scss | 22 + .../scss/mixins/_reset-text.scss | 17 + .../bootstrap-4.2.1/scss/mixins/_resize.scss | 6 + .../scss/mixins/_screen-reader.scss | 33 + .../bootstrap-4.2.1/scss/mixins/_size.scss | 6 + .../scss/mixins/_table-row.scss | 39 + .../scss/mixins/_text-emphasis.scss | 14 + .../scss/mixins/_text-hide.scss | 13 + .../scss/mixins/_text-truncate.scss | 8 + .../scss/mixins/_transition.scss | 16 + .../scss/mixins/_visibility.scss | 7 + .../scss/utilities/_align.scss | 8 + .../scss/utilities/_background.scss | 19 + .../scss/utilities/_borders.scss | 63 + .../scss/utilities/_clearfix.scss | 3 + .../scss/utilities/_display.scss | 38 + .../scss/utilities/_embed.scss | 39 + .../bootstrap-4.2.1/scss/utilities/_flex.scss | 51 + .../scss/utilities/_float.scss | 9 + .../scss/utilities/_overflow.scss | 5 + .../scss/utilities/_position.scss | 32 + .../scss/utilities/_screenreaders.scss | 11 + .../scss/utilities/_shadows.scss | 6 + .../scss/utilities/_sizing.scss | 20 + .../scss/utilities/_spacing.scss | 73 + .../bootstrap-4.2.1/scss/utilities/_text.scss | 67 + .../scss/utilities/_visibility.scss | 11 + app/static/datatables/datatables.min.css | 21 + app/static/datatables/datatables.min.js | 250 + app/static/font-awesome/LICENSE.txt | 34 + app/static/font-awesome/attribution.js | 3 + app/static/font-awesome/css/all.min.css | 5 + .../font-awesome/webfonts/fa-brands-400.eot | Bin 0 -> 134294 bytes .../font-awesome/webfonts/fa-brands-400.svg | 3717 ++++++++++ .../font-awesome/webfonts/fa-brands-400.ttf | Bin 0 -> 133988 bytes .../font-awesome/webfonts/fa-brands-400.woff | Bin 0 -> 89988 bytes .../font-awesome/webfonts/fa-brands-400.woff2 | Bin 0 -> 76736 bytes .../font-awesome/webfonts/fa-regular-400.eot | Bin 0 -> 34034 bytes .../font-awesome/webfonts/fa-regular-400.svg | 801 +++ .../font-awesome/webfonts/fa-regular-400.ttf | Bin 0 -> 33736 bytes .../font-awesome/webfonts/fa-regular-400.woff | Bin 0 -> 16276 bytes .../webfonts/fa-regular-400.woff2 | Bin 0 -> 13224 bytes .../font-awesome/webfonts/fa-solid-900.eot | Bin 0 -> 203030 bytes .../font-awesome/webfonts/fa-solid-900.svg | 5034 +++++++++++++ .../font-awesome/webfonts/fa-solid-900.ttf | Bin 0 -> 202744 bytes .../font-awesome/webfonts/fa-solid-900.woff | Bin 0 -> 101648 bytes .../font-awesome/webfonts/fa-solid-900.woff2 | Bin 0 -> 78268 bytes app/static/lddviewer/base64-binary.js | 94 + app/static/lddviewer/main.css | 84 + app/static/logo/favicon.ico | Bin 0 -> 1793 bytes app/static/logo/logo.png | Bin 0 -> 29452 bytes app/static/scss/site.scss | 94 + app/templates/_formhelpers.jinja2 | 56 + app/templates/accounts/edit_gm_level.html.j2 | 21 + app/templates/accounts/index.html.j2 | 50 + app/templates/accounts/view.html.j2 | 30 + app/templates/admin/dashboard.html.j2 | 188 + app/templates/base.html.j2 | 100 + app/templates/bug_reports/index.html.j2 | 40 + app/templates/bug_reports/resolve.html.j2 | 78 + app/templates/bug_reports/view.html.j2 | 105 + app/templates/character/index.html.j2 | 52 + app/templates/character/view.html.j2 | 34 + app/templates/flask_user/_common_base.html | 14 + app/templates/flask_user/_macros.html | 42 + app/templates/flask_user/login.html | 70 + .../flask_user/login_or_register.html | 71 + app/templates/flask_user/register.html | 53 + app/templates/header.html.j2 | 94 + app/templates/ldd/ldd.html.j2 | 1190 ++++ app/templates/logs/activity.html.j2 | 45 + app/templates/logs/command.html.j2 | 47 + app/templates/mail/send.html.j2 | 46 + app/templates/main/about.html.j2 | 73 + app/templates/main/account_creation.html.j2 | 82 + app/templates/main/data_download.html.j2 | 53 + app/templates/main/index.html.j2 | 49 + app/templates/moderation/index.html.j2 | 92 + app/templates/partials/_account.html.j2 | 196 + app/templates/partials/_character.html.j2 | 101 + app/templates/partials/_charxml.html.j2 | 221 + app/templates/partials/_gm_level.html.j2 | 35 + app/templates/partials/_mail.html.j2 | 0 app/templates/partials/_property.html.j2 | 101 + .../partials/_property_content.html.j2 | 71 + .../partials/charxml/_char_stats.html.j2 | 242 + .../partials/charxml/_inv_grid.html.j2 | 24 + .../partials/charxml/_item_tooltip.html.j2 | 67 + app/templates/partials/charxml/_stats.html.j2 | 38 + .../partials/charxml/_zone_stats.html.j2 | 51 + app/templates/play_keys/bulk.html.j2 | 22 + app/templates/play_keys/edit.html.j2 | 23 + app/templates/play_keys/index.html.j2 | 56 + app/templates/play_keys/view.html.j2 | 96 + app/templates/properties/index.html.j2 | 58 + app/templates/properties/view.html.j2 | 39 + app/templates/reports/index.html.j2 | 33 + app/templates/reports/items/by_date.html.j2 | 46 + characterxml->json.example.json | 6253 +++++++++++++++++ entrypoint.sh | 7 + migrations/README | 1 + migrations/alembic.ini | 50 + migrations/env.py | 91 + migrations/script.py.mako | 24 + .../712d42956a47_initial_migration.py | 270 + ...f7ee_add_column_to_track_times_key_uses.py | 28 + .../versions/b89c21b5112f_itemreport_table.py | 33 + requirements.txt | 61 + wsgi.py | 16 + 196 files changed, 33149 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 Jenkinsfile create mode 100644 README.md create mode 100644 app/__init__.py create mode 100644 app/accounts.py create mode 100644 app/bug_reports.py create mode 100644 app/characters.py create mode 100644 app/commands.py create mode 100644 app/forms.py create mode 100644 app/log.py create mode 100644 app/luclient.py create mode 100644 app/mail.py create mode 100644 app/main.py create mode 100644 app/models.py create mode 100644 app/moderation.py create mode 100644 app/play_keys.py create mode 100644 app/properties.py create mode 100644 app/pylddlib.py create mode 100644 app/reports.py create mode 100644 app/schemas.py create mode 100644 app/settings.py create mode 100644 app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js create mode 100644 app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js.map create mode 100644 app/static/bootstrap-4.2.1/js/jquery-3.3.1.min.js create mode 100644 app/static/bootstrap-4.2.1/scss/_alert.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_badge.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_breadcrumb.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_button-group.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_buttons.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_card.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_carousel.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_close.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_code.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_custom-forms.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_dropdown.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_forms.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_functions.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_grid.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_images.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_input-group.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_jumbotron.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_list-group.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_media.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_mixins.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_modal.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_nav.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_navbar.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_pagination.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_popover.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_print.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_progress.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_reboot.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_root.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_spinners.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_tables.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_toasts.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_tooltip.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_transitions.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_type.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_utilities.scss create mode 100644 app/static/bootstrap-4.2.1/scss/_variables.scss create mode 100644 app/static/bootstrap-4.2.1/scss/bootstrap-grid.scss create mode 100644 app/static/bootstrap-4.2.1/scss/bootstrap-reboot.scss create mode 100644 app/static/bootstrap-4.2.1/scss/bootstrap.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_alert.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_background-variant.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_badge.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_border-radius.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_box-shadow.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_breakpoints.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_buttons.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_caret.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_clearfix.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_float.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_forms.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_gradients.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_grid-framework.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_grid.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_hover.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_image.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_list-group.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_lists.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_nav-divider.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_pagination.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_reset-text.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_resize.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_screen-reader.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_size.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_table-row.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_text-emphasis.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_text-hide.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_text-truncate.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_transition.scss create mode 100644 app/static/bootstrap-4.2.1/scss/mixins/_visibility.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_align.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_background.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_borders.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_clearfix.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_display.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_embed.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_flex.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_float.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_overflow.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_position.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_screenreaders.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_shadows.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_sizing.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_spacing.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_text.scss create mode 100644 app/static/bootstrap-4.2.1/scss/utilities/_visibility.scss create mode 100644 app/static/datatables/datatables.min.css create mode 100644 app/static/datatables/datatables.min.js create mode 100644 app/static/font-awesome/LICENSE.txt create mode 100644 app/static/font-awesome/attribution.js create mode 100644 app/static/font-awesome/css/all.min.css create mode 100644 app/static/font-awesome/webfonts/fa-brands-400.eot create mode 100644 app/static/font-awesome/webfonts/fa-brands-400.svg create mode 100644 app/static/font-awesome/webfonts/fa-brands-400.ttf create mode 100644 app/static/font-awesome/webfonts/fa-brands-400.woff create mode 100644 app/static/font-awesome/webfonts/fa-brands-400.woff2 create mode 100644 app/static/font-awesome/webfonts/fa-regular-400.eot create mode 100644 app/static/font-awesome/webfonts/fa-regular-400.svg create mode 100644 app/static/font-awesome/webfonts/fa-regular-400.ttf create mode 100644 app/static/font-awesome/webfonts/fa-regular-400.woff create mode 100644 app/static/font-awesome/webfonts/fa-regular-400.woff2 create mode 100644 app/static/font-awesome/webfonts/fa-solid-900.eot create mode 100644 app/static/font-awesome/webfonts/fa-solid-900.svg create mode 100644 app/static/font-awesome/webfonts/fa-solid-900.ttf create mode 100644 app/static/font-awesome/webfonts/fa-solid-900.woff create mode 100644 app/static/font-awesome/webfonts/fa-solid-900.woff2 create mode 100644 app/static/lddviewer/base64-binary.js create mode 100644 app/static/lddviewer/main.css create mode 100644 app/static/logo/favicon.ico create mode 100644 app/static/logo/logo.png create mode 100644 app/static/scss/site.scss create mode 100644 app/templates/_formhelpers.jinja2 create mode 100644 app/templates/accounts/edit_gm_level.html.j2 create mode 100644 app/templates/accounts/index.html.j2 create mode 100644 app/templates/accounts/view.html.j2 create mode 100644 app/templates/admin/dashboard.html.j2 create mode 100644 app/templates/base.html.j2 create mode 100644 app/templates/bug_reports/index.html.j2 create mode 100644 app/templates/bug_reports/resolve.html.j2 create mode 100644 app/templates/bug_reports/view.html.j2 create mode 100644 app/templates/character/index.html.j2 create mode 100644 app/templates/character/view.html.j2 create mode 100644 app/templates/flask_user/_common_base.html create mode 100644 app/templates/flask_user/_macros.html create mode 100644 app/templates/flask_user/login.html create mode 100644 app/templates/flask_user/login_or_register.html create mode 100644 app/templates/flask_user/register.html create mode 100644 app/templates/header.html.j2 create mode 100644 app/templates/ldd/ldd.html.j2 create mode 100644 app/templates/logs/activity.html.j2 create mode 100644 app/templates/logs/command.html.j2 create mode 100644 app/templates/mail/send.html.j2 create mode 100644 app/templates/main/about.html.j2 create mode 100644 app/templates/main/account_creation.html.j2 create mode 100644 app/templates/main/data_download.html.j2 create mode 100644 app/templates/main/index.html.j2 create mode 100644 app/templates/moderation/index.html.j2 create mode 100644 app/templates/partials/_account.html.j2 create mode 100644 app/templates/partials/_character.html.j2 create mode 100644 app/templates/partials/_charxml.html.j2 create mode 100644 app/templates/partials/_gm_level.html.j2 create mode 100644 app/templates/partials/_mail.html.j2 create mode 100644 app/templates/partials/_property.html.j2 create mode 100644 app/templates/partials/_property_content.html.j2 create mode 100644 app/templates/partials/charxml/_char_stats.html.j2 create mode 100644 app/templates/partials/charxml/_inv_grid.html.j2 create mode 100644 app/templates/partials/charxml/_item_tooltip.html.j2 create mode 100644 app/templates/partials/charxml/_stats.html.j2 create mode 100644 app/templates/partials/charxml/_zone_stats.html.j2 create mode 100644 app/templates/play_keys/bulk.html.j2 create mode 100644 app/templates/play_keys/edit.html.j2 create mode 100644 app/templates/play_keys/index.html.j2 create mode 100644 app/templates/play_keys/view.html.j2 create mode 100644 app/templates/properties/index.html.j2 create mode 100644 app/templates/properties/view.html.j2 create mode 100644 app/templates/reports/index.html.j2 create mode 100644 app/templates/reports/items/by_date.html.j2 create mode 100644 characterxml->json.example.json create mode 100644 entrypoint.sh create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/712d42956a47_initial_migration.py create mode 100644 migrations/versions/8a2966b9f7ee_add_column_to_track_times_key_uses.py create mode 100644 migrations/versions/b89c21b5112f_itemreport_table.py create mode 100644 requirements.txt create mode 100644 wsgi.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..7a2c4f4 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +credentials.py +.idea/ +__pycache__/ +venv/ +.git/ \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..53ca9bf --- /dev/null +++ b/.editorconfig @@ -0,0 +1,37 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true + +# Matches multiple files with brace expansion notation +# Set default charset +[*.{js,py}] +charset = utf-8 + +# 4 space indentation +[*.py] +indent_style = space +indent_size = 4 + +[{*.jinja2,*.html.j2}] +indent_style = space +indent_size = 2 + +# Tab indentation (no size specified) +[Makefile] +indent_style = tab + +# Indentation override for all JS under lib directory +[lib/**.js] +indent_style = space +indent_size = 2 + +# Matches the exact files either package.json or .travis.yml +[{package.json,.travis.yml}] +indent_style = space +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f17c0c0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +app/static/bootstrap-4.2.1/* linguist-vendored +app/static/bootswatch-master/* linguist-vendored +app/static/datatables/* linguist-vendored +app/static/font-awesome/* linguist-vendored diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f31a16e --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +credentials.py +resources.py +.idea/ +__pycache__/ +venv/ +static/policy/ +app/static/site.css +app/static/.webassets-cache/**/* +app/static/brickdb/* +locale.json +app/static/ldddb/* +.vscode/settings.json +locale.xml +app/luclient/* +app/cache/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6775ed1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +# syntax=docker/dockerfile:1 + +FROM python:3.8-slim-buster + +RUN apt update +RUN apt -y install zip +RUN apt -y install imagemagick + +COPY requirements.txt requirements.txt + +RUN pip install -r requirements.txt +RUN pip install gunicorn + +COPY wsgi.py wsgi.py +COPY entrypoint.sh entrypoint.sh +COPY ./app /app +COPY ./migrations /migrations + +EXPOSE 8000 +RUN chmod +x entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..9382731 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,50 @@ +properties([ + parameters([ + gitParameter( + branch: '', + branchFilter: 'origin/(.*)', + defaultValue: 'origin/refactor', + description: '', + name: 'BRANCH', + quickFilterEnabled: false, + selectedValue: 'NONE', + sortMode: 'NONE', + tagFilter: '*', + useRepository: 'git@github.com:aronwk-aaron/AccountManager.git', + type: 'PT_BRANCH' + ) + ]) +]) + +node('worker'){ + stage('Clone Code'){ + checkout([ + $class: 'GitSCM', + branches: [[name: params.BRANCH]], + extensions: [], + userRemoteConfigs: [ + [ + credentialsId: 'aronwk', + url: 'git@github.com:aronwk-aaron/AccountManager.git' + ] + ] + ]) + } + def tag = '' + stage("Build Container"){ + + if (params.BRANCH.contains('master')){ + tag = 'latest' + } else { + tag = params.BRANCH.replace('\\', '-') + } + sh "docker build -t aronwk/dlu-account_manager:${tag} ." + } + stage("Push Container"){ + withCredentials([usernamePassword(credentialsId: 'docker-hub-token', passwordVariable: 'password', usernameVariable: 'username')]) { + sh "docker login -u ${username} -p ${password}" + sh "docker push aronwk/dlu-account_manager:${tag}" + sh 'docker logout' + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..250bfe2 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# Nexus Dashboard + +**This is a WIP: For Advanced Users** + +

+ Sublime's custom image +

+ +# Deployment + +## Docker + +```bash + +docker run -d \ + -e APP_SECRET_KEY='' \ + -e APP_DATABASE_URI='mysql+pymysql://:@:/' \ + # you can include other optional Environment Variables from below like this + -e REQUIRE_PLAY_KEY=True + -p 8000:8000/tcp + -v /path/to/unpacked/client:/app/luclient:rw \ + -v /path/to/cachedir:/app/cache:rw \ # optional for persistent cache for conversions + aronwk/dlu-account_manager:refactor + +``` + + * /app/luclient must be mapped to the location of an unpacked client + * you only need `res/` and `locale/` from the client, but dropping the whole cleint in there won't hurt + * Use `fdb_to_sqlite.py` in lcdr's utilities on `res/cdclient.fdb` in the unpacked client to convert the client database to `cdclient.sqlite` + * Put teh resulting `cdclient.sqlite` in the res folder: `res/cdclient.sqlite` + * unzip `res/brickdb.zip` in-place + * **Docker will do this for you** + * you should have new folders and files in the following places: + * `res/Assemblies/../..` with a bunch of sub folders + * `res/Primitives/../..` with a bunch of sub folders + * `res/info.xml` + * `res/Materials.xml` + +### Environmental Variables + * Required: + * APP_SECRET_KEY (Must be provided) + * APP_DATABASE_URI (Must be provided) + * Optional + * USER_ENABLE_REGISTER (Default: True) + * USER_ENABLE_EMAIL (Default: True, Needs Mail to be configured) + * USER_ENABLE_CONFIRM_EMAIL (Default: True) + * USER_ENABLE_INVITE_USER (Default: False) + * USER_REQUIRE_INVITATION (Default: False) + * REQUIRE_PLAY_KEY (Default: True) + * MAIL_SERVER (Default: smtp.gmail.com) + * MAIL_PORT (Default: 587) + * MAIL_USE_SSL (Default: False) + * MAIL_USE_TLS (Default: True) + * MAIL_USERNAME (Default: None) + * MAIL_PASSWORD (Default: None) + * USER_EMAIL_SENDER_NAME (Default: None) + * USER_EMAIL_SENDER_EMAIL (Default: None) + +## Manual + +Don't, use Docker /s + +TODO: Make manual deployment easier to configure + +# Development + +Please use [Editor Config](https://editorconfig.org/) + + * `flask run` to run local dev server diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..61050e5 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,203 @@ +import os +from flask import Flask, url_for, g, redirect +from functools import wraps +from flask_assets import Environment +from webassets import Bundle +import time +from app.models import db, migrate, PlayKey +from app.schemas import ma +from app.forms import CustomUserManager +from flask_user import user_registered, current_user +from flask_wtf.csrf import CSRFProtect +from flask_apscheduler import APScheduler +from app.luclient import query_cdclient, register_luclient_jinja_helpers + +from app.commands import init_db, init_accounts +from app.models import Account, AccountInvitation + +# Instantiate Flask extensions +csrf_protect = CSRFProtect() +scheduler = APScheduler() +# db and migrate is instantiated in models.py + + +def create_app(): + + app = Flask(__name__, instance_relative_config=True) + + # decrement uses on a play key after a successful registration + # and increment the times it has been used + @user_registered.connect_via(app) + def after_register_hook(sender, user, **extra): + if app.config["REQUIRE_PLAY_KEY"]: + play_key_used = PlayKey.query.filter(PlayKey.id == user.play_key_id).first() + play_key_used.key_uses = play_key_used.key_uses - 1 + play_key_used.times_used = play_key_used.times_used + 1 + db.session.add(play_key_used) + db.session.commit() + + # A bunch of jinja filters to make things easiers + @app.template_filter('ctime') + def timectime(s): + if s: + return time.ctime(s) # or datetime.datetime.fromtimestamp(s) + else: + return "Never" + + @app.template_filter('check_perm_map') + def check_perm_map(perm_map, bit): + if perm_map: + return perm_map & (1 << bit) + else: + return 0 & (1 << bit) + + @app.teardown_appcontext + def close_connection(exception): + cdclient = getattr(g, '_cdclient', None) + if cdclient is not None: + cdclient.close() + + # add the commands to flask cli + app.cli.add_command(init_db) + app.cli.add_command(init_accounts) + + register_settings(app) + register_extensions(app) + register_blueprints(app) + register_luclient_jinja_helpers(app) + + return app + + +def register_extensions(app): + """Register extensions for Flask app + + Args: + app (Flask): Flask app to register for + """ + db.init_app(app) + migrate.init_app(app, db) + ma.init_app(app) + + scheduler.init_app(app) + scheduler.start() + + csrf_protect.init_app(app) + + user_manager = CustomUserManager( + app, db, Account, UserInvitationClass=AccountInvitation + ) + + assets = Environment(app) + assets.url = app.static_url_path + scss = Bundle('scss/site.scss', filters='libsass', output='site.css') + assets.register('scss_all', scss) + + +def register_blueprints(app): + """Register blueprints for Flask app + + Args: + app (Flask): Flask app to register for + """ + + from .main import main_blueprint + app.register_blueprint(main_blueprint) + from .play_keys import play_keys_blueprint + app.register_blueprint(play_keys_blueprint, url_prefix='/play_keys') + from .accounts import accounts_blueprint + app.register_blueprint(accounts_blueprint, url_prefix='/accounts') + from .characters import character_blueprint + app.register_blueprint(character_blueprint, url_prefix='/characters') + from .properties import property_blueprint + app.register_blueprint(property_blueprint, url_prefix='/properties') + from .moderation import moderation_blueprint + app.register_blueprint(moderation_blueprint, url_prefix='/moderation') + from .log import log_blueprint + app.register_blueprint(log_blueprint, url_prefix='/log') + from .bug_reports import bug_report_blueprint + app.register_blueprint(bug_report_blueprint, url_prefix='/bug_reports') + from .mail import mail_blueprint + app.register_blueprint(mail_blueprint, url_prefix='/mail') + from .luclient import luclient_blueprint + app.register_blueprint(luclient_blueprint, url_prefix='/luclient') + from .reports import reports_blueprint + app.register_blueprint(reports_blueprint, url_prefix='/reports') + + + +def register_settings(app): + """Register setting from setting and env + + Args: + app (Flask): Flask app to register for + """ + + # Load common settings + app.config.from_object('app.settings') + + # Load environment specific settings + app.config['TESTING'] = False + app.config['DEBUG'] = False + + # always pull these two from the env + app.config['SECRET_KEY'] = os.getenv('APP_SECRET_KEY') + app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('APP_DATABASE_URI') + + # try to get overides, otherwise just use what we have already + app.config['USER_ENABLE_REGISTER'] = os.getenv( + 'USER_ENABLE_REGISTER', + app.config['USER_ENABLE_REGISTER'] + ) + app.config['USER_ENABLE_EMAIL'] = os.getenv( + 'USER_ENABLE_EMAIL', + app.config['USER_ENABLE_EMAIL'] + ) + app.config['USER_ENABLE_CONFIRM_EMAIL'] = os.getenv( + 'USER_ENABLE_CONFIRM_EMAIL', + app.config['USER_ENABLE_CONFIRM_EMAIL'] + ) + app.config['REQUIRE_PLAY_KEY'] = os.getenv( + 'REQUIRE_PLAY_KEY', + app.config['REQUIRE_PLAY_KEY'] + ) + app.config['USER_ENABLE_INVITE_USER'] = os.getenv( + 'USER_ENABLE_INVITE_USER', + app.config['USER_ENABLE_INVITE_USER'] + ) + app.config['USER_REQUIRE_INVITATION'] = os.getenv( + 'USER_REQUIRE_INVITATION', + app.config['USER_REQUIRE_INVITATION'] + ) + app.config['SQLALCHEMY_ENGINE_OPTIONS'] = { + "pool_pre_ping": True, + "pool_size": 10, + "max_overflow": 2, + "pool_recycle": 300, + "pool_pre_ping": True, + "pool_use_lifo": True + } + app.config['MAIL_SERVER'] = os.getenv('MAIL_SERVER', 'smtp.gmail.com') + app.config['MAIL_PORT'] = os.getenv('MAIL_USE_SSL', 587) + app.config['MAIL_USE_SSL'] = os.getenv('MAIL_USE_SSL', False) + app.config['MAIL_USE_TLS'] = os.getenv('MAIL_USE_TLS', True) + app.config['MAIL_USERNAME'] = os.getenv('MAIL_USERNAME', None) + app.config['MAIL_PASSWORD'] = os.getenv('MAIL_PASSWORD', None) + app.config['USER_EMAIL_SENDER_NAME'] = os.getenv('USER_EMAIL_SENDER_NAME', None) + app.config['USER_EMAIL_SENDER_EMAIL'] = os.getenv('USER_EMAIL_SENDER_EMAIL', None) + + +def gm_level(gm_level): + """Decorator for handling permissions based on the user's GM Level + + Args: + gm_level (int): 0-9 + """ + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + if current_user.gm_level < gm_level: + return redirect(url_for('main.index')) + return func(*args, **kwargs) + return wrapper + return decorator diff --git a/app/accounts.py b/app/accounts.py new file mode 100644 index 0000000..3e52560 --- /dev/null +++ b/app/accounts.py @@ -0,0 +1,170 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, current_app, flash +from flask_user import login_required, current_user +import json +from datatables import ColumnDT, DataTables +import datetime +import time +from app.models import Account, AccountInvitation, db +from app.schemas import AccountSchema +from app import gm_level +from app.forms import EditGMLevelForm + +accounts_blueprint = Blueprint('accounts', __name__) + +account_schema = AccountSchema() + +@accounts_blueprint.route('/', methods=['GET']) +@login_required +@gm_level(3) +def index(): + return render_template('accounts/index.html.j2') + + +@accounts_blueprint.route('/view/', methods=['GET']) +@login_required +@gm_level(3) +def view(id): + account_data = Account.query.filter(Account.id == id).first() + if account_data: + return render_template('accounts/view.html.j2', account_data=account_data) + else: + return redirect(url_for('main.index')) + + +@accounts_blueprint.route('/edit_gm_level/', methods=('GET', 'POST')) +@login_required +@gm_level(8) +def edit_gm_level(id): + if current_user.id == int(id): + flash("You cannot your own GM Level", "danger") + return redirect(request.referrer if request.referrer else url_for("main.index")) + account_data = Account.query.filter(Account.id==id).first() + if account_data.gm_level >= 8 and current_user.gm_level == 8: + flash("You cannot edit this user's GM Level", "warning") + return redirect(request.referrer if request.referrer else url_for("main.index")) + + form = EditGMLevelForm() + + if form.validate_on_submit(): + account_data.gm_level = form.gm_level.data + account_data.save() + return redirect(url_for('accounts.view', id=account_data.id)) + + form.gm_level.data = account_data.gm_level + + return render_template('accounts/edit_gm_level.html.j2', form=form, username=account_data.username) + + +@accounts_blueprint.route('/lock/', methods=['GET']) +@login_required +@gm_level(3) +def lock(id): + account = Account.query.filter(Account.id == id).first() + account.locked = not account.locked + account.save() + if account.locked: + flash("Locked Account", "danger") + else: + flash("Unlocked account", "success") + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@accounts_blueprint.route('/ban/', methods=['GET']) +@login_required +@gm_level(3) +def ban(id): + account = Account.query.filter(Account.id == id).first() + account.banned = not account.banned + account.save() + if account.banned: + flash("Banned Account", "danger") + else: + flash("Unbanned account", "success") + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@accounts_blueprint.route('/muted//', methods=['GET']) +@login_required +@gm_level(3) +def mute(id, days=0): + account = Account.query.filter(Account.id == id).first() + if days == "0": + account.mute_expire = 0 + flash("Unmuted Account", "success") + else: + muted_intil = datetime.datetime.now() + datetime.timedelta(days=int(days)) + account.mute_expire = muted_intil.timestamp() + flash(f"Muted account for {days} days", "danger") + account.save() + + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@accounts_blueprint.route('/get', methods=['GET']) +@login_required +@gm_level(3) +def get(): + columns = [ + ColumnDT(Account.id), # 0 + ColumnDT(Account.username), # 1 + ColumnDT(Account.email), # 2 + ColumnDT(Account.gm_level), # 3 + ColumnDT(Account.locked), # 4 + ColumnDT(Account.banned), # 5 + ColumnDT(Account.mute_expire), # 6 + ColumnDT(Account.created_at), # 7 + ColumnDT(Account.email_confirmed_at) # 8 + ] + + query = db.session.query().select_from(Account) + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for account in data["data"]: + account["0"] = f""" + + View + + """ + # + # Delete + # + + if account["4"]: + account["4"] = '''

''' + else: + account["4"] = '''

''' + + if account["5"]: + account["5"] = '''

''' + else: + account["5"] = '''

''' + + if account["6"]: + account["6"] = f'''

''' + else: + account["6"] = '''

''' + + if current_app.config["USER_ENABLE_EMAIL"]: + if account["8"]: + account["8"] = f'''

''' + else: + account["8"] = '''

''' + else: + # shift columns to fill in gap of 2 + account["2"] = account["3"] + account["3"] = account["4"] + account["4"] = account["5"] + account["5"] = account["6"] + account["6"] = account["7"] + # remove last two columns + del account["7"] + del account["8"] + + return data + diff --git a/app/bug_reports.py b/app/bug_reports.py new file mode 100644 index 0000000..835950c --- /dev/null +++ b/app/bug_reports.py @@ -0,0 +1,115 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, flash +from flask_user import login_required, current_user +from app.models import db, BugReport, CharacterInfo +from datatables import ColumnDT, DataTables +from app.forms import ResolveBugReportForm +from app import gm_level +from app.luclient import translate_from_locale + +bug_report_blueprint = Blueprint('bug_reports', __name__) + +@bug_report_blueprint.route('/', methods=['GET']) +@login_required +@gm_level(3) +def index(status): + return render_template('bug_reports/index.html.j2', status=status) + + +@bug_report_blueprint.route('/view/', methods=['GET']) +@login_required +@gm_level(3) +def view(id): + report = BugReport.query.filter(BugReport.id == id).first() + if report.resoleved_by: + rb = report.resoleved_by.username + else: + rb="" + return render_template('bug_reports/view.html.j2', report=report, resolved_by=rb) + + +@bug_report_blueprint.route('/resolve/', methods=['GET', 'POST']) +@login_required +@gm_level(3) +def resolve(id): + report = BugReport.query.filter(BugReport.id == id).first() + if report.resolved_time: + flash("Bug report already resolved!", "danger") + return redirect(request.referrer if request.referrer else url_for("main.index")) + + form = ResolveBugReportForm() + if form.validate_on_submit(): + report.resolution = form.resolution.data + report.resoleved_by_id = current_user.id + report.resolved_time = db.func.now() + report.save() + return redirect(url_for("bug_reports.index", status="unresolved")) + + return render_template('bug_reports/resolve.html.j2', form=form, report=report) + + +@bug_report_blueprint.route('/get/', methods=['GET']) +@login_required +@gm_level(3) +def get(status): + columns = [ + ColumnDT(BugReport.id), # 0 + ColumnDT(BugReport.body), # 1 + ColumnDT(BugReport.client_version), # 2 + ColumnDT(BugReport.other_player_id), # 3 + ColumnDT(BugReport.selection), # 4 + ColumnDT(BugReport.submitted), # 5 + ColumnDT(BugReport.resolved_time), # 6 + ] + + query = None + if status=="all": + query = db.session.query().select_from(BugReport) + elif status=="resolved": + query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time != None) + elif status=="unresolved": + query = db.session.query().select_from(BugReport).filter(BugReport.resolved_time == None) + else: + raise Exception("Not a valid filter") + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for report in data["data"]: + id = report["0"] + report["0"] = f""" + + View + + """ + + if not report["6"]: + report["0"] += f""" + + Resolve + + """ + + if report["3"] == "0": + report["3"] = "None" + else: + character = CharacterInfo.query.filter(CharacterInfo.id == int(report["3"]) & 0xFFFFFFFF).first() + if character: + report["3"] = f""" + + {character.name} + + """ + else: + report["3"] = "Player Deleted" + + report["4"] = translate_from_locale(report["4"][2:-1]) + + if not report["6"]: + report["6"] = '''

''' + + return data diff --git a/app/characters.py b/app/characters.py new file mode 100644 index 0000000..a6a099f --- /dev/null +++ b/app/characters.py @@ -0,0 +1,195 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, flash +from flask_user import login_required, current_user +import json +from datatables import ColumnDT, DataTables +import datetime, time +from app.models import CharacterInfo, CharacterXML, Account, db +from app.schemas import CharacterInfoSchema +from app import gm_level +import xmltodict + +character_blueprint = Blueprint('characters', __name__) + +character_schema = CharacterInfoSchema() + +@character_blueprint.route('/', methods=['GET']) +@login_required +@gm_level(3) +def index(): + return render_template('character/index.html.j2') + + +@character_blueprint.route('/approve_name//', methods=['GET']) +@login_required +@gm_level(3) +def approve_name(id, action): + character = CharacterInfo.query.filter(CharacterInfo.id == id).first() + + if action == "approve": + if character.pending_name: + character.name = character.pending_name + character.pending_name = "" + character.needs_rename = False + flash( + f"Approved name {character.name}", + "success" + ) + elif action == "rename": + character.needs_rename = True + flash( + f"Marked character {character.name} (Pending Name: {character.pending_name if character.pending_name else 'None'}) as needing Rename", + "danger" + ) + + character.save() + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@character_blueprint.route('/view/', methods=['GET']) +@login_required +def view(id): + + character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first() + + if character_data == {}: + abort(404) + return + + if current_user.gm_level < 3: + if character_data.account_id and character_data.account_id != current_user.id: + abort(403) + return + character_json = xmltodict.parse( + CharacterXML.query.filter( + CharacterXML.id==id + ).first().xml_data, + attr_prefix="attr_" + ) + + # print json for reference + # with open("errorchar.json", "a") as file: + # file.write( + # json.dumps(character_json, indent=4) + # ) + + # stupid fix for jinja parsing + character_json["obj"]["inv"]["holdings"] = character_json["obj"]["inv"].pop("items") + # sort by items slot index + for inv in character_json["obj"]["inv"]["holdings"]["in"]: + if "i" in inv.keys() and type(inv["i"]) == list: + inv["i"] = sorted(inv["i"], key = lambda i: int(i['attr_s'])) + + + return render_template( + 'character/view.html.j2', + character_data=character_data, + character_json=character_json + ) + + +@character_blueprint.route('/restrict//', methods=['GET']) +@login_required +@gm_level(3) +def restrict(id, bit): + + # restrict to bit 4-6 + if 6 < int(bit) < 3: + abort(403) + return + + character_data = CharacterInfo.query.filter(CharacterInfo.id == id).first() + + if character_data == {}: + abort(404) + return + + character_data.permission_map ^= (1 << int(bit)) + character_data.save() + + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@character_blueprint.route('/get/', methods=['GET']) +@login_required +@gm_level(3) +def get(status): + columns = [ + ColumnDT(CharacterInfo.id), # 0 + ColumnDT(Account.username), # 1 + ColumnDT(CharacterInfo.name), # 2 + ColumnDT(CharacterInfo.pending_name), # 3 + ColumnDT(CharacterInfo.needs_rename), # 4 + ColumnDT(CharacterInfo.last_login), # 5 + ColumnDT(CharacterInfo.permission_map), # 6 + ] + + query = None + if status=="all": + query = db.session.query().select_from(CharacterInfo).join(Account) + elif status=="approved": + query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name == "") & (CharacterInfo.needs_rename == False)) + elif status=="unapproved": + query = db.session.query().select_from(CharacterInfo).join(Account).filter((CharacterInfo.pending_name != "") | (CharacterInfo.needs_rename == True)) + else: + raise Exception("Not a valid filter") + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for character in data["data"]: + id = character["0"] + character["0"] = f""" +
{id}
+ + View + + """ + + if not character["4"]: + character["0"] += f""" + + Needs Rename + + """ + + if character["3"] or character["4"]: + character["0"] += f""" + + Approve Name + + """ + + character["1"] = f""" + + View {character["1"]} + + """ + + if character["4"]: + character["4"] = '''

''' + else: + character["4"] = '''

''' + + character["5"] = time.ctime(character["5"]) + + perm_map = character["6"] + character["6"] = "" + + if perm_map & (1 << 4): + character["6"] += "Restricted Trade
" + + if perm_map & (1 << 5): + character["6"] += "Restricted Mail
" + + if perm_map & (1 << 6): + character["6"] += "Restricted Chat
" + + + return data + diff --git a/app/commands.py b/app/commands.py new file mode 100644 index 0000000..f7729a7 --- /dev/null +++ b/app/commands.py @@ -0,0 +1,70 @@ +import click +import json +from flask.cli import with_appcontext +import random, string, datetime +from flask_user import current_app +from app import db +from app.models import Account, PlayKey + +@click.command("init_db") +@click.argument('drop_tables', nargs=1) +@with_appcontext +def init_db(drop_tables=False): + """ Initialize the database.""" + + print('Initializing Database.') + if drop_tables: + print('Dropping all tables.') + db.drop_all() + print('Creating all tables.') + db.create_all() + print('Database has been initialized.') + return + + +@click.command("init_accounts") +@with_appcontext +def init_accounts(): + """ Initialize the accounts.""" + + # Add accounts + print('Creating Admin account.') + admin_account = find_or_create_account( + 'admin', + 'example@example.com', + 'Nope', + ) + + + return + + +def find_or_create_account(name, email, password, gm_level=9): + """ Find existing account or create new account """ + account = Account.query.filter(Account.email == email).first() + if not account: + key = "" + for j in range(4): + key += ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(4)) + '-' + # Remove last dash + key = key[:-1] + + play_key = PlayKey( + key_string=key + ) + db.session.add(play_key) + db.session.commit() + + play_key = PlayKey.query.filter(PlayKey.key_string == key).first() + account = Account(email=email, + username=name, + password=current_app.user_manager.password_manager.hash_password(password), + play_key_id=play_key.id, + email_confirmed_at=datetime.datetime.utcnow(), + gm_level=gm_level + ) + play_key.key_uses = 0 + db.session.add(account) + db.session.add(play_key) + db.session.commit() + return # account diff --git a/app/forms.py b/app/forms.py new file mode 100644 index 0000000..7f4b5a3 --- /dev/null +++ b/app/forms.py @@ -0,0 +1,173 @@ +from flask_wtf import FlaskForm +from flask import current_app + +from flask_user.forms import ( + unique_email_validator, + password_validator, + unique_username_validator +) +from flask_user import UserManager +from wtforms.widgets import TextArea, NumberInput +from wtforms import ( + StringField, + HiddenField, + PasswordField, + BooleanField, + SubmitField, + validators, + IntegerField, + StringField, + SelectField +) + +from wtforms.validators import DataRequired, Optional +from app.models import PlayKey + +def validate_play_key(form, field): + """Validates a field for a valid phone number + Args: + form: REQUIRED, the field's parent form + field: REQUIRED, the field with data + Returns: + None, raises ValidationError if failed + """ + # jank to get the fireign key that we need back into the field + if current_app.config["REQUIRE_PLAY_KEY"]: + field.data = PlayKey.key_is_valid(key_string=field.data) + return + + +class CustomUserManager(UserManager): + def customize(self, app): + self.RegisterFormClass = CustomRegisterForm + + +class CustomRegisterForm(FlaskForm): + """Registration form""" + next = HiddenField() + reg_next = HiddenField() + + # Login Info + email = StringField( + 'E-Mail', + validators=[ + Optional(), + validators.Email('Invalid email address'), + unique_email_validator, + ] + ) + + username = StringField( + 'Username', + validators=[ + DataRequired(), + unique_username_validator, + ] + ) + + play_key_id = StringField( + 'Play Key', + validators=[ + Optional(), + validate_play_key, + ] + ) + + password = PasswordField('Password', validators=[ + DataRequired(), + password_validator + ]) + retype_password = PasswordField('Retype Password', validators=[ + validators.EqualTo('password', message='Passwords did not match') + ]) + + invite_token = HiddenField('Token') + + submit = SubmitField('Register') + +class CreatePlayKeyForm(FlaskForm): + + count = IntegerField( + 'How many Play Keys to create', + validators=[DataRequired()] + ) + uses = IntegerField( + 'How many uses each new play key will have', + validators=[DataRequired()] + ) + submit = SubmitField('Create!') + +class EditPlayKeyForm(FlaskForm): + + active = BooleanField( + 'Active' + ) + + uses = IntegerField( + 'Play Key Uses' + ) + + notes = StringField( + 'Notes', + widget=TextArea() + ) + + submit = SubmitField('Submit') + + +class EditGMLevelForm(FlaskForm): + + gm_level = IntegerField( + 'GM Level', + widget=NumberInput(min = 0, max = 9) + ) + + submit = SubmitField('Submit') + + +class ResolveBugReportForm(FlaskForm): + + resolution = StringField( + 'Resolution', + widget=TextArea(), + validators=[DataRequired()] + ) + + submit = SubmitField('Submit') + + +class SendMailForm(FlaskForm): + + recipient = SelectField( + 'Recipient: ', + coerce=str, + choices=[ + ("",""), + ("0","All Characters"), + ], + validators=[validators.DataRequired()] + ) + + subject = StringField( + 'Subject', + validators=[validators.DataRequired()] + ) + + body = StringField( + 'Body', + widget=TextArea(), + validators=[validators.DataRequired()] + ) + + attachment = SelectField( + "Attachment", + coerce=str, + choices=[(0,"No Attachment")] + ) + + attachment_count = IntegerField( + 'Attachment Count', + default=0 + ) + + submit = SubmitField('Submit') diff --git a/app/log.py b/app/log.py new file mode 100644 index 0000000..f5a5426 --- /dev/null +++ b/app/log.py @@ -0,0 +1,98 @@ +from flask import render_template, Blueprint, request, url_for +from flask_user import login_required, current_user +from app.models import CommandLog, ActivityLog, db, Account, CharacterInfo +from datatables import ColumnDT, DataTables +import time +from app import gm_level + +log_blueprint = Blueprint('log', __name__) + +@log_blueprint.route('/activities', methods=['GET']) +@login_required +@gm_level(8) +def activity(): + return render_template('logs/activity.html.j2') + + +@log_blueprint.route('/commands', methods=['GET']) +@login_required +@gm_level(8) +def command(): + return render_template('logs/command.html.j2') + + +@log_blueprint.route('/get_activities', methods=['GET']) +@login_required +@gm_level(8) +def get_activities(): + columns = [ + ColumnDT(ActivityLog.id), # 0 + ColumnDT(ActivityLog.character_id), # 1 + ColumnDT(ActivityLog.activity), # 2 + ColumnDT(ActivityLog.time), # 3 + ColumnDT(ActivityLog.map_id), # 4 + ] + + query = db.session.query().select_from(ActivityLog) + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for activity in data["data"]: + char_id = activity["1"] + activity["1"] = f""" + + View Character: {CharacterInfo.query.filter(CharacterInfo.id==char_id).first().name} + + + View Account: {Account.query.filter(Account.id==CharacterInfo.query.filter(CharacterInfo.id==char_id).first().account_id).first().username} + + """ + + if activity["2"] == 0: + activity["2"] = "Entered World" + elif activity["2"] == 1: + activity["2"] = "Left World" + + activity["3"] = time.ctime(activity["3"]) + + return data + + +@log_blueprint.route('/get_commands', methods=['GET']) +@login_required +@gm_level(8) +def get_commands(): + columns = [ + ColumnDT(CommandLog.id), # 0 + ColumnDT(CommandLog.character_id), # 1 + ColumnDT(CommandLog.command), # 2 + ] + + query = db.session.query().select_from(CommandLog) + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for command in data["data"]: + char_id = command["1"] + command["1"] = f""" + + View Character: {CharacterInfo.query.filter(CharacterInfo.id==command['1']).first().name} + + """ + command["1"] += f""" + + View Account: {Account.query.filter(Account.id==CharacterInfo.query.filter(CharacterInfo.id==char_id).first().account_id).first().username} + + """ + + return data diff --git a/app/luclient.py b/app/luclient.py new file mode 100644 index 0000000..bb07f8c --- /dev/null +++ b/app/luclient.py @@ -0,0 +1,346 @@ +from flask import ( + Blueprint, + send_file, + g, + redirect, + url_for +) +from flask_user import login_required +from app.models import CharacterInfo +import glob +import os +from wand import image +from wand.exceptions import BlobError as BE + +import sqlite3 +import xml.etree.ElementTree as ET + +luclient_blueprint = Blueprint('luclient', __name__) +locale = {} + +@luclient_blueprint.route('/get_dds_as_png/') +@login_required +def get_dds_as_png(filename): + if filename.split('.')[-1] != 'dds': + return (404, "NO") + + cache = f'cache/{filename.split(".")[0]}.png' + + if not os.path.exists("app/" + cache): + root = 'app/luclient/res/' + + path = glob.glob( + root + f'**/{filename}', + recursive=True + )[0] + + with image.Image(filename=path) as img: + img.compression = "no" + img.save(filename='app/cache/'+filename.split('.')[0] + '.png') + + return send_file(cache) + + +@luclient_blueprint.route('/get_dds/') +@login_required +def get_dds(filename): + if filename.split('.')[-1] != 'dds': + return 404 + + root = 'app/luclient/res/' + + dds = glob.glob( + root + f'**/{filename}', + recursive=True + )[0] + + return send_file(dds) + + +@luclient_blueprint.route('/get_icon_lot/') +@login_required +def get_icon_lot(id): + + render_component_id = query_cdclient( + 'select component_id from ComponentsRegistry where component_type = 2 and id = ?', + [id], + one=True + )[0] + + # find the asset from rendercomponent given the component id + filename = query_cdclient('select icon_asset from RenderComponent where id = ?', + [render_component_id], + one=True + )[0] + + filename = filename.replace("..\\", "").replace("\\", "/") + + cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png' + + if not os.path.exists("app/" + cache): + root = 'app/luclient/res/' + try: + + with image.Image(filename=f'{root}{filename}'.lower()) as img: + img.compression = "no" + img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png') + except BE: + return redirect(url_for('luclient.unknown')) + + return send_file(cache) + + +@luclient_blueprint.route('/get_icon_iconid/') +@login_required +def get_icon_iconid(id): + + filename = query_cdclient( + 'select IconPath from Icons where IconID = ?', + [id], + one=True + )[0] + + filename = filename.replace("..\\", "").replace("\\", "/") + + cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png' + + if not os.path.exists("app/" + cache): + root = 'app/luclient/res/' + try: + + with image.Image(filename=f'{root}{filename}'.lower()) as img: + img.compression = "no" + img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png') + except BE: + return redirect(url_for('luclient.unknown')) + + return send_file(cache) + +@luclient_blueprint.route('/unknown') +@login_required +def unknown(): + filename = "textures/ui/inventory/unknown.dds" + + cache = f'cache/{filename.split("/")[-1].split(".")[0]}.png' + + if not os.path.exists("app/" + cache): + root = 'app/luclient/res/' + + with image.Image(filename=f'{root}{filename}'.lower()) as img: + img.compression = "no" + img.save(filename=f'app/cache/{filename.split("/")[-1].split(".")[0]}.png') + + + return send_file(cache) + + +def get_cdclient(): + """Connect to CDClient from file system Relative Path + + Args: + None + """ + cdclient = getattr(g, '_cdclient', None) + if cdclient is None: + cdclient = g._database = sqlite3.connect('app/luclient/res/cdclient.sqlite') + return cdclient + + +def query_cdclient(query, args=(), one=False): + """Run sql queries on CDClient + + Args: + query (string) : SQL query + args (list) : List of args to place in query + one (bool) : Return only on result or all results + """ + cur = get_cdclient().execute(query, args) + rv = cur.fetchall() + cur.close() + return (rv[0] if rv else None) if one else rv + + +def translate_from_locale(trans_string): + """Finds the string translation from locale.xml + + Args: + trans_string (string) : ID to find translation + """ + if not trans_string: + return "INVALID STRING" + + global locale + + locale_data = "" + + if not locale: + locale_path = "app/luclient/locale/locale.xml" + + with open(locale_path, 'r') as file: + locale_data = file.read() + locale_xml = ET.XML(locale_data) + for item in locale_xml.findall('.//phrase'): + translation = "" + for translation_item in item.findall('.//translation'): + if translation_item.attrib["locale"] == "en_US": + translation = translation_item.text + + locale[item.attrib['id']] = translation + + if trans_string in locale: + return locale[trans_string] + else: + return trans_string + +def register_luclient_jinja_helpers(app): + + @app.template_filter('get_zone_name') + def get_zone_name(zone_id): + return translate_from_locale(f'ZoneTable_{zone_id}_DisplayDescription') + + @app.template_filter('get_skill_desc') + def get_skill_desc(skill_id): + return translate_from_locale(f'SkillBehavior_{skill_id}_descriptionUI').replace( + "%(DamageCombo)", "Damage Combo: " + ).replace( + "%(AltCombo)", "
Skeleton Combo: " + ).replace( + "%(Description)", "
" + ).replace( + "%(ChargeUp)", "
Charge-up: " + ) + + @app.template_filter('parse_lzid') + def parse_lzid(lzid): + return[ + (int(lzid) & ((1 << 16) - 1)), + ((int(lzid) >> 16) & ((1 << 16) - 1)), + ((int(lzid) >> 32) & ((1 << 30) - 1)) + ] + + @app.template_filter('parse_other_player_id') + def parse_other_player_id(other_player_id): + char_id = (int(other_player_id) & 0xFFFFFFFF) + character = CharacterInfo.query.filter(CharacterInfo.id == char_id).first() + if character: + return[character.id, character.name] + else: + return None + + @app.template_filter('get_lot_name') + def get_lot_name(lot_id): + name = translate_from_locale(f'Objects_{lot_id}_name') + if name == translate_from_locale(f'Objects_{lot_id}_name'): + intermed = query_cdclient( + 'select * from Objects where id = ?', + [lot_id], + one=True + ) + name = intermed[7] if (intermed[7] != "None" and intermed[7] !="" and intermed[7] != None) else intermed[1] + return name + + @app.template_filter('get_lot_rarity') + def get_lot_rarity(lot_id): + + render_component_id = query_cdclient( + 'select component_id from ComponentsRegistry where component_type = 2 and id = ?', + [lot_id], + one=True + )[0] + + rarity = query_cdclient('select rarity from ItemComponent where id = ?', + [render_component_id], + one=True + ) + if rarity: + rarity = rarity[0] + return rarity + + @app.template_filter('get_lot_desc') + def get_lot_desc(lot_id): + desc = translate_from_locale(f'Objects_{lot_id}_description') + if desc == f'Objects_{lot_id}_description': + desc = query_cdclient( + 'select description from Objects where id = ?', + [lot_id], + one=True + )[0] + if desc in ("", None): + desc = None + return desc + + @app.template_filter('get_item_set') + def check_if_in_set(lot_id): + item_set = query_cdclient( + 'select * from ItemSets where itemIDs like ? or itemIDs like ? or itemIDs like ?', + [f'{lot_id}%', f'%, {lot_id}%', f'%,{lot_id}%'], + one=True + ) + if item_set in ("", None): + return None + else: + return item_set + + @app.template_filter('get_lot_stats') + def get_lot_stats(lot_id): + stats = query_cdclient( + 'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\ + SELECT skillID FROM ObjectSkills WHERE objectTemplate=?\ + )', + [lot_id] + ) + + return consolidate_stats(stats) + + + @app.template_filter('get_set_stats') + def get_set_stats(lot_id): + stats = query_cdclient( + 'SELECT imBonusUI, lifeBonusUI, armorBonusUI, skillID, skillIcon FROM SkillBehavior WHERE skillID IN (\ + SELECT skillID FROM ItemSetSkills WHERE SkillSetID=?\ + )', + [lot_id] + ) + + return consolidate_stats(stats) + + @app.template_filter('query_cdclient') + def jinja_query_cdclient(query, items): + print(query, items) + return query_cdclient( + query, + items, + one=True + )[0] + + @app.template_filter('lu_translate') + def lu_translate(to_translate): + return translate_from_locale(to_translate) + + +def consolidate_stats(stats): + + if len(stats) > 1: + consolidated_stats = {"im": 0,"life": 0,"armor": 0, "skill": []} + for stat in stats: + if stat[0]: + consolidated_stats["im"] += stat[0] + if stat[1]: + consolidated_stats["life"] += stat[1] + if stat[2]: + consolidated_stats["armor"] += stat[2] + if stat[3]: + consolidated_stats["skill"].append([stat[3],stat[4]]) + + + stats = consolidated_stats + elif len(stats) == 1: + stats = { + "im": stats[0][0] if stats[0][0] else 0, + "life": stats[0][1] if stats[0][1] else 0, + "armor": stats[0][2] if stats[0][2] else 0, + "skill": [[stats[0][3], stats[0][4]]] if stats[0][3] else None, + } + else: + stats = None + return stats diff --git a/app/mail.py b/app/mail.py new file mode 100644 index 0000000..6f22ded --- /dev/null +++ b/app/mail.py @@ -0,0 +1,85 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request +from flask_user import login_required, current_user +from app.models import db, Mail, CharacterInfo +from datatables import ColumnDT, DataTables +from app.forms import SendMailForm +from app import gm_level +from app.luclient import translate_from_locale, query_cdclient +import time + +mail_blueprint = Blueprint('mail', __name__) + + +@mail_blueprint.route('/view/', methods=['GET']) +@login_required +def view(id): + mail = Mail.query.filter(Mail.id == id).first() + + return render_template('mail/view.html.j2', mail=mail) + + +@mail_blueprint.route('/send', methods=['GET', 'POST']) +@login_required +@gm_level(3) +def send(): + form = SendMailForm() + + if request.method == "POST": + # if form.validate_on_submit(): + if form.attachment.data != "0" and form.attachment_count.data == 0: + form.attachment_count.data = 1 + if form.recipient.data == "0": + for character in CharacterInfo.query.all(): + Mail( + sender_id = 0, + sender_name = f"[GM] {current_user.username}", + receiver_id = character.id, + receiver_name = character.name, + time_sent = time.time(), + subject = form.subject.data, + body = form.body.data, + attachment_id = 0, + attachment_lot = form.attachment.data, + attachment_count = form.attachment_count.data + ).save() + else: + Mail( + sender_id = 0, + sender_name = f"[GM] {current_user.username}", + receiver_id = form.recipient.data, + receiver_name = CharacterInfo.query.filter(CharacterInfo.id == form.recipient.data).first().name, + time_sent = time.time(), + subject = form.subject.data, + body = form.body.data, + attachment_id = 0, + attachment_lot = form.attachment.data, + attachment_count = form.attachment_count.data + ).save() + + flash("Sent Mail", "success") + return redirect(url_for('mail.send')) + + + recipients = CharacterInfo.query.all() + for character in recipients: + form.recipient.choices.append((character.id, character.name)) + + items = query_cdclient( + 'Select id, name, displayName from Objects where type = ?', + ["Loot"] + ) + + for item in items: + name = translate_from_locale(f'Objects_{item[0]}_name') + if name == f'Objects_{item[0]}_name': + name = (item[2] if (item[2] != "None" and item[2] !="" and item[2] != None) else item[1]) + form.attachment.choices.append( + ( + item[0], + f'({item[0]}) {name}' + ) + ) + + + return render_template('mail/send.html.j2', form=form) + diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000..0bdc5e1 --- /dev/null +++ b/app/main.py @@ -0,0 +1,42 @@ +from flask import render_template, Blueprint, redirect, request, send_from_directory, make_response, send_file +from flask_user import login_required, current_user +import json, glob, os +from wand import image + +from app.models import Account, AccountInvitation, CharacterInfo +from app.schemas import AccountSchema, CharacterInfoSchema +from app.luclient import query_cdclient + +main_blueprint = Blueprint('main', __name__) + +account_schema = AccountSchema() +char_info_schema = CharacterInfoSchema() + +@main_blueprint.route('/', methods=['GET']) +def index(): + """Home/Index Page""" + if current_user.is_authenticated: + + account_data = Account.query.filter(Account.id == current_user.id).first() + + return render_template( + 'main/index.html.j2', + account_data=account_data + ) + else: + return render_template('main/index.html.j2') + + +@main_blueprint.route('/about') +def about(): + """About Page""" + return render_template('main/about.html.j2') + + +@main_blueprint.route('/favicon.ico') +def favicon(): + return send_from_directory( + 'static/logo/', + 'favicon.ico', + mimetype='image/vnd.microsoft.icon' + ) diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000..6dc6af2 --- /dev/null +++ b/app/models.py @@ -0,0 +1,1010 @@ +from flask_sqlalchemy import SQLAlchemy +from flask_migrate import Migrate +from flask_user import UserMixin +from wtforms import ValidationError + +import logging +from flask_sqlalchemy import BaseQuery +from sqlalchemy.dialects import mysql +from sqlalchemy.exc import OperationalError, StatementError +from time import sleep +import random +import string + +# retrying query to work around python trash collector +# killing connections of other gunicorn workers +class RetryingQuery(BaseQuery): + __retry_count__ = 3 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def __iter__(self): + attempts = 0 + while True: + attempts += 1 + try: + return super().__iter__() + except OperationalError as ex: + if "server closed the connection unexpectedly" not in str(ex): + raise + if attempts < self.__retry_count__: + sleep_for = 2 ** (attempts - 1) + logging.error( + "Database connection error: {} - sleeping for {}s" + " and will retry (attempt #{} of {})".format( + ex, sleep_for, attempts, self.__retry_count__ + ) + ) + sleep(sleep_for) + continue + else: + raise + except StatementError as ex: + if "reconnect until invalid transaction is rolled back" not in str(ex): + raise + self.session.rollback() + +db = SQLAlchemy(query_class=RetryingQuery) +migrate = Migrate() + +class PlayKey(db.Model): + __tablename__ = 'play_keys' + id = db.Column(db.Integer, primary_key=True) + + key_string = db.Column( + mysql.CHAR(19), + nullable=False, + unique=True + ) + + key_uses = db.Column( + mysql.INTEGER, + nullable=False, + server_default='1' + ) + created_at = db.Column( + mysql.TIMESTAMP, + nullable=False, + server_default=db.func.now() + ) + active = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='1' + ) + + notes = db.Column( + mysql.TEXT, + nullable=True, + ) + + times_used = db.Column( + mysql.INTEGER, + nullable=False, + server_default='0' + ) + + @staticmethod + def key_is_valid(*, key_string=None): + key = PlayKey.query.filter(PlayKey.key_string == key_string).first() + if not (key and key.active and key.key_uses > 0): + raise ValidationError( + 'Not a valid Play Key' + ) + else: + return key.id + + @staticmethod + def create(*, count=1, uses=1): + for i in range(count): + key = "" + for j in range(4): + key += ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(4)) + '-' + # Remove last dash + key = key[:-1] + + new_key = PlayKey( + key_string=key, + key_uses=uses + ) + db.session.add(new_key) + db.session.commit() + + def delete(self): + db.session.delete(self) + db.session.commit() + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + +class Account(db.Model, UserMixin): + __tablename__ = 'accounts' + id = db.Column( + db.Integer(), + primary_key=True + ) + + username = db.Column( + 'name', + db.VARCHAR(35), + nullable=False, + unique=True + ) + + email = db.Column( + db.Unicode(255), + nullable=True, + server_default='', + unique=False + ) + + email_confirmed_at = db.Column(db.DateTime()) + + password = db.Column( + db.Text(), + nullable=False, + server_default='' + ) + + gm_level = db.Column( + mysql.INTEGER(unsigned=True), + nullable=False, + server_default='0' + ) + + locked = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + active = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='1' + ) + + banned = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + play_key_id = db.Column( + mysql.INTEGER, + db.ForeignKey(PlayKey.id, ondelete='CASCADE'), + nullable=True + ) + + play_key = db.relationship( + 'PlayKey', + backref="accounts", + passive_deletes=True + ) + + created_at = db.Column( + mysql.TIMESTAMP, + nullable=False, + server_default=db.func.now() + ) + + mute_expire = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + server_default='0' + ) + + @staticmethod + def get_user_by_id(*, user_id=None): + return User.query.filter(user_id == User.id).first() + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class AccountInvitation(db.Model): + __tablename__ = 'account_invites' + id = db.Column(db.Integer, primary_key=True) + email = db.Column(db.String(255), nullable=False) + + # save the user of the invitee + invited_by_user_id = db.Column( + db.Integer, + db.ForeignKey(Account.id, ondelete='CASCADE') + ) + + invited_by_account = db.relationship( + 'Account', + backref="account_invites", + passive_deletes=True + ) + + # token used for registration page to + # identify user registering + token = db.Column( + db.String(100), + nullable=False, + server_default='' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + + + @staticmethod + def get_user_by_id(*, user_id=None): + return User.query.filter(user_id == User.id).first() + + def delete(self): + db.session.delete(self) + db.session.commit() + +# This table is cursed, see prop_clone_id +class CharacterInfo(db.Model): + __tablename__ = 'charinfo' + id = db.Column( + mysql.BIGINT, + primary_key=True, + autoincrement=False + ) + + account_id = db.Column( + db.Integer(), + db.ForeignKey(Account.id, ondelete='CASCADE'), + nullable=False + ) + + account = db.relationship( + 'Account', + backref="charinfo", + passive_deletes=True + ) + + name = db.Column( + mysql.VARCHAR(35), + nullable=False, + ) + + pending_name = db.Column( + mysql.VARCHAR(35), + nullable=False, + ) + + needs_rename = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + # Cursed column + # So what this has to be in an autoincrementing entry for a foreign key + # and so to achieve that with sqlalchemy, we have to make it a primary key + # if you look at the initil migration, it the drops this as a primary key, + # cause it's not supposed to be a primary key + # but why does it have to be a primary key? + # sqlalchemy ignores the autoincrement variable for non-primary keys + # thanks for reading this + prop_clone_id = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + primary_key=True, + autoincrement=True, + unique=True, + ) + + last_login = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + server_default='0' + ) + + permission_map = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + server_default='0' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class CharacterXML(db.Model): + __tablename__ = 'charxml' + id = db.Column( + mysql.BIGINT, + primary_key=True, + ) + + xml_data = db.Column( + db.Text(4294000000), + nullable=False + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class CommandLog(db.Model): + __tablename__ = 'command_log' + id = db.Column(db.Integer, primary_key=True) + + character_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + nullable=False + ) + + character = db.relationship( + 'CharacterInfo', + backref="command_log", + passive_deletes=True + ) + + command = db.Column( + mysql.VARCHAR(256), + nullable=False + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class Friends(db.Model): + __tablename__ = 'friends' + player_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + primary_key=True, + nullable=False + ) + + player = db.relationship( + 'CharacterInfo', + foreign_keys=[player_id], + backref="player", + passive_deletes=True + ) + + friend_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + primary_key=True, + nullable=False + ) + + friend = db.relationship( + 'CharacterInfo', + foreign_keys=[friend_id], + backref="friend", + passive_deletes=True + ) + + best_friend = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class Leaderboard(db.Model): + __tablename__ = 'leaderboard' + id = db.Column(db.Integer, primary_key=True) + + game_id = db.Column( + mysql.INTEGER(unsigned=True), + nullable=False, + server_default='0' + ) + + last_played = db.Column( + mysql.TIMESTAMP, + nullable=False, + server_default=db.func.now() + ) + + character_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + nullable=False + ) + + character = db.relationship( + 'CharacterInfo', + backref="leaderboards", + passive_deletes=True + ) + + time = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + server_default='0' + ) + + score = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + server_default='0' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class Mail(db.Model): + __tablename__ = 'mail' + id = db.Column( + mysql.INTEGER, + primary_key=True + ) + + sender_id = db.Column( + mysql.INTEGER, + nullable=False + ) + + sender_name = db.Column( + mysql.VARCHAR(35), + nullable=False + ) + + receiver_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + nullable=False + ) + + receiver = db.relationship( + 'CharacterInfo', + backref="mail", + passive_deletes=True + ) + + receiver_name = db.Column( + mysql.VARCHAR(35), + nullable=False + ) + + time_sent = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False + ) + + subject = db.Column( + mysql.TEXT, + nullable=False + ) + + body = db.Column( + mysql.TEXT, + nullable=False + ) + + attachment_id = db.Column( + mysql.BIGINT, + nullable=False, + server_default='0' + ) + + attachment_lot = db.Column( + mysql.INTEGER, + nullable=False, + server_default='0' + ) + + attachment_subkey = db.Column( + mysql.BIGINT, + nullable=False, + server_default='0' + ) + + attachment_count = db.Column( + mysql.INTEGER(), + nullable=False, + server_default='0' + ) + + was_read = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class ObjectIDTracker(db.Model): + __tablename__ = 'object_id_tracker' + last_object_id = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + primary_key=True, + server_default='0' + ) + +class PetNames(db.Model): + __tablename__ = 'pet_names' + id = db.Column(mysql.BIGINT, primary_key=True) + pet_name = db.Column( + mysql.TEXT, + nullable=False + ) + approved = db.Column( + mysql.INTEGER(unsigned=True), + nullable=False, + server_default='0' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + + +class Property(db.Model): + __tablename__ = 'properties' + id = db.Column( + mysql.BIGINT, + primary_key=True, + autoincrement=False + ) + + owner_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + nullable=False + ) + + owner = db.relationship( + 'CharacterInfo', + foreign_keys=[owner_id], + backref="properties_owner", + passive_deletes=True + ) + + template_id = db.Column( + mysql.INTEGER(unsigned=True), + nullable=False, + ) + + clone_id = db.Column( + mysql.BIGINT(unsigned=True), + db.ForeignKey(CharacterInfo.prop_clone_id, ondelete='CASCADE'), + ) + + clone = db.relationship( + 'CharacterInfo', + foreign_keys=[clone_id], + backref="properties_clone", + passive_deletes=True + ) + + name = db.Column( + mysql.TEXT, + nullable=False + ) + + description = db.Column( + mysql.TEXT, + nullable=False + ) + + rent_amount = db.Column( + mysql.INTEGER, + nullable=False, + ) + + rent_due = db.Column( + mysql.BIGINT, + nullable=False, + ) + + privacy_option = db.Column( + mysql.INTEGER, + nullable=False, + ) + + mod_approved = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + last_updated = db.Column( + mysql.BIGINT, + nullable=False, + ) + + time_claimed = db.Column( + mysql.BIGINT, + nullable=False, + ) + + rejection_reason = db.Column( + mysql.TEXT, + nullable=False + ) + + reputation = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + ) + + zone_id = db.Column( + mysql.INTEGER, + nullable=False, + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + + +class UGC(db.Model): + __tablename__ = 'ugc' + id = db.Column( + mysql.INTEGER, + primary_key=True + ) + account_id = db.Column( + db.Integer(), + db.ForeignKey(Account.id, ondelete='CASCADE'), + nullable=False + ) + + account = db.relationship( + 'Account', + backref="ugc", + passive_deletes=True + ) + + character_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + nullable=False + ) + + character = db.relationship( + 'CharacterInfo', + backref="ugc", + passive_deletes=True + ) + + is_optimized = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + lxfml = db.Column( + mysql.MEDIUMBLOB(), + nullable=False + ) + + bake_ao = db.Column( + mysql.BOOLEAN, + nullable=False, + server_default='0' + ) + + filename = db.Column( + mysql.TEXT, + nullable=False, + server_default='' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class PropertyContent(db.Model): + __tablename__ = 'properties_contents' + id = db.Column( + mysql.BIGINT, + primary_key=True, + autoincrement=False + ) + property_id = db.Column( + db.BIGINT, + db.ForeignKey(Property.id, ondelete='CASCADE'), + nullable=False + ) + + property_data = db.relationship( + 'Property', + backref="properties_contents", + passive_deletes=True + ) + + ugc_id = db.Column( + db.INT, + db.ForeignKey(UGC.id, ondelete='CASCADE'), + nullable=True + ) + + ugc = db.relationship( + 'UGC', + backref="properties_contents", + passive_deletes=True + ) + + lot = db.Column( + mysql.INTEGER, + nullable=False, + ) + + x = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + y = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + z = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + rx = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + ry = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + rz = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + rw = db.Column( + mysql.FLOAT(), + nullable=False, + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class ActivityLog(db.Model): + __tablename__ = 'activity_log' + id = db.Column(mysql.INTEGER, primary_key=True) + + character_id = db.Column( + mysql.BIGINT, + db.ForeignKey(CharacterInfo.id, ondelete='CASCADE'), + nullable=False + ) + + character = db.relationship( + 'CharacterInfo', + backref="avtivity_log", + passive_deletes=True + ) + + activity = db.Column( + mysql.INTEGER, + nullable=False, + ) + + time = db.Column( + mysql.BIGINT(unsigned=True), + nullable=False, + ) + + map_id = db.Column( + mysql.INTEGER, + nullable=False, + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class BugReport(db.Model): + __tablename__ = 'bug_reports' + id = db.Column(mysql.INTEGER, primary_key=True) + + body = db.Column( + mysql.TEXT, + nullable=False + ) + + client_version = db.Column( + mysql.TEXT, + nullable=False + ) + + other_player_id = db.Column( + mysql.TEXT, + nullable=False + ) + + selection = db.Column( + mysql.TEXT, + nullable=False + ) + + submitted = db.Column( + mysql.TIMESTAMP, + nullable=False, + server_default=db.func.now() + ) + + resolved_time = db.Column( + mysql.TIMESTAMP, + nullable=True, + ) + + resoleved_by_id = db.Column( + db.Integer(), + db.ForeignKey(Account.id, ondelete='CASCADE'), + nullable=True + ) + + resoleved_by = db.relationship( + 'Account', + backref="bugreports", + passive_deletes=True + ) + + resolution = db.Column( + mysql.TEXT, + nullable=True + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + +class Server(db.Model): + __tablename__ = 'servers' + id = db.Column( + mysql.INTEGER, + primary_key=True + ) + name = db.Column( + mysql.TEXT, + nullable=False + ) + + ip = db.Column( + mysql.TEXT, + nullable=False + ) + + port = db.Column( + mysql.INTEGER, + nullable=False + ) + + state = db.Column( + mysql.INTEGER, + nullable=False + ) + + version = db.Column( + mysql.INTEGER, + nullable=False, + server_default='0' + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + + +class ItemReports(db.Model): + __tablename__ = 'item_reports' + + item = db.Column( + db.Integer(), + primary_key=True, + nullable=False + ) + + count = db.Column( + db.Integer(), + nullable=False + ) + + date = db.Column( + db.Date(), + primary_key=False, + ) + + def save(self): + db.session.add(self) + db.session.commit() + db.session.refresh(self) + + def delete(self): + db.session.delete(self) + db.session.commit() + diff --git a/app/moderation.py b/app/moderation.py new file mode 100644 index 0000000..a20eaf3 --- /dev/null +++ b/app/moderation.py @@ -0,0 +1,108 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, flash +from flask_user import login_required +from app.models import PetNames, db +from datatables import ColumnDT, DataTables +from app.forms import CreatePlayKeyForm, EditPlayKeyForm +from app import gm_level + +moderation_blueprint = Blueprint('moderation', __name__) + + +@moderation_blueprint.route('/', methods=['GET']) +@login_required +@gm_level(3) +def index(status): + return render_template('moderation/index.html.j2', status=status) + + +@moderation_blueprint.route('/approve_pet/', methods=['GET']) +@login_required +@gm_level(3) +def approve_pet(id): + + pet_data = PetNames.query.filter(PetNames.id == id).first() + + pet_data.approved = 2 + flash(f"Approved pet name {pet_data.pet_name}", "success") + pet_data.save() + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@moderation_blueprint.route('/reject_pet/', methods=['GET']) +@login_required +@gm_level(3) +def reject_pet(id): + + pet_data = PetNames.query.filter(PetNames.id == id).first() + + pet_data.approved = 0 + flash(f"Rejected pet name {pet_data.pet_name}", "danger") + pet_data.save() + return redirect(request.referrer if request.referrer else url_for("main.index")) + + +@moderation_blueprint.route('/get_pets/', methods=['GET']) +@login_required +@gm_level(3) +def get_pets(status="all"): + columns = [ + ColumnDT(PetNames.id), + ColumnDT(PetNames.pet_name), + ColumnDT(PetNames.approved), + ] + + query = None + if status=="all": + query = db.session.query().select_from(PetNames) + elif status=="approved": + query = db.session.query().select_from(PetNames).filter(PetNames.approved==2) + elif status=="unapproved": + query = db.session.query().select_from(PetNames).filter(PetNames.approved==1) + else: + raise Exception("Not a valid filter") + + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for pet_data in data["data"]: + id = pet_data["0"] + status = pet_data["2"] + if status == 1: + pet_data["0"] = f""" + + """ + pet_data["2"] = "Awaiting Moderation" + elif status == 2: + pet_data["0"] = f""" + + Reject + + """ + pet_data["2"] = "Approved" + elif status == 0: + pet_data["0"] = f""" + + Approve + + """ + pet_data["2"] = "Rejected" + + return data diff --git a/app/play_keys.py b/app/play_keys.py new file mode 100644 index 0000000..f1cab6a --- /dev/null +++ b/app/play_keys.py @@ -0,0 +1,150 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, flash +from flask_user import login_required, current_user +from app.models import Account, AccountInvitation, PlayKey, db +from datatables import ColumnDT, DataTables +from app.forms import CreatePlayKeyForm, EditPlayKeyForm +from app import gm_level + +play_keys_blueprint = Blueprint('play_keys', __name__) + +# Key creation page + +@play_keys_blueprint.route('/', methods=['GET']) +@login_required +@gm_level(9) +def index(): + return render_template('play_keys/index.html.j2') + + +@play_keys_blueprint.route('/create//', methods=['GET'], defaults={'count': 1, 'uses': 1}) +@login_required +@gm_level(9) +def create(count=1, uses=1): + PlayKey.create(count=count, uses=uses) + flash(f"Created {count} Play Key(s) with {uses} uses!", "success") + return redirect(url_for('play_keys.index')) + + +@play_keys_blueprint.route('/create/bulk', methods=('GET', 'POST')) +@login_required +@gm_level(9) +def bulk_create(): + form = CreatePlayKeyForm() + if form.validate_on_submit(): + PlayKey.create(count=form.count.data, uses=form.uses.data) + return redirect(url_for('play_keys.index')) + + return render_template('play_keys/bulk.html.j2', form=form) + + +@play_keys_blueprint.route('/delete/', methods=('GET', 'POST')) +@login_required +@gm_level(9) +def delete(id): + key = PlayKey.query.filter(PlayKey.id == id).first() + associated_accounts = Account.query.filter(Account.play_key_id==id).all() + flash(f"Deleted Play Key {key.key_string}", "danger") + key.delete() + return redirect(url_for('play_keys.index')) + + +@play_keys_blueprint.route('/edit/', methods=('GET', 'POST')) +@login_required +@gm_level(9) +def edit(id): + key = PlayKey.query.filter(PlayKey.id==id).first() + form = EditPlayKeyForm() + + if form.validate_on_submit(): + key.key_uses = form.uses.data + key.active = form.active.data + key.notes = form.notes.data + key.save() + return redirect(url_for('play_keys.index')) + + form.uses.data = key.key_uses + form.active.data = key.active + form.notes.data = key.notes + + return render_template('play_keys/edit.html.j2', form=form, key=key) + + +@play_keys_blueprint.route('/view/', methods=('GET', 'POST')) +@login_required +@gm_level(9) +def view(id): + key = PlayKey.query.filter(PlayKey.id == id).first() + accounts = Account.query.filter(Account.play_key_id==id).all() + return render_template('play_keys/view.html.j2', key=key, accounts=accounts) + + +@play_keys_blueprint.route('/get', methods=['GET']) +@login_required +@gm_level(9) +def get(): + columns = [ + ColumnDT(PlayKey.id), + ColumnDT(PlayKey.key_string), + ColumnDT(PlayKey.key_uses), + ColumnDT(PlayKey.times_used), + ColumnDT(PlayKey.created_at), + ColumnDT(PlayKey.active), + ] + + query = db.session.query().select_from(PlayKey) + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for play_key in data["data"]: + # Hackily shove buttons into response + play_key["0"] = f""" + + View + + + + Edit + + + + Delete + + + + """ + + if play_key["5"]: + play_key["5"] = '''

''' + else: + play_key["5"] = '''

''' + + return data diff --git a/app/properties.py b/app/properties.py new file mode 100644 index 0000000..d2e86d2 --- /dev/null +++ b/app/properties.py @@ -0,0 +1,387 @@ +from flask import ( + render_template, + Blueprint, + redirect, + url_for, + request, + abort, + jsonify, + send_from_directory, + make_response, + flash +) +from flask_user import login_required, current_user +import json +from datatables import ColumnDT, DataTables +import time +from app.models import Property, db, UGC, CharacterInfo, PropertyContent, Account +from app.schemas import PropertySchema +from app import gm_level +from app.luclient import query_cdclient + +import zlib +import xmltodict +import os +import app.pylddlib as ldd + +property_blueprint = Blueprint('properties', __name__) + +property_schema = PropertySchema() + +@property_blueprint.route('/', methods=['GET']) +@login_required +@gm_level(3) +def index(): + return render_template('properties/index.html.j2') + + +@property_blueprint.route('/approve/', methods=['GET']) +@login_required +@gm_level(3) +def approve(id): + + property_data = Property.query.filter(Property.id == id).first() + + property_data.mod_approved = not property_data.mod_approved + + # If we approved it, clear the rejection reason + if property_data.mod_approved: + property_data.rejection_reason = "" + + if property_data.mod_approved: + flash( + f"""Approved Property + {property_data.name if property_data.name else query_cdclient( + 'select DisplayDescription from ZoneTable where zoneID = ?', + [property_data.zone_id], + one=True + )[0]} + from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""", + "success" + ) + else: + flash( + f"""Unapproved Property + {property_data.name if property_data.name else query_cdclient( + 'select DisplayDescription from ZoneTable where zoneID = ?', + [property_data.zone_id], + one=True + )[0]} + from {CharacterInfo.query.filter(CharacterInfo.id==property_data.owner_id).first().name}""", + "danger" + ) + + property_data.save() + + go_to = "" + + if request.referrer: + if "view_models" in request.referrer: + go_to = url_for('properties.view', id=id) + else: + go_to = request.referrer + else: + go_to = url_for('main.index') + + + + return redirect(go_to) + + +@property_blueprint.route('/view/', methods=['GET']) +@login_required +def view(id): + + property_data = Property.query.filter(Property.id == id).first() + + if current_user.gm_level < 3: + if property_data.owner_id and property_data.owner.account_id != current_user.id: + abort(403) + return + + if property_data == {}: + abort(404) + return + + return render_template('properties/view.html.j2', property_data=property_data) + + +@property_blueprint.route('/get/', methods=['GET']) +@login_required +@gm_level(3) +def get(status="all"): + columns = [ + ColumnDT(Property.id), # 0 + ColumnDT(CharacterInfo.name), # 1 + ColumnDT(Property.template_id), # 2 + ColumnDT(Property.clone_id), # 3 + ColumnDT(Property.name), # 4 + ColumnDT(Property.description), # 5 + ColumnDT(Property.privacy_option), # 6 + ColumnDT(Property.mod_approved), # 7 + ColumnDT(Property.last_updated), # 8 + ColumnDT(Property.time_claimed), # 9 + ColumnDT(Property.rejection_reason), # 10 + ColumnDT(Property.reputation), # 11 + ColumnDT(Property.zone_id), # 12 + ColumnDT(Account.username) # 13 + ] + + query = None + if status=="all": + query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account) + elif status=="approved": + query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==True) + elif status=="unapproved": + query = db.session.query().select_from(Property).join(CharacterInfo, CharacterInfo.id==Property.owner_id).join(Account).filter(Property.mod_approved==False) + else: + raise Exception("Not a valid filter") + + + params = request.args.to_dict() + + rowTable = DataTables(params, query, columns) + + data = rowTable.output_result() + for property_data in data["data"]: + id = property_data["0"] + + property_data["0"] = f""" + + View + + """ + + if not property_data["7"]: + property_data["0"] += f""" + + Approve + + """ + else: + property_data["0"] += f""" + + Unapprove + + """ + + property_data["1"] = f""" + + {property_data["1"]} + + """ + + if property_data["4"] == "": + property_data["4"] = query_cdclient( + 'select DisplayDescription from ZoneTable where zoneID = ?', + [property_data["12"]], + one=True + ) + + if property_data["6"] == 0: + property_data["6"] = "Private" + elif property_data["6"] == 1: + property_data["6"] = "Best Friends" + else: + property_data["6"] = "Public" + + property_data["8"] = time.ctime(property_data["8"]) + property_data["9"] = time.ctime(property_data["9"]) + + if not property_data["7"]: + property_data["7"] = '''

''' + else: + property_data["7"] = '''

''' + + property_data["12"] = query_cdclient( + 'select DisplayDescription from ZoneTable where zoneID = ?', + [property_data["12"]], + one=True + ) + + return data + + +@property_blueprint.route('/view_model/', methods=['GET']) +@login_required +def view_model(id): + property_content_data = PropertyContent.query.filter(PropertyContent.id==id).all() + + # TODO: Restrict somehow + formatted_data = [ + { + "obj": url_for('properties.get_model', id=property_content_data[0].id, file_format='obj'), + "mtl": url_for('properties.get_model', id=property_content_data[0].id, file_format='mtl'), + "lot": property_content_data[0].lot, + "id": property_content_data[0].id, + "pos": [{ + "x": property_content_data[0].x, + "y": property_content_data[0].y, + "z": property_content_data[0].z, + "rx": property_content_data[0].rx, + "ry": property_content_data[0].ry, + "rz": property_content_data[0].rz, + "rw": property_content_data[0].rw + }] + } + ] + + return render_template( + 'ldd/ldd.html.j2', + content=formatted_data + ) + +property_center = { + 1150: "(-17, 432, -60)", + 1151: "(0, 455, -110)", + 1250: "(-16, 432,-60)", + 1251: "(0, 455, 100)", + 1350: "(-10, 432, -57)", + 1450: "(-10, 432, -77)" +} + + +@property_blueprint.route('/view_models/', methods=['GET']) +@login_required +def view_models(id): + property_content_data = PropertyContent.query.filter( + PropertyContent.property_id==id + ).order_by(PropertyContent.lot).all() + + consolidated_list = [] + + for item in range(len(property_content_data)): + if any((d["lot"] != 14 and d["lot"] == property_content_data[item].lot) for d in consolidated_list): + # exiting lot, add rotations + lot_index = next((index for (index, d) in enumerate(consolidated_list) if d["lot"] == property_content_data[item].lot), None) + consolidated_list[lot_index]["pos"].append( + { + "x": property_content_data[item].x, + "y": property_content_data[item].y, + "z": property_content_data[item].z, + "rx": property_content_data[item].rx, + "ry": property_content_data[item].ry, + "rz": property_content_data[item].rz, + "rw": property_content_data[item].rw + } + ) + else: + # add new lot + consolidated_list.append( + { + "obj": url_for('properties.get_model', id=property_content_data[item].id, file_format='obj'), + "mtl": url_for('properties.get_model', id=property_content_data[item].id, file_format='mtl'), + "lot": property_content_data[item].lot, + "id": property_content_data[item].id, + "pos": [{ + "x": property_content_data[item].x, + "y": property_content_data[item].y, + "z": property_content_data[item].z, + "rx": property_content_data[item].rx, + "ry": property_content_data[item].ry, + "rz": property_content_data[item].rz, + "rw": property_content_data[item].rw + }] + } + ) + property_data = Property.query.filter(Property.id==id).first() + return render_template( + 'ldd/ldd.html.j2', + property_data=property_data, + content=consolidated_list, + center=property_center[property_data.zone_id] + ) + +@property_blueprint.route('/get_model//', methods=['GET']) +@login_required +def get_model(id, file_format): + content = PropertyContent.query.filter(PropertyContent.id==id).first() + + if content.lot == 14: # ugc model + response = ugc(content)[0] + else: # prebuild model + response = prebuilt(content, file_format)[0] + + response.headers.set('Content-Type', 'text/xml') + return response + + + +@property_blueprint.route('/download_model/', methods=['GET']) +@login_required +def download_model(id): + content = PropertyContent.query.filter(PropertyContent.id==id).first() + + if content.lot == 14: # ugc model + response, filename = ugc(content) + else: # prebuild model + response, filename = prebuilt(content, "lxfml") + + response.headers.set('Content-Type', 'attachment/xml') + response.headers.set( + 'Content-Disposition', + 'attachment', + filename=filename + ) + return response + + +def ugc(content): + ugc_data = UGC.query.filter(UGC.id==content.ugc_id).first() + uncompressed_lxfml = zlib.decompress(ugc_data.lxfml) + response = make_response(uncompressed_lxfml) + return response, ugc_data.filename + + +def prebuilt(content, file_format): + # translate LOT to component id + # we need to get a type of 2 because reasons + render_component_id = query_cdclient( + 'select component_id from ComponentsRegistry where component_type = 2 and id = ?', + [content.lot], + one=True + )[0] + # find the asset from rendercomponent given the component id + filename = query_cdclient('select render_asset from RenderComponent where id = ?', + [render_component_id], + one=True + ) + + if filename: + filename = filename[0].split("\\\\")[-1].lower().split(".")[0] + else: + return f"No filename for LOT {content.lot}" + + if file_format == "lxfml": + lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml' + with open(lxfml, 'r') as file: + lxfml_data = file.read() + # print(lxfml_data) + response = make_response(lxfml_data) + + elif file_format in ["obj", "mtl"]: + + cache = f"app/cache/{filename}.{file_format}" + + if os.path.exists(cache): + with open(cache, 'r') as file: + cache_data = file.read() + response = make_response(cache_data) + + else: + lxfml = f'app/luclient/res/BrickModels/{filename.split(".")[0]}.lxfml' + ldd.main(lxfml, cache.split('.')[0]) # convert to OBJ + + if os.path.exists(cache): + with open(cache, 'r') as file: + cache_data = file.read() + response = make_response(cache_data) + + else: + raise(Exception("INVALID FILE FORMAT")) + + return response, f"{filename}.{file_format}" diff --git a/app/pylddlib.py b/app/pylddlib.py new file mode 100644 index 0000000..1054cb1 --- /dev/null +++ b/app/pylddlib.py @@ -0,0 +1,902 @@ +#!/usr/bin/env python +# pylddlib version 0.4.9.7 +# based on pyldd2obj version 0.4.8 - Copyright (c) 2019 by jonnysp +# +# Updates: +# 0.4.9.8 Make work with LEGO Universe brickdb +# 0.4.9.7 corrected bug of incorrectly parsing the primitive xml file, specifically with comments. Add support LDDLIFTREE envirnment variable to set location of db.lif. +# 0.4.9.6 preliminary Linux support +# 0.4.9.5 corrected bug of incorrectly Bounding / GeometryBounding parsing the primitive xml file. +# 0.4.9.4 improved lif.db checking for crucial files (because of the infamous botched 4.3.12 LDD Windows update). +# 0.4.9.3 improved Windows and Python 3 compatibility +# 0.4.9.2 changed handling of material = 0 for a part. Now a 0 will choose the 1st material (the base material of a part) and not the previous material of the subpart before. This will fix "Chicken Helmet Part 11262". It may break other parts and this change needs further regression. +# 0.4.9.1 improved custom2DField handling, fixed decorations bug, improved material assignments handling +# 0.4.9 updates to support reading extracted db.lif from db folder +# +# License: MIT License +# + +import os +import platform +import sys +import math +import struct +import zipfile +from xml.dom import minidom +import time + +if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf-8') + +PRIMITIVEPATH = '/Primitives/' +GEOMETRIEPATH = PRIMITIVEPATH + 'LOD0/' +DECORATIONPATH = '/Decorations/' +MATERIALNAMESPATH = '/MaterialNames/' + +LOGOONSTUDSCONNTYPE = {"0:4", "0:4:1", "0:4:2", "0:4:33", "2:4:1", "2:4:34"} + +class Matrix3D: + def __init__(self, n11=1,n12=0,n13=0,n14=0,n21=0,n22=1,n23=0,n24=0,n31=0,n32=0,n33=1,n34=0,n41=0,n42=0,n43=0,n44=1): + self.n11 = n11 + self.n12 = n12 + self.n13 = n13 + self.n14 = n14 + self.n21 = n21 + self.n22 = n22 + self.n23 = n23 + self.n24 = n24 + self.n31 = n31 + self.n32 = n32 + self.n33 = n33 + self.n34 = n34 + self.n41 = n41 + self.n42 = n42 + self.n43 = n43 + self.n44 = n44 + + def __str__(self): + return '[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15}]'.format(self.n11, self.n12, self.n13,self.n14,self.n21, self.n22, self.n23,self.n24,self.n31, self.n32, self.n33,self.n34,self.n41, self.n42, self.n43,self.n44) + + def rotate(self,angle=0,axis=0): + c = math.cos(angle) + s = math.sin(angle) + t = 1 - c + + tx = t * axis.x + ty = t * axis.y + tz = t * axis.z + + sx = s * axis.x + sy = s * axis.y + sz = s * axis.z + + self.n11 = c + axis.x * tx + self.n12 = axis.y * tx + sz + self.n13 = axis.z * tx - sy + self.n14 = 0 + + self.n21 = axis.x * ty - sz + self.n22 = c + axis.y * ty + self.n23 = axis.z * ty + sx + self.n24 = 0 + + self.n31 = axis.x * tz + sy + self.n32 = axis.y * tz - sx + self.n33 = c + axis.z * tz + self.n34 = 0 + + self.n41 = 0 + self.n42 = 0 + self.n43 = 0 + self.n44 = 1 + + def __mul__(self, other): + return Matrix3D( + self.n11 * other.n11 + self.n21 * other.n12 + self.n31 * other.n13 + self.n41 * other.n14, + self.n12 * other.n11 + self.n22 * other.n12 + self.n32 * other.n13 + self.n42 * other.n14, + self.n13 * other.n11 + self.n23 * other.n12 + self.n33 * other.n13 + self.n43 * other.n14, + self.n14 * other.n11 + self.n24 * other.n12 + self.n34 * other.n13 + self.n44 * other.n14, + self.n11 * other.n21 + self.n21 * other.n22 + self.n31 * other.n23 + self.n41 * other.n24, + self.n12 * other.n21 + self.n22 * other.n22 + self.n32 * other.n23 + self.n42 * other.n24, + self.n13 * other.n21 + self.n23 * other.n22 + self.n33 * other.n23 + self.n43 * other.n24, + self.n14 * other.n21 + self.n24 * other.n22 + self.n34 * other.n23 + self.n44 * other.n24, + self.n11 * other.n31 + self.n21 * other.n32 + self.n31 * other.n33 + self.n41 * other.n34, + self.n12 * other.n31 + self.n22 * other.n32 + self.n32 * other.n33 + self.n42 * other.n34, + self.n13 * other.n31 + self.n23 * other.n32 + self.n33 * other.n33 + self.n43 * other.n34, + self.n14 * other.n31 + self.n24 * other.n32 + self.n34 * other.n33 + self.n44 * other.n34, + self.n11 * other.n41 + self.n21 * other.n42 + self.n31 * other.n43 + self.n41 * other.n44, + self.n12 * other.n41 + self.n22 * other.n42 + self.n32 * other.n43 + self.n42 * other.n44, + self.n13 * other.n41 + self.n23 * other.n42 + self.n33 * other.n43 + self.n43 * other.n44, + self.n14 * other.n41 + self.n24 * other.n42 + self.n34 * other.n43 + self.n44 * other.n44 + ) + +class Point3D: + def __init__(self, x=0,y=0,z=0): + self.x = x + self.y = y + self.z = z + + def __str__(self): + return '[{0},{1},{2}]'.format(self.x, self.y,self.z) + + def string(self,prefix = "v"): + return '{0} {1:f} {2:f} {3:f}\n'.format(prefix ,self.x , self.y, self.z) + + def transformW(self,matrix): + x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + self.x = x + self.y = y + self.z = z + + def transform(self,matrix): + x = matrix.n11 * self.x + matrix.n21 * self.y + matrix.n31 * self.z + matrix.n41 + y = matrix.n12 * self.x + matrix.n22 * self.y + matrix.n32 * self.z + matrix.n42 + z = matrix.n13 * self.x + matrix.n23 * self.y + matrix.n33 * self.z + matrix.n43 + self.x = x + self.y = y + self.z = z + + def copy(self): + return Point3D(x=self.x,y=self.y,z=self.z) + +class Point2D: + def __init__(self, x=0,y=0): + self.x = x + self.y = y + def __str__(self): + return '[{0},{1}]'.format(self.x, self.y * -1) + def string(self,prefix="t"): + return '{0} {1:f} {2:f}\n'.format(prefix , self.x, self.y * -1 ) + def copy(self): + return Point2D(x=self.x,y=self.y) + +class Face: + def __init__(self,a=0,b=0,c=0): + self.a = a + self.b = b + self.c = c + def string(self,prefix="f", indexOffset=0 ,textureoffset=0): + if textureoffset == 0: + return prefix + ' {0}//{0} {1}//{1} {2}//{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset) + else: + return prefix + ' {0}/{3}/{0} {1}/{4}/{1} {2}/{5}/{2}\n'.format(self.a + indexOffset, self.b + indexOffset, self.c + indexOffset,self.a + textureoffset, self.b + textureoffset, self.c + textureoffset) + def __str__(self): + return '[{0},{1},{2}]'.format(self.a, self.b, self.c) + +class Group: + def __init__(self, node): + self.partRefs = node.getAttribute('partRefs').split(',') + +class Bone: + def __init__(self, node): + self.refID = node.getAttribute('refID') + (a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(',')) + self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1) + +class Part: + def __init__(self, node): + self.isGrouped = False + self.GroupIDX = 0 + self.Bones = [] + self.refID = node.getAttribute('refID') + self.designID = node.getAttribute('designID') + self.materials = list(map(str, node.getAttribute('materials').split(','))) + + lastm = '0' + for i, m in enumerate(self.materials): + if (m == '0'): + # self.materials[i] = lastm + self.materials[i] = self.materials[0] #in case of 0 choose the 'base' material + else: + lastm = m + if node.hasAttribute('decoration'): + self.decoration = list(map(str,node.getAttribute('decoration').split(','))) + for childnode in node.childNodes: + if childnode.nodeName == 'Bone': + self.Bones.append(Bone(node=childnode)) + +class Brick: + def __init__(self, node): + self.refID = node.getAttribute('refID') + self.designID = node.getAttribute('designID') + self.Parts = [] + for childnode in node.childNodes: + if childnode.nodeName == 'Part': + self.Parts.append(Part(node=childnode)) + +class SceneCamera: + def __init__(self, node): + self.refID = node.getAttribute('refID') + (a, b, c, d, e, f, g, h, i, x, y, z) = map(float, node.getAttribute('transformation').split(',')) + self.matrix = Matrix3D(n11=a,n12=b,n13=c,n14=0,n21=d,n22=e,n23=f,n24=0,n31=g,n32=h,n33=i,n34=0,n41=x,n42=y,n43=z,n44=1) + self.fieldOfView = float(node.getAttribute('fieldOfView')) + self.distance = float(node.getAttribute('distance')) + +class Scene: + def __init__(self, file): + self.Bricks = [] + self.Scenecamera = [] + self.Groups = [] + + if file.endswith('.lxfml'): + with open(file, "rb") as file: + data = file.read() + elif file.endswith('.lxf'): + zf = zipfile.ZipFile(file, 'r') + data = zf.read('IMAGE100.LXFML') + else: + return + + xml = minidom.parseString(data) + self.Name = xml.firstChild.getAttribute('name') + + for node in xml.firstChild.childNodes: + if node.nodeName == 'Meta': + for childnode in node.childNodes: + if childnode.nodeName == 'BrickSet': + self.Version = str(childnode.getAttribute('version')) + elif node.nodeName == 'Cameras': + for childnode in node.childNodes: + if childnode.nodeName == 'Camera': + self.Scenecamera.append(SceneCamera(node=childnode)) + elif node.nodeName == 'Bricks': + for childnode in node.childNodes: + if childnode.nodeName == 'Brick': + self.Bricks.append(Brick(node=childnode)) + elif node.nodeName == 'GroupSystems': + for childnode in node.childNodes: + if childnode.nodeName == 'GroupSystem': + for childnode in childnode.childNodes: + if childnode.nodeName == 'Group': + self.Groups.append(Group(node=childnode)) + + for i in range(len(self.Groups)): + for brick in self.Bricks: + for part in brick.Parts: + if part.refID in self.Groups[i].partRefs: + part.isGrouped = True + part.GroupIDX = i + + # print('Scene "'+ self.Name + '" Brickversion: ' + str(self.Version)) + +class GeometryReader: + def __init__(self, data): + self.offset = 0 + self.data = data + self.positions = [] + self.normals = [] + self.textures = [] + self.faces = [] + self.bonemap = {} + self.texCount = 0 + self.outpositions = [] + self.outnormals = [] + + if self.readInt() == 1111961649: + self.valueCount = self.readInt() + self.indexCount = self.readInt() + self.faceCount = int(self.indexCount / 3) + options = self.readInt() + + for i in range(0, self.valueCount): + self.positions.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat())) + + for i in range(0, self.valueCount): + self.normals.append(Point3D(x=self.readFloat(),y= self.readFloat(),z=self.readFloat())) + + if (options & 3) == 3: + self.texCount = self.valueCount + for i in range(0, self.valueCount): + self.textures.append(Point2D(x=self.readFloat(), y=self.readFloat())) + + for i in range(0, self.faceCount): + self.faces.append(Face(a=self.readInt(),b=self.readInt(),c=self.readInt())) + + if (options & 48) == 48: + num = self.readInt() + self.offset += (num * 4) + (self.indexCount * 4) + num = self.readInt() + self.offset += (3 * num * 4) + (self.indexCount * 4) + + bonelength = self.readInt() + self.bonemap = [0] * self.valueCount + + if (bonelength > self.valueCount) or (bonelength > self.faceCount): + datastart = self.offset + self.offset += bonelength + for i in range(0, self.valueCount): + boneoffset = self.readInt() + 4 + self.bonemap[i] = self.read_Int(datastart + boneoffset) + + def read_Int(self,_offset): + if sys.version_info < (3, 0): + return int(struct.unpack_from('i', self.data, _offset)[0]) + else: + return int.from_bytes(self.data[_offset:_offset + 4], byteorder='little') + + def readInt(self): + if sys.version_info < (3, 0): + ret = int(struct.unpack_from('i', self.data, self.offset)[0]) + else: + ret = int.from_bytes(self.data[self.offset:self.offset + 4], byteorder='little') + self.offset += 4 + return ret + + def readFloat(self): + ret = float(struct.unpack_from('f', self.data, self.offset)[0]) + self.offset += 4 + return ret + +class Geometry: + def __init__(self, designID, database): + self.designID = designID + self.Parts = {} + self.maxGeoBounding = -1 + self.studsFields2D = [] + + GeometryLocation = '{0}{1}{2}'.format(GEOMETRIEPATH, designID,'.g') + GeometryCount = 0 + while str(GeometryLocation) in database.filelist: + self.Parts[GeometryCount] = GeometryReader(data=database.filelist[GeometryLocation].read()) + GeometryCount += 1 + GeometryLocation = '{0}{1}{2}{3}'.format(GEOMETRIEPATH, designID,'.g',GeometryCount) + + primitive = Primitive(data = database.filelist[PRIMITIVEPATH + designID + '.xml'].read()) + self.Partname = primitive.Designname + self.studsFields2D = primitive.Fields2D + try: + geoBoundingList = [abs(float(primitive.Bounding['minX']) - float(primitive.Bounding['maxX'])), abs(float(primitive.Bounding['minY']) - float(primitive.Bounding['maxY'])), abs(float(primitive.Bounding['minZ']) - float(primitive.Bounding['maxZ']))] + geoBoundingList.sort() + self.maxGeoBounding = geoBoundingList[-1] + except KeyError as e: + # print('\nBounding errror in part {0}: {1}\n'.format(designID, e)) + pass + + # preflex + for part in self.Parts: + # transform + for i, b in enumerate(primitive.Bones): + # positions + for j, p in enumerate(self.Parts[part].positions): + if (self.Parts[part].bonemap[j] == i): + self.Parts[part].positions[j].transform(b.matrix) + # normals + for k, n in enumerate(self.Parts[part].normals): + if (self.Parts[part].bonemap[k] == i): + self.Parts[part].normals[k].transformW(b.matrix) + + def valuecount(self): + count = 0 + for part in self.Parts: + count += self.Parts[part].valueCount + return count + + def facecount(self): + count = 0 + for part in self.Parts: + count += self.Parts[part].faceCount + return count + + def texcount(self): + count = 0 + for part in self.Parts: + count += self.Parts[part].texCount + return count + +class Bone2: + def __init__(self,boneId=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0): + self.boneId = boneId + rotationMatrix = Matrix3D() + rotationMatrix.rotate(angle = -angle * math.pi / 180.0,axis = Point3D(x=ax,y=ay,z=az)) + p = Point3D(x=tx,y=ty,z=tz) + p.transformW(rotationMatrix) + rotationMatrix.n41 -= p.x + rotationMatrix.n42 -= p.y + rotationMatrix.n43 -= p.z + self.matrix = rotationMatrix + +class Field2D: + def __init__(self, type=0, width=0, height=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0, field2DRawData='none'): + self.type = type + self.field2DRawData = field2DRawData + rotationMatrix = Matrix3D() + rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az)) + p = Point3D(x=tx,y=ty,z=tz) + p.transformW(rotationMatrix) + rotationMatrix.n41 -= p.x + rotationMatrix.n42 -= p.y + rotationMatrix.n43 -= p.z + + self.matrix = rotationMatrix + self.custom2DField = [] + + #The height and width are always double the number of studs. The contained text is a 2D array that is always height + 1 and width + 1. + rows_count = height + 1 + cols_count = width + 1 + # creation looks reverse + # create an array of "cols_count" cols, for each of the "rows_count" rows + # all elements are initialized to 0 + self.custom2DField = [[0 for j in range(cols_count)] for i in range(rows_count)] + custom2DFieldString = field2DRawData.replace('\r', '').replace('\n', '').replace(' ', '') + custom2DFieldArr = custom2DFieldString.strip().split(',') + + k = 0 + for i in range(rows_count): + for j in range(cols_count): + self.custom2DField[i][j] = custom2DFieldArr[k] + k += 1 + + def __str__(self): + return '[type="{0}" transform="{1}" custom2DField="{2}"]'.format(self.type, self.matrix, self.custom2DField) + +class CollisionBox: + def __init__(self, sX=0, sY=0, sZ=0, angle=0, ax=0, ay=0, az=0, tx=0, ty=0, tz=0): + rotationMatrix = Matrix3D() + rotationMatrix.rotate(angle = -angle * math.pi / 180.0, axis = Point3D(x=ax,y=ay,z=az)) + p = Point3D(x=tx,y=ty,z=tz) + p.transformW(rotationMatrix) + rotationMatrix.n41 -= p.x + rotationMatrix.n42 -= p.y + rotationMatrix.n43 -= p.z + + self.matrix = rotationMatrix + self.corner = Point3D(x=sX,y=sY,z=sZ) + self.positions = [] + + self.positions.append(Point3D(x=0, y=0, z=0)) + self.positions.append(Point3D(x=sX, y=0, z=0)) + self.positions.append(Point3D(x=0, y=sY, z=0)) + self.positions.append(Point3D(x=sX, y=sY, z=0)) + self.positions.append(Point3D(x=0, y=0, z=sZ)) + self.positions.append(Point3D(x=0, y=sY, z=sZ)) + self.positions.append(Point3D(x=sX ,y=0, z=sZ)) + self.positions.append(Point3D(x=sX ,y=sY, z=sZ)) + + def __str__(self): + return '[0,0,0] [{0},0,0] [0,{1},0] [{0},{1},0] [0,0,{2}] [0,{1},{2}] [{0},0,{2}] [{0},{1},{2}]'.format(self.corner.x, self.corner.y, self.corner.z) + +class Primitive: + def __init__(self, data): + self.Designname = '' + self.Bones = [] + self.Fields2D = [] + self.CollisionBoxes = [] + self.PhysicsAttributes = {} + self.Bounding = {} + self.GeometryBounding = {} + xml = minidom.parseString(data) + root = xml.documentElement + for node in root.childNodes: + if node.__class__.__name__.lower() == 'comment': + self.comment = node[0].nodeValue + if node.nodeName == 'Flex': + for node in node.childNodes: + if node.nodeName == 'Bone': + self.Bones.append(Bone2(boneId=int(node.getAttribute('boneId')), angle=float(node.getAttribute('angle')), ax=float(node.getAttribute('ax')), ay=float(node.getAttribute('ay')), az=float(node.getAttribute('az')), tx=float(node.getAttribute('tx')), ty=float(node.getAttribute('ty')), tz=float(node.getAttribute('tz')))) + elif node.nodeName == 'Annotations': + for childnode in node.childNodes: + if childnode.nodeName == 'Annotation' and childnode.hasAttribute('designname'): + self.Designname = childnode.getAttribute('designname') + elif node.nodeName == 'Collision': + for childnode in node.childNodes: + if childnode.nodeName == 'Box': + self.CollisionBoxes.append(CollisionBox(sX=float(childnode.getAttribute('sX')), sY=float(childnode.getAttribute('sY')), sZ=float(childnode.getAttribute('sZ')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz')))) + elif node.nodeName == 'PhysicsAttributes': + self.PhysicsAttributes = {"inertiaTensor": node.getAttribute('inertiaTensor'),"centerOfMass": node.getAttribute('centerOfMass'),"mass": node.getAttribute('mass'),"frictionType": node.getAttribute('frictionType')} + elif node.nodeName == 'Bounding': + for childnode in node.childNodes: + if childnode.nodeName == 'AABB': + self.Bounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')} + elif node.nodeName == 'GeometryBounding': + for childnode in node.childNodes: + if childnode.nodeName == 'AABB': + self.GeometryBounding = {"minX": childnode.getAttribute('minX'), "minY": childnode.getAttribute('minY'), "minZ": childnode.getAttribute('minZ'), "maxX": childnode.getAttribute('maxX'), "maxY": childnode.getAttribute('maxY'), "maxZ": childnode.getAttribute('maxZ')} + elif node.nodeName == 'Connectivity': + for childnode in node.childNodes: + if childnode.nodeName == 'Custom2DField': + self.Fields2D.append(Field2D(type=int(childnode.getAttribute('type')), width=int(childnode.getAttribute('width')), height=int(childnode.getAttribute('height')), angle=float(childnode.getAttribute('angle')), ax=float(childnode.getAttribute('ax')), ay=float(childnode.getAttribute('ay')), az=float(childnode.getAttribute('az')), tx=float(childnode.getAttribute('tx')), ty=float(childnode.getAttribute('ty')), tz=float(childnode.getAttribute('tz')), field2DRawData=str(childnode.firstChild.data))) + elif node.nodeName == 'Decoration': + self.Decoration = {"faces": node.getAttribute('faces'), "subMaterialRedirectLookupTable": node.getAttribute('subMaterialRedirectLookupTable')} + +class Materials: + def __init__(self, data): + self.Materials = {} + xml = minidom.parseString(data) + for node in xml.firstChild.childNodes: + if node.nodeName == 'Material': + self.Materials[node.getAttribute('MatID')] = Material( + node.getAttribute('MatID'), + r=int(node.getAttribute('Red')), + g=int(node.getAttribute('Green')), + b=int(node.getAttribute('Blue')), + a=int(node.getAttribute('Alpha')), + mtype=str(node.getAttribute('MaterialType')) + ) + + def getMaterialbyId(self, mid): + return self.Materials[mid] + +class Material: + def __init__(self,id, r, g, b, a, mtype): + self.id = id + self.name = id + self.mattype = mtype + self.r = float(r) + self.g = float(g) + self.b = float(b) + self.a = float(a) + def string(self): + out = 'Kd {0} {1} {2}\nKa 1.600000 1.600000 1.600000\nKs 0.400000 0.400000 0.400000\nNs 3.482202\nTf 1 1 1\n'.format( self.r / 255, self.g / 255,self.b / 255) + if self.a < 255: + out += 'Ni 1.575\n' + 'd {0}'.format(0.05) + '\n' + 'Tr {0}\n'.format(0.05) + return out + +class DBinfo: + def __init__(self, data): + xml = minidom.parseString(data) + self.Version = xml.getElementsByTagName('Bricks')[0].attributes['version'].value + # print('DB Version: ' + str(self.Version)) + +class DBFolderFile: + def __init__(self, name, handle): + self.handle = handle + self.name = name + + def read(self): + reader = open(self.handle, "rb") + try: + filecontent = reader.read() + reader.close() + return filecontent + finally: + reader.close() + +class LIFFile: + def __init__(self, name, offset, size, handle): + self.handle = handle + self.name = name + self.offset = offset + self.size = size + + def read(self): + self.handle.seek(self.offset, 0) + return self.handle.read(self.size) + +class DBFolderReader: + def __init__(self, folder): + self.filelist = {} + self.initok = False + self.location = folder + self.dbinfo = None + + try: + os.path.isdir(self.location) + except Exception as e: + self.initok = False + # print("db folder read FAIL") + return + else: + self.parse() + if self.fileexist(os.path.join(self.location,'Materials.xml')) and self.fileexist(os.path.join(self.location, 'info.xml')): + self.dbinfo = DBinfo(data=self.filelist[os.path.join(self.location,'info.xml')].read()) + # print("DB folder OK.") + self.initok = True + else: + # print("DB folder ERROR") + # print(os.path.join(self.location,'Materials.xml')) + # print(self.fileexist(os.path.join(self.location,'Materials.xml'))) + # print(os.path.join(self.location,'info.xml')) + # print(self.fileexist(os.path.join(self.location, 'info.xml'))) + # print(MATERIALNAMESPATH) + pass + + + def fileexist(self, filename): + return filename in self.filelist + + def parse(self): + for path, subdirs, files in os.walk(self.location): + for name in files: + entryName = os.path.join(path, name) + self.filelist[entryName] = DBFolderFile(name=entryName, handle=entryName) + +class LIFReader: + def __init__(self, file): + self.packedFilesOffset = 84 + self.filelist = {} + self.initok = False + self.location = file + self.dbinfo = None + + try: + self.filehandle = open(self.location, "rb") + self.filehandle.seek(0, 0) + except Exception as e: + self.initok = False + # print("Database FAIL") + return + else: + if self.filehandle.read(4).decode() == "LIFF": + self.parse(prefix='', offset=self.readInt(offset=72) + 64) + if self.fileexist('/Materials.xml') and self.fileexist('/info.xml'): + self.dbinfo = DBinfo(data=self.filelist['/info.xml'].read()) + # print("Database OK.") + self.initok = True + else: + # print("Database ERROR") + pass + else: + # print("Database FAIL") + self.initok = False + + def fileexist(self,filename): + return filename in self.filelist + + def parse(self, prefix='', offset=0): + if prefix == '': + offset += 36 + else: + offset += 4 + + count = self.readInt(offset=offset) + + for i in range(0, count): + offset += 4 + entryType = self.readShort(offset=offset) + offset += 6 + + entryName = '{0}{1}'.format(prefix,'/'); + self.filehandle.seek(offset + 1, 0) + if sys.version_info < (3, 0): + t = ord(self.filehandle.read(1)) + else: + t = int.from_bytes(self.filehandle.read(1), byteorder='big') + + while not t == 0: + entryName ='{0}{1}'.format(entryName,chr(t)) + self.filehandle.seek(1, 1) + if sys.version_info < (3, 0): + t = ord(self.filehandle.read(1)) + else: + t = int.from_bytes(self.filehandle.read(1), byteorder='big') + + offset += 2 + + offset += 6 + self.packedFilesOffset += 20 + + if entryType == 1: + offset = self.parse(prefix=entryName, offset=offset) + elif entryType == 2: + fileSize = self.readInt(offset=offset) - 20 + self.filelist[entryName] = LIFFile(name=entryName, offset=self.packedFilesOffset, size=fileSize, handle=self.filehandle) + offset += 24 + self.packedFilesOffset += fileSize + + return offset + + def readInt(self, offset=0): + self.filehandle.seek(offset, 0) + if sys.version_info < (3, 0): + return int(struct.unpack('>i', self.filehandle.read(4))[0]) + else: + return int.from_bytes(self.filehandle.read(4), byteorder='big') + + def readShort(self, offset=0): + self.filehandle.seek(offset, 0) + if sys.version_info < (3, 0): + return int(struct.unpack('>h', self.filehandle.read(2))[0]) + else: + return int.from_bytes(self.filehandle.read(2), byteorder='big') + +class Converter: + def LoadDBFolder(self, dbfolderlocation): + self.database = DBFolderReader(folder=dbfolderlocation) + if self.database.initok and self.database.fileexist(os.path.join(dbfolderlocation,'Materials.xml')): + self.allMaterials = Materials(data=self.database.filelist[os.path.join(dbfolderlocation,'Materials.xml')].read()); + + def LoadDatabase(self,databaselocation): + self.database = LIFReader(file=databaselocation) + + if self.database.initok and self.database.fileexist('/Materials.xml'): + self.allMaterials = Materials(data=self.database.filelist['/Materials.xml'].read()); + + def LoadScene(self,filename): + if self.database.initok: + self.scene = Scene(file=filename) + + def Export(self,filename): + invert = Matrix3D() + #invert.n33 = -1 #uncomment to invert the Z-Axis + + indexOffset = 1 + textOffset = 1 + usedmaterials = [] + geometriecache = {} + + start_time = time.time() + + out = open(filename + ".obj.tmp", "w+") + out.write("mtllib " + filename + ".mtl" + '\n\n') + outtext = open(filename + ".mtl.tmp", "w+") + + total = len(self.scene.Bricks) + current = 0 + + for bri in self.scene.Bricks: + current += 1 + + for pa in bri.Parts: + + if pa.designID not in geometriecache: + geo = Geometry(designID=pa.designID, database=self.database) + progress(current ,total , "(" + geo.designID + ") " + geo.Partname, ' ') + geometriecache[pa.designID] = geo + else: + geo = geometriecache[pa.designID] + + progress(current ,total , "(" + geo.designID + ") " + geo.Partname ,'-') + + out.write("o\n") + + for part in geo.Parts: + geo.Parts[part].outpositions = [elem.copy() for elem in geo.Parts[part].positions] + geo.Parts[part].outnormals = [elem.copy() for elem in geo.Parts[part].normals] + + for i, b in enumerate(pa.Bones): + # positions + for j, p in enumerate(geo.Parts[part].outpositions): + if (geo.Parts[part].bonemap[j] == i): + p.transform( invert * b.matrix) + # normals + for k, n in enumerate(geo.Parts[part].outnormals): + if (geo.Parts[part].bonemap[k] == i): + n.transformW( invert * b.matrix) + + for point in geo.Parts[part].outpositions: + out.write(point.string("v")) + + for normal in geo.Parts[part].outnormals: + out.write(normal.string("vn")) + + for text in geo.Parts[part].textures: + out.write(text.string("vt")) + + decoCount = 0 + out.write("g " + "(" + geo.designID + ") " + geo.Partname + '\n') + last_color = 0 + for part in geo.Parts: + + + #try catch here for possible problems in materials assignment of various g, g1, g2, .. files in lxf file + try: + materialCurrentPart = pa.materials[part] + last_color = pa.materials[part] + except IndexError: + # print('WARNING: {0}.g{1} has NO material assignment in lxf. Replaced with color {2}. Fix {0}.xml faces values.'.format(pa.designID, part, last_color)) + materialCurrentPart = last_color + + lddmat = self.allMaterials.getMaterialbyId(materialCurrentPart) + matname = lddmat.name + + deco = '0' + if hasattr(pa, 'decoration') and len(geo.Parts[part].textures) > 0: + #if decoCount <= len(pa.decoration): + if decoCount < len(pa.decoration): + deco = pa.decoration[decoCount] + decoCount += 1 + + extfile = '' + if not deco == '0': + extfile = deco + '.png' + matname += "_" + deco + decofilename = DECORATIONPATH + deco + '.png' + if not os.path.isfile(extfile) and self.database.fileexist(decofilename): + with open(extfile, "wb") as f: + f.write(self.database.filelist[decofilename].read()) + f.close() + + if not matname in usedmaterials: + usedmaterials.append(matname) + outtext.write("newmtl " + matname + '\n') + outtext.write(lddmat.string()) + if not deco == '0': + outtext.write("map_Kd " + deco + ".png" + '\n') + + out.write("usemtl " + matname + '\n') + for face in geo.Parts[part].faces: + if len(geo.Parts[part].textures) > 0: + out.write(face.string("f",indexOffset,textOffset)) + else: + out.write(face.string("f",indexOffset)) + + indexOffset += len(geo.Parts[part].outpositions) + textOffset += len(geo.Parts[part].textures) + # ----------------------------------------------------------------- + out.write('\n') + os.rename(filename + ".obj.tmp", filename + ".obj") + os.rename(filename + ".mtl.tmp", filename + ".mtl") + + sys.stdout.write('%s\r' % (' ')) + # print("--- %s seconds ---" % (time.time() - start_time)) + +def setDBFolderVars(dbfolderlocation): + global PRIMITIVEPATH + global GEOMETRIEPATH + global DECORATIONPATH + global MATERIALNAMESPATH + PRIMITIVEPATH = os.path.join(dbfolderlocation, 'Primitives', '') + GEOMETRIEPATH = os.path.join(dbfolderlocation, 'brickprimitives', 'lod0', '') + DECORATIONPATH = os.path.join(dbfolderlocation, 'Decorations', '') + MATERIALNAMESPATH = os.path.join(dbfolderlocation, 'MaterialNames', '') + # print(MATERIALNAMESPATH) + +def FindDatabase(): + lddliftree = os.getenv('LDDLIFTREE') + if lddliftree is not None: + if os.path.isdir(str(lddliftree)): #LDDLIFTREE points to folder + return str(lddliftree) + elif os.path.isfile(str(lddliftree)): #LDDLIFTREE points to file (should be db.lif) + return str(lddliftree) + + else: #Env variable LDDLIFTREE not set. Check for default locations per different platform. + if platform.system() == 'Darwin': + if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db'))): + return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db')) + elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif'))): + return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'Library','Application Support','LEGO Company','LEGO Digital Designer','db.lif')) + else: + # print("no LDD database found please install LEGO-Digital-Designer") + os._exit() + elif platform.system() == 'Windows': + if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db'))): + return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db')) + elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif'))): + return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'AppData','Roaming','LEGO Company','LEGO Digital Designer','db.lif')) + else: + # print("no LDD database found please install LEGO-Digital-Designer") + os._exit() + elif platform.system() == 'Linux': + if os.path.isdir(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db'))): + return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db')) + elif os.path.isfile(str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif'))): + return str(os.path.join(str(os.getenv('USERPROFILE') or os.getenv('HOME')),'.wine','drive_c','users',os.getenv('USER'),'Application Data','LEGO Company','LEGO Digital Designer','db.lif')) + else: + # print("no LDD database found please install LEGO-Digital-Designer") + os._exit() + else: + # print('Your OS {0} is not supported yet.'.format(platform.system())) + os._exit() + +def progress(count, total, status='', suffix = ''): + bar_len = 40 + filled_len = int(round(bar_len * count / float(total))) + percents = round(100.0 * count / float(total), 1) + bar = '#' * filled_len + '-' * (bar_len - filled_len) + sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, ' ')) + sys.stdout.write('Progress: [%s] %s%s %s %s\r' % (bar, percents, '%', suffix, status)) + sys.stdout.flush() + +def main(lxf_filename, obj_filename): + # print("- - - pylddlib - - -") + # print(" _ ") + # print(" [_]") + # print(" /| |\\") + # print(" ()'---' C") + # print(" | | |") + # print(" [=|=]") + # print("") + # print("- - - - - - - - - - - -") + + converter = Converter() + # print("Found DB folder. Will use this instead of db.lif!") + setDBFolderVars(dbfolderlocation = "app/luclient/res/") + converter.LoadDBFolder(dbfolderlocation = "app/luclient/res/") + converter.LoadScene(filename=lxf_filename) + converter.Export(filename=obj_filename) + +if __name__ == "__main__": + main() diff --git a/app/reports.py b/app/reports.py new file mode 100644 index 0000000..3ec353f --- /dev/null +++ b/app/reports.py @@ -0,0 +1,58 @@ +from flask import render_template, Blueprint, redirect, url_for, request, abort, flash, request +from flask_user import login_required, current_user +from app.models import db, CharacterInfo, Account, CharacterXML, ItemReports +from app import gm_level, scheduler +import datetime, xmltodict + +reports_blueprint = Blueprint('reports', __name__) + +@reports_blueprint.route('/', methods=['GET', 'POST']) +@login_required +@gm_level(3) +def index(): + items = ItemReports.query.distinct(ItemReports.date).group_by(ItemReports.date).all() + + return render_template('reports/index.html.j2', items=items) + +@reports_blueprint.route('/items/by_date/', methods=['GET', 'POST']) +@login_required +@gm_level(3) +def items_by_date(date): + items = ItemReports.query.filter(ItemReports.date==date).order_by(ItemReports.count.desc()).all() + return render_template('reports/items/by_date.html.j2', items=items, date=date) + + +@scheduler.task("cron", id="gen_item_report", hour=3) +def gen_item_report(): + char_xmls = CharacterXML.query.join( + CharacterInfo, + CharacterInfo.id==CharacterXML.id + ).join( + Account, + CharacterInfo.account_id==Account.id + ).filter(Account.gm_level < 3).all() + date = datetime.date.today().strftime('%Y-%m-%d') + for char_xml in char_xmls: + character_json = xmltodict.parse( + char_xml.xml_data, + attr_prefix="attr_" + ) + for inv in character_json["obj"]["inv"]["items"]["in"]: + if "i" in inv.keys() and type(inv["i"]) == list and (int(inv["attr_t"])==0 or int(inv["attr_t"])==0): + for item in inv["i"]: + entry = ItemReports.query.filter( + ItemReports.item == int(item["attr_l"]) and \ + ItemReports.date == date + ).first() + if entry: + entry.count = entry.count + int(item["attr_c"]) + entry.save() + else: + new_entry = ItemReports( + item=int(item["attr_l"]), + count=int(item["attr_c"]), + date=date + ) + new_entry.save() + + return "Done" diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000..cb55a7a --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,117 @@ +from flask_marshmallow import Marshmallow +from app.models import * +ma = Marshmallow() + +class PlayKeySchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = PlayKey + include_relationships = False + load_instance = True + include_fk = True + + +class PetNamesSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = PetNames + include_relationships = False + load_instance = True + include_fk = False + + +class MailSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Mail + include_relationships = False + load_instance = True + include_fk = False + + +class UGCSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = UGC + include_relationships = False + load_instance = True + include_fk = False + + +class PropertyContentSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = PropertyContent + include_relationships = True + load_instance = True + include_fk = True + + ugc = ma.Nested(UGCSchema) + + + +class PropertySchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Property + include_relationships = False + load_instance = True + include_fk = False + + properties_contents = ma.Nested(PropertyContentSchema, many=True) + + +class CharacterXMLSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = CharacterXML + include_relationships = False + load_instance = True + include_fk = False + + +class CharacterInfoSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = CharacterInfo + include_relationships = False + load_instance = True + include_fk = False + + charxml = ma.Nested(CharacterXMLSchema) + properties_owner = ma.Nested(PropertySchema, many=True) + pets = ma.Nested(PetNamesSchema, many=True) + mail = ma.Nested(MailSchema, many=True) + + +class AccountSchema(ma.SQLAlchemyAutoSchema): + class Meta: + model = Account + include_relationships = False + load_instance = True + include_fk = False + + play_key = ma.Nested(PlayKeySchema) + charinfo = ma.Nested(CharacterInfoSchema, many=True) + + +class AccountInvitationSchema(ma.SQLAlchemyAutoSchema): # noqa + class Meta: + model = AccountInvitation + include_relationships = True + load_instance = True + include_fk = True + + invite_by_user = ma.Nested(AccountSchema) + + +class ActivityLogSchema(ma.SQLAlchemyAutoSchema): # noqa + class Meta: + model = ActivityLog + include_relationships = True + load_instance = True + include_fk = True + + character = ma.Nested(CharacterInfoSchema()) + + +class CommandLogSchema(ma.SQLAlchemyAutoSchema): # noqa + class Meta: + model = CommandLog + include_relationships = True + load_instance = True + include_fk = True + + character = ma.Nested(CharacterInfoSchema()) diff --git a/app/settings.py b/app/settings.py new file mode 100644 index 0000000..3ed103f --- /dev/null +++ b/app/settings.py @@ -0,0 +1,39 @@ +# Settings common to all environments (development|staging|production) + +# Application settings +APP_NAME = "Nexus Dashboard" +APP_SYSTEM_ERROR_SUBJECT_LINE = APP_NAME + " system error" + +# Flask settings +CSRF_ENABLED = True + +# Flask-SQLAlchemy settings +SQLALCHEMY_TRACK_MODIFICATIONS = False +WTF_CSRF_TIME_LIMIT = 86400 + +# Flask-User settings +USER_APP_NAME = APP_NAME +USER_ENABLE_CHANGE_PASSWORD = True # Allow users to change their password +USER_ENABLE_CHANGE_USERNAME = True # Allow users to change their username +USER_ENABLE_REGISTER = False # Allow new users to register + +# Should alwyas be set to true +USER_REQUIRE_RETYPE_PASSWORD = True # Prompt for `retype password` +USER_ENABLE_USERNAME = True # Register and Login with username + +# Email Related Settings +USER_ENABLE_EMAIL = True # Register with Email WILL - DISABLE OTHER THINGS TOO +USER_ENABLE_CONFIRM_EMAIL = True # Force users to confirm their email +USER_ENABLE_INVITE_USER = False # Allow users to be invited +USER_REQUIRE_INVITATION = False # Only invited users may - WILL DISABLE REGISTRATION +USER_ENABLE_FORGOT_PASSWORD = True # Allow users to reset their passwords + +# Require Play Key +REQUIRE_PLAY_KEY = True + +# Password hashing settings +USER_PASSLIB_CRYPTCONTEXT_SCHEMES = ['bcrypt'] # bcrypt for password hashing + +# Flask-User routing settings +USER_AFTER_LOGIN_ENDPOINT = "main.index" +USER_AFTER_LOGOUT_ENDPOINT = "main.index" diff --git a/app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js b/app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js new file mode 100644 index 0000000..97f14c0 --- /dev/null +++ b/app/static/bootstrap-4.2.1/js/bootstrap.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * Bootstrap v4.2.1 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery")):"function"==typeof define&&define.amd?define(["exports","jquery"],e):e(t.bootstrap={},t.jQuery)}(this,function(t,p){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)p(this._element).one(q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=n=i.clientWidth&&n>=i.clientHeight}),h=0l[t]&&!i.escapeWithReference&&(n=Math.min(h[e],l[t]-("right"===t?h.width:h.height))),Kt({},e,n)}};return c.forEach(function(t){var e=-1!==["left","top"].indexOf(t)?"primary":"secondary";h=Qt({},h,u[e](t))}),t.offsets.popper=h,t},priority:["left","right","top","bottom"],padding:5,boundariesElement:"scrollParent"},keepTogether:{order:400,enabled:!0,fn:function(t){var e=t.offsets,n=e.popper,i=e.reference,o=t.placement.split("-")[0],r=Math.floor,s=-1!==["top","bottom"].indexOf(o),a=s?"right":"bottom",l=s?"left":"top",c=s?"width":"height";return n[a]r(i[a])&&(t.offsets.popper[l]=r(i[a])),t}},arrow:{order:500,enabled:!0,fn:function(t,e){var n;if(!fe(t.instance.modifiers,"arrow","keepTogether"))return t;var i=e.element;if("string"==typeof i){if(!(i=t.instance.popper.querySelector(i)))return t}else if(!t.instance.popper.contains(i))return console.warn("WARNING: `arrow.element` must be child of its popper element!"),t;var o=t.placement.split("-")[0],r=t.offsets,s=r.popper,a=r.reference,l=-1!==["left","right"].indexOf(o),c=l?"height":"width",h=l?"Top":"Left",u=h.toLowerCase(),f=l?"left":"top",d=l?"bottom":"right",p=$t(i)[c];a[d]-ps[d]&&(t.offsets.popper[u]+=a[u]+p-s[d]),t.offsets.popper=Yt(t.offsets.popper);var m=a[u]+a[c]/2-p/2,g=Nt(t.instance.popper),_=parseFloat(g["margin"+h],10),v=parseFloat(g["border"+h+"Width"],10),y=m-t.offsets.popper[u]-_-v;return y=Math.max(Math.min(s[c]-p,y),0),t.arrowElement=i,t.offsets.arrow=(Kt(n={},u,Math.round(y)),Kt(n,f,""),n),t},element:"[x-arrow]"},flip:{order:600,enabled:!0,fn:function(p,m){if(oe(p.instance.modifiers,"inner"))return p;if(p.flipped&&p.placement===p.originalPlacement)return p;var g=Gt(p.instance.popper,p.instance.reference,m.padding,m.boundariesElement,p.positionFixed),_=p.placement.split("-")[0],v=te(_),y=p.placement.split("-")[1]||"",E=[];switch(m.behavior){case ge:E=[_,v];break;case _e:E=me(_);break;case ve:E=me(_,!0);break;default:E=m.behavior}return E.forEach(function(t,e){if(_!==t||E.length===e+1)return p;_=p.placement.split("-")[0],v=te(_);var n,i=p.offsets.popper,o=p.offsets.reference,r=Math.floor,s="left"===_&&r(i.right)>r(o.left)||"right"===_&&r(i.left)r(o.top)||"bottom"===_&&r(i.top)r(g.right),c=r(i.top)r(g.bottom),u="left"===_&&a||"right"===_&&l||"top"===_&&c||"bottom"===_&&h,f=-1!==["top","bottom"].indexOf(_),d=!!m.flipVariations&&(f&&"start"===y&&a||f&&"end"===y&&l||!f&&"start"===y&&c||!f&&"end"===y&&h);(s||u||d)&&(p.flipped=!0,(s||u)&&(_=E[e+1]),d&&(y="end"===(n=y)?"start":"start"===n?"end":n),p.placement=_+(y?"-"+y:""),p.offsets.popper=Qt({},p.offsets.popper,ee(p.instance.popper,p.offsets.reference,p.placement)),p=ie(p.instance.modifiers,p,"flip"))}),p},behavior:"flip",padding:5,boundariesElement:"viewport"},inner:{order:700,enabled:!1,fn:function(t){var e=t.placement,n=e.split("-")[0],i=t.offsets,o=i.popper,r=i.reference,s=-1!==["left","right"].indexOf(n),a=-1===["top","left"].indexOf(n);return o[s?"left":"top"]=r[n]-(a?o[s?"width":"height"]:0),t.placement=te(e),t.offsets.popper=Yt(o),t}},hide:{order:800,enabled:!0,fn:function(t){if(!fe(t.instance.modifiers,"hide","preventOverflow"))return t;var e=t.offsets.reference,n=ne(t.instance.modifiers,function(t){return"preventOverflow"===t.name}).boundaries;if(e.bottomn.right||e.top>n.bottom||e.rightdocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent"},Cn="show",Sn="out",Dn={HIDE:"hide"+_n,HIDDEN:"hidden"+_n,SHOW:"show"+_n,SHOWN:"shown"+_n,INSERTED:"inserted"+_n,CLICK:"click"+_n,FOCUSIN:"focusin"+_n,FOCUSOUT:"focusout"+_n,MOUSEENTER:"mouseenter"+_n,MOUSELEAVE:"mouseleave"+_n},In="fade",An="show",On=".tooltip-inner",Nn=".arrow",kn="hover",Ln="focus",Pn="click",xn="manual",Hn=function(){function i(t,e){if("undefined"==typeof be)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p(this.getTipElement()).hasClass(An))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),p.removeData(this.element,this.constructor.DATA_KEY),p(this.element).off(this.constructor.EVENT_KEY),p(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&p(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===p(this.element).css("display"))throw new Error("Please use show on visible elements");var t=p.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p(this.element).trigger(t);var n=m.findShadowRoot(this.element),i=p.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=m.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&p(o).addClass(In);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();p(o).data(this.constructor.DATA_KEY,this),p.contains(this.element.ownerDocument.documentElement,this.tip)||p(o).appendTo(l),p(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new be(this.element,o,{placement:a,modifiers:{offset:{offset:this.config.offset},flip:{behavior:this.config.fallbackPlacement},arrow:{element:Nn},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),p(o).addClass(An),"ontouchstart"in document.documentElement&&p(document.body).children().on("mouseover",null,p.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,p(e.element).trigger(e.constructor.Event.SHOWN),t===Sn&&e._leave(null,e)};if(p(this.tip).hasClass(In)){var h=m.getTransitionDurationFromElement(this.tip);p(this.tip).one(m.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=p.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==Cn&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p(this.element).trigger(i),!i.isDefaultPrevented()){if(p(n).removeClass(An),"ontouchstart"in document.documentElement&&p(document.body).children().off("mouseover",null,p.noop),this._activeTrigger[Pn]=!1,this._activeTrigger[Ln]=!1,this._activeTrigger[kn]=!1,p(this.tip).hasClass(In)){var r=m.getTransitionDurationFromElement(n);p(n).one(m.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){p(this.getTipElement()).addClass(yn+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(p(t.querySelectorAll(On)),this.getTitle()),p(t).removeClass(In+" "+An)},t.setElementContent=function(t,e){var n=this.config.html;"object"==typeof e&&(e.nodeType||e.jquery)?n?p(e).parent().is(t)||t.empty().append(e):t.text(p(e).text()):t[n?"html":"text"](e)},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getContainer=function(){return!1===this.config.container?document.body:m.isElement(this.config.container)?p(this.config.container):p(document).find(this.config.container)},t._getAttachment=function(t){return wn[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)p(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==xn){var e=t===kn?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===kn?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;p(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),p(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Ln:kn]=!0),p(e.getTipElement()).hasClass(An)||e._hoverState===Cn?e._hoverState=Cn:(clearTimeout(e._timeout),e._hoverState=Cn,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===Cn&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Ln:kn]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=Sn,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===Sn&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){return"number"==typeof(t=l({},this.constructor.Default,p(this.element).data(),"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),m.typeCheckConfig(mn,t,this.constructor.DefaultType),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(En);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p(t).removeClass(In),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=p(this).data(gn),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),p(this).data(gn,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.2.1"}},{key:"Default",get:function(){return Tn}},{key:"NAME",get:function(){return mn}},{key:"DATA_KEY",get:function(){return gn}},{key:"Event",get:function(){return Dn}},{key:"EVENT_KEY",get:function(){return _n}},{key:"DefaultType",get:function(){return bn}}]),i}();p.fn[mn]=Hn._jQueryInterface,p.fn[mn].Constructor=Hn,p.fn[mn].noConflict=function(){return p.fn[mn]=vn,Hn._jQueryInterface};var jn="popover",Rn="bs.popover",Fn="."+Rn,Mn=p.fn[jn],Wn="bs-popover",Un=new RegExp("(^|\\s)"+Wn+"\\S+","g"),Bn=l({},Hn.Default,{placement:"right",trigger:"click",content:"",template:''}),qn=l({},Hn.DefaultType,{content:"(string|element|function)"}),Kn="fade",Qn="show",Yn=".popover-header",Vn=".popover-body",Xn={HIDE:"hide"+Fn,HIDDEN:"hidden"+Fn,SHOW:"show"+Fn,SHOWN:"shown"+Fn,INSERTED:"inserted"+Fn,CLICK:"click"+Fn,FOCUSIN:"focusin"+Fn,FOCUSOUT:"focusout"+Fn,MOUSEENTER:"mouseenter"+Fn,MOUSELEAVE:"mouseleave"+Fn},zn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){p(this.getTipElement()).addClass(Wn+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||p(this.config.template)[0],this.tip},o.setContent=function(){var t=p(this.getTipElement());this.setElementContent(t.find(Yn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(Vn),e),t.removeClass(Kn+" "+Qn)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=p(this.getTipElement()),e=t.attr("class").match(Un);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t {\n called = true\n })\n\n setTimeout(() => {\n if (!called) {\n Util.triggerTransitionEnd(this)\n }\n }, duration)\n\n return this\n}\n\nfunction setTransitionEndSupport() {\n $.fn.emulateTransitionEnd = transitionEndEmulator\n $.event.special[Util.TRANSITION_END] = getSpecialTransitionEndEvent()\n}\n\n/**\n * --------------------------------------------------------------------------\n * Public Util Api\n * --------------------------------------------------------------------------\n */\n\nconst Util = {\n\n TRANSITION_END: 'bsTransitionEnd',\n\n getUID(prefix) {\n do {\n // eslint-disable-next-line no-bitwise\n prefix += ~~(Math.random() * MAX_UID) // \"~~\" acts like a faster Math.floor() here\n } while (document.getElementById(prefix))\n return prefix\n },\n\n getSelectorFromElement(element) {\n let selector = element.getAttribute('data-target')\n\n if (!selector || selector === '#') {\n const hrefAttr = element.getAttribute('href')\n selector = hrefAttr && hrefAttr !== '#' ? hrefAttr.trim() : ''\n }\n\n return selector && document.querySelector(selector) ? selector : null\n },\n\n getTransitionDurationFromElement(element) {\n if (!element) {\n return 0\n }\n\n // Get transition-duration of the element\n let transitionDuration = $(element).css('transition-duration')\n let transitionDelay = $(element).css('transition-delay')\n\n const floatTransitionDuration = parseFloat(transitionDuration)\n const floatTransitionDelay = parseFloat(transitionDelay)\n\n // Return 0 if element or transition duration is not found\n if (!floatTransitionDuration && !floatTransitionDelay) {\n return 0\n }\n\n // If multiple durations are defined, take the first\n transitionDuration = transitionDuration.split(',')[0]\n transitionDelay = transitionDelay.split(',')[0]\n\n return (parseFloat(transitionDuration) + parseFloat(transitionDelay)) * MILLISECONDS_MULTIPLIER\n },\n\n reflow(element) {\n return element.offsetHeight\n },\n\n triggerTransitionEnd(element) {\n $(element).trigger(TRANSITION_END)\n },\n\n // TODO: Remove in v5\n supportsTransitionEnd() {\n return Boolean(TRANSITION_END)\n },\n\n isElement(obj) {\n return (obj[0] || obj).nodeType\n },\n\n typeCheckConfig(componentName, config, configTypes) {\n for (const property in configTypes) {\n if (Object.prototype.hasOwnProperty.call(configTypes, property)) {\n const expectedTypes = configTypes[property]\n const value = config[property]\n const valueType = value && Util.isElement(value)\n ? 'element' : toType(value)\n\n if (!new RegExp(expectedTypes).test(valueType)) {\n throw new Error(\n `${componentName.toUpperCase()}: ` +\n `Option \"${property}\" provided type \"${valueType}\" ` +\n `but expected type \"${expectedTypes}\".`)\n }\n }\n }\n },\n\n findShadowRoot(element) {\n if (!document.documentElement.attachShadow) {\n return null\n }\n\n // Can find the shadow root otherwise it'll return the document\n if (typeof element.getRootNode === 'function') {\n const root = element.getRootNode()\n return root instanceof ShadowRoot ? root : null\n }\n\n if (element instanceof ShadowRoot) {\n return element\n }\n\n // when we don't find a shadow root\n if (!element.parentNode) {\n return null\n }\n\n return Util.findShadowRoot(element.parentNode)\n }\n}\n\nsetTransitionEndSupport()\n\nexport default Util\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): alert.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'alert'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.alert'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Selector = {\n DISMISS : '[data-dismiss=\"alert\"]'\n}\n\nconst Event = {\n CLOSE : `close${EVENT_KEY}`,\n CLOSED : `closed${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n ALERT : 'alert',\n FADE : 'fade',\n SHOW : 'show'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Alert {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n close(element) {\n let rootElement = this._element\n if (element) {\n rootElement = this._getRootElement(element)\n }\n\n const customEvent = this._triggerCloseEvent(rootElement)\n\n if (customEvent.isDefaultPrevented()) {\n return\n }\n\n this._removeElement(rootElement)\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Private\n\n _getRootElement(element) {\n const selector = Util.getSelectorFromElement(element)\n let parent = false\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n if (!parent) {\n parent = $(element).closest(`.${ClassName.ALERT}`)[0]\n }\n\n return parent\n }\n\n _triggerCloseEvent(element) {\n const closeEvent = $.Event(Event.CLOSE)\n\n $(element).trigger(closeEvent)\n return closeEvent\n }\n\n _removeElement(element) {\n $(element).removeClass(ClassName.SHOW)\n\n if (!$(element).hasClass(ClassName.FADE)) {\n this._destroyElement(element)\n return\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(element)\n\n $(element)\n .one(Util.TRANSITION_END, (event) => this._destroyElement(element, event))\n .emulateTransitionEnd(transitionDuration)\n }\n\n _destroyElement(element) {\n $(element)\n .detach()\n .trigger(Event.CLOSED)\n .remove()\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $element = $(this)\n let data = $element.data(DATA_KEY)\n\n if (!data) {\n data = new Alert(this)\n $element.data(DATA_KEY, data)\n }\n\n if (config === 'close') {\n data[config](this)\n }\n })\n }\n\n static _handleDismiss(alertInstance) {\n return function (event) {\n if (event) {\n event.preventDefault()\n }\n\n alertInstance.close(this)\n }\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(\n Event.CLICK_DATA_API,\n Selector.DISMISS,\n Alert._handleDismiss(new Alert())\n)\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Alert._jQueryInterface\n$.fn[NAME].Constructor = Alert\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Alert._jQueryInterface\n}\n\nexport default Alert\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): button.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'button'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.button'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst ClassName = {\n ACTIVE : 'active',\n BUTTON : 'btn',\n FOCUS : 'focus'\n}\n\nconst Selector = {\n DATA_TOGGLE_CARROT : '[data-toggle^=\"button\"]',\n DATA_TOGGLE : '[data-toggle=\"buttons\"]',\n INPUT : 'input:not([type=\"hidden\"])',\n ACTIVE : '.active',\n BUTTON : '.btn'\n}\n\nconst Event = {\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n FOCUS_BLUR_DATA_API : `focus${EVENT_KEY}${DATA_API_KEY} ` +\n `blur${EVENT_KEY}${DATA_API_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Button {\n constructor(element) {\n this._element = element\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n // Public\n\n toggle() {\n let triggerChangeEvent = true\n let addAriaPressed = true\n const rootElement = $(this._element).closest(\n Selector.DATA_TOGGLE\n )[0]\n\n if (rootElement) {\n const input = this._element.querySelector(Selector.INPUT)\n\n if (input) {\n if (input.type === 'radio') {\n if (input.checked &&\n this._element.classList.contains(ClassName.ACTIVE)) {\n triggerChangeEvent = false\n } else {\n const activeElement = rootElement.querySelector(Selector.ACTIVE)\n\n if (activeElement) {\n $(activeElement).removeClass(ClassName.ACTIVE)\n }\n }\n }\n\n if (triggerChangeEvent) {\n if (input.hasAttribute('disabled') ||\n rootElement.hasAttribute('disabled') ||\n input.classList.contains('disabled') ||\n rootElement.classList.contains('disabled')) {\n return\n }\n input.checked = !this._element.classList.contains(ClassName.ACTIVE)\n $(input).trigger('change')\n }\n\n input.focus()\n addAriaPressed = false\n }\n }\n\n if (addAriaPressed) {\n this._element.setAttribute('aria-pressed',\n !this._element.classList.contains(ClassName.ACTIVE))\n }\n\n if (triggerChangeEvent) {\n $(this._element).toggleClass(ClassName.ACTIVE)\n }\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n this._element = null\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n\n if (!data) {\n data = new Button(this)\n $(this).data(DATA_KEY, data)\n }\n\n if (config === 'toggle') {\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n event.preventDefault()\n\n let button = event.target\n\n if (!$(button).hasClass(ClassName.BUTTON)) {\n button = $(button).closest(Selector.BUTTON)\n }\n\n Button._jQueryInterface.call($(button), 'toggle')\n })\n .on(Event.FOCUS_BLUR_DATA_API, Selector.DATA_TOGGLE_CARROT, (event) => {\n const button = $(event.target).closest(Selector.BUTTON)[0]\n $(button).toggleClass(ClassName.FOCUS, /^focus(in)?$/.test(event.type))\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Button._jQueryInterface\n$.fn[NAME].Constructor = Button\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Button._jQueryInterface\n}\n\nexport default Button\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): carousel.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'carousel'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.carousel'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ARROW_LEFT_KEYCODE = 37 // KeyboardEvent.which value for left arrow key\nconst ARROW_RIGHT_KEYCODE = 39 // KeyboardEvent.which value for right arrow key\nconst TOUCHEVENT_COMPAT_WAIT = 500 // Time for mouse compat events to fire after touch\nconst SWIPE_THRESHOLD = 40\n\nconst Default = {\n interval : 5000,\n keyboard : true,\n slide : false,\n pause : 'hover',\n wrap : true,\n touch : true\n}\n\nconst DefaultType = {\n interval : '(number|boolean)',\n keyboard : 'boolean',\n slide : '(boolean|string)',\n pause : '(string|boolean)',\n wrap : 'boolean',\n touch : 'boolean'\n}\n\nconst Direction = {\n NEXT : 'next',\n PREV : 'prev',\n LEFT : 'left',\n RIGHT : 'right'\n}\n\nconst Event = {\n SLIDE : `slide${EVENT_KEY}`,\n SLID : `slid${EVENT_KEY}`,\n KEYDOWN : `keydown${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`,\n TOUCHSTART : `touchstart${EVENT_KEY}`,\n TOUCHMOVE : `touchmove${EVENT_KEY}`,\n TOUCHEND : `touchend${EVENT_KEY}`,\n POINTERDOWN : `pointerdown${EVENT_KEY}`,\n POINTERUP : `pointerup${EVENT_KEY}`,\n DRAG_START : `dragstart${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n CAROUSEL : 'carousel',\n ACTIVE : 'active',\n SLIDE : 'slide',\n RIGHT : 'carousel-item-right',\n LEFT : 'carousel-item-left',\n NEXT : 'carousel-item-next',\n PREV : 'carousel-item-prev',\n ITEM : 'carousel-item',\n POINTER_EVENT : 'pointer-event'\n}\n\nconst Selector = {\n ACTIVE : '.active',\n ACTIVE_ITEM : '.active.carousel-item',\n ITEM : '.carousel-item',\n ITEM_IMG : '.carousel-item img',\n NEXT_PREV : '.carousel-item-next, .carousel-item-prev',\n INDICATORS : '.carousel-indicators',\n DATA_SLIDE : '[data-slide], [data-slide-to]',\n DATA_RIDE : '[data-ride=\"carousel\"]'\n}\n\nconst PointerType = {\n TOUCH : 'touch',\n PEN : 'pen'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\nclass Carousel {\n constructor(element, config) {\n this._items = null\n this._interval = null\n this._activeElement = null\n this._isPaused = false\n this._isSliding = false\n this.touchTimeout = null\n this.touchStartX = 0\n this.touchDeltaX = 0\n\n this._config = this._getConfig(config)\n this._element = element\n this._indicatorsElement = this._element.querySelector(Selector.INDICATORS)\n this._touchSupported = 'ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0\n this._pointerEvent = Boolean(window.PointerEvent || window.MSPointerEvent)\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n next() {\n if (!this._isSliding) {\n this._slide(Direction.NEXT)\n }\n }\n\n nextWhenVisible() {\n // Don't call next when the page isn't visible\n // or the carousel or its parent isn't visible\n if (!document.hidden &&\n ($(this._element).is(':visible') && $(this._element).css('visibility') !== 'hidden')) {\n this.next()\n }\n }\n\n prev() {\n if (!this._isSliding) {\n this._slide(Direction.PREV)\n }\n }\n\n pause(event) {\n if (!event) {\n this._isPaused = true\n }\n\n if (this._element.querySelector(Selector.NEXT_PREV)) {\n Util.triggerTransitionEnd(this._element)\n this.cycle(true)\n }\n\n clearInterval(this._interval)\n this._interval = null\n }\n\n cycle(event) {\n if (!event) {\n this._isPaused = false\n }\n\n if (this._interval) {\n clearInterval(this._interval)\n this._interval = null\n }\n\n if (this._config.interval && !this._isPaused) {\n this._interval = setInterval(\n (document.visibilityState ? this.nextWhenVisible : this.next).bind(this),\n this._config.interval\n )\n }\n }\n\n to(index) {\n this._activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n\n const activeIndex = this._getItemIndex(this._activeElement)\n\n if (index > this._items.length - 1 || index < 0) {\n return\n }\n\n if (this._isSliding) {\n $(this._element).one(Event.SLID, () => this.to(index))\n return\n }\n\n if (activeIndex === index) {\n this.pause()\n this.cycle()\n return\n }\n\n const direction = index > activeIndex\n ? Direction.NEXT\n : Direction.PREV\n\n this._slide(direction, this._items[index])\n }\n\n dispose() {\n $(this._element).off(EVENT_KEY)\n $.removeData(this._element, DATA_KEY)\n\n this._items = null\n this._config = null\n this._element = null\n this._interval = null\n this._isPaused = null\n this._isSliding = null\n this._activeElement = null\n this._indicatorsElement = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _handleSwipe() {\n const absDeltax = Math.abs(this.touchDeltaX)\n\n if (absDeltax <= SWIPE_THRESHOLD) {\n return\n }\n\n const direction = absDeltax / this.touchDeltaX\n\n // swipe left\n if (direction > 0) {\n this.prev()\n }\n\n // swipe right\n if (direction < 0) {\n this.next()\n }\n }\n\n _addEventListeners() {\n if (this._config.keyboard) {\n $(this._element)\n .on(Event.KEYDOWN, (event) => this._keydown(event))\n }\n\n if (this._config.pause === 'hover') {\n $(this._element)\n .on(Event.MOUSEENTER, (event) => this.pause(event))\n .on(Event.MOUSELEAVE, (event) => this.cycle(event))\n }\n\n this._addTouchEventListeners()\n }\n\n _addTouchEventListeners() {\n if (!this._touchSupported) {\n return\n }\n\n const start = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchStartX = event.originalEvent.clientX\n } else if (!this._pointerEvent) {\n this.touchStartX = event.originalEvent.touches[0].clientX\n }\n }\n\n const move = (event) => {\n // ensure swiping with one touch and not pinching\n if (event.originalEvent.touches && event.originalEvent.touches.length > 1) {\n this.touchDeltaX = 0\n } else {\n this.touchDeltaX = event.originalEvent.touches[0].clientX - this.touchStartX\n }\n }\n\n const end = (event) => {\n if (this._pointerEvent && PointerType[event.originalEvent.pointerType.toUpperCase()]) {\n this.touchDeltaX = event.originalEvent.clientX - this.touchStartX\n }\n\n this._handleSwipe()\n if (this._config.pause === 'hover') {\n // If it's a touch-enabled device, mouseenter/leave are fired as\n // part of the mouse compatibility events on first tap - the carousel\n // would stop cycling until user tapped out of it;\n // here, we listen for touchend, explicitly pause the carousel\n // (as if it's the second time we tap on it, mouseenter compat event\n // is NOT fired) and after a timeout (to allow for mouse compatibility\n // events to fire) we explicitly restart cycling\n\n this.pause()\n if (this.touchTimeout) {\n clearTimeout(this.touchTimeout)\n }\n this.touchTimeout = setTimeout((event) => this.cycle(event), TOUCHEVENT_COMPAT_WAIT + this._config.interval)\n }\n }\n\n $(this._element.querySelectorAll(Selector.ITEM_IMG)).on(Event.DRAG_START, (e) => e.preventDefault())\n if (this._pointerEvent) {\n $(this._element).on(Event.POINTERDOWN, (event) => start(event))\n $(this._element).on(Event.POINTERUP, (event) => end(event))\n\n this._element.classList.add(ClassName.POINTER_EVENT)\n } else {\n $(this._element).on(Event.TOUCHSTART, (event) => start(event))\n $(this._element).on(Event.TOUCHMOVE, (event) => move(event))\n $(this._element).on(Event.TOUCHEND, (event) => end(event))\n }\n }\n\n _keydown(event) {\n if (/input|textarea/i.test(event.target.tagName)) {\n return\n }\n\n switch (event.which) {\n case ARROW_LEFT_KEYCODE:\n event.preventDefault()\n this.prev()\n break\n case ARROW_RIGHT_KEYCODE:\n event.preventDefault()\n this.next()\n break\n default:\n }\n }\n\n _getItemIndex(element) {\n this._items = element && element.parentNode\n ? [].slice.call(element.parentNode.querySelectorAll(Selector.ITEM))\n : []\n return this._items.indexOf(element)\n }\n\n _getItemByDirection(direction, activeElement) {\n const isNextDirection = direction === Direction.NEXT\n const isPrevDirection = direction === Direction.PREV\n const activeIndex = this._getItemIndex(activeElement)\n const lastItemIndex = this._items.length - 1\n const isGoingToWrap = isPrevDirection && activeIndex === 0 ||\n isNextDirection && activeIndex === lastItemIndex\n\n if (isGoingToWrap && !this._config.wrap) {\n return activeElement\n }\n\n const delta = direction === Direction.PREV ? -1 : 1\n const itemIndex = (activeIndex + delta) % this._items.length\n\n return itemIndex === -1\n ? this._items[this._items.length - 1] : this._items[itemIndex]\n }\n\n _triggerSlideEvent(relatedTarget, eventDirectionName) {\n const targetIndex = this._getItemIndex(relatedTarget)\n const fromIndex = this._getItemIndex(this._element.querySelector(Selector.ACTIVE_ITEM))\n const slideEvent = $.Event(Event.SLIDE, {\n relatedTarget,\n direction: eventDirectionName,\n from: fromIndex,\n to: targetIndex\n })\n\n $(this._element).trigger(slideEvent)\n\n return slideEvent\n }\n\n _setActiveIndicatorElement(element) {\n if (this._indicatorsElement) {\n const indicators = [].slice.call(this._indicatorsElement.querySelectorAll(Selector.ACTIVE))\n $(indicators)\n .removeClass(ClassName.ACTIVE)\n\n const nextIndicator = this._indicatorsElement.children[\n this._getItemIndex(element)\n ]\n\n if (nextIndicator) {\n $(nextIndicator).addClass(ClassName.ACTIVE)\n }\n }\n }\n\n _slide(direction, element) {\n const activeElement = this._element.querySelector(Selector.ACTIVE_ITEM)\n const activeElementIndex = this._getItemIndex(activeElement)\n const nextElement = element || activeElement &&\n this._getItemByDirection(direction, activeElement)\n const nextElementIndex = this._getItemIndex(nextElement)\n const isCycling = Boolean(this._interval)\n\n let directionalClassName\n let orderClassName\n let eventDirectionName\n\n if (direction === Direction.NEXT) {\n directionalClassName = ClassName.LEFT\n orderClassName = ClassName.NEXT\n eventDirectionName = Direction.LEFT\n } else {\n directionalClassName = ClassName.RIGHT\n orderClassName = ClassName.PREV\n eventDirectionName = Direction.RIGHT\n }\n\n if (nextElement && $(nextElement).hasClass(ClassName.ACTIVE)) {\n this._isSliding = false\n return\n }\n\n const slideEvent = this._triggerSlideEvent(nextElement, eventDirectionName)\n if (slideEvent.isDefaultPrevented()) {\n return\n }\n\n if (!activeElement || !nextElement) {\n // Some weirdness is happening, so we bail\n return\n }\n\n this._isSliding = true\n\n if (isCycling) {\n this.pause()\n }\n\n this._setActiveIndicatorElement(nextElement)\n\n const slidEvent = $.Event(Event.SLID, {\n relatedTarget: nextElement,\n direction: eventDirectionName,\n from: activeElementIndex,\n to: nextElementIndex\n })\n\n if ($(this._element).hasClass(ClassName.SLIDE)) {\n $(nextElement).addClass(orderClassName)\n\n Util.reflow(nextElement)\n\n $(activeElement).addClass(directionalClassName)\n $(nextElement).addClass(directionalClassName)\n\n const nextElementInterval = parseInt(nextElement.getAttribute('data-interval'), 10)\n if (nextElementInterval) {\n this._config.defaultInterval = this._config.defaultInterval || this._config.interval\n this._config.interval = nextElementInterval\n } else {\n this._config.interval = this._config.defaultInterval || this._config.interval\n }\n\n const transitionDuration = Util.getTransitionDurationFromElement(activeElement)\n\n $(activeElement)\n .one(Util.TRANSITION_END, () => {\n $(nextElement)\n .removeClass(`${directionalClassName} ${orderClassName}`)\n .addClass(ClassName.ACTIVE)\n\n $(activeElement).removeClass(`${ClassName.ACTIVE} ${orderClassName} ${directionalClassName}`)\n\n this._isSliding = false\n\n setTimeout(() => $(this._element).trigger(slidEvent), 0)\n })\n .emulateTransitionEnd(transitionDuration)\n } else {\n $(activeElement).removeClass(ClassName.ACTIVE)\n $(nextElement).addClass(ClassName.ACTIVE)\n\n this._isSliding = false\n $(this._element).trigger(slidEvent)\n }\n\n if (isCycling) {\n this.cycle()\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n let _config = {\n ...Default,\n ...$(this).data()\n }\n\n if (typeof config === 'object') {\n _config = {\n ..._config,\n ...config\n }\n }\n\n const action = typeof config === 'string' ? config : _config.slide\n\n if (!data) {\n data = new Carousel(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'number') {\n data.to(config)\n } else if (typeof action === 'string') {\n if (typeof data[action] === 'undefined') {\n throw new TypeError(`No method named \"${action}\"`)\n }\n data[action]()\n } else if (_config.interval) {\n data.pause()\n data.cycle()\n }\n })\n }\n\n static _dataApiClickHandler(event) {\n const selector = Util.getSelectorFromElement(this)\n\n if (!selector) {\n return\n }\n\n const target = $(selector)[0]\n\n if (!target || !$(target).hasClass(ClassName.CAROUSEL)) {\n return\n }\n\n const config = {\n ...$(target).data(),\n ...$(this).data()\n }\n const slideIndex = this.getAttribute('data-slide-to')\n\n if (slideIndex) {\n config.interval = false\n }\n\n Carousel._jQueryInterface.call($(target), config)\n\n if (slideIndex) {\n $(target).data(DATA_KEY).to(slideIndex)\n }\n\n event.preventDefault()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.CLICK_DATA_API, Selector.DATA_SLIDE, Carousel._dataApiClickHandler)\n\n$(window).on(Event.LOAD_DATA_API, () => {\n const carousels = [].slice.call(document.querySelectorAll(Selector.DATA_RIDE))\n for (let i = 0, len = carousels.length; i < len; i++) {\n const $carousel = $(carousels[i])\n Carousel._jQueryInterface.call($carousel, $carousel.data())\n }\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Carousel._jQueryInterface\n$.fn[NAME].Constructor = Carousel\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Carousel._jQueryInterface\n}\n\nexport default Carousel\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): collapse.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'collapse'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.collapse'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n toggle : true,\n parent : ''\n}\n\nconst DefaultType = {\n toggle : 'boolean',\n parent : '(string|element)'\n}\n\nconst Event = {\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SHOW : 'show',\n COLLAPSE : 'collapse',\n COLLAPSING : 'collapsing',\n COLLAPSED : 'collapsed'\n}\n\nconst Dimension = {\n WIDTH : 'width',\n HEIGHT : 'height'\n}\n\nconst Selector = {\n ACTIVES : '.show, .collapsing',\n DATA_TOGGLE : '[data-toggle=\"collapse\"]'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Collapse {\n constructor(element, config) {\n this._isTransitioning = false\n this._element = element\n this._config = this._getConfig(config)\n this._triggerArray = [].slice.call(document.querySelectorAll(\n `[data-toggle=\"collapse\"][href=\"#${element.id}\"],` +\n `[data-toggle=\"collapse\"][data-target=\"#${element.id}\"]`\n ))\n\n const toggleList = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n for (let i = 0, len = toggleList.length; i < len; i++) {\n const elem = toggleList[i]\n const selector = Util.getSelectorFromElement(elem)\n const filterElement = [].slice.call(document.querySelectorAll(selector))\n .filter((foundElem) => foundElem === element)\n\n if (selector !== null && filterElement.length > 0) {\n this._selector = selector\n this._triggerArray.push(elem)\n }\n }\n\n this._parent = this._config.parent ? this._getParent() : null\n\n if (!this._config.parent) {\n this._addAriaAndCollapsedClass(this._element, this._triggerArray)\n }\n\n if (this._config.toggle) {\n this.toggle()\n }\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle() {\n if ($(this._element).hasClass(ClassName.SHOW)) {\n this.hide()\n } else {\n this.show()\n }\n }\n\n show() {\n if (this._isTransitioning ||\n $(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n let actives\n let activesData\n\n if (this._parent) {\n actives = [].slice.call(this._parent.querySelectorAll(Selector.ACTIVES))\n .filter((elem) => {\n if (typeof this._config.parent === 'string') {\n return elem.getAttribute('data-parent') === this._config.parent\n }\n\n return elem.classList.contains(ClassName.COLLAPSE)\n })\n\n if (actives.length === 0) {\n actives = null\n }\n }\n\n if (actives) {\n activesData = $(actives).not(this._selector).data(DATA_KEY)\n if (activesData && activesData._isTransitioning) {\n return\n }\n }\n\n const startEvent = $.Event(Event.SHOW)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n if (actives) {\n Collapse._jQueryInterface.call($(actives).not(this._selector), 'hide')\n if (!activesData) {\n $(actives).data(DATA_KEY, null)\n }\n }\n\n const dimension = this._getDimension()\n\n $(this._element)\n .removeClass(ClassName.COLLAPSE)\n .addClass(ClassName.COLLAPSING)\n\n this._element.style[dimension] = 0\n\n if (this._triggerArray.length) {\n $(this._triggerArray)\n .removeClass(ClassName.COLLAPSED)\n .attr('aria-expanded', true)\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .addClass(ClassName.SHOW)\n\n this._element.style[dimension] = ''\n\n this.setTransitioning(false)\n\n $(this._element).trigger(Event.SHOWN)\n }\n\n const capitalizedDimension = dimension[0].toUpperCase() + dimension.slice(1)\n const scrollSize = `scroll${capitalizedDimension}`\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n\n this._element.style[dimension] = `${this._element[scrollSize]}px`\n }\n\n hide() {\n if (this._isTransitioning ||\n !$(this._element).hasClass(ClassName.SHOW)) {\n return\n }\n\n const startEvent = $.Event(Event.HIDE)\n $(this._element).trigger(startEvent)\n if (startEvent.isDefaultPrevented()) {\n return\n }\n\n const dimension = this._getDimension()\n\n this._element.style[dimension] = `${this._element.getBoundingClientRect()[dimension]}px`\n\n Util.reflow(this._element)\n\n $(this._element)\n .addClass(ClassName.COLLAPSING)\n .removeClass(ClassName.COLLAPSE)\n .removeClass(ClassName.SHOW)\n\n const triggerArrayLength = this._triggerArray.length\n if (triggerArrayLength > 0) {\n for (let i = 0; i < triggerArrayLength; i++) {\n const trigger = this._triggerArray[i]\n const selector = Util.getSelectorFromElement(trigger)\n\n if (selector !== null) {\n const $elem = $([].slice.call(document.querySelectorAll(selector)))\n if (!$elem.hasClass(ClassName.SHOW)) {\n $(trigger).addClass(ClassName.COLLAPSED)\n .attr('aria-expanded', false)\n }\n }\n }\n }\n\n this.setTransitioning(true)\n\n const complete = () => {\n this.setTransitioning(false)\n $(this._element)\n .removeClass(ClassName.COLLAPSING)\n .addClass(ClassName.COLLAPSE)\n .trigger(Event.HIDDEN)\n }\n\n this._element.style[dimension] = ''\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n }\n\n setTransitioning(isTransitioning) {\n this._isTransitioning = isTransitioning\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._parent = null\n this._element = null\n this._triggerArray = null\n this._isTransitioning = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n config.toggle = Boolean(config.toggle) // Coerce string values\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _getDimension() {\n const hasWidth = $(this._element).hasClass(Dimension.WIDTH)\n return hasWidth ? Dimension.WIDTH : Dimension.HEIGHT\n }\n\n _getParent() {\n let parent\n\n if (Util.isElement(this._config.parent)) {\n parent = this._config.parent\n\n // It's a jQuery object\n if (typeof this._config.parent.jquery !== 'undefined') {\n parent = this._config.parent[0]\n }\n } else {\n parent = document.querySelector(this._config.parent)\n }\n\n const selector =\n `[data-toggle=\"collapse\"][data-parent=\"${this._config.parent}\"]`\n\n const children = [].slice.call(parent.querySelectorAll(selector))\n $(children).each((i, element) => {\n this._addAriaAndCollapsedClass(\n Collapse._getTargetFromElement(element),\n [element]\n )\n })\n\n return parent\n }\n\n _addAriaAndCollapsedClass(element, triggerArray) {\n const isOpen = $(element).hasClass(ClassName.SHOW)\n\n if (triggerArray.length) {\n $(triggerArray)\n .toggleClass(ClassName.COLLAPSED, !isOpen)\n .attr('aria-expanded', isOpen)\n }\n }\n\n // Static\n\n static _getTargetFromElement(element) {\n const selector = Util.getSelectorFromElement(element)\n return selector ? document.querySelector(selector) : null\n }\n\n static _jQueryInterface(config) {\n return this.each(function () {\n const $this = $(this)\n let data = $this.data(DATA_KEY)\n const _config = {\n ...Default,\n ...$this.data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data && _config.toggle && /show|hide/.test(config)) {\n _config.toggle = false\n }\n\n if (!data) {\n data = new Collapse(this, _config)\n $this.data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n // preventDefault only for elements (which change the URL) not inside the collapsible element\n if (event.currentTarget.tagName === 'A') {\n event.preventDefault()\n }\n\n const $trigger = $(this)\n const selector = Util.getSelectorFromElement(this)\n const selectors = [].slice.call(document.querySelectorAll(selector))\n\n $(selectors).each(function () {\n const $target = $(this)\n const data = $target.data(DATA_KEY)\n const config = data ? 'toggle' : $trigger.data()\n Collapse._jQueryInterface.call($target, config)\n })\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Collapse._jQueryInterface\n$.fn[NAME].Constructor = Collapse\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Collapse._jQueryInterface\n}\n\nexport default Collapse\n","/**!\n * @fileOverview Kickass library to create and place poppers near their reference elements.\n * @version 1.14.6\n * @license\n * Copyright (c) 2016 Federico Zivolo and contributors\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\nvar isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined';\n\nvar longerTimeoutBrowsers = ['Edge', 'Trident', 'Firefox'];\nvar timeoutDuration = 0;\nfor (var i = 0; i < longerTimeoutBrowsers.length; i += 1) {\n if (isBrowser && navigator.userAgent.indexOf(longerTimeoutBrowsers[i]) >= 0) {\n timeoutDuration = 1;\n break;\n }\n}\n\nfunction microtaskDebounce(fn) {\n var called = false;\n return function () {\n if (called) {\n return;\n }\n called = true;\n window.Promise.resolve().then(function () {\n called = false;\n fn();\n });\n };\n}\n\nfunction taskDebounce(fn) {\n var scheduled = false;\n return function () {\n if (!scheduled) {\n scheduled = true;\n setTimeout(function () {\n scheduled = false;\n fn();\n }, timeoutDuration);\n }\n };\n}\n\nvar supportsMicroTasks = isBrowser && window.Promise;\n\n/**\n* Create a debounced version of a method, that's asynchronously deferred\n* but called in the minimum time possible.\n*\n* @method\n* @memberof Popper.Utils\n* @argument {Function} fn\n* @returns {Function}\n*/\nvar debounce = supportsMicroTasks ? microtaskDebounce : taskDebounce;\n\n/**\n * Check if the given variable is a function\n * @method\n * @memberof Popper.Utils\n * @argument {Any} functionToCheck - variable to check\n * @returns {Boolean} answer to: is a function?\n */\nfunction isFunction(functionToCheck) {\n var getType = {};\n return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';\n}\n\n/**\n * Get CSS computed property of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Eement} element\n * @argument {String} property\n */\nfunction getStyleComputedProperty(element, property) {\n if (element.nodeType !== 1) {\n return [];\n }\n // NOTE: 1 DOM access here\n var window = element.ownerDocument.defaultView;\n var css = window.getComputedStyle(element, null);\n return property ? css[property] : css;\n}\n\n/**\n * Returns the parentNode or the host of the element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} parent\n */\nfunction getParentNode(element) {\n if (element.nodeName === 'HTML') {\n return element;\n }\n return element.parentNode || element.host;\n}\n\n/**\n * Returns the scrolling parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} scroll parent\n */\nfunction getScrollParent(element) {\n // Return body, `getScroll` will take care to get the correct `scrollTop` from it\n if (!element) {\n return document.body;\n }\n\n switch (element.nodeName) {\n case 'HTML':\n case 'BODY':\n return element.ownerDocument.body;\n case '#document':\n return element.body;\n }\n\n // Firefox want us to check `-x` and `-y` variations as well\n\n var _getStyleComputedProp = getStyleComputedProperty(element),\n overflow = _getStyleComputedProp.overflow,\n overflowX = _getStyleComputedProp.overflowX,\n overflowY = _getStyleComputedProp.overflowY;\n\n if (/(auto|scroll|overlay)/.test(overflow + overflowY + overflowX)) {\n return element;\n }\n\n return getScrollParent(getParentNode(element));\n}\n\nvar isIE11 = isBrowser && !!(window.MSInputMethodContext && document.documentMode);\nvar isIE10 = isBrowser && /MSIE 10/.test(navigator.userAgent);\n\n/**\n * Determines if the browser is Internet Explorer\n * @method\n * @memberof Popper.Utils\n * @param {Number} version to check\n * @returns {Boolean} isIE\n */\nfunction isIE(version) {\n if (version === 11) {\n return isIE11;\n }\n if (version === 10) {\n return isIE10;\n }\n return isIE11 || isIE10;\n}\n\n/**\n * Returns the offset parent of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} offset parent\n */\nfunction getOffsetParent(element) {\n if (!element) {\n return document.documentElement;\n }\n\n var noOffsetParent = isIE(10) ? document.body : null;\n\n // NOTE: 1 DOM access here\n var offsetParent = element.offsetParent || null;\n // Skip hidden elements which don't have an offsetParent\n while (offsetParent === noOffsetParent && element.nextElementSibling) {\n offsetParent = (element = element.nextElementSibling).offsetParent;\n }\n\n var nodeName = offsetParent && offsetParent.nodeName;\n\n if (!nodeName || nodeName === 'BODY' || nodeName === 'HTML') {\n return element ? element.ownerDocument.documentElement : document.documentElement;\n }\n\n // .offsetParent will return the closest TH, TD or TABLE in case\n // no offsetParent is present, I hate this job...\n if (['TH', 'TD', 'TABLE'].indexOf(offsetParent.nodeName) !== -1 && getStyleComputedProperty(offsetParent, 'position') === 'static') {\n return getOffsetParent(offsetParent);\n }\n\n return offsetParent;\n}\n\nfunction isOffsetContainer(element) {\n var nodeName = element.nodeName;\n\n if (nodeName === 'BODY') {\n return false;\n }\n return nodeName === 'HTML' || getOffsetParent(element.firstElementChild) === element;\n}\n\n/**\n * Finds the root node (document, shadowDOM root) of the given element\n * @method\n * @memberof Popper.Utils\n * @argument {Element} node\n * @returns {Element} root node\n */\nfunction getRoot(node) {\n if (node.parentNode !== null) {\n return getRoot(node.parentNode);\n }\n\n return node;\n}\n\n/**\n * Finds the offset parent common to the two provided nodes\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element1\n * @argument {Element} element2\n * @returns {Element} common offset parent\n */\nfunction findCommonOffsetParent(element1, element2) {\n // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n if (!element1 || !element1.nodeType || !element2 || !element2.nodeType) {\n return document.documentElement;\n }\n\n // Here we make sure to give as \"start\" the element that comes first in the DOM\n var order = element1.compareDocumentPosition(element2) & Node.DOCUMENT_POSITION_FOLLOWING;\n var start = order ? element1 : element2;\n var end = order ? element2 : element1;\n\n // Get common ancestor container\n var range = document.createRange();\n range.setStart(start, 0);\n range.setEnd(end, 0);\n var commonAncestorContainer = range.commonAncestorContainer;\n\n // Both nodes are inside #document\n\n if (element1 !== commonAncestorContainer && element2 !== commonAncestorContainer || start.contains(end)) {\n if (isOffsetContainer(commonAncestorContainer)) {\n return commonAncestorContainer;\n }\n\n return getOffsetParent(commonAncestorContainer);\n }\n\n // one of the nodes is inside shadowDOM, find which one\n var element1root = getRoot(element1);\n if (element1root.host) {\n return findCommonOffsetParent(element1root.host, element2);\n } else {\n return findCommonOffsetParent(element1, getRoot(element2).host);\n }\n}\n\n/**\n * Gets the scroll value of the given element in the given side (top and left)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {String} side `top` or `left`\n * @returns {number} amount of scrolled pixels\n */\nfunction getScroll(element) {\n var side = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'top';\n\n var upperSide = side === 'top' ? 'scrollTop' : 'scrollLeft';\n var nodeName = element.nodeName;\n\n if (nodeName === 'BODY' || nodeName === 'HTML') {\n var html = element.ownerDocument.documentElement;\n var scrollingElement = element.ownerDocument.scrollingElement || html;\n return scrollingElement[upperSide];\n }\n\n return element[upperSide];\n}\n\n/*\n * Sum or subtract the element scroll values (left and top) from a given rect object\n * @method\n * @memberof Popper.Utils\n * @param {Object} rect - Rect object you want to change\n * @param {HTMLElement} element - The element from the function reads the scroll values\n * @param {Boolean} subtract - set to true if you want to subtract the scroll values\n * @return {Object} rect - The modifier rect object\n */\nfunction includeScroll(rect, element) {\n var subtract = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n\n var scrollTop = getScroll(element, 'top');\n var scrollLeft = getScroll(element, 'left');\n var modifier = subtract ? -1 : 1;\n rect.top += scrollTop * modifier;\n rect.bottom += scrollTop * modifier;\n rect.left += scrollLeft * modifier;\n rect.right += scrollLeft * modifier;\n return rect;\n}\n\n/*\n * Helper to detect borders of a given element\n * @method\n * @memberof Popper.Utils\n * @param {CSSStyleDeclaration} styles\n * Result of `getStyleComputedProperty` on the given element\n * @param {String} axis - `x` or `y`\n * @return {number} borders - The borders size of the given axis\n */\n\nfunction getBordersSize(styles, axis) {\n var sideA = axis === 'x' ? 'Left' : 'Top';\n var sideB = sideA === 'Left' ? 'Right' : 'Bottom';\n\n return parseFloat(styles['border' + sideA + 'Width'], 10) + parseFloat(styles['border' + sideB + 'Width'], 10);\n}\n\nfunction getSize(axis, body, html, computedStyle) {\n return Math.max(body['offset' + axis], body['scroll' + axis], html['client' + axis], html['offset' + axis], html['scroll' + axis], isIE(10) ? parseInt(html['offset' + axis]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Top' : 'Left')]) + parseInt(computedStyle['margin' + (axis === 'Height' ? 'Bottom' : 'Right')]) : 0);\n}\n\nfunction getWindowSizes(document) {\n var body = document.body;\n var html = document.documentElement;\n var computedStyle = isIE(10) && getComputedStyle(html);\n\n return {\n height: getSize('Height', body, html, computedStyle),\n width: getSize('Width', body, html, computedStyle)\n };\n}\n\nvar classCallCheck = function (instance, Constructor) {\n if (!(instance instanceof Constructor)) {\n throw new TypeError(\"Cannot call a class as a function\");\n }\n};\n\nvar createClass = function () {\n function defineProperties(target, props) {\n for (var i = 0; i < props.length; i++) {\n var descriptor = props[i];\n descriptor.enumerable = descriptor.enumerable || false;\n descriptor.configurable = true;\n if (\"value\" in descriptor) descriptor.writable = true;\n Object.defineProperty(target, descriptor.key, descriptor);\n }\n }\n\n return function (Constructor, protoProps, staticProps) {\n if (protoProps) defineProperties(Constructor.prototype, protoProps);\n if (staticProps) defineProperties(Constructor, staticProps);\n return Constructor;\n };\n}();\n\n\n\n\n\nvar defineProperty = function (obj, key, value) {\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n\n return obj;\n};\n\nvar _extends = Object.assign || function (target) {\n for (var i = 1; i < arguments.length; i++) {\n var source = arguments[i];\n\n for (var key in source) {\n if (Object.prototype.hasOwnProperty.call(source, key)) {\n target[key] = source[key];\n }\n }\n }\n\n return target;\n};\n\n/**\n * Given element offsets, generate an output similar to getBoundingClientRect\n * @method\n * @memberof Popper.Utils\n * @argument {Object} offsets\n * @returns {Object} ClientRect like output\n */\nfunction getClientRect(offsets) {\n return _extends({}, offsets, {\n right: offsets.left + offsets.width,\n bottom: offsets.top + offsets.height\n });\n}\n\n/**\n * Get bounding client rect of given element\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} element\n * @return {Object} client rect\n */\nfunction getBoundingClientRect(element) {\n var rect = {};\n\n // IE10 10 FIX: Please, don't ask, the element isn't\n // considered in DOM in some circumstances...\n // This isn't reproducible in IE10 compatibility mode of IE11\n try {\n if (isIE(10)) {\n rect = element.getBoundingClientRect();\n var scrollTop = getScroll(element, 'top');\n var scrollLeft = getScroll(element, 'left');\n rect.top += scrollTop;\n rect.left += scrollLeft;\n rect.bottom += scrollTop;\n rect.right += scrollLeft;\n } else {\n rect = element.getBoundingClientRect();\n }\n } catch (e) {}\n\n var result = {\n left: rect.left,\n top: rect.top,\n width: rect.right - rect.left,\n height: rect.bottom - rect.top\n };\n\n // subtract scrollbar size from sizes\n var sizes = element.nodeName === 'HTML' ? getWindowSizes(element.ownerDocument) : {};\n var width = sizes.width || element.clientWidth || result.right - result.left;\n var height = sizes.height || element.clientHeight || result.bottom - result.top;\n\n var horizScrollbar = element.offsetWidth - width;\n var vertScrollbar = element.offsetHeight - height;\n\n // if an hypothetical scrollbar is detected, we must be sure it's not a `border`\n // we make this check conditional for performance reasons\n if (horizScrollbar || vertScrollbar) {\n var styles = getStyleComputedProperty(element);\n horizScrollbar -= getBordersSize(styles, 'x');\n vertScrollbar -= getBordersSize(styles, 'y');\n\n result.width -= horizScrollbar;\n result.height -= vertScrollbar;\n }\n\n return getClientRect(result);\n}\n\nfunction getOffsetRectRelativeToArbitraryNode(children, parent) {\n var fixedPosition = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;\n\n var isIE10 = isIE(10);\n var isHTML = parent.nodeName === 'HTML';\n var childrenRect = getBoundingClientRect(children);\n var parentRect = getBoundingClientRect(parent);\n var scrollParent = getScrollParent(children);\n\n var styles = getStyleComputedProperty(parent);\n var borderTopWidth = parseFloat(styles.borderTopWidth, 10);\n var borderLeftWidth = parseFloat(styles.borderLeftWidth, 10);\n\n // In cases where the parent is fixed, we must ignore negative scroll in offset calc\n if (fixedPosition && isHTML) {\n parentRect.top = Math.max(parentRect.top, 0);\n parentRect.left = Math.max(parentRect.left, 0);\n }\n var offsets = getClientRect({\n top: childrenRect.top - parentRect.top - borderTopWidth,\n left: childrenRect.left - parentRect.left - borderLeftWidth,\n width: childrenRect.width,\n height: childrenRect.height\n });\n offsets.marginTop = 0;\n offsets.marginLeft = 0;\n\n // Subtract margins of documentElement in case it's being used as parent\n // we do this only on HTML because it's the only element that behaves\n // differently when margins are applied to it. The margins are included in\n // the box of the documentElement, in the other cases not.\n if (!isIE10 && isHTML) {\n var marginTop = parseFloat(styles.marginTop, 10);\n var marginLeft = parseFloat(styles.marginLeft, 10);\n\n offsets.top -= borderTopWidth - marginTop;\n offsets.bottom -= borderTopWidth - marginTop;\n offsets.left -= borderLeftWidth - marginLeft;\n offsets.right -= borderLeftWidth - marginLeft;\n\n // Attach marginTop and marginLeft because in some circumstances we may need them\n offsets.marginTop = marginTop;\n offsets.marginLeft = marginLeft;\n }\n\n if (isIE10 && !fixedPosition ? parent.contains(scrollParent) : parent === scrollParent && scrollParent.nodeName !== 'BODY') {\n offsets = includeScroll(offsets, parent);\n }\n\n return offsets;\n}\n\nfunction getViewportOffsetRectRelativeToArtbitraryNode(element) {\n var excludeScroll = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n var html = element.ownerDocument.documentElement;\n var relativeOffset = getOffsetRectRelativeToArbitraryNode(element, html);\n var width = Math.max(html.clientWidth, window.innerWidth || 0);\n var height = Math.max(html.clientHeight, window.innerHeight || 0);\n\n var scrollTop = !excludeScroll ? getScroll(html) : 0;\n var scrollLeft = !excludeScroll ? getScroll(html, 'left') : 0;\n\n var offset = {\n top: scrollTop - relativeOffset.top + relativeOffset.marginTop,\n left: scrollLeft - relativeOffset.left + relativeOffset.marginLeft,\n width: width,\n height: height\n };\n\n return getClientRect(offset);\n}\n\n/**\n * Check if the given element is fixed or is inside a fixed parent\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @argument {Element} customContainer\n * @returns {Boolean} answer to \"isFixed?\"\n */\nfunction isFixed(element) {\n var nodeName = element.nodeName;\n if (nodeName === 'BODY' || nodeName === 'HTML') {\n return false;\n }\n if (getStyleComputedProperty(element, 'position') === 'fixed') {\n return true;\n }\n return isFixed(getParentNode(element));\n}\n\n/**\n * Finds the first parent of an element that has a transformed property defined\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Element} first transformed parent or documentElement\n */\n\nfunction getFixedPositionOffsetParent(element) {\n // This check is needed to avoid errors in case one of the elements isn't defined for any reason\n if (!element || !element.parentElement || isIE()) {\n return document.documentElement;\n }\n var el = element.parentElement;\n while (el && getStyleComputedProperty(el, 'transform') === 'none') {\n el = el.parentElement;\n }\n return el || document.documentElement;\n}\n\n/**\n * Computed the boundaries limits and return them\n * @method\n * @memberof Popper.Utils\n * @param {HTMLElement} popper\n * @param {HTMLElement} reference\n * @param {number} padding\n * @param {HTMLElement} boundariesElement - Element used to define the boundaries\n * @param {Boolean} fixedPosition - Is in fixed position mode\n * @returns {Object} Coordinates of the boundaries\n */\nfunction getBoundaries(popper, reference, padding, boundariesElement) {\n var fixedPosition = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;\n\n // NOTE: 1 DOM access here\n\n var boundaries = { top: 0, left: 0 };\n var offsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n\n // Handle viewport case\n if (boundariesElement === 'viewport') {\n boundaries = getViewportOffsetRectRelativeToArtbitraryNode(offsetParent, fixedPosition);\n } else {\n // Handle other cases based on DOM element used as boundaries\n var boundariesNode = void 0;\n if (boundariesElement === 'scrollParent') {\n boundariesNode = getScrollParent(getParentNode(reference));\n if (boundariesNode.nodeName === 'BODY') {\n boundariesNode = popper.ownerDocument.documentElement;\n }\n } else if (boundariesElement === 'window') {\n boundariesNode = popper.ownerDocument.documentElement;\n } else {\n boundariesNode = boundariesElement;\n }\n\n var offsets = getOffsetRectRelativeToArbitraryNode(boundariesNode, offsetParent, fixedPosition);\n\n // In case of HTML, we need a different computation\n if (boundariesNode.nodeName === 'HTML' && !isFixed(offsetParent)) {\n var _getWindowSizes = getWindowSizes(popper.ownerDocument),\n height = _getWindowSizes.height,\n width = _getWindowSizes.width;\n\n boundaries.top += offsets.top - offsets.marginTop;\n boundaries.bottom = height + offsets.top;\n boundaries.left += offsets.left - offsets.marginLeft;\n boundaries.right = width + offsets.left;\n } else {\n // for all the other DOM elements, this one is good\n boundaries = offsets;\n }\n }\n\n // Add paddings\n padding = padding || 0;\n var isPaddingNumber = typeof padding === 'number';\n boundaries.left += isPaddingNumber ? padding : padding.left || 0;\n boundaries.top += isPaddingNumber ? padding : padding.top || 0;\n boundaries.right -= isPaddingNumber ? padding : padding.right || 0;\n boundaries.bottom -= isPaddingNumber ? padding : padding.bottom || 0;\n\n return boundaries;\n}\n\nfunction getArea(_ref) {\n var width = _ref.width,\n height = _ref.height;\n\n return width * height;\n}\n\n/**\n * Utility used to transform the `auto` placement to the placement with more\n * available space.\n * @method\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction computeAutoPlacement(placement, refRect, popper, reference, boundariesElement) {\n var padding = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : 0;\n\n if (placement.indexOf('auto') === -1) {\n return placement;\n }\n\n var boundaries = getBoundaries(popper, reference, padding, boundariesElement);\n\n var rects = {\n top: {\n width: boundaries.width,\n height: refRect.top - boundaries.top\n },\n right: {\n width: boundaries.right - refRect.right,\n height: boundaries.height\n },\n bottom: {\n width: boundaries.width,\n height: boundaries.bottom - refRect.bottom\n },\n left: {\n width: refRect.left - boundaries.left,\n height: boundaries.height\n }\n };\n\n var sortedAreas = Object.keys(rects).map(function (key) {\n return _extends({\n key: key\n }, rects[key], {\n area: getArea(rects[key])\n });\n }).sort(function (a, b) {\n return b.area - a.area;\n });\n\n var filteredAreas = sortedAreas.filter(function (_ref2) {\n var width = _ref2.width,\n height = _ref2.height;\n return width >= popper.clientWidth && height >= popper.clientHeight;\n });\n\n var computedPlacement = filteredAreas.length > 0 ? filteredAreas[0].key : sortedAreas[0].key;\n\n var variation = placement.split('-')[1];\n\n return computedPlacement + (variation ? '-' + variation : '');\n}\n\n/**\n * Get offsets to the reference element\n * @method\n * @memberof Popper.Utils\n * @param {Object} state\n * @param {Element} popper - the popper element\n * @param {Element} reference - the reference element (the popper will be relative to this)\n * @param {Element} fixedPosition - is in fixed position mode\n * @returns {Object} An object containing the offsets which will be applied to the popper\n */\nfunction getReferenceOffsets(state, popper, reference) {\n var fixedPosition = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null;\n\n var commonOffsetParent = fixedPosition ? getFixedPositionOffsetParent(popper) : findCommonOffsetParent(popper, reference);\n return getOffsetRectRelativeToArbitraryNode(reference, commonOffsetParent, fixedPosition);\n}\n\n/**\n * Get the outer sizes of the given element (offset size + margins)\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element\n * @returns {Object} object containing width and height properties\n */\nfunction getOuterSizes(element) {\n var window = element.ownerDocument.defaultView;\n var styles = window.getComputedStyle(element);\n var x = parseFloat(styles.marginTop || 0) + parseFloat(styles.marginBottom || 0);\n var y = parseFloat(styles.marginLeft || 0) + parseFloat(styles.marginRight || 0);\n var result = {\n width: element.offsetWidth + y,\n height: element.offsetHeight + x\n };\n return result;\n}\n\n/**\n * Get the opposite placement of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement\n * @returns {String} flipped placement\n */\nfunction getOppositePlacement(placement) {\n var hash = { left: 'right', right: 'left', bottom: 'top', top: 'bottom' };\n return placement.replace(/left|right|bottom|top/g, function (matched) {\n return hash[matched];\n });\n}\n\n/**\n * Get offsets to the popper\n * @method\n * @memberof Popper.Utils\n * @param {Object} position - CSS position the Popper will get applied\n * @param {HTMLElement} popper - the popper element\n * @param {Object} referenceOffsets - the reference offsets (the popper will be relative to this)\n * @param {String} placement - one of the valid placement options\n * @returns {Object} popperOffsets - An object containing the offsets which will be applied to the popper\n */\nfunction getPopperOffsets(popper, referenceOffsets, placement) {\n placement = placement.split('-')[0];\n\n // Get popper node sizes\n var popperRect = getOuterSizes(popper);\n\n // Add position, width and height to our offsets object\n var popperOffsets = {\n width: popperRect.width,\n height: popperRect.height\n };\n\n // depending by the popper placement we have to compute its offsets slightly differently\n var isHoriz = ['right', 'left'].indexOf(placement) !== -1;\n var mainSide = isHoriz ? 'top' : 'left';\n var secondarySide = isHoriz ? 'left' : 'top';\n var measurement = isHoriz ? 'height' : 'width';\n var secondaryMeasurement = !isHoriz ? 'height' : 'width';\n\n popperOffsets[mainSide] = referenceOffsets[mainSide] + referenceOffsets[measurement] / 2 - popperRect[measurement] / 2;\n if (placement === secondarySide) {\n popperOffsets[secondarySide] = referenceOffsets[secondarySide] - popperRect[secondaryMeasurement];\n } else {\n popperOffsets[secondarySide] = referenceOffsets[getOppositePlacement(secondarySide)];\n }\n\n return popperOffsets;\n}\n\n/**\n * Mimics the `find` method of Array\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nfunction find(arr, check) {\n // use native find if supported\n if (Array.prototype.find) {\n return arr.find(check);\n }\n\n // use `filter` to obtain the same behavior of `find`\n return arr.filter(check)[0];\n}\n\n/**\n * Return the index of the matching object\n * @method\n * @memberof Popper.Utils\n * @argument {Array} arr\n * @argument prop\n * @argument value\n * @returns index or -1\n */\nfunction findIndex(arr, prop, value) {\n // use native findIndex if supported\n if (Array.prototype.findIndex) {\n return arr.findIndex(function (cur) {\n return cur[prop] === value;\n });\n }\n\n // use `find` + `indexOf` if `findIndex` isn't supported\n var match = find(arr, function (obj) {\n return obj[prop] === value;\n });\n return arr.indexOf(match);\n}\n\n/**\n * Loop trough the list of modifiers and run them in order,\n * each of them will then edit the data object.\n * @method\n * @memberof Popper.Utils\n * @param {dataObject} data\n * @param {Array} modifiers\n * @param {String} ends - Optional modifier name used as stopper\n * @returns {dataObject}\n */\nfunction runModifiers(modifiers, data, ends) {\n var modifiersToRun = ends === undefined ? modifiers : modifiers.slice(0, findIndex(modifiers, 'name', ends));\n\n modifiersToRun.forEach(function (modifier) {\n if (modifier['function']) {\n // eslint-disable-line dot-notation\n console.warn('`modifier.function` is deprecated, use `modifier.fn`!');\n }\n var fn = modifier['function'] || modifier.fn; // eslint-disable-line dot-notation\n if (modifier.enabled && isFunction(fn)) {\n // Add properties to offsets to make them a complete clientRect object\n // we do this before each modifier to make sure the previous one doesn't\n // mess with these values\n data.offsets.popper = getClientRect(data.offsets.popper);\n data.offsets.reference = getClientRect(data.offsets.reference);\n\n data = fn(data, modifier);\n }\n });\n\n return data;\n}\n\n/**\n * Updates the position of the popper, computing the new offsets and applying\n * the new style.
\n * Prefer `scheduleUpdate` over `update` because of performance reasons.\n * @method\n * @memberof Popper\n */\nfunction update() {\n // if popper is destroyed, don't perform any further update\n if (this.state.isDestroyed) {\n return;\n }\n\n var data = {\n instance: this,\n styles: {},\n arrowStyles: {},\n attributes: {},\n flipped: false,\n offsets: {}\n };\n\n // compute reference element offsets\n data.offsets.reference = getReferenceOffsets(this.state, this.popper, this.reference, this.options.positionFixed);\n\n // compute auto placement, store placement inside the data object,\n // modifiers will be able to edit `placement` if needed\n // and refer to originalPlacement to know the original value\n data.placement = computeAutoPlacement(this.options.placement, data.offsets.reference, this.popper, this.reference, this.options.modifiers.flip.boundariesElement, this.options.modifiers.flip.padding);\n\n // store the computed placement inside `originalPlacement`\n data.originalPlacement = data.placement;\n\n data.positionFixed = this.options.positionFixed;\n\n // compute the popper offsets\n data.offsets.popper = getPopperOffsets(this.popper, data.offsets.reference, data.placement);\n\n data.offsets.popper.position = this.options.positionFixed ? 'fixed' : 'absolute';\n\n // run the modifiers\n data = runModifiers(this.modifiers, data);\n\n // the first `update` will call `onCreate` callback\n // the other ones will call `onUpdate` callback\n if (!this.state.isCreated) {\n this.state.isCreated = true;\n this.options.onCreate(data);\n } else {\n this.options.onUpdate(data);\n }\n}\n\n/**\n * Helper used to know if the given modifier is enabled.\n * @method\n * @memberof Popper.Utils\n * @returns {Boolean}\n */\nfunction isModifierEnabled(modifiers, modifierName) {\n return modifiers.some(function (_ref) {\n var name = _ref.name,\n enabled = _ref.enabled;\n return enabled && name === modifierName;\n });\n}\n\n/**\n * Get the prefixed supported property name\n * @method\n * @memberof Popper.Utils\n * @argument {String} property (camelCase)\n * @returns {String} prefixed property (camelCase or PascalCase, depending on the vendor prefix)\n */\nfunction getSupportedPropertyName(property) {\n var prefixes = [false, 'ms', 'Webkit', 'Moz', 'O'];\n var upperProp = property.charAt(0).toUpperCase() + property.slice(1);\n\n for (var i = 0; i < prefixes.length; i++) {\n var prefix = prefixes[i];\n var toCheck = prefix ? '' + prefix + upperProp : property;\n if (typeof document.body.style[toCheck] !== 'undefined') {\n return toCheck;\n }\n }\n return null;\n}\n\n/**\n * Destroys the popper.\n * @method\n * @memberof Popper\n */\nfunction destroy() {\n this.state.isDestroyed = true;\n\n // touch DOM only if `applyStyle` modifier is enabled\n if (isModifierEnabled(this.modifiers, 'applyStyle')) {\n this.popper.removeAttribute('x-placement');\n this.popper.style.position = '';\n this.popper.style.top = '';\n this.popper.style.left = '';\n this.popper.style.right = '';\n this.popper.style.bottom = '';\n this.popper.style.willChange = '';\n this.popper.style[getSupportedPropertyName('transform')] = '';\n }\n\n this.disableEventListeners();\n\n // remove the popper if user explicity asked for the deletion on destroy\n // do not use `remove` because IE11 doesn't support it\n if (this.options.removeOnDestroy) {\n this.popper.parentNode.removeChild(this.popper);\n }\n return this;\n}\n\n/**\n * Get the window associated with the element\n * @argument {Element} element\n * @returns {Window}\n */\nfunction getWindow(element) {\n var ownerDocument = element.ownerDocument;\n return ownerDocument ? ownerDocument.defaultView : window;\n}\n\nfunction attachToScrollParents(scrollParent, event, callback, scrollParents) {\n var isBody = scrollParent.nodeName === 'BODY';\n var target = isBody ? scrollParent.ownerDocument.defaultView : scrollParent;\n target.addEventListener(event, callback, { passive: true });\n\n if (!isBody) {\n attachToScrollParents(getScrollParent(target.parentNode), event, callback, scrollParents);\n }\n scrollParents.push(target);\n}\n\n/**\n * Setup needed event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nfunction setupEventListeners(reference, options, state, updateBound) {\n // Resize event listener on window\n state.updateBound = updateBound;\n getWindow(reference).addEventListener('resize', state.updateBound, { passive: true });\n\n // Scroll event listener on scroll parents\n var scrollElement = getScrollParent(reference);\n attachToScrollParents(scrollElement, 'scroll', state.updateBound, state.scrollParents);\n state.scrollElement = scrollElement;\n state.eventsEnabled = true;\n\n return state;\n}\n\n/**\n * It will add resize/scroll events and start recalculating\n * position of the popper element when they are triggered.\n * @method\n * @memberof Popper\n */\nfunction enableEventListeners() {\n if (!this.state.eventsEnabled) {\n this.state = setupEventListeners(this.reference, this.options, this.state, this.scheduleUpdate);\n }\n}\n\n/**\n * Remove event listeners used to update the popper position\n * @method\n * @memberof Popper.Utils\n * @private\n */\nfunction removeEventListeners(reference, state) {\n // Remove resize event listener on window\n getWindow(reference).removeEventListener('resize', state.updateBound);\n\n // Remove scroll event listener on scroll parents\n state.scrollParents.forEach(function (target) {\n target.removeEventListener('scroll', state.updateBound);\n });\n\n // Reset state\n state.updateBound = null;\n state.scrollParents = [];\n state.scrollElement = null;\n state.eventsEnabled = false;\n return state;\n}\n\n/**\n * It will remove resize/scroll events and won't recalculate popper position\n * when they are triggered. It also won't trigger `onUpdate` callback anymore,\n * unless you call `update` method manually.\n * @method\n * @memberof Popper\n */\nfunction disableEventListeners() {\n if (this.state.eventsEnabled) {\n cancelAnimationFrame(this.scheduleUpdate);\n this.state = removeEventListeners(this.reference, this.state);\n }\n}\n\n/**\n * Tells if a given input is a number\n * @method\n * @memberof Popper.Utils\n * @param {*} input to check\n * @return {Boolean}\n */\nfunction isNumeric(n) {\n return n !== '' && !isNaN(parseFloat(n)) && isFinite(n);\n}\n\n/**\n * Set the style to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the style to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nfunction setStyles(element, styles) {\n Object.keys(styles).forEach(function (prop) {\n var unit = '';\n // add unit if the value is numeric and is one of the following\n if (['width', 'height', 'top', 'right', 'bottom', 'left'].indexOf(prop) !== -1 && isNumeric(styles[prop])) {\n unit = 'px';\n }\n element.style[prop] = styles[prop] + unit;\n });\n}\n\n/**\n * Set the attributes to the given popper\n * @method\n * @memberof Popper.Utils\n * @argument {Element} element - Element to apply the attributes to\n * @argument {Object} styles\n * Object with a list of properties and values which will be applied to the element\n */\nfunction setAttributes(element, attributes) {\n Object.keys(attributes).forEach(function (prop) {\n var value = attributes[prop];\n if (value !== false) {\n element.setAttribute(prop, attributes[prop]);\n } else {\n element.removeAttribute(prop);\n }\n });\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} data.styles - List of style properties - values to apply to popper element\n * @argument {Object} data.attributes - List of attribute properties - values to apply to popper element\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The same data object\n */\nfunction applyStyle(data) {\n // any property present in `data.styles` will be applied to the popper,\n // in this way we can make the 3rd party modifiers add custom styles to it\n // Be aware, modifiers could override the properties defined in the previous\n // lines of this modifier!\n setStyles(data.instance.popper, data.styles);\n\n // any property present in `data.attributes` will be applied to the popper,\n // they will be set as HTML attributes of the element\n setAttributes(data.instance.popper, data.attributes);\n\n // if arrowElement is defined and arrowStyles has some properties\n if (data.arrowElement && Object.keys(data.arrowStyles).length) {\n setStyles(data.arrowElement, data.arrowStyles);\n }\n\n return data;\n}\n\n/**\n * Set the x-placement attribute before everything else because it could be used\n * to add margins to the popper margins needs to be calculated to get the\n * correct popper offsets.\n * @method\n * @memberof Popper.modifiers\n * @param {HTMLElement} reference - The reference element used to position the popper\n * @param {HTMLElement} popper - The HTML element used as popper\n * @param {Object} options - Popper.js options\n */\nfunction applyStyleOnLoad(reference, popper, options, modifierOptions, state) {\n // compute reference element offsets\n var referenceOffsets = getReferenceOffsets(state, popper, reference, options.positionFixed);\n\n // compute auto placement, store placement inside the data object,\n // modifiers will be able to edit `placement` if needed\n // and refer to originalPlacement to know the original value\n var placement = computeAutoPlacement(options.placement, referenceOffsets, popper, reference, options.modifiers.flip.boundariesElement, options.modifiers.flip.padding);\n\n popper.setAttribute('x-placement', placement);\n\n // Apply `position` to popper before anything else because\n // without the position applied we can't guarantee correct computations\n setStyles(popper, { position: options.positionFixed ? 'fixed' : 'absolute' });\n\n return options;\n}\n\n/**\n * @function\n * @memberof Popper.Utils\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Boolean} shouldRound - If the offsets should be rounded at all\n * @returns {Object} The popper's position offsets rounded\n *\n * The tale of pixel-perfect positioning. It's still not 100% perfect, but as\n * good as it can be within reason.\n * Discussion here: https://github.com/FezVrasta/popper.js/pull/715\n *\n * Low DPI screens cause a popper to be blurry if not using full pixels (Safari\n * as well on High DPI screens).\n *\n * Firefox prefers no rounding for positioning and does not have blurriness on\n * high DPI screens.\n *\n * Only horizontal placement and left/right values need to be considered.\n */\nfunction getRoundedOffsets(data, shouldRound) {\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n\n var isVertical = ['left', 'right'].indexOf(data.placement) !== -1;\n var isVariation = data.placement.indexOf('-') !== -1;\n var sameWidthOddness = reference.width % 2 === popper.width % 2;\n var bothOddWidth = reference.width % 2 === 1 && popper.width % 2 === 1;\n var noRound = function noRound(v) {\n return v;\n };\n\n var horizontalToInteger = !shouldRound ? noRound : isVertical || isVariation || sameWidthOddness ? Math.round : Math.floor;\n var verticalToInteger = !shouldRound ? noRound : Math.round;\n\n return {\n left: horizontalToInteger(bothOddWidth && !isVariation && shouldRound ? popper.left - 1 : popper.left),\n top: verticalToInteger(popper.top),\n bottom: verticalToInteger(popper.bottom),\n right: horizontalToInteger(popper.right)\n };\n}\n\nvar isFirefox = isBrowser && /Firefox/i.test(navigator.userAgent);\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction computeStyle(data, options) {\n var x = options.x,\n y = options.y;\n var popper = data.offsets.popper;\n\n // Remove this legacy support in Popper.js v2\n\n var legacyGpuAccelerationOption = find(data.instance.modifiers, function (modifier) {\n return modifier.name === 'applyStyle';\n }).gpuAcceleration;\n if (legacyGpuAccelerationOption !== undefined) {\n console.warn('WARNING: `gpuAcceleration` option moved to `computeStyle` modifier and will not be supported in future versions of Popper.js!');\n }\n var gpuAcceleration = legacyGpuAccelerationOption !== undefined ? legacyGpuAccelerationOption : options.gpuAcceleration;\n\n var offsetParent = getOffsetParent(data.instance.popper);\n var offsetParentRect = getBoundingClientRect(offsetParent);\n\n // Styles\n var styles = {\n position: popper.position\n };\n\n var offsets = getRoundedOffsets(data, window.devicePixelRatio < 2 || !isFirefox);\n\n var sideA = x === 'bottom' ? 'top' : 'bottom';\n var sideB = y === 'right' ? 'left' : 'right';\n\n // if gpuAcceleration is set to `true` and transform is supported,\n // we use `translate3d` to apply the position to the popper we\n // automatically use the supported prefixed version if needed\n var prefixedProperty = getSupportedPropertyName('transform');\n\n // now, let's make a step back and look at this code closely (wtf?)\n // If the content of the popper grows once it's been positioned, it\n // may happen that the popper gets misplaced because of the new content\n // overflowing its reference element\n // To avoid this problem, we provide two options (x and y), which allow\n // the consumer to define the offset origin.\n // If we position a popper on top of a reference element, we can set\n // `x` to `top` to make the popper grow towards its top instead of\n // its bottom.\n var left = void 0,\n top = void 0;\n if (sideA === 'bottom') {\n // when offsetParent is the positioning is relative to the bottom of the screen (excluding the scrollbar)\n // and not the bottom of the html element\n if (offsetParent.nodeName === 'HTML') {\n top = -offsetParent.clientHeight + offsets.bottom;\n } else {\n top = -offsetParentRect.height + offsets.bottom;\n }\n } else {\n top = offsets.top;\n }\n if (sideB === 'right') {\n if (offsetParent.nodeName === 'HTML') {\n left = -offsetParent.clientWidth + offsets.right;\n } else {\n left = -offsetParentRect.width + offsets.right;\n }\n } else {\n left = offsets.left;\n }\n if (gpuAcceleration && prefixedProperty) {\n styles[prefixedProperty] = 'translate3d(' + left + 'px, ' + top + 'px, 0)';\n styles[sideA] = 0;\n styles[sideB] = 0;\n styles.willChange = 'transform';\n } else {\n // othwerise, we use the standard `top`, `left`, `bottom` and `right` properties\n var invertTop = sideA === 'bottom' ? -1 : 1;\n var invertLeft = sideB === 'right' ? -1 : 1;\n styles[sideA] = top * invertTop;\n styles[sideB] = left * invertLeft;\n styles.willChange = sideA + ', ' + sideB;\n }\n\n // Attributes\n var attributes = {\n 'x-placement': data.placement\n };\n\n // Update `data` attributes, styles and arrowStyles\n data.attributes = _extends({}, attributes, data.attributes);\n data.styles = _extends({}, styles, data.styles);\n data.arrowStyles = _extends({}, data.offsets.arrow, data.arrowStyles);\n\n return data;\n}\n\n/**\n * Helper used to know if the given modifier depends from another one.
\n * It checks if the needed modifier is listed and enabled.\n * @method\n * @memberof Popper.Utils\n * @param {Array} modifiers - list of modifiers\n * @param {String} requestingName - name of requesting modifier\n * @param {String} requestedName - name of requested modifier\n * @returns {Boolean}\n */\nfunction isModifierRequired(modifiers, requestingName, requestedName) {\n var requesting = find(modifiers, function (_ref) {\n var name = _ref.name;\n return name === requestingName;\n });\n\n var isRequired = !!requesting && modifiers.some(function (modifier) {\n return modifier.name === requestedName && modifier.enabled && modifier.order < requesting.order;\n });\n\n if (!isRequired) {\n var _requesting = '`' + requestingName + '`';\n var requested = '`' + requestedName + '`';\n console.warn(requested + ' modifier is required by ' + _requesting + ' modifier in order to work, be sure to include it before ' + _requesting + '!');\n }\n return isRequired;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction arrow(data, options) {\n var _data$offsets$arrow;\n\n // arrow depends on keepTogether in order to work\n if (!isModifierRequired(data.instance.modifiers, 'arrow', 'keepTogether')) {\n return data;\n }\n\n var arrowElement = options.element;\n\n // if arrowElement is a string, suppose it's a CSS selector\n if (typeof arrowElement === 'string') {\n arrowElement = data.instance.popper.querySelector(arrowElement);\n\n // if arrowElement is not found, don't run the modifier\n if (!arrowElement) {\n return data;\n }\n } else {\n // if the arrowElement isn't a query selector we must check that the\n // provided DOM node is child of its popper node\n if (!data.instance.popper.contains(arrowElement)) {\n console.warn('WARNING: `arrow.element` must be child of its popper element!');\n return data;\n }\n }\n\n var placement = data.placement.split('-')[0];\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var isVertical = ['left', 'right'].indexOf(placement) !== -1;\n\n var len = isVertical ? 'height' : 'width';\n var sideCapitalized = isVertical ? 'Top' : 'Left';\n var side = sideCapitalized.toLowerCase();\n var altSide = isVertical ? 'left' : 'top';\n var opSide = isVertical ? 'bottom' : 'right';\n var arrowElementSize = getOuterSizes(arrowElement)[len];\n\n //\n // extends keepTogether behavior making sure the popper and its\n // reference have enough pixels in conjunction\n //\n\n // top/left side\n if (reference[opSide] - arrowElementSize < popper[side]) {\n data.offsets.popper[side] -= popper[side] - (reference[opSide] - arrowElementSize);\n }\n // bottom/right side\n if (reference[side] + arrowElementSize > popper[opSide]) {\n data.offsets.popper[side] += reference[side] + arrowElementSize - popper[opSide];\n }\n data.offsets.popper = getClientRect(data.offsets.popper);\n\n // compute center of the popper\n var center = reference[side] + reference[len] / 2 - arrowElementSize / 2;\n\n // Compute the sideValue using the updated popper offsets\n // take popper margin in account because we don't have this info available\n var css = getStyleComputedProperty(data.instance.popper);\n var popperMarginSide = parseFloat(css['margin' + sideCapitalized], 10);\n var popperBorderSide = parseFloat(css['border' + sideCapitalized + 'Width'], 10);\n var sideValue = center - data.offsets.popper[side] - popperMarginSide - popperBorderSide;\n\n // prevent arrowElement from being placed not contiguously to its popper\n sideValue = Math.max(Math.min(popper[len] - arrowElementSize, sideValue), 0);\n\n data.arrowElement = arrowElement;\n data.offsets.arrow = (_data$offsets$arrow = {}, defineProperty(_data$offsets$arrow, side, Math.round(sideValue)), defineProperty(_data$offsets$arrow, altSide, ''), _data$offsets$arrow);\n\n return data;\n}\n\n/**\n * Get the opposite placement variation of the given one\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement variation\n * @returns {String} flipped placement variation\n */\nfunction getOppositeVariation(variation) {\n if (variation === 'end') {\n return 'start';\n } else if (variation === 'start') {\n return 'end';\n }\n return variation;\n}\n\n/**\n * List of accepted placements to use as values of the `placement` option.
\n * Valid placements are:\n * - `auto`\n * - `top`\n * - `right`\n * - `bottom`\n * - `left`\n *\n * Each placement can have a variation from this list:\n * - `-start`\n * - `-end`\n *\n * Variations are interpreted easily if you think of them as the left to right\n * written languages. Horizontally (`top` and `bottom`), `start` is left and `end`\n * is right.
\n * Vertically (`left` and `right`), `start` is top and `end` is bottom.\n *\n * Some valid examples are:\n * - `top-end` (on top of reference, right aligned)\n * - `right-start` (on right of reference, top aligned)\n * - `bottom` (on bottom, centered)\n * - `auto-end` (on the side with more space available, alignment depends by placement)\n *\n * @static\n * @type {Array}\n * @enum {String}\n * @readonly\n * @method placements\n * @memberof Popper\n */\nvar placements = ['auto-start', 'auto', 'auto-end', 'top-start', 'top', 'top-end', 'right-start', 'right', 'right-end', 'bottom-end', 'bottom', 'bottom-start', 'left-end', 'left', 'left-start'];\n\n// Get rid of `auto` `auto-start` and `auto-end`\nvar validPlacements = placements.slice(3);\n\n/**\n * Given an initial placement, returns all the subsequent placements\n * clockwise (or counter-clockwise).\n *\n * @method\n * @memberof Popper.Utils\n * @argument {String} placement - A valid placement (it accepts variations)\n * @argument {Boolean} counter - Set to true to walk the placements counterclockwise\n * @returns {Array} placements including their variations\n */\nfunction clockwise(placement) {\n var counter = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n var index = validPlacements.indexOf(placement);\n var arr = validPlacements.slice(index + 1).concat(validPlacements.slice(0, index));\n return counter ? arr.reverse() : arr;\n}\n\nvar BEHAVIORS = {\n FLIP: 'flip',\n CLOCKWISE: 'clockwise',\n COUNTERCLOCKWISE: 'counterclockwise'\n};\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction flip(data, options) {\n // if `inner` modifier is enabled, we can't use the `flip` modifier\n if (isModifierEnabled(data.instance.modifiers, 'inner')) {\n return data;\n }\n\n if (data.flipped && data.placement === data.originalPlacement) {\n // seems like flip is trying to loop, probably there's not enough space on any of the flippable sides\n return data;\n }\n\n var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, options.boundariesElement, data.positionFixed);\n\n var placement = data.placement.split('-')[0];\n var placementOpposite = getOppositePlacement(placement);\n var variation = data.placement.split('-')[1] || '';\n\n var flipOrder = [];\n\n switch (options.behavior) {\n case BEHAVIORS.FLIP:\n flipOrder = [placement, placementOpposite];\n break;\n case BEHAVIORS.CLOCKWISE:\n flipOrder = clockwise(placement);\n break;\n case BEHAVIORS.COUNTERCLOCKWISE:\n flipOrder = clockwise(placement, true);\n break;\n default:\n flipOrder = options.behavior;\n }\n\n flipOrder.forEach(function (step, index) {\n if (placement !== step || flipOrder.length === index + 1) {\n return data;\n }\n\n placement = data.placement.split('-')[0];\n placementOpposite = getOppositePlacement(placement);\n\n var popperOffsets = data.offsets.popper;\n var refOffsets = data.offsets.reference;\n\n // using floor because the reference offsets may contain decimals we are not going to consider here\n var floor = Math.floor;\n var overlapsRef = placement === 'left' && floor(popperOffsets.right) > floor(refOffsets.left) || placement === 'right' && floor(popperOffsets.left) < floor(refOffsets.right) || placement === 'top' && floor(popperOffsets.bottom) > floor(refOffsets.top) || placement === 'bottom' && floor(popperOffsets.top) < floor(refOffsets.bottom);\n\n var overflowsLeft = floor(popperOffsets.left) < floor(boundaries.left);\n var overflowsRight = floor(popperOffsets.right) > floor(boundaries.right);\n var overflowsTop = floor(popperOffsets.top) < floor(boundaries.top);\n var overflowsBottom = floor(popperOffsets.bottom) > floor(boundaries.bottom);\n\n var overflowsBoundaries = placement === 'left' && overflowsLeft || placement === 'right' && overflowsRight || placement === 'top' && overflowsTop || placement === 'bottom' && overflowsBottom;\n\n // flip the variation if required\n var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n var flippedVariation = !!options.flipVariations && (isVertical && variation === 'start' && overflowsLeft || isVertical && variation === 'end' && overflowsRight || !isVertical && variation === 'start' && overflowsTop || !isVertical && variation === 'end' && overflowsBottom);\n\n if (overlapsRef || overflowsBoundaries || flippedVariation) {\n // this boolean to detect any flip loop\n data.flipped = true;\n\n if (overlapsRef || overflowsBoundaries) {\n placement = flipOrder[index + 1];\n }\n\n if (flippedVariation) {\n variation = getOppositeVariation(variation);\n }\n\n data.placement = placement + (variation ? '-' + variation : '');\n\n // this object contains `position`, we want to preserve it along with\n // any additional property we may add in the future\n data.offsets.popper = _extends({}, data.offsets.popper, getPopperOffsets(data.instance.popper, data.offsets.reference, data.placement));\n\n data = runModifiers(data.instance.modifiers, data, 'flip');\n }\n });\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction keepTogether(data) {\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var placement = data.placement.split('-')[0];\n var floor = Math.floor;\n var isVertical = ['top', 'bottom'].indexOf(placement) !== -1;\n var side = isVertical ? 'right' : 'bottom';\n var opSide = isVertical ? 'left' : 'top';\n var measurement = isVertical ? 'width' : 'height';\n\n if (popper[side] < floor(reference[opSide])) {\n data.offsets.popper[opSide] = floor(reference[opSide]) - popper[measurement];\n }\n if (popper[opSide] > floor(reference[side])) {\n data.offsets.popper[opSide] = floor(reference[side]);\n }\n\n return data;\n}\n\n/**\n * Converts a string containing value + unit into a px value number\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} str - Value + unit string\n * @argument {String} measurement - `height` or `width`\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @returns {Number|String}\n * Value in pixels, or original string if no values were extracted\n */\nfunction toValue(str, measurement, popperOffsets, referenceOffsets) {\n // separate value from unit\n var split = str.match(/((?:\\-|\\+)?\\d*\\.?\\d*)(.*)/);\n var value = +split[1];\n var unit = split[2];\n\n // If it's not a number it's an operator, I guess\n if (!value) {\n return str;\n }\n\n if (unit.indexOf('%') === 0) {\n var element = void 0;\n switch (unit) {\n case '%p':\n element = popperOffsets;\n break;\n case '%':\n case '%r':\n default:\n element = referenceOffsets;\n }\n\n var rect = getClientRect(element);\n return rect[measurement] / 100 * value;\n } else if (unit === 'vh' || unit === 'vw') {\n // if is a vh or vw, we calculate the size based on the viewport\n var size = void 0;\n if (unit === 'vh') {\n size = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);\n } else {\n size = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);\n }\n return size / 100 * value;\n } else {\n // if is an explicit pixel unit, we get rid of the unit and keep the value\n // if is an implicit unit, it's px, and we return just the value\n return value;\n }\n}\n\n/**\n * Parse an `offset` string to extrapolate `x` and `y` numeric offsets.\n * @function\n * @memberof {modifiers~offset}\n * @private\n * @argument {String} offset\n * @argument {Object} popperOffsets\n * @argument {Object} referenceOffsets\n * @argument {String} basePlacement\n * @returns {Array} a two cells array with x and y offsets in numbers\n */\nfunction parseOffset(offset, popperOffsets, referenceOffsets, basePlacement) {\n var offsets = [0, 0];\n\n // Use height if placement is left or right and index is 0 otherwise use width\n // in this way the first offset will use an axis and the second one\n // will use the other one\n var useHeight = ['right', 'left'].indexOf(basePlacement) !== -1;\n\n // Split the offset string to obtain a list of values and operands\n // The regex addresses values with the plus or minus sign in front (+10, -20, etc)\n var fragments = offset.split(/(\\+|\\-)/).map(function (frag) {\n return frag.trim();\n });\n\n // Detect if the offset string contains a pair of values or a single one\n // they could be separated by comma or space\n var divider = fragments.indexOf(find(fragments, function (frag) {\n return frag.search(/,|\\s/) !== -1;\n }));\n\n if (fragments[divider] && fragments[divider].indexOf(',') === -1) {\n console.warn('Offsets separated by white space(s) are deprecated, use a comma (,) instead.');\n }\n\n // If divider is found, we divide the list of values and operands to divide\n // them by ofset X and Y.\n var splitRegex = /\\s*,\\s*|\\s+/;\n var ops = divider !== -1 ? [fragments.slice(0, divider).concat([fragments[divider].split(splitRegex)[0]]), [fragments[divider].split(splitRegex)[1]].concat(fragments.slice(divider + 1))] : [fragments];\n\n // Convert the values with units to absolute pixels to allow our computations\n ops = ops.map(function (op, index) {\n // Most of the units rely on the orientation of the popper\n var measurement = (index === 1 ? !useHeight : useHeight) ? 'height' : 'width';\n var mergeWithPrevious = false;\n return op\n // This aggregates any `+` or `-` sign that aren't considered operators\n // e.g.: 10 + +5 => [10, +, +5]\n .reduce(function (a, b) {\n if (a[a.length - 1] === '' && ['+', '-'].indexOf(b) !== -1) {\n a[a.length - 1] = b;\n mergeWithPrevious = true;\n return a;\n } else if (mergeWithPrevious) {\n a[a.length - 1] += b;\n mergeWithPrevious = false;\n return a;\n } else {\n return a.concat(b);\n }\n }, [])\n // Here we convert the string values into number values (in px)\n .map(function (str) {\n return toValue(str, measurement, popperOffsets, referenceOffsets);\n });\n });\n\n // Loop trough the offsets arrays and execute the operations\n ops.forEach(function (op, index) {\n op.forEach(function (frag, index2) {\n if (isNumeric(frag)) {\n offsets[index] += frag * (op[index2 - 1] === '-' ? -1 : 1);\n }\n });\n });\n return offsets;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @argument {Number|String} options.offset=0\n * The offset value as described in the modifier description\n * @returns {Object} The data object, properly modified\n */\nfunction offset(data, _ref) {\n var offset = _ref.offset;\n var placement = data.placement,\n _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var basePlacement = placement.split('-')[0];\n\n var offsets = void 0;\n if (isNumeric(+offset)) {\n offsets = [+offset, 0];\n } else {\n offsets = parseOffset(offset, popper, reference, basePlacement);\n }\n\n if (basePlacement === 'left') {\n popper.top += offsets[0];\n popper.left -= offsets[1];\n } else if (basePlacement === 'right') {\n popper.top += offsets[0];\n popper.left += offsets[1];\n } else if (basePlacement === 'top') {\n popper.left += offsets[0];\n popper.top -= offsets[1];\n } else if (basePlacement === 'bottom') {\n popper.left += offsets[0];\n popper.top += offsets[1];\n }\n\n data.popper = popper;\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction preventOverflow(data, options) {\n var boundariesElement = options.boundariesElement || getOffsetParent(data.instance.popper);\n\n // If offsetParent is the reference element, we really want to\n // go one step up and use the next offsetParent as reference to\n // avoid to make this modifier completely useless and look like broken\n if (data.instance.reference === boundariesElement) {\n boundariesElement = getOffsetParent(boundariesElement);\n }\n\n // NOTE: DOM access here\n // resets the popper's position so that the document size can be calculated excluding\n // the size of the popper element itself\n var transformProp = getSupportedPropertyName('transform');\n var popperStyles = data.instance.popper.style; // assignment to help minification\n var top = popperStyles.top,\n left = popperStyles.left,\n transform = popperStyles[transformProp];\n\n popperStyles.top = '';\n popperStyles.left = '';\n popperStyles[transformProp] = '';\n\n var boundaries = getBoundaries(data.instance.popper, data.instance.reference, options.padding, boundariesElement, data.positionFixed);\n\n // NOTE: DOM access here\n // restores the original style properties after the offsets have been computed\n popperStyles.top = top;\n popperStyles.left = left;\n popperStyles[transformProp] = transform;\n\n options.boundaries = boundaries;\n\n var order = options.priority;\n var popper = data.offsets.popper;\n\n var check = {\n primary: function primary(placement) {\n var value = popper[placement];\n if (popper[placement] < boundaries[placement] && !options.escapeWithReference) {\n value = Math.max(popper[placement], boundaries[placement]);\n }\n return defineProperty({}, placement, value);\n },\n secondary: function secondary(placement) {\n var mainSide = placement === 'right' ? 'left' : 'top';\n var value = popper[mainSide];\n if (popper[placement] > boundaries[placement] && !options.escapeWithReference) {\n value = Math.min(popper[mainSide], boundaries[placement] - (placement === 'right' ? popper.width : popper.height));\n }\n return defineProperty({}, mainSide, value);\n }\n };\n\n order.forEach(function (placement) {\n var side = ['left', 'top'].indexOf(placement) !== -1 ? 'primary' : 'secondary';\n popper = _extends({}, popper, check[side](placement));\n });\n\n data.offsets.popper = popper;\n\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction shift(data) {\n var placement = data.placement;\n var basePlacement = placement.split('-')[0];\n var shiftvariation = placement.split('-')[1];\n\n // if shift shiftvariation is specified, run the modifier\n if (shiftvariation) {\n var _data$offsets = data.offsets,\n reference = _data$offsets.reference,\n popper = _data$offsets.popper;\n\n var isVertical = ['bottom', 'top'].indexOf(basePlacement) !== -1;\n var side = isVertical ? 'left' : 'top';\n var measurement = isVertical ? 'width' : 'height';\n\n var shiftOffsets = {\n start: defineProperty({}, side, reference[side]),\n end: defineProperty({}, side, reference[side] + reference[measurement] - popper[measurement])\n };\n\n data.offsets.popper = _extends({}, popper, shiftOffsets[shiftvariation]);\n }\n\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by update method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction hide(data) {\n if (!isModifierRequired(data.instance.modifiers, 'hide', 'preventOverflow')) {\n return data;\n }\n\n var refRect = data.offsets.reference;\n var bound = find(data.instance.modifiers, function (modifier) {\n return modifier.name === 'preventOverflow';\n }).boundaries;\n\n if (refRect.bottom < bound.top || refRect.left > bound.right || refRect.top > bound.bottom || refRect.right < bound.left) {\n // Avoid unnecessary DOM access if visibility hasn't changed\n if (data.hide === true) {\n return data;\n }\n\n data.hide = true;\n data.attributes['x-out-of-boundaries'] = '';\n } else {\n // Avoid unnecessary DOM access if visibility hasn't changed\n if (data.hide === false) {\n return data;\n }\n\n data.hide = false;\n data.attributes['x-out-of-boundaries'] = false;\n }\n\n return data;\n}\n\n/**\n * @function\n * @memberof Modifiers\n * @argument {Object} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {Object} The data object, properly modified\n */\nfunction inner(data) {\n var placement = data.placement;\n var basePlacement = placement.split('-')[0];\n var _data$offsets = data.offsets,\n popper = _data$offsets.popper,\n reference = _data$offsets.reference;\n\n var isHoriz = ['left', 'right'].indexOf(basePlacement) !== -1;\n\n var subtractLength = ['top', 'left'].indexOf(basePlacement) === -1;\n\n popper[isHoriz ? 'left' : 'top'] = reference[basePlacement] - (subtractLength ? popper[isHoriz ? 'width' : 'height'] : 0);\n\n data.placement = getOppositePlacement(placement);\n data.offsets.popper = getClientRect(popper);\n\n return data;\n}\n\n/**\n * Modifier function, each modifier can have a function of this type assigned\n * to its `fn` property.
\n * These functions will be called on each update, this means that you must\n * make sure they are performant enough to avoid performance bottlenecks.\n *\n * @function ModifierFn\n * @argument {dataObject} data - The data object generated by `update` method\n * @argument {Object} options - Modifiers configuration and options\n * @returns {dataObject} The data object, properly modified\n */\n\n/**\n * Modifiers are plugins used to alter the behavior of your poppers.
\n * Popper.js uses a set of 9 modifiers to provide all the basic functionalities\n * needed by the library.\n *\n * Usually you don't want to override the `order`, `fn` and `onLoad` props.\n * All the other properties are configurations that could be tweaked.\n * @namespace modifiers\n */\nvar modifiers = {\n /**\n * Modifier used to shift the popper on the start or end of its reference\n * element.
\n * It will read the variation of the `placement` property.
\n * It can be one either `-end` or `-start`.\n * @memberof modifiers\n * @inner\n */\n shift: {\n /** @prop {number} order=100 - Index used to define the order of execution */\n order: 100,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: shift\n },\n\n /**\n * The `offset` modifier can shift your popper on both its axis.\n *\n * It accepts the following units:\n * - `px` or unit-less, interpreted as pixels\n * - `%` or `%r`, percentage relative to the length of the reference element\n * - `%p`, percentage relative to the length of the popper element\n * - `vw`, CSS viewport width unit\n * - `vh`, CSS viewport height unit\n *\n * For length is intended the main axis relative to the placement of the popper.
\n * This means that if the placement is `top` or `bottom`, the length will be the\n * `width`. In case of `left` or `right`, it will be the `height`.\n *\n * You can provide a single value (as `Number` or `String`), or a pair of values\n * as `String` divided by a comma or one (or more) white spaces.
\n * The latter is a deprecated method because it leads to confusion and will be\n * removed in v2.
\n * Additionally, it accepts additions and subtractions between different units.\n * Note that multiplications and divisions aren't supported.\n *\n * Valid examples are:\n * ```\n * 10\n * '10%'\n * '10, 10'\n * '10%, 10'\n * '10 + 10%'\n * '10 - 5vh + 3%'\n * '-10px + 5vh, 5px - 6%'\n * ```\n * > **NB**: If you desire to apply offsets to your poppers in a way that may make them overlap\n * > with their reference element, unfortunately, you will have to disable the `flip` modifier.\n * > You can read more on this at this [issue](https://github.com/FezVrasta/popper.js/issues/373).\n *\n * @memberof modifiers\n * @inner\n */\n offset: {\n /** @prop {number} order=200 - Index used to define the order of execution */\n order: 200,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: offset,\n /** @prop {Number|String} offset=0\n * The offset value as described in the modifier description\n */\n offset: 0\n },\n\n /**\n * Modifier used to prevent the popper from being positioned outside the boundary.\n *\n * A scenario exists where the reference itself is not within the boundaries.
\n * We can say it has \"escaped the boundaries\" — or just \"escaped\".
\n * In this case we need to decide whether the popper should either:\n *\n * - detach from the reference and remain \"trapped\" in the boundaries, or\n * - if it should ignore the boundary and \"escape with its reference\"\n *\n * When `escapeWithReference` is set to`true` and reference is completely\n * outside its boundaries, the popper will overflow (or completely leave)\n * the boundaries in order to remain attached to the edge of the reference.\n *\n * @memberof modifiers\n * @inner\n */\n preventOverflow: {\n /** @prop {number} order=300 - Index used to define the order of execution */\n order: 300,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: preventOverflow,\n /**\n * @prop {Array} [priority=['left','right','top','bottom']]\n * Popper will try to prevent overflow following these priorities by default,\n * then, it could overflow on the left and on top of the `boundariesElement`\n */\n priority: ['left', 'right', 'top', 'bottom'],\n /**\n * @prop {number} padding=5\n * Amount of pixel used to define a minimum distance between the boundaries\n * and the popper. This makes sure the popper always has a little padding\n * between the edges of its container\n */\n padding: 5,\n /**\n * @prop {String|HTMLElement} boundariesElement='scrollParent'\n * Boundaries used by the modifier. Can be `scrollParent`, `window`,\n * `viewport` or any DOM element.\n */\n boundariesElement: 'scrollParent'\n },\n\n /**\n * Modifier used to make sure the reference and its popper stay near each other\n * without leaving any gap between the two. Especially useful when the arrow is\n * enabled and you want to ensure that it points to its reference element.\n * It cares only about the first axis. You can still have poppers with margin\n * between the popper and its reference element.\n * @memberof modifiers\n * @inner\n */\n keepTogether: {\n /** @prop {number} order=400 - Index used to define the order of execution */\n order: 400,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: keepTogether\n },\n\n /**\n * This modifier is used to move the `arrowElement` of the popper to make\n * sure it is positioned between the reference element and its popper element.\n * It will read the outer size of the `arrowElement` node to detect how many\n * pixels of conjunction are needed.\n *\n * It has no effect if no `arrowElement` is provided.\n * @memberof modifiers\n * @inner\n */\n arrow: {\n /** @prop {number} order=500 - Index used to define the order of execution */\n order: 500,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: arrow,\n /** @prop {String|HTMLElement} element='[x-arrow]' - Selector or node used as arrow */\n element: '[x-arrow]'\n },\n\n /**\n * Modifier used to flip the popper's placement when it starts to overlap its\n * reference element.\n *\n * Requires the `preventOverflow` modifier before it in order to work.\n *\n * **NOTE:** this modifier will interrupt the current update cycle and will\n * restart it if it detects the need to flip the placement.\n * @memberof modifiers\n * @inner\n */\n flip: {\n /** @prop {number} order=600 - Index used to define the order of execution */\n order: 600,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: flip,\n /**\n * @prop {String|Array} behavior='flip'\n * The behavior used to change the popper's placement. It can be one of\n * `flip`, `clockwise`, `counterclockwise` or an array with a list of valid\n * placements (with optional variations)\n */\n behavior: 'flip',\n /**\n * @prop {number} padding=5\n * The popper will flip if it hits the edges of the `boundariesElement`\n */\n padding: 5,\n /**\n * @prop {String|HTMLElement} boundariesElement='viewport'\n * The element which will define the boundaries of the popper position.\n * The popper will never be placed outside of the defined boundaries\n * (except if `keepTogether` is enabled)\n */\n boundariesElement: 'viewport'\n },\n\n /**\n * Modifier used to make the popper flow toward the inner of the reference element.\n * By default, when this modifier is disabled, the popper will be placed outside\n * the reference element.\n * @memberof modifiers\n * @inner\n */\n inner: {\n /** @prop {number} order=700 - Index used to define the order of execution */\n order: 700,\n /** @prop {Boolean} enabled=false - Whether the modifier is enabled or not */\n enabled: false,\n /** @prop {ModifierFn} */\n fn: inner\n },\n\n /**\n * Modifier used to hide the popper when its reference element is outside of the\n * popper boundaries. It will set a `x-out-of-boundaries` attribute which can\n * be used to hide with a CSS selector the popper when its reference is\n * out of boundaries.\n *\n * Requires the `preventOverflow` modifier before it in order to work.\n * @memberof modifiers\n * @inner\n */\n hide: {\n /** @prop {number} order=800 - Index used to define the order of execution */\n order: 800,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: hide\n },\n\n /**\n * Computes the style that will be applied to the popper element to gets\n * properly positioned.\n *\n * Note that this modifier will not touch the DOM, it just prepares the styles\n * so that `applyStyle` modifier can apply it. This separation is useful\n * in case you need to replace `applyStyle` with a custom implementation.\n *\n * This modifier has `850` as `order` value to maintain backward compatibility\n * with previous versions of Popper.js. Expect the modifiers ordering method\n * to change in future major versions of the library.\n *\n * @memberof modifiers\n * @inner\n */\n computeStyle: {\n /** @prop {number} order=850 - Index used to define the order of execution */\n order: 850,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: computeStyle,\n /**\n * @prop {Boolean} gpuAcceleration=true\n * If true, it uses the CSS 3D transformation to position the popper.\n * Otherwise, it will use the `top` and `left` properties\n */\n gpuAcceleration: true,\n /**\n * @prop {string} [x='bottom']\n * Where to anchor the X axis (`bottom` or `top`). AKA X offset origin.\n * Change this if your popper should grow in a direction different from `bottom`\n */\n x: 'bottom',\n /**\n * @prop {string} [x='left']\n * Where to anchor the Y axis (`left` or `right`). AKA Y offset origin.\n * Change this if your popper should grow in a direction different from `right`\n */\n y: 'right'\n },\n\n /**\n * Applies the computed styles to the popper element.\n *\n * All the DOM manipulations are limited to this modifier. This is useful in case\n * you want to integrate Popper.js inside a framework or view library and you\n * want to delegate all the DOM manipulations to it.\n *\n * Note that if you disable this modifier, you must make sure the popper element\n * has its position set to `absolute` before Popper.js can do its work!\n *\n * Just disable this modifier and define your own to achieve the desired effect.\n *\n * @memberof modifiers\n * @inner\n */\n applyStyle: {\n /** @prop {number} order=900 - Index used to define the order of execution */\n order: 900,\n /** @prop {Boolean} enabled=true - Whether the modifier is enabled or not */\n enabled: true,\n /** @prop {ModifierFn} */\n fn: applyStyle,\n /** @prop {Function} */\n onLoad: applyStyleOnLoad,\n /**\n * @deprecated since version 1.10.0, the property moved to `computeStyle` modifier\n * @prop {Boolean} gpuAcceleration=true\n * If true, it uses the CSS 3D transformation to position the popper.\n * Otherwise, it will use the `top` and `left` properties\n */\n gpuAcceleration: undefined\n }\n};\n\n/**\n * The `dataObject` is an object containing all the information used by Popper.js.\n * This object is passed to modifiers and to the `onCreate` and `onUpdate` callbacks.\n * @name dataObject\n * @property {Object} data.instance The Popper.js instance\n * @property {String} data.placement Placement applied to popper\n * @property {String} data.originalPlacement Placement originally defined on init\n * @property {Boolean} data.flipped True if popper has been flipped by flip modifier\n * @property {Boolean} data.hide True if the reference element is out of boundaries, useful to know when to hide the popper\n * @property {HTMLElement} data.arrowElement Node used as arrow by arrow modifier\n * @property {Object} data.styles Any CSS property defined here will be applied to the popper. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.arrowStyles Any CSS property defined here will be applied to the popper arrow. It expects the JavaScript nomenclature (eg. `marginBottom`)\n * @property {Object} data.boundaries Offsets of the popper boundaries\n * @property {Object} data.offsets The measurements of popper, reference and arrow elements\n * @property {Object} data.offsets.popper `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.reference `top`, `left`, `width`, `height` values\n * @property {Object} data.offsets.arrow] `top` and `left` offsets, only one of them will be different from 0\n */\n\n/**\n * Default options provided to Popper.js constructor.
\n * These can be overridden using the `options` argument of Popper.js.
\n * To override an option, simply pass an object with the same\n * structure of the `options` object, as the 3rd argument. For example:\n * ```\n * new Popper(ref, pop, {\n * modifiers: {\n * preventOverflow: { enabled: false }\n * }\n * })\n * ```\n * @type {Object}\n * @static\n * @memberof Popper\n */\nvar Defaults = {\n /**\n * Popper's placement.\n * @prop {Popper.placements} placement='bottom'\n */\n placement: 'bottom',\n\n /**\n * Set this to true if you want popper to position it self in 'fixed' mode\n * @prop {Boolean} positionFixed=false\n */\n positionFixed: false,\n\n /**\n * Whether events (resize, scroll) are initially enabled.\n * @prop {Boolean} eventsEnabled=true\n */\n eventsEnabled: true,\n\n /**\n * Set to true if you want to automatically remove the popper when\n * you call the `destroy` method.\n * @prop {Boolean} removeOnDestroy=false\n */\n removeOnDestroy: false,\n\n /**\n * Callback called when the popper is created.
\n * By default, it is set to no-op.
\n * Access Popper.js instance with `data.instance`.\n * @prop {onCreate}\n */\n onCreate: function onCreate() {},\n\n /**\n * Callback called when the popper is updated. This callback is not called\n * on the initialization/creation of the popper, but only on subsequent\n * updates.
\n * By default, it is set to no-op.
\n * Access Popper.js instance with `data.instance`.\n * @prop {onUpdate}\n */\n onUpdate: function onUpdate() {},\n\n /**\n * List of modifiers used to modify the offsets before they are applied to the popper.\n * They provide most of the functionalities of Popper.js.\n * @prop {modifiers}\n */\n modifiers: modifiers\n};\n\n/**\n * @callback onCreate\n * @param {dataObject} data\n */\n\n/**\n * @callback onUpdate\n * @param {dataObject} data\n */\n\n// Utils\n// Methods\nvar Popper = function () {\n /**\n * Creates a new Popper.js instance.\n * @class Popper\n * @param {HTMLElement|referenceObject} reference - The reference element used to position the popper\n * @param {HTMLElement} popper - The HTML element used as the popper\n * @param {Object} options - Your custom options to override the ones defined in [Defaults](#defaults)\n * @return {Object} instance - The generated Popper.js instance\n */\n function Popper(reference, popper) {\n var _this = this;\n\n var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};\n classCallCheck(this, Popper);\n\n this.scheduleUpdate = function () {\n return requestAnimationFrame(_this.update);\n };\n\n // make update() debounced, so that it only runs at most once-per-tick\n this.update = debounce(this.update.bind(this));\n\n // with {} we create a new object with the options inside it\n this.options = _extends({}, Popper.Defaults, options);\n\n // init state\n this.state = {\n isDestroyed: false,\n isCreated: false,\n scrollParents: []\n };\n\n // get reference and popper elements (allow jQuery wrappers)\n this.reference = reference && reference.jquery ? reference[0] : reference;\n this.popper = popper && popper.jquery ? popper[0] : popper;\n\n // Deep merge modifiers options\n this.options.modifiers = {};\n Object.keys(_extends({}, Popper.Defaults.modifiers, options.modifiers)).forEach(function (name) {\n _this.options.modifiers[name] = _extends({}, Popper.Defaults.modifiers[name] || {}, options.modifiers ? options.modifiers[name] : {});\n });\n\n // Refactoring modifiers' list (Object => Array)\n this.modifiers = Object.keys(this.options.modifiers).map(function (name) {\n return _extends({\n name: name\n }, _this.options.modifiers[name]);\n })\n // sort the modifiers by order\n .sort(function (a, b) {\n return a.order - b.order;\n });\n\n // modifiers have the ability to execute arbitrary code when Popper.js get inited\n // such code is executed in the same order of its modifier\n // they could add new properties to their options configuration\n // BE AWARE: don't add options to `options.modifiers.name` but to `modifierOptions`!\n this.modifiers.forEach(function (modifierOptions) {\n if (modifierOptions.enabled && isFunction(modifierOptions.onLoad)) {\n modifierOptions.onLoad(_this.reference, _this.popper, _this.options, modifierOptions, _this.state);\n }\n });\n\n // fire the first update to position the popper in the right place\n this.update();\n\n var eventsEnabled = this.options.eventsEnabled;\n if (eventsEnabled) {\n // setup event listeners, they will take care of update the position in specific situations\n this.enableEventListeners();\n }\n\n this.state.eventsEnabled = eventsEnabled;\n }\n\n // We can't use class properties because they don't get listed in the\n // class prototype and break stuff like Sinon stubs\n\n\n createClass(Popper, [{\n key: 'update',\n value: function update$$1() {\n return update.call(this);\n }\n }, {\n key: 'destroy',\n value: function destroy$$1() {\n return destroy.call(this);\n }\n }, {\n key: 'enableEventListeners',\n value: function enableEventListeners$$1() {\n return enableEventListeners.call(this);\n }\n }, {\n key: 'disableEventListeners',\n value: function disableEventListeners$$1() {\n return disableEventListeners.call(this);\n }\n\n /**\n * Schedules an update. It will run on the next UI update available.\n * @method scheduleUpdate\n * @memberof Popper\n */\n\n\n /**\n * Collection of utilities useful when writing custom modifiers.\n * Starting from version 1.7, this method is available only if you\n * include `popper-utils.js` before `popper.js`.\n *\n * **DEPRECATION**: This way to access PopperUtils is deprecated\n * and will be removed in v2! Use the PopperUtils module directly instead.\n * Due to the high instability of the methods contained in Utils, we can't\n * guarantee them to follow semver. Use them at your own risk!\n * @static\n * @private\n * @type {Object}\n * @deprecated since version 1.8\n * @member Utils\n * @memberof Popper\n */\n\n }]);\n return Popper;\n}();\n\n/**\n * The `referenceObject` is an object that provides an interface compatible with Popper.js\n * and lets you use it as replacement of a real DOM node.
\n * You can use this method to position a popper relatively to a set of coordinates\n * in case you don't have a DOM node to use as reference.\n *\n * ```\n * new Popper(referenceObject, popperNode);\n * ```\n *\n * NB: This feature isn't supported in Internet Explorer 10.\n * @name referenceObject\n * @property {Function} data.getBoundingClientRect\n * A function that returns a set of coordinates compatible with the native `getBoundingClientRect` method.\n * @property {number} data.clientWidth\n * An ES6 getter that will return the width of the virtual reference element.\n * @property {number} data.clientHeight\n * An ES6 getter that will return the height of the virtual reference element.\n */\n\n\nPopper.Utils = (typeof window !== 'undefined' ? window : global).PopperUtils;\nPopper.placements = placements;\nPopper.Defaults = Defaults;\n\nexport default Popper;\n//# sourceMappingURL=popper.js.map\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): dropdown.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'dropdown'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.dropdown'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\nconst SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key\nconst TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key\nconst ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key\nconst ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key\nconst RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)\nconst REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,\n KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,\n KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DISABLED : 'disabled',\n SHOW : 'show',\n DROPUP : 'dropup',\n DROPRIGHT : 'dropright',\n DROPLEFT : 'dropleft',\n MENURIGHT : 'dropdown-menu-right',\n MENULEFT : 'dropdown-menu-left',\n POSITION_STATIC : 'position-static'\n}\n\nconst Selector = {\n DATA_TOGGLE : '[data-toggle=\"dropdown\"]',\n FORM_CHILD : '.dropdown form',\n MENU : '.dropdown-menu',\n NAVBAR_NAV : '.navbar-nav',\n VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'\n}\n\nconst AttachmentMap = {\n TOP : 'top-start',\n TOPEND : 'top-end',\n BOTTOM : 'bottom-start',\n BOTTOMEND : 'bottom-end',\n RIGHT : 'right-start',\n RIGHTEND : 'right-end',\n LEFT : 'left-start',\n LEFTEND : 'left-end'\n}\n\nconst Default = {\n offset : 0,\n flip : true,\n boundary : 'scrollParent',\n reference : 'toggle',\n display : 'dynamic'\n}\n\nconst DefaultType = {\n offset : '(number|string|function)',\n flip : 'boolean',\n boundary : '(string|element)',\n reference : '(string|element)',\n display : 'string'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Dropdown {\n constructor(element, config) {\n this._element = element\n this._popper = null\n this._config = this._getConfig(config)\n this._menu = this._getMenuElement()\n this._inNavbar = this._detectNavbar()\n\n this._addEventListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n toggle() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this._element)\n const isActive = $(this._menu).hasClass(ClassName.SHOW)\n\n Dropdown._clearMenus()\n\n if (isActive) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n // Disable totally Popper.js for Dropdown in Navbar\n if (!this._inNavbar) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s dropdowns require Popper.js (https://popper.js.org/)')\n }\n\n let referenceElement = this._element\n\n if (this._config.reference === 'parent') {\n referenceElement = parent\n } else if (Util.isElement(this._config.reference)) {\n referenceElement = this._config.reference\n\n // Check if it's jQuery element\n if (typeof this._config.reference.jquery !== 'undefined') {\n referenceElement = this._config.reference[0]\n }\n }\n\n // If boundary is not `scrollParent`, then set position to `static`\n // to allow the menu to \"escape\" the scroll parent's boundaries\n // https://github.com/twbs/bootstrap/issues/24251\n if (this._config.boundary !== 'scrollParent') {\n $(parent).addClass(ClassName.POSITION_STATIC)\n }\n this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())\n }\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement &&\n $(parent).closest(Selector.NAVBAR_NAV).length === 0) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n this._element.focus()\n this._element.setAttribute('aria-expanded', true)\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n show() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || $(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const showEvent = $.Event(Event.SHOW, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(showEvent)\n\n if (showEvent.isDefaultPrevented()) {\n return\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.SHOWN, relatedTarget))\n }\n\n hide() {\n if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED) || !$(this._menu).hasClass(ClassName.SHOW)) {\n return\n }\n\n const relatedTarget = {\n relatedTarget: this._element\n }\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n const parent = Dropdown._getParentFromElement(this._element)\n\n $(parent).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(this._menu).toggleClass(ClassName.SHOW)\n $(parent)\n .toggleClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._element).off(EVENT_KEY)\n this._element = null\n this._menu = null\n if (this._popper !== null) {\n this._popper.destroy()\n this._popper = null\n }\n }\n\n update() {\n this._inNavbar = this._detectNavbar()\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Private\n\n _addEventListeners() {\n $(this._element).on(Event.CLICK, (event) => {\n event.preventDefault()\n event.stopPropagation()\n this.toggle()\n })\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this._element).data(),\n ...config\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getMenuElement() {\n if (!this._menu) {\n const parent = Dropdown._getParentFromElement(this._element)\n\n if (parent) {\n this._menu = parent.querySelector(Selector.MENU)\n }\n }\n return this._menu\n }\n\n _getPlacement() {\n const $parentDropdown = $(this._element.parentNode)\n let placement = AttachmentMap.BOTTOM\n\n // Handle dropup\n if ($parentDropdown.hasClass(ClassName.DROPUP)) {\n placement = AttachmentMap.TOP\n if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.TOPEND\n }\n } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {\n placement = AttachmentMap.RIGHT\n } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {\n placement = AttachmentMap.LEFT\n } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {\n placement = AttachmentMap.BOTTOMEND\n }\n return placement\n }\n\n _detectNavbar() {\n return $(this._element).closest('.navbar').length > 0\n }\n\n _getPopperConfig() {\n const offsetConf = {}\n if (typeof this._config.offset === 'function') {\n offsetConf.fn = (data) => {\n data.offsets = {\n ...data.offsets,\n ...this._config.offset(data.offsets) || {}\n }\n return data\n }\n } else {\n offsetConf.offset = this._config.offset\n }\n\n const popperConfig = {\n placement: this._getPlacement(),\n modifiers: {\n offset: offsetConf,\n flip: {\n enabled: this._config.flip\n },\n preventOverflow: {\n boundariesElement: this._config.boundary\n }\n }\n }\n\n // Disable Popper.js if we have a static display\n if (this._config.display === 'static') {\n popperConfig.modifiers.applyStyle = {\n enabled: false\n }\n }\n return popperConfig\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data) {\n data = new Dropdown(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n\n static _clearMenus(event) {\n if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||\n event.type === 'keyup' && event.which !== TAB_KEYCODE)) {\n return\n }\n\n const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))\n\n for (let i = 0, len = toggles.length; i < len; i++) {\n const parent = Dropdown._getParentFromElement(toggles[i])\n const context = $(toggles[i]).data(DATA_KEY)\n const relatedTarget = {\n relatedTarget: toggles[i]\n }\n\n if (event && event.type === 'click') {\n relatedTarget.clickEvent = event\n }\n\n if (!context) {\n continue\n }\n\n const dropdownMenu = context._menu\n if (!$(parent).hasClass(ClassName.SHOW)) {\n continue\n }\n\n if (event && (event.type === 'click' &&\n /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&\n $.contains(parent, event.target)) {\n continue\n }\n\n const hideEvent = $.Event(Event.HIDE, relatedTarget)\n $(parent).trigger(hideEvent)\n if (hideEvent.isDefaultPrevented()) {\n continue\n }\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n toggles[i].setAttribute('aria-expanded', 'false')\n\n $(dropdownMenu).removeClass(ClassName.SHOW)\n $(parent)\n .removeClass(ClassName.SHOW)\n .trigger($.Event(Event.HIDDEN, relatedTarget))\n }\n }\n\n static _getParentFromElement(element) {\n let parent\n const selector = Util.getSelectorFromElement(element)\n\n if (selector) {\n parent = document.querySelector(selector)\n }\n\n return parent || element.parentNode\n }\n\n // eslint-disable-next-line complexity\n static _dataApiKeydownHandler(event) {\n // If not input/textarea:\n // - And not a key in REGEXP_KEYDOWN => not a dropdown command\n // If input/textarea:\n // - If space key => not a dropdown command\n // - If key is other than escape\n // - If key is not up or down => not a dropdown command\n // - If trigger inside the menu => not a dropdown command\n if (/input|textarea/i.test(event.target.tagName)\n ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&\n (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||\n $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {\n return\n }\n\n event.preventDefault()\n event.stopPropagation()\n\n if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {\n return\n }\n\n const parent = Dropdown._getParentFromElement(this)\n const isActive = $(parent).hasClass(ClassName.SHOW)\n\n if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {\n if (event.which === ESCAPE_KEYCODE) {\n const toggle = parent.querySelector(Selector.DATA_TOGGLE)\n $(toggle).trigger('focus')\n }\n\n $(this).trigger('click')\n return\n }\n\n const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))\n\n if (items.length === 0) {\n return\n }\n\n let index = items.indexOf(event.target)\n\n if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up\n index--\n }\n\n if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down\n index++\n }\n\n if (index < 0) {\n index = 0\n }\n\n items[index].focus()\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document)\n .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)\n .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)\n .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)\n .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n event.preventDefault()\n event.stopPropagation()\n Dropdown._jQueryInterface.call($(this), 'toggle')\n })\n .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {\n e.stopPropagation()\n })\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Dropdown._jQueryInterface\n$.fn[NAME].Constructor = Dropdown\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Dropdown._jQueryInterface\n}\n\n\nexport default Dropdown\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): modal.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'modal'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.modal'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key\n\nconst Default = {\n backdrop : true,\n keyboard : true,\n focus : true,\n show : true\n}\n\nconst DefaultType = {\n backdrop : '(boolean|string)',\n keyboard : 'boolean',\n focus : 'boolean',\n show : 'boolean'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n RESIZE : `resize${EVENT_KEY}`,\n CLICK_DISMISS : `click.dismiss${EVENT_KEY}`,\n KEYDOWN_DISMISS : `keydown.dismiss${EVENT_KEY}`,\n MOUSEUP_DISMISS : `mouseup.dismiss${EVENT_KEY}`,\n MOUSEDOWN_DISMISS : `mousedown.dismiss${EVENT_KEY}`,\n CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n SCROLLBAR_MEASURER : 'modal-scrollbar-measure',\n BACKDROP : 'modal-backdrop',\n OPEN : 'modal-open',\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n DIALOG : '.modal-dialog',\n DATA_TOGGLE : '[data-toggle=\"modal\"]',\n DATA_DISMISS : '[data-dismiss=\"modal\"]',\n FIXED_CONTENT : '.fixed-top, .fixed-bottom, .is-fixed, .sticky-top',\n STICKY_CONTENT : '.sticky-top'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Modal {\n constructor(element, config) {\n this._config = this._getConfig(config)\n this._element = element\n this._dialog = element.querySelector(Selector.DIALOG)\n this._backdrop = null\n this._isShown = false\n this._isBodyOverflowing = false\n this._ignoreBackdropClick = false\n this._isTransitioning = false\n this._scrollbarWidth = 0\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n toggle(relatedTarget) {\n return this._isShown ? this.hide() : this.show(relatedTarget)\n }\n\n show(relatedTarget) {\n if (this._isShown || this._isTransitioning) {\n return\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n this._isTransitioning = true\n }\n\n const showEvent = $.Event(Event.SHOW, {\n relatedTarget\n })\n\n $(this._element).trigger(showEvent)\n\n if (this._isShown || showEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = true\n\n this._checkScrollbar()\n this._setScrollbar()\n\n this._adjustDialog()\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(this._element).on(\n Event.CLICK_DISMISS,\n Selector.DATA_DISMISS,\n (event) => this.hide(event)\n )\n\n $(this._dialog).on(Event.MOUSEDOWN_DISMISS, () => {\n $(this._element).one(Event.MOUSEUP_DISMISS, (event) => {\n if ($(event.target).is(this._element)) {\n this._ignoreBackdropClick = true\n }\n })\n })\n\n this._showBackdrop(() => this._showElement(relatedTarget))\n }\n\n hide(event) {\n if (event) {\n event.preventDefault()\n }\n\n if (!this._isShown || this._isTransitioning) {\n return\n }\n\n const hideEvent = $.Event(Event.HIDE)\n\n $(this._element).trigger(hideEvent)\n\n if (!this._isShown || hideEvent.isDefaultPrevented()) {\n return\n }\n\n this._isShown = false\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (transition) {\n this._isTransitioning = true\n }\n\n this._setEscapeEvent()\n this._setResizeEvent()\n\n $(document).off(Event.FOCUSIN)\n\n $(this._element).removeClass(ClassName.SHOW)\n\n $(this._element).off(Event.CLICK_DISMISS)\n $(this._dialog).off(Event.MOUSEDOWN_DISMISS)\n\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._element)\n\n $(this._element)\n .one(Util.TRANSITION_END, (event) => this._hideModal(event))\n .emulateTransitionEnd(transitionDuration)\n } else {\n this._hideModal()\n }\n }\n\n dispose() {\n [window, this._element, this._dialog]\n .forEach((htmlElement) => $(htmlElement).off(EVENT_KEY))\n\n /**\n * `document` has 2 events `Event.FOCUSIN` and `Event.CLICK_DATA_API`\n * Do not move `document` in `htmlElements` array\n * It will remove `Event.CLICK_DATA_API` event that should remain\n */\n $(document).off(Event.FOCUSIN)\n\n $.removeData(this._element, DATA_KEY)\n\n this._config = null\n this._element = null\n this._dialog = null\n this._backdrop = null\n this._isShown = null\n this._isBodyOverflowing = null\n this._ignoreBackdropClick = null\n this._isTransitioning = null\n this._scrollbarWidth = null\n }\n\n handleUpdate() {\n this._adjustDialog()\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...config\n }\n Util.typeCheckConfig(NAME, config, DefaultType)\n return config\n }\n\n _showElement(relatedTarget) {\n const transition = $(this._element).hasClass(ClassName.FADE)\n\n if (!this._element.parentNode ||\n this._element.parentNode.nodeType !== Node.ELEMENT_NODE) {\n // Don't move modal's DOM position\n document.body.appendChild(this._element)\n }\n\n this._element.style.display = 'block'\n this._element.removeAttribute('aria-hidden')\n this._element.setAttribute('aria-modal', true)\n this._element.scrollTop = 0\n\n if (transition) {\n Util.reflow(this._element)\n }\n\n $(this._element).addClass(ClassName.SHOW)\n\n if (this._config.focus) {\n this._enforceFocus()\n }\n\n const shownEvent = $.Event(Event.SHOWN, {\n relatedTarget\n })\n\n const transitionComplete = () => {\n if (this._config.focus) {\n this._element.focus()\n }\n this._isTransitioning = false\n $(this._element).trigger(shownEvent)\n }\n\n if (transition) {\n const transitionDuration = Util.getTransitionDurationFromElement(this._dialog)\n\n $(this._dialog)\n .one(Util.TRANSITION_END, transitionComplete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n transitionComplete()\n }\n }\n\n _enforceFocus() {\n $(document)\n .off(Event.FOCUSIN) // Guard against infinite focus loop\n .on(Event.FOCUSIN, (event) => {\n if (document !== event.target &&\n this._element !== event.target &&\n $(this._element).has(event.target).length === 0) {\n this._element.focus()\n }\n })\n }\n\n _setEscapeEvent() {\n if (this._isShown && this._config.keyboard) {\n $(this._element).on(Event.KEYDOWN_DISMISS, (event) => {\n if (event.which === ESCAPE_KEYCODE) {\n event.preventDefault()\n this.hide()\n }\n })\n } else if (!this._isShown) {\n $(this._element).off(Event.KEYDOWN_DISMISS)\n }\n }\n\n _setResizeEvent() {\n if (this._isShown) {\n $(window).on(Event.RESIZE, (event) => this.handleUpdate(event))\n } else {\n $(window).off(Event.RESIZE)\n }\n }\n\n _hideModal() {\n this._element.style.display = 'none'\n this._element.setAttribute('aria-hidden', true)\n this._element.removeAttribute('aria-modal')\n this._isTransitioning = false\n this._showBackdrop(() => {\n $(document.body).removeClass(ClassName.OPEN)\n this._resetAdjustments()\n this._resetScrollbar()\n $(this._element).trigger(Event.HIDDEN)\n })\n }\n\n _removeBackdrop() {\n if (this._backdrop) {\n $(this._backdrop).remove()\n this._backdrop = null\n }\n }\n\n _showBackdrop(callback) {\n const animate = $(this._element).hasClass(ClassName.FADE)\n ? ClassName.FADE : ''\n\n if (this._isShown && this._config.backdrop) {\n this._backdrop = document.createElement('div')\n this._backdrop.className = ClassName.BACKDROP\n\n if (animate) {\n this._backdrop.classList.add(animate)\n }\n\n $(this._backdrop).appendTo(document.body)\n\n $(this._element).on(Event.CLICK_DISMISS, (event) => {\n if (this._ignoreBackdropClick) {\n this._ignoreBackdropClick = false\n return\n }\n if (event.target !== event.currentTarget) {\n return\n }\n if (this._config.backdrop === 'static') {\n this._element.focus()\n } else {\n this.hide()\n }\n })\n\n if (animate) {\n Util.reflow(this._backdrop)\n }\n\n $(this._backdrop).addClass(ClassName.SHOW)\n\n if (!callback) {\n return\n }\n\n if (!animate) {\n callback()\n return\n }\n\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callback)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else if (!this._isShown && this._backdrop) {\n $(this._backdrop).removeClass(ClassName.SHOW)\n\n const callbackRemove = () => {\n this._removeBackdrop()\n if (callback) {\n callback()\n }\n }\n\n if ($(this._element).hasClass(ClassName.FADE)) {\n const backdropTransitionDuration = Util.getTransitionDurationFromElement(this._backdrop)\n\n $(this._backdrop)\n .one(Util.TRANSITION_END, callbackRemove)\n .emulateTransitionEnd(backdropTransitionDuration)\n } else {\n callbackRemove()\n }\n } else if (callback) {\n callback()\n }\n }\n\n // ----------------------------------------------------------------------\n // the following methods are used to handle overflowing modals\n // todo (fat): these should probably be refactored out of modal.js\n // ----------------------------------------------------------------------\n\n _adjustDialog() {\n const isModalOverflowing =\n this._element.scrollHeight > document.documentElement.clientHeight\n\n if (!this._isBodyOverflowing && isModalOverflowing) {\n this._element.style.paddingLeft = `${this._scrollbarWidth}px`\n }\n\n if (this._isBodyOverflowing && !isModalOverflowing) {\n this._element.style.paddingRight = `${this._scrollbarWidth}px`\n }\n }\n\n _resetAdjustments() {\n this._element.style.paddingLeft = ''\n this._element.style.paddingRight = ''\n }\n\n _checkScrollbar() {\n const rect = document.body.getBoundingClientRect()\n this._isBodyOverflowing = rect.left + rect.right < window.innerWidth\n this._scrollbarWidth = this._getScrollbarWidth()\n }\n\n _setScrollbar() {\n if (this._isBodyOverflowing) {\n // Note: DOMNode.style.paddingRight returns the actual value or '' if not set\n // while $(DOMNode).css('padding-right') returns the calculated value or 0 if not set\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n const stickyContent = [].slice.call(document.querySelectorAll(Selector.STICKY_CONTENT))\n\n // Adjust fixed content padding\n $(fixedContent).each((index, element) => {\n const actualPadding = element.style.paddingRight\n const calculatedPadding = $(element).css('padding-right')\n $(element)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n })\n\n // Adjust sticky content margin\n $(stickyContent).each((index, element) => {\n const actualMargin = element.style.marginRight\n const calculatedMargin = $(element).css('margin-right')\n $(element)\n .data('margin-right', actualMargin)\n .css('margin-right', `${parseFloat(calculatedMargin) - this._scrollbarWidth}px`)\n })\n\n // Adjust body padding\n const actualPadding = document.body.style.paddingRight\n const calculatedPadding = $(document.body).css('padding-right')\n $(document.body)\n .data('padding-right', actualPadding)\n .css('padding-right', `${parseFloat(calculatedPadding) + this._scrollbarWidth}px`)\n }\n\n $(document.body).addClass(ClassName.OPEN)\n }\n\n _resetScrollbar() {\n // Restore fixed content padding\n const fixedContent = [].slice.call(document.querySelectorAll(Selector.FIXED_CONTENT))\n $(fixedContent).each((index, element) => {\n const padding = $(element).data('padding-right')\n $(element).removeData('padding-right')\n element.style.paddingRight = padding ? padding : ''\n })\n\n // Restore sticky content\n const elements = [].slice.call(document.querySelectorAll(`${Selector.STICKY_CONTENT}`))\n $(elements).each((index, element) => {\n const margin = $(element).data('margin-right')\n if (typeof margin !== 'undefined') {\n $(element).css('margin-right', margin).removeData('margin-right')\n }\n })\n\n // Restore body padding\n const padding = $(document.body).data('padding-right')\n $(document.body).removeData('padding-right')\n document.body.style.paddingRight = padding ? padding : ''\n }\n\n _getScrollbarWidth() { // thx d.walsh\n const scrollDiv = document.createElement('div')\n scrollDiv.className = ClassName.SCROLLBAR_MEASURER\n document.body.appendChild(scrollDiv)\n const scrollbarWidth = scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth\n document.body.removeChild(scrollDiv)\n return scrollbarWidth\n }\n\n // Static\n\n static _jQueryInterface(config, relatedTarget) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = {\n ...Default,\n ...$(this).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (!data) {\n data = new Modal(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config](relatedTarget)\n } else if (_config.show) {\n data.show(relatedTarget)\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * Data Api implementation\n * ------------------------------------------------------------------------\n */\n\n$(document).on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {\n let target\n const selector = Util.getSelectorFromElement(this)\n\n if (selector) {\n target = document.querySelector(selector)\n }\n\n const config = $(target).data(DATA_KEY)\n ? 'toggle' : {\n ...$(target).data(),\n ...$(this).data()\n }\n\n if (this.tagName === 'A' || this.tagName === 'AREA') {\n event.preventDefault()\n }\n\n const $target = $(target).one(Event.SHOW, (showEvent) => {\n if (showEvent.isDefaultPrevented()) {\n // Only register focus restorer if modal will actually get shown\n return\n }\n\n $target.one(Event.HIDDEN, () => {\n if ($(this).is(':visible')) {\n this.focus()\n }\n })\n })\n\n Modal._jQueryInterface.call($(target), config, this)\n})\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Modal._jQueryInterface\n$.fn[NAME].Constructor = Modal\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Modal._jQueryInterface\n}\n\nexport default Modal\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): tooltip.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Popper from 'popper.js'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'tooltip'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.tooltip'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-tooltip'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst DefaultType = {\n animation : 'boolean',\n template : 'string',\n title : '(string|element|function)',\n trigger : 'string',\n delay : '(number|object)',\n html : 'boolean',\n selector : '(string|boolean)',\n placement : '(string|function)',\n offset : '(number|string)',\n container : '(string|element|boolean)',\n fallbackPlacement : '(string|array)',\n boundary : '(string|element)'\n}\n\nconst AttachmentMap = {\n AUTO : 'auto',\n TOP : 'top',\n RIGHT : 'right',\n BOTTOM : 'bottom',\n LEFT : 'left'\n}\n\nconst Default = {\n animation : true,\n template : '
' +\n '
' +\n '
',\n trigger : 'hover focus',\n title : '',\n delay : 0,\n html : false,\n selector : false,\n placement : 'top',\n offset : 0,\n container : false,\n fallbackPlacement : 'flip',\n boundary : 'scrollParent'\n}\n\nconst HoverState = {\n SHOW : 'show',\n OUT : 'out'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TOOLTIP : '.tooltip',\n TOOLTIP_INNER : '.tooltip-inner',\n ARROW : '.arrow'\n}\n\nconst Trigger = {\n HOVER : 'hover',\n FOCUS : 'focus',\n CLICK : 'click',\n MANUAL : 'manual'\n}\n\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Tooltip {\n constructor(element, config) {\n /**\n * Check for Popper dependency\n * Popper - https://popper.js.org\n */\n if (typeof Popper === 'undefined') {\n throw new TypeError('Bootstrap\\'s tooltips require Popper.js (https://popper.js.org/)')\n }\n\n // private\n this._isEnabled = true\n this._timeout = 0\n this._hoverState = ''\n this._activeTrigger = {}\n this._popper = null\n\n // Protected\n this.element = element\n this.config = this._getConfig(config)\n this.tip = null\n\n this._setListeners()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Public\n\n enable() {\n this._isEnabled = true\n }\n\n disable() {\n this._isEnabled = false\n }\n\n toggleEnabled() {\n this._isEnabled = !this._isEnabled\n }\n\n toggle(event) {\n if (!this._isEnabled) {\n return\n }\n\n if (event) {\n const dataKey = this.constructor.DATA_KEY\n let context = $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n context._activeTrigger.click = !context._activeTrigger.click\n\n if (context._isWithActiveTrigger()) {\n context._enter(null, context)\n } else {\n context._leave(null, context)\n }\n } else {\n if ($(this.getTipElement()).hasClass(ClassName.SHOW)) {\n this._leave(null, this)\n return\n }\n\n this._enter(null, this)\n }\n }\n\n dispose() {\n clearTimeout(this._timeout)\n\n $.removeData(this.element, this.constructor.DATA_KEY)\n\n $(this.element).off(this.constructor.EVENT_KEY)\n $(this.element).closest('.modal').off('hide.bs.modal')\n\n if (this.tip) {\n $(this.tip).remove()\n }\n\n this._isEnabled = null\n this._timeout = null\n this._hoverState = null\n this._activeTrigger = null\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n this._popper = null\n this.element = null\n this.config = null\n this.tip = null\n }\n\n show() {\n if ($(this.element).css('display') === 'none') {\n throw new Error('Please use show on visible elements')\n }\n\n const showEvent = $.Event(this.constructor.Event.SHOW)\n if (this.isWithContent() && this._isEnabled) {\n $(this.element).trigger(showEvent)\n\n const shadowRoot = Util.findShadowRoot(this.element)\n const isInTheDom = $.contains(\n shadowRoot !== null ? shadowRoot : this.element.ownerDocument.documentElement,\n this.element\n )\n\n if (showEvent.isDefaultPrevented() || !isInTheDom) {\n return\n }\n\n const tip = this.getTipElement()\n const tipId = Util.getUID(this.constructor.NAME)\n\n tip.setAttribute('id', tipId)\n this.element.setAttribute('aria-describedby', tipId)\n\n this.setContent()\n\n if (this.config.animation) {\n $(tip).addClass(ClassName.FADE)\n }\n\n const placement = typeof this.config.placement === 'function'\n ? this.config.placement.call(this, tip, this.element)\n : this.config.placement\n\n const attachment = this._getAttachment(placement)\n this.addAttachmentClass(attachment)\n\n const container = this._getContainer()\n $(tip).data(this.constructor.DATA_KEY, this)\n\n if (!$.contains(this.element.ownerDocument.documentElement, this.tip)) {\n $(tip).appendTo(container)\n }\n\n $(this.element).trigger(this.constructor.Event.INSERTED)\n\n this._popper = new Popper(this.element, tip, {\n placement: attachment,\n modifiers: {\n offset: {\n offset: this.config.offset\n },\n flip: {\n behavior: this.config.fallbackPlacement\n },\n arrow: {\n element: Selector.ARROW\n },\n preventOverflow: {\n boundariesElement: this.config.boundary\n }\n },\n onCreate: (data) => {\n if (data.originalPlacement !== data.placement) {\n this._handlePopperPlacementChange(data)\n }\n },\n onUpdate: (data) => this._handlePopperPlacementChange(data)\n })\n\n $(tip).addClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we add extra\n // empty mouseover listeners to the body's immediate children;\n // only needed because of broken event delegation on iOS\n // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().on('mouseover', null, $.noop)\n }\n\n const complete = () => {\n if (this.config.animation) {\n this._fixTransition()\n }\n const prevHoverState = this._hoverState\n this._hoverState = null\n\n $(this.element).trigger(this.constructor.Event.SHOWN)\n\n if (prevHoverState === HoverState.OUT) {\n this._leave(null, this)\n }\n }\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(this.tip)\n\n $(this.tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n }\n }\n\n hide(callback) {\n const tip = this.getTipElement()\n const hideEvent = $.Event(this.constructor.Event.HIDE)\n const complete = () => {\n if (this._hoverState !== HoverState.SHOW && tip.parentNode) {\n tip.parentNode.removeChild(tip)\n }\n\n this._cleanTipClass()\n this.element.removeAttribute('aria-describedby')\n $(this.element).trigger(this.constructor.Event.HIDDEN)\n if (this._popper !== null) {\n this._popper.destroy()\n }\n\n if (callback) {\n callback()\n }\n }\n\n $(this.element).trigger(hideEvent)\n\n if (hideEvent.isDefaultPrevented()) {\n return\n }\n\n $(tip).removeClass(ClassName.SHOW)\n\n // If this is a touch-enabled device we remove the extra\n // empty mouseover listeners we added for iOS support\n if ('ontouchstart' in document.documentElement) {\n $(document.body).children().off('mouseover', null, $.noop)\n }\n\n this._activeTrigger[Trigger.CLICK] = false\n this._activeTrigger[Trigger.FOCUS] = false\n this._activeTrigger[Trigger.HOVER] = false\n\n if ($(this.tip).hasClass(ClassName.FADE)) {\n const transitionDuration = Util.getTransitionDurationFromElement(tip)\n\n $(tip)\n .one(Util.TRANSITION_END, complete)\n .emulateTransitionEnd(transitionDuration)\n } else {\n complete()\n }\n\n this._hoverState = ''\n }\n\n update() {\n if (this._popper !== null) {\n this._popper.scheduleUpdate()\n }\n }\n\n // Protected\n\n isWithContent() {\n return Boolean(this.getTitle())\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const tip = this.getTipElement()\n this.setElementContent($(tip.querySelectorAll(Selector.TOOLTIP_INNER)), this.getTitle())\n $(tip).removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n setElementContent($element, content) {\n const html = this.config.html\n if (typeof content === 'object' && (content.nodeType || content.jquery)) {\n // Content is a DOM node or a jQuery\n if (html) {\n if (!$(content).parent().is($element)) {\n $element.empty().append(content)\n }\n } else {\n $element.text($(content).text())\n }\n } else {\n $element[html ? 'html' : 'text'](content)\n }\n }\n\n getTitle() {\n let title = this.element.getAttribute('data-original-title')\n\n if (!title) {\n title = typeof this.config.title === 'function'\n ? this.config.title.call(this.element)\n : this.config.title\n }\n\n return title\n }\n\n // Private\n\n _getContainer() {\n if (this.config.container === false) {\n return document.body\n }\n\n if (Util.isElement(this.config.container)) {\n return $(this.config.container)\n }\n\n return $(document).find(this.config.container)\n }\n\n _getAttachment(placement) {\n return AttachmentMap[placement.toUpperCase()]\n }\n\n _setListeners() {\n const triggers = this.config.trigger.split(' ')\n\n triggers.forEach((trigger) => {\n if (trigger === 'click') {\n $(this.element).on(\n this.constructor.Event.CLICK,\n this.config.selector,\n (event) => this.toggle(event)\n )\n } else if (trigger !== Trigger.MANUAL) {\n const eventIn = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSEENTER\n : this.constructor.Event.FOCUSIN\n const eventOut = trigger === Trigger.HOVER\n ? this.constructor.Event.MOUSELEAVE\n : this.constructor.Event.FOCUSOUT\n\n $(this.element)\n .on(\n eventIn,\n this.config.selector,\n (event) => this._enter(event)\n )\n .on(\n eventOut,\n this.config.selector,\n (event) => this._leave(event)\n )\n }\n })\n\n $(this.element).closest('.modal').on(\n 'hide.bs.modal',\n () => {\n if (this.element) {\n this.hide()\n }\n }\n )\n\n if (this.config.selector) {\n this.config = {\n ...this.config,\n trigger: 'manual',\n selector: ''\n }\n } else {\n this._fixTitle()\n }\n }\n\n _fixTitle() {\n const titleType = typeof this.element.getAttribute('data-original-title')\n\n if (this.element.getAttribute('title') || titleType !== 'string') {\n this.element.setAttribute(\n 'data-original-title',\n this.element.getAttribute('title') || ''\n )\n\n this.element.setAttribute('title', '')\n }\n }\n\n _enter(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusin' ? Trigger.FOCUS : Trigger.HOVER\n ] = true\n }\n\n if ($(context.getTipElement()).hasClass(ClassName.SHOW) || context._hoverState === HoverState.SHOW) {\n context._hoverState = HoverState.SHOW\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.SHOW\n\n if (!context.config.delay || !context.config.delay.show) {\n context.show()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.SHOW) {\n context.show()\n }\n }, context.config.delay.show)\n }\n\n _leave(event, context) {\n const dataKey = this.constructor.DATA_KEY\n context = context || $(event.currentTarget).data(dataKey)\n\n if (!context) {\n context = new this.constructor(\n event.currentTarget,\n this._getDelegateConfig()\n )\n $(event.currentTarget).data(dataKey, context)\n }\n\n if (event) {\n context._activeTrigger[\n event.type === 'focusout' ? Trigger.FOCUS : Trigger.HOVER\n ] = false\n }\n\n if (context._isWithActiveTrigger()) {\n return\n }\n\n clearTimeout(context._timeout)\n\n context._hoverState = HoverState.OUT\n\n if (!context.config.delay || !context.config.delay.hide) {\n context.hide()\n return\n }\n\n context._timeout = setTimeout(() => {\n if (context._hoverState === HoverState.OUT) {\n context.hide()\n }\n }, context.config.delay.hide)\n }\n\n _isWithActiveTrigger() {\n for (const trigger in this._activeTrigger) {\n if (this._activeTrigger[trigger]) {\n return true\n }\n }\n\n return false\n }\n\n _getConfig(config) {\n config = {\n ...this.constructor.Default,\n ...$(this.element).data(),\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.delay === 'number') {\n config.delay = {\n show: config.delay,\n hide: config.delay\n }\n }\n\n if (typeof config.title === 'number') {\n config.title = config.title.toString()\n }\n\n if (typeof config.content === 'number') {\n config.content = config.content.toString()\n }\n\n Util.typeCheckConfig(\n NAME,\n config,\n this.constructor.DefaultType\n )\n\n return config\n }\n\n _getDelegateConfig() {\n const config = {}\n\n if (this.config) {\n for (const key in this.config) {\n if (this.constructor.Default[key] !== this.config[key]) {\n config[key] = this.config[key]\n }\n }\n }\n\n return config\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n _handlePopperPlacementChange(popperData) {\n const popperInstance = popperData.instance\n this.tip = popperInstance.popper\n this._cleanTipClass()\n this.addAttachmentClass(this._getAttachment(popperData.placement))\n }\n\n _fixTransition() {\n const tip = this.getTipElement()\n const initConfigAnimation = this.config.animation\n\n if (tip.getAttribute('x-placement') !== null) {\n return\n }\n\n $(tip).removeClass(ClassName.FADE)\n this.config.animation = false\n this.hide()\n this.show()\n this.config.animation = initConfigAnimation\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' && config\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Tooltip(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Tooltip._jQueryInterface\n$.fn[NAME].Constructor = Tooltip\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Tooltip._jQueryInterface\n}\n\nexport default Tooltip\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): popover.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Tooltip from './tooltip'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'popover'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.popover'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\nconst CLASS_PREFIX = 'bs-popover'\nconst BSCLS_PREFIX_REGEX = new RegExp(`(^|\\\\s)${CLASS_PREFIX}\\\\S+`, 'g')\n\nconst Default = {\n ...Tooltip.Default,\n placement : 'right',\n trigger : 'click',\n content : '',\n template : '
' +\n '
' +\n '

' +\n '
'\n}\n\nconst DefaultType = {\n ...Tooltip.DefaultType,\n content : '(string|element|function)'\n}\n\nconst ClassName = {\n FADE : 'fade',\n SHOW : 'show'\n}\n\nconst Selector = {\n TITLE : '.popover-header',\n CONTENT : '.popover-body'\n}\n\nconst Event = {\n HIDE : `hide${EVENT_KEY}`,\n HIDDEN : `hidden${EVENT_KEY}`,\n SHOW : `show${EVENT_KEY}`,\n SHOWN : `shown${EVENT_KEY}`,\n INSERTED : `inserted${EVENT_KEY}`,\n CLICK : `click${EVENT_KEY}`,\n FOCUSIN : `focusin${EVENT_KEY}`,\n FOCUSOUT : `focusout${EVENT_KEY}`,\n MOUSEENTER : `mouseenter${EVENT_KEY}`,\n MOUSELEAVE : `mouseleave${EVENT_KEY}`\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass Popover extends Tooltip {\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n static get NAME() {\n return NAME\n }\n\n static get DATA_KEY() {\n return DATA_KEY\n }\n\n static get Event() {\n return Event\n }\n\n static get EVENT_KEY() {\n return EVENT_KEY\n }\n\n static get DefaultType() {\n return DefaultType\n }\n\n // Overrides\n\n isWithContent() {\n return this.getTitle() || this._getContent()\n }\n\n addAttachmentClass(attachment) {\n $(this.getTipElement()).addClass(`${CLASS_PREFIX}-${attachment}`)\n }\n\n getTipElement() {\n this.tip = this.tip || $(this.config.template)[0]\n return this.tip\n }\n\n setContent() {\n const $tip = $(this.getTipElement())\n\n // We use append for html objects to maintain js events\n this.setElementContent($tip.find(Selector.TITLE), this.getTitle())\n let content = this._getContent()\n if (typeof content === 'function') {\n content = content.call(this.element)\n }\n this.setElementContent($tip.find(Selector.CONTENT), content)\n\n $tip.removeClass(`${ClassName.FADE} ${ClassName.SHOW}`)\n }\n\n // Private\n\n _getContent() {\n return this.element.getAttribute('data-content') ||\n this.config.content\n }\n\n _cleanTipClass() {\n const $tip = $(this.getTipElement())\n const tabClass = $tip.attr('class').match(BSCLS_PREFIX_REGEX)\n if (tabClass !== null && tabClass.length > 0) {\n $tip.removeClass(tabClass.join(''))\n }\n }\n\n // Static\n\n static _jQueryInterface(config) {\n return this.each(function () {\n let data = $(this).data(DATA_KEY)\n const _config = typeof config === 'object' ? config : null\n\n if (!data && /dispose|hide/.test(config)) {\n return\n }\n\n if (!data) {\n data = new Popover(this, _config)\n $(this).data(DATA_KEY, data)\n }\n\n if (typeof config === 'string') {\n if (typeof data[config] === 'undefined') {\n throw new TypeError(`No method named \"${config}\"`)\n }\n data[config]()\n }\n })\n }\n}\n\n/**\n * ------------------------------------------------------------------------\n * jQuery\n * ------------------------------------------------------------------------\n */\n\n$.fn[NAME] = Popover._jQueryInterface\n$.fn[NAME].Constructor = Popover\n$.fn[NAME].noConflict = () => {\n $.fn[NAME] = JQUERY_NO_CONFLICT\n return Popover._jQueryInterface\n}\n\nexport default Popover\n","/**\n * --------------------------------------------------------------------------\n * Bootstrap (v4.2.1): scrollspy.js\n * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)\n * --------------------------------------------------------------------------\n */\n\nimport $ from 'jquery'\nimport Util from './util'\n\n/**\n * ------------------------------------------------------------------------\n * Constants\n * ------------------------------------------------------------------------\n */\n\nconst NAME = 'scrollspy'\nconst VERSION = '4.2.1'\nconst DATA_KEY = 'bs.scrollspy'\nconst EVENT_KEY = `.${DATA_KEY}`\nconst DATA_API_KEY = '.data-api'\nconst JQUERY_NO_CONFLICT = $.fn[NAME]\n\nconst Default = {\n offset : 10,\n method : 'auto',\n target : ''\n}\n\nconst DefaultType = {\n offset : 'number',\n method : 'string',\n target : '(string|element)'\n}\n\nconst Event = {\n ACTIVATE : `activate${EVENT_KEY}`,\n SCROLL : `scroll${EVENT_KEY}`,\n LOAD_DATA_API : `load${EVENT_KEY}${DATA_API_KEY}`\n}\n\nconst ClassName = {\n DROPDOWN_ITEM : 'dropdown-item',\n DROPDOWN_MENU : 'dropdown-menu',\n ACTIVE : 'active'\n}\n\nconst Selector = {\n DATA_SPY : '[data-spy=\"scroll\"]',\n ACTIVE : '.active',\n NAV_LIST_GROUP : '.nav, .list-group',\n NAV_LINKS : '.nav-link',\n NAV_ITEMS : '.nav-item',\n LIST_ITEMS : '.list-group-item',\n DROPDOWN : '.dropdown',\n DROPDOWN_ITEMS : '.dropdown-item',\n DROPDOWN_TOGGLE : '.dropdown-toggle'\n}\n\nconst OffsetMethod = {\n OFFSET : 'offset',\n POSITION : 'position'\n}\n\n/**\n * ------------------------------------------------------------------------\n * Class Definition\n * ------------------------------------------------------------------------\n */\n\nclass ScrollSpy {\n constructor(element, config) {\n this._element = element\n this._scrollElement = element.tagName === 'BODY' ? window : element\n this._config = this._getConfig(config)\n this._selector = `${this._config.target} ${Selector.NAV_LINKS},` +\n `${this._config.target} ${Selector.LIST_ITEMS},` +\n `${this._config.target} ${Selector.DROPDOWN_ITEMS}`\n this._offsets = []\n this._targets = []\n this._activeTarget = null\n this._scrollHeight = 0\n\n $(this._scrollElement).on(Event.SCROLL, (event) => this._process(event))\n\n this.refresh()\n this._process()\n }\n\n // Getters\n\n static get VERSION() {\n return VERSION\n }\n\n static get Default() {\n return Default\n }\n\n // Public\n\n refresh() {\n const autoMethod = this._scrollElement === this._scrollElement.window\n ? OffsetMethod.OFFSET : OffsetMethod.POSITION\n\n const offsetMethod = this._config.method === 'auto'\n ? autoMethod : this._config.method\n\n const offsetBase = offsetMethod === OffsetMethod.POSITION\n ? this._getScrollTop() : 0\n\n this._offsets = []\n this._targets = []\n\n this._scrollHeight = this._getScrollHeight()\n\n const targets = [].slice.call(document.querySelectorAll(this._selector))\n\n targets\n .map((element) => {\n let target\n const targetSelector = Util.getSelectorFromElement(element)\n\n if (targetSelector) {\n target = document.querySelector(targetSelector)\n }\n\n if (target) {\n const targetBCR = target.getBoundingClientRect()\n if (targetBCR.width || targetBCR.height) {\n // TODO (fat): remove sketch reliance on jQuery position/offset\n return [\n $(target)[offsetMethod]().top + offsetBase,\n targetSelector\n ]\n }\n }\n return null\n })\n .filter((item) => item)\n .sort((a, b) => a[0] - b[0])\n .forEach((item) => {\n this._offsets.push(item[0])\n this._targets.push(item[1])\n })\n }\n\n dispose() {\n $.removeData(this._element, DATA_KEY)\n $(this._scrollElement).off(EVENT_KEY)\n\n this._element = null\n this._scrollElement = null\n this._config = null\n this._selector = null\n this._offsets = null\n this._targets = null\n this._activeTarget = null\n this._scrollHeight = null\n }\n\n // Private\n\n _getConfig(config) {\n config = {\n ...Default,\n ...typeof config === 'object' && config ? config : {}\n }\n\n if (typeof config.target !== 'string') {\n let id = $(config.target).attr('id')\n if (!id) {\n id = Util.getUID(NAME)\n $(config.target).attr('id', id)\n }\n config.target = `#${id}`\n }\n\n Util.typeCheckConfig(NAME, config, DefaultType)\n\n return config\n }\n\n _getScrollTop() {\n return this._scrollElement === window\n ? this._scrollElement.pageYOffset : this._scrollElement.scrollTop\n }\n\n _getScrollHeight() {\n return this._scrollElement.scrollHeight || Math.max(\n document.body.scrollHeight,\n document.documentElement.scrollHeight\n )\n }\n\n _getOffsetHeight() {\n return this._scrollElement === window\n ? window.innerHeight : this._scrollElement.getBoundingClientRect().height\n }\n\n _process() {\n const scrollTop = this._getScrollTop() + this._config.offset\n const scrollHeight = this._getScrollHeight()\n const maxScroll = this._config.offset +\n scrollHeight -\n this._getOffsetHeight()\n\n if (this._scrollHeight !== scrollHeight) {\n this.refresh()\n }\n\n if (scrollTop >= maxScroll) {\n const target = this._targets[this._targets.length - 1]\n\n if (this._activeTarget !== target) {\n this._activate(target)\n }\n return\n }\n\n if (this._activeTarget && scrollTop < this._offsets[0] && this._offsets[0] > 0) {\n this._activeTarget = null\n this._clear()\n return\n }\n\n const offsetLength = this._offsets.length\n for (let i = offsetLength; i--;) {\n const isActiveTarget = this._activeTarget !== this._targets[i] &&\n scrollTop >= this._offsets[i] &&\n (typeof this._offsets[i + 1] === 'undefined' ||\n scrollTop < this._offsets[i + 1])\n\n if (isActiveTarget) {\n this._activate(this._targets[i])\n }\n }\n }\n\n _activate(target) {\n this._activeTarget = target\n\n this._clear()\n\n const queries = this._selector\n .split(',')\n .map((selector) => `${selector}[data-target=\"${target}\"],${selector}[href=\"${target}\"]`)\n\n const $link = $([].slice.call(document.querySelectorAll(queries.join(','))))\n\n if ($link.hasClass(ClassName.DROPDOWN_ITEM)) {\n $link.closest(Selector.DROPDOWN).find(Selector.DROPDOWN_TOGGLE).addClass(ClassName.ACTIVE)\n $link.addClass(ClassName.ACTIVE)\n } else {\n // Set triggered link as active\n $link.addClass(ClassName.ACTIVE)\n // Set triggered links parents as active\n // With both
    and