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