diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..3b5395e --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +APP_ENV=development +SECRET_KEY=replace-with-a-long-random-secret +DATABASE_URL=sqlite:///instance/face_attendance.sqlite3 +FACE_MATCH_TOLERANCE=0.55 +MAX_CONTENT_LENGTH=8388608 diff --git a/.gitignore b/.gitignore index a741601..78ad020 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,9 @@ coverage.xml *.py.cover .hypothesis/ .pytest_cache/ +pytest-cache-files-*/ +.pytest_tmp/ +tmp_pytest/ cover/ # Translations @@ -64,6 +67,9 @@ db.sqlite3-journal # Flask stuff: instance/ .webassets-cache +*.sqlite +*.sqlite3 +*.db # Scrapy stuff: .scrapy @@ -149,6 +155,8 @@ activemq-data/ # Environments .env +.env.* +!.env.example .envrc .venv env/ diff --git a/README.md b/README.md index af6bbf2..f664082 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,74 @@ -# face-attendance -Attendance registration project with facial recognition. +# Face Attendance + +A modern Flask attendance application with staff authentication, role-based access control, face enrollment, camera-based recognition, and attendance reporting. + +## Roles + +- `admin`: manages staff accounts, enrolls people, records attendance, and views reports. +- `operator`: enrolls people, uses the attendance kiosk, and views reports. +- `viewer`: read-only access to people and attendance reports. + +Only admins can create staff accounts. + +## Requirements + +- Python 3.11 or 3.12 is recommended for production. +- Native build prerequisites for `face-recognition`/`dlib` may be required by your OS. +- A production WSGI server such as Gunicorn on Linux or Waitress on Windows. + +## Setup + +```powershell +py -3.12 -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +Copy-Item .env.example .env +``` + +Edit `.env` and set a strong `SECRET_KEY`. For production, set `APP_ENV=production` and use a durable database URL. + +## Database + +Initialize the database with migrations: + +```powershell +$env:FLASK_APP = "wsgi:app" +flask db upgrade +flask create-admin +``` + +## Run Locally + +```powershell +$env:FLASK_APP = "wsgi:app" +flask run +``` + +Open `http://127.0.0.1:5000` and sign in with the admin account created above. + +## Tests + +```powershell +pytest +``` + +The tests use an isolated SQLite database and disable CSRF only for test requests. + +## Production Notes + +- Use HTTPS. Production config enables secure cookies. +- Set `SECRET_KEY`, `DATABASE_URL`, and `APP_ENV=production`. +- Run `flask db upgrade` during deployment. +- Run behind a reverse proxy that forwards HTTPS headers correctly. +- Treat face encodings as biometric data. Restrict database access, define a retention policy, and back up securely. + +## What Changed + +- Replaced the legacy single-file Flask script with an application factory and Blueprints. +- Added SQLAlchemy models and Alembic migrations. +- Added Flask-Login authentication, admin-created accounts, password hashing, and role checks. +- Replaced raw SQL with ORM queries. +- Removed global enrollment state. +- Rebuilt the UI in English with reusable templates and responsive Bootstrap-based layouts. +- Added CSRF protection, secure cookie settings, security headers, validation, and error pages. +- Added pytest coverage for authentication, roles, models, and attendance recording. diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..e80bb55 --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import logging +import os +from pathlib import Path + +from flask import Flask, render_template + +from app.config import ProductionConfig, config_by_name +from app.extensions import csrf, db, login_manager, migrate + + +def create_app(config_name: str | None = None, config_overrides: dict | None = None) -> Flask: + try: + from dotenv import load_dotenv + + load_dotenv() + except ImportError: + pass + + app = Flask(__name__, instance_relative_config=True) + Path(app.instance_path).mkdir(parents=True, exist_ok=True) + + selected_config = config_name or os.getenv("APP_ENV", "development") + config_class = config_by_name.get(selected_config, config_by_name["development"]) + app.config.from_object(config_class) + if config_overrides: + app.config.update(config_overrides) + if config_class is ProductionConfig: + validate_production_config(app) + + configure_logging(app) + init_extensions(app) + register_blueprints(app) + register_error_handlers(app) + register_security_headers(app) + register_template_context(app) + + from app.commands import register_commands + + register_commands(app) + return app + + +def init_extensions(app: Flask) -> None: + from app.models import Account + + db.init_app(app) + migrate.init_app(app, db) + csrf.init_app(app) + + login_manager.login_view = "auth.login" + login_manager.login_message_category = "warning" + login_manager.init_app(app) + + @login_manager.user_loader + def load_user(account_id: str) -> Account | None: + if not account_id.isdigit(): + return None + return db.session.get(Account, int(account_id)) + + +def register_blueprints(app: Flask) -> None: + from app.admin.routes import bp as admin_bp + from app.attendance.routes import bp as attendance_bp + from app.auth.routes import bp as auth_bp + from app.main.routes import bp as main_bp + from app.people.routes import bp as people_bp + from app.recognition.routes import bp as recognition_bp + + app.register_blueprint(auth_bp) + app.register_blueprint(main_bp) + app.register_blueprint(admin_bp) + app.register_blueprint(people_bp) + app.register_blueprint(attendance_bp) + app.register_blueprint(recognition_bp) + + +def register_error_handlers(app: Flask) -> None: + @app.errorhandler(403) + def forbidden(error): + return render_template("errors/403.html"), 403 + + @app.errorhandler(404) + def not_found(error): + return render_template("errors/404.html"), 404 + + @app.errorhandler(500) + def internal_error(error): + db.session.rollback() + app.logger.exception("Unhandled application error: %s", error) + return render_template("errors/500.html"), 500 + + +def register_security_headers(app: Flask) -> None: + @app.after_request + def set_default_security_headers(response): + response.headers.setdefault("X-Content-Type-Options", "nosniff") + response.headers.setdefault("X-Frame-Options", "SAMEORIGIN") + response.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin") + response.headers.setdefault("Permissions-Policy", "camera=(self), microphone=()") + return response + + +def register_template_context(app: Flask) -> None: + from datetime import datetime, timezone + + from app.models import Role + + @app.context_processor + def inject_globals(): + return {"current_year": datetime.now(timezone.utc).year, "Role": Role} + + +def configure_logging(app: Flask) -> None: + if app.debug or app.testing: + return + logging.basicConfig(level=logging.INFO) + + +def validate_production_config(app: Flask) -> None: + if app.config["SECRET_KEY"] in {"", "dev-change-me"}: + raise RuntimeError("SECRET_KEY must be set for production.") diff --git a/app/admin/__init__.py b/app/admin/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/admin/__init__.py @@ -0,0 +1 @@ + diff --git a/app/admin/forms.py b/app/admin/forms.py new file mode 100644 index 0000000..96c29fe --- /dev/null +++ b/app/admin/forms.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import re + +from flask_wtf import FlaskForm +from wtforms import EmailField, PasswordField, SelectField, StringField, SubmitField +from wtforms.validators import DataRequired, Email, EqualTo, Length, ValidationError + +from app.models import Account, Role + + +class AccountCreateForm(FlaskForm): + full_name = StringField("Full name", validators=[DataRequired(), Length(max=160)]) + username = StringField("Username", validators=[DataRequired(), Length(min=3, max=80)]) + email = EmailField("Email", validators=[DataRequired(), Email(), Length(max=255)]) + role = SelectField( + "Role", + choices=[(Role.OPERATOR.value, "Operator"), (Role.VIEWER.value, "Viewer"), (Role.ADMIN.value, "Admin")], + validators=[DataRequired()], + ) + password = PasswordField("Password", validators=[DataRequired(), Length(min=12, max=128)]) + confirm_password = PasswordField( + "Confirm password", + validators=[DataRequired(), EqualTo("password", message="Passwords must match.")], + ) + submit = SubmitField("Create account") + + def validate_username(self, field) -> None: + username = field.data.strip().lower() + if not re.fullmatch(r"[a-z0-9_.-]+", username): + raise ValidationError("Use only letters, numbers, dots, hyphens, and underscores.") + if Account.query.filter_by(username=username).first(): + raise ValidationError("That username is already in use.") + field.data = username + + def validate_email(self, field) -> None: + email = field.data.strip().lower() + if Account.query.filter_by(email=email).first(): + raise ValidationError("That email is already in use.") + field.data = email + + def validate_password(self, field) -> None: + password = field.data + if not re.search(r"[A-Z]", password) or not re.search(r"[a-z]", password) or not re.search(r"\d", password): + raise ValidationError("Use at least one uppercase letter, one lowercase letter, and one number.") diff --git a/app/admin/routes.py b/app/admin/routes.py new file mode 100644 index 0000000..a21eea1 --- /dev/null +++ b/app/admin/routes.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from flask import Blueprint, abort, flash, redirect, render_template, url_for +from flask_login import current_user + +from app.admin.forms import AccountCreateForm +from app.extensions import db +from app.models import Account, Role +from app.services.security import roles_required + +bp = Blueprint("admin", __name__, url_prefix="/admin") + + +@bp.get("/accounts") +@roles_required(Role.ADMIN) +def accounts(): + items = Account.query.order_by(Account.created_at.desc()).all() + return render_template("admin/accounts.html", accounts=items) + + +@bp.route("/accounts/new", methods=["GET", "POST"]) +@roles_required(Role.ADMIN) +def new_account(): + form = AccountCreateForm() + if form.validate_on_submit(): + account = Account( + full_name=form.full_name.data.strip(), + username=form.username.data.strip().lower(), + email=form.email.data.strip().lower(), + role=form.role.data, + ) + account.set_password(form.password.data) + db.session.add(account) + db.session.commit() + flash(f"Account created for {account.full_name}.", "success") + return redirect(url_for("admin.accounts")) + return render_template("admin/account_form.html", form=form) + + +@bp.post("/accounts//toggle") +@roles_required(Role.ADMIN) +def toggle_account(account_id: int): + account = db.session.get(Account, account_id) + if not account: + abort(404) + if account.id == current_user.id: + flash("You cannot deactivate your own account.", "warning") + return redirect(url_for("admin.accounts")) + account.active = not account.active + db.session.commit() + flash(f"{account.full_name} is now {'active' if account.active else 'inactive'}.", "success") + return redirect(url_for("admin.accounts")) diff --git a/app/attendance/__init__.py b/app/attendance/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/attendance/__init__.py @@ -0,0 +1 @@ + diff --git a/app/attendance/routes.py b/app/attendance/routes.py new file mode 100644 index 0000000..bee0977 --- /dev/null +++ b/app/attendance/routes.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from flask import Blueprint, abort, jsonify, redirect, render_template, request, url_for +from flask_login import current_user + +from app.extensions import db +from app.models import AttendanceEvent, AttendanceType, Person, Role +from app.services.attendance import last_presence_summary, record_attendance +from app.services.security import roles_required + +bp = Blueprint("attendance", __name__, url_prefix="/attendance") + + +@bp.get("/") +@roles_required(Role.ADMIN, Role.OPERATOR, Role.VIEWER) +def index(): + events = AttendanceEvent.query.order_by(AttendanceEvent.occurred_at.desc()).limit(250).all() + return render_template("attendance/index.html", events=events) + + +@bp.get("/kiosk") +@roles_required(Role.ADMIN, Role.OPERATOR) +def kiosk(): + return render_template("attendance/kiosk.html") + + +@bp.post("/events") +@roles_required(Role.ADMIN, Role.OPERATOR) +def create_event(): + person_id = request.form.get("person_id", type=int) + raw_event_type = request.form.get("event_type", "") + confidence = request.form.get("confidence", type=float) + + person = db.session.get(Person, person_id) if person_id else None + if not person or not person.active: + abort(404) + + try: + event_type = AttendanceType(raw_event_type) + except ValueError: + abort(400) + + event = record_attendance( + person, + event_type, + created_by_id=current_user.id, + confidence=confidence, + source="web-kiosk", + ) + + if request.accept_mimetypes.accept_json: + return jsonify( + { + "id": event.id, + "event_type": event.type_label, + "occurred_at": event.occurred_at.strftime("%Y-%m-%d %H:%M"), + "summary": last_presence_summary(person), + } + ) + return redirect(url_for("attendance.index")) diff --git a/app/auth/__init__.py b/app/auth/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/auth/__init__.py @@ -0,0 +1 @@ + diff --git a/app/auth/forms.py b/app/auth/forms.py new file mode 100644 index 0000000..c16f16d --- /dev/null +++ b/app/auth/forms.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from flask_wtf import FlaskForm +from wtforms import BooleanField, PasswordField, StringField, SubmitField +from wtforms.validators import DataRequired, Length + + +class LoginForm(FlaskForm): + username = StringField("Username", validators=[DataRequired(), Length(max=80)]) + password = PasswordField("Password", validators=[DataRequired()]) + remember = BooleanField("Remember this device") + submit = SubmitField("Sign in") diff --git a/app/auth/routes.py b/app/auth/routes.py new file mode 100644 index 0000000..505e4ad --- /dev/null +++ b/app/auth/routes.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +from urllib.parse import urlsplit + +from flask import Blueprint, flash, redirect, render_template, request, url_for +from flask_login import current_user, login_required, login_user, logout_user + +from app.auth.forms import LoginForm +from app.models import Account + +bp = Blueprint("auth", __name__, url_prefix="/auth") + + +@bp.route("/login", methods=["GET", "POST"]) +def login(): + if current_user.is_authenticated: + return redirect(url_for("main.dashboard")) + + form = LoginForm() + if form.validate_on_submit(): + username = form.username.data.strip().lower() + account = Account.query.filter_by(username=username).first() + if account and account.active and account.check_password(form.password.data): + login_user(account, remember=form.remember.data) + next_url = request.args.get("next") + if not next_url or urlsplit(next_url).netloc: + next_url = url_for("main.dashboard") + return redirect(next_url) + flash("Invalid username or password.", "danger") + + return render_template("auth/login.html", form=form) + + +@bp.post("/logout") +@login_required +def logout(): + logout_user() + flash("You have been signed out.", "info") + return redirect(url_for("auth.login")) diff --git a/app/commands.py b/app/commands.py new file mode 100644 index 0000000..4bff482 --- /dev/null +++ b/app/commands.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +import click +from flask import Flask + +from app.extensions import db +from app.models import Account, Role + + +def register_commands(app: Flask) -> None: + @app.cli.command("create-admin") + @click.option("--username", prompt=True) + @click.option("--email", prompt=True) + @click.option("--full-name", prompt=True) + @click.password_option() + def create_admin(username: str, email: str, full_name: str, password: str) -> None: + """Create an initial administrator account.""" + username = username.strip().lower() + email = email.strip().lower() + + if Account.query.filter((Account.username == username) | (Account.email == email)).first(): + raise click.ClickException("An account with that username or email already exists.") + + account = Account(username=username, email=email, full_name=full_name.strip(), role=Role.ADMIN.value) + account.set_password(password) + db.session.add(account) + db.session.commit() + click.echo(f"Created admin account: {username}") diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..d27929e --- /dev/null +++ b/app/config.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +import os +from datetime import timedelta +from pathlib import Path + + +BASE_DIR = Path(__file__).resolve().parent.parent + + +class Config: + SECRET_KEY = os.getenv("SECRET_KEY", "dev-change-me") + SQLALCHEMY_DATABASE_URI = os.getenv( + "DATABASE_URL", + f"sqlite:///{BASE_DIR / 'instance' / 'face_attendance.sqlite3'}", + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + + MAX_CONTENT_LENGTH = int(os.getenv("MAX_CONTENT_LENGTH", str(8 * 1024 * 1024))) + FACE_MATCH_TOLERANCE = float(os.getenv("FACE_MATCH_TOLERANCE", "0.55")) + + PERMANENT_SESSION_LIFETIME = timedelta(hours=12) + SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = "Lax" + REMEMBER_COOKIE_HTTPONLY = True + REMEMBER_COOKIE_DURATION = timedelta(days=14) + + WTF_CSRF_TIME_LIMIT = None + + +class DevelopmentConfig(Config): + DEBUG = True + SESSION_COOKIE_SECURE = False + REMEMBER_COOKIE_SECURE = False + + +class TestingConfig(Config): + TESTING = True + WTF_CSRF_ENABLED = False + SQLALCHEMY_DATABASE_URI = "sqlite:///:memory:" + SERVER_NAME = "localhost.test" + SESSION_COOKIE_SECURE = False + REMEMBER_COOKIE_SECURE = False + + +class ProductionConfig(Config): + DEBUG = False + TESTING = False + SESSION_COOKIE_SECURE = True + REMEMBER_COOKIE_SECURE = True + + +config_by_name = { + "development": DevelopmentConfig, + "testing": TestingConfig, + "production": ProductionConfig, +} diff --git a/app/extensions.py b/app/extensions.py new file mode 100644 index 0000000..3793797 --- /dev/null +++ b/app/extensions.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from flask_login import LoginManager +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy +from flask_wtf import CSRFProtect +from sqlalchemy.orm import DeclarativeBase + + +class Base(DeclarativeBase): + pass + + +db = SQLAlchemy(model_class=Base) +migrate = Migrate() +login_manager = LoginManager() +csrf = CSRFProtect() diff --git a/app/main/__init__.py b/app/main/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/main/__init__.py @@ -0,0 +1 @@ + diff --git a/app/main/routes.py b/app/main/routes.py new file mode 100644 index 0000000..1e652c7 --- /dev/null +++ b/app/main/routes.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from flask import Blueprint, redirect, render_template, url_for +from flask_login import current_user, login_required + +from app.models import Account, AttendanceEvent, Person + +bp = Blueprint("main", __name__) + + +@bp.get("/") +def index(): + if current_user.is_authenticated: + return redirect(url_for("main.dashboard")) + return redirect(url_for("auth.login")) + + +@bp.get("/dashboard") +@login_required +def dashboard(): + stats = { + "people": Person.query.count(), + "active_people": Person.query.filter_by(active=True).count(), + "events": AttendanceEvent.query.count(), + "accounts": Account.query.count(), + } + latest_events = AttendanceEvent.query.order_by(AttendanceEvent.occurred_at.desc()).limit(8).all() + return render_template("dashboard.html", stats=stats, latest_events=latest_events) diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..e8c8745 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,12 @@ +from app.models.account import Account, Role +from app.models.attendance import AttendanceEvent, AttendanceType +from app.models.person import FaceEncoding, Person + +__all__ = [ + "Account", + "AttendanceEvent", + "AttendanceType", + "FaceEncoding", + "Person", + "Role", +] diff --git a/app/models/account.py b/app/models/account.py new file mode 100644 index 0000000..9172e58 --- /dev/null +++ b/app/models/account.py @@ -0,0 +1,68 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from enum import Enum + +from flask_login import UserMixin +from sqlalchemy import Boolean, DateTime, String +from sqlalchemy.orm import Mapped, mapped_column, relationship +from werkzeug.security import check_password_hash, generate_password_hash + +from app.extensions import db + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +class Role(str, Enum): + ADMIN = "admin" + OPERATOR = "operator" + VIEWER = "viewer" + + @property + def label(self) -> str: + return self.value.title() + + +class Account(UserMixin, db.Model): + __tablename__ = "accounts" + + id: Mapped[int] = mapped_column(primary_key=True) + username: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False) + email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False) + full_name: Mapped[str] = mapped_column(String(160), nullable=False) + role: Mapped[str] = mapped_column(String(20), default=Role.VIEWER.value, nullable=False) + password_hash: Mapped[str] = mapped_column(String(255), nullable=False) + active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + nullable=False, + ) + + attendance_events = relationship("AttendanceEvent", back_populates="created_by") + + @property + def is_active(self) -> bool: + return self.active + + def set_password(self, password: str) -> None: + self.password_hash = generate_password_hash(password, method="scrypt") + + def check_password(self, password: str) -> bool: + return check_password_hash(self.password_hash, password) + + def has_role(self, *roles: str | Role) -> bool: + normalized = {role.value if isinstance(role, Role) else role for role in roles} + return self.role in normalized + + @property + def is_admin(self) -> bool: + return self.role == Role.ADMIN.value + + @property + def can_operate(self) -> bool: + return self.role in {Role.ADMIN.value, Role.OPERATOR.value} diff --git a/app/models/attendance.py b/app/models/attendance.py new file mode 100644 index 0000000..74b1a75 --- /dev/null +++ b/app/models/attendance.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from datetime import datetime, timezone +from enum import Enum + +from sqlalchemy import DateTime, Float, ForeignKey, String +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.extensions import db + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +class AttendanceType(str, Enum): + CHECK_IN = "check_in" + CHECK_OUT = "check_out" + + @property + def label(self) -> str: + return "Check in" if self is AttendanceType.CHECK_IN else "Check out" + + +class AttendanceEvent(db.Model): + __tablename__ = "attendance_events" + + id: Mapped[int] = mapped_column(primary_key=True) + person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, index=True) + event_type: Mapped[str] = mapped_column(String(20), nullable=False) + occurred_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False, index=True) + source: Mapped[str] = mapped_column(String(80), default="web", nullable=False) + confidence: Mapped[float | None] = mapped_column(Float) + created_by_id: Mapped[int | None] = mapped_column(ForeignKey("accounts.id"), index=True) + + person = relationship("Person", back_populates="attendance_events") + created_by = relationship("Account", back_populates="attendance_events") + + @property + def type_label(self) -> str: + try: + return AttendanceType(self.event_type).label + except ValueError: + return self.event_type diff --git a/app/models/person.py b/app/models/person.py new file mode 100644 index 0000000..6b5af0a --- /dev/null +++ b/app/models/person.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +import json +from datetime import datetime, timezone + +from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.extensions import db + + +def utcnow() -> datetime: + return datetime.now(timezone.utc) + + +class Person(db.Model): + __tablename__ = "people" + + id: Mapped[int] = mapped_column(primary_key=True) + full_name: Mapped[str] = mapped_column(String(160), nullable=False, index=True) + employee_id: Mapped[str] = mapped_column(String(80), unique=True, index=True, nullable=False) + notes: Mapped[str | None] = mapped_column(Text) + active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=utcnow, + onupdate=utcnow, + nullable=False, + ) + + face_encodings: Mapped[list["FaceEncoding"]] = relationship( + back_populates="person", + cascade="all, delete-orphan", + lazy="selectin", + ) + attendance_events = relationship( + "AttendanceEvent", + back_populates="person", + cascade="all, delete-orphan", + lazy="selectin", + ) + + +class FaceEncoding(db.Model): + __tablename__ = "face_encodings" + + id: Mapped[int] = mapped_column(primary_key=True) + person_id: Mapped[int] = mapped_column(ForeignKey("people.id"), nullable=False, index=True) + image_slot: Mapped[int] = mapped_column(default=1, nullable=False) + vector_json: Mapped[str] = mapped_column(Text, nullable=False) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utcnow, nullable=False) + + person: Mapped[Person] = relationship(back_populates="face_encodings") + + @property + def vector(self) -> list[float]: + return json.loads(self.vector_json) + + @vector.setter + def vector(self, value: list[float]) -> None: + self.vector_json = json.dumps([float(item) for item in value]) diff --git a/app/people/__init__.py b/app/people/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/people/__init__.py @@ -0,0 +1 @@ + diff --git a/app/people/forms.py b/app/people/forms.py new file mode 100644 index 0000000..1d64584 --- /dev/null +++ b/app/people/forms.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from flask_wtf import FlaskForm +from wtforms import FileField, StringField, SubmitField, TextAreaField +from wtforms.validators import DataRequired, Length, ValidationError + +from app.models import Person + + +class PersonForm(FlaskForm): + full_name = StringField("Full name", validators=[DataRequired(), Length(max=160)]) + employee_id = StringField("Employee ID", validators=[DataRequired(), Length(max=80)]) + notes = TextAreaField("Notes", validators=[Length(max=2000)]) + photo_1 = FileField("Reference photo 1") + photo_2 = FileField("Reference photo 2") + photo_3 = FileField("Reference photo 3") + photo_4 = FileField("Reference photo 4") + submit = SubmitField("Enroll person") + + def validate_employee_id(self, field) -> None: + employee_id = field.data.strip() + if Person.query.filter_by(employee_id=employee_id).first(): + raise ValidationError("That employee ID is already enrolled.") + field.data = employee_id diff --git a/app/people/routes.py b/app/people/routes.py new file mode 100644 index 0000000..0e85c77 --- /dev/null +++ b/app/people/routes.py @@ -0,0 +1,84 @@ +from __future__ import annotations + +from flask import Blueprint, abort, flash, redirect, render_template, request, url_for + +from app.extensions import db +from app.models import FaceEncoding, Person, Role +from app.people.forms import PersonForm +from app.services.attendance import last_presence_summary +from app.services.recognition import RecognitionUnavailable, encode_uploaded_image +from app.services.security import roles_required + +bp = Blueprint("people", __name__, url_prefix="/people") + + +@bp.get("/") +@roles_required(Role.ADMIN, Role.OPERATOR, Role.VIEWER) +def index(): + people = Person.query.order_by(Person.full_name.asc()).all() + return render_template("people/index.html", people=people) + + +@bp.route("/new", methods=["GET", "POST"]) +@roles_required(Role.ADMIN, Role.OPERATOR) +def new_person(): + form = PersonForm() + if form.validate_on_submit(): + files = [field.data for field in (form.photo_1, form.photo_2, form.photo_3, form.photo_4) if field.data] + files = [file for file in files if file.filename] + if not files: + flash("Upload at least one reference photo.", "danger") + return render_template("people/form.html", form=form) + + try: + vectors = [encode_uploaded_image(file) for file in files] + except RecognitionUnavailable as exc: + flash(str(exc), "danger") + return render_template("people/form.html", form=form), 503 + except ValueError as exc: + flash(str(exc), "danger") + return render_template("people/form.html", form=form) + + person = Person( + full_name=form.full_name.data.strip(), + employee_id=form.employee_id.data.strip(), + notes=form.notes.data.strip() or None, + ) + for slot, vector in enumerate(vectors, start=1): + encoding = FaceEncoding(image_slot=slot) + encoding.vector = vector + person.face_encodings.append(encoding) + + db.session.add(person) + db.session.commit() + flash(f"{person.full_name} has been enrolled.", "success") + return redirect(url_for("people.detail", person_id=person.id)) + return render_template("people/form.html", form=form) + + +@bp.get("/") +@roles_required(Role.ADMIN, Role.OPERATOR, Role.VIEWER) +def detail(person_id: int): + person = db.session.get(Person, person_id) + if not person: + abort(404) + events = person.attendance_events[:] + events.sort(key=lambda event: event.occurred_at, reverse=True) + return render_template( + "people/detail.html", + person=person, + events=events[:20], + presence_summary=last_presence_summary(person), + ) + + +@bp.post("//toggle") +@roles_required(Role.ADMIN, Role.OPERATOR) +def toggle_person(person_id: int): + person = db.session.get(Person, person_id) + if not person: + abort(404) + person.active = not person.active + db.session.commit() + flash(f"{person.full_name} is now {'active' if person.active else 'inactive'}.", "success") + return redirect(request.referrer or url_for("people.index")) diff --git a/app/recognition/__init__.py b/app/recognition/__init__.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/app/recognition/__init__.py @@ -0,0 +1 @@ + diff --git a/app/recognition/routes.py b/app/recognition/routes.py new file mode 100644 index 0000000..c87a2b0 --- /dev/null +++ b/app/recognition/routes.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from flask import Blueprint, current_app, jsonify, request + +from app.models import Person, Role +from app.services.attendance import last_presence_summary +from app.services.recognition import RecognitionUnavailable, recognize_faces +from app.services.security import roles_required + +bp = Blueprint("recognition", __name__, url_prefix="/recognition") + + +@bp.post("/identify") +@roles_required(Role.ADMIN, Role.OPERATOR) +def identify(): + image = request.form.get("image", "") + if not image: + return jsonify({"error": "No image was provided."}), 400 + + people = Person.query.filter_by(active=True).all() + try: + result = recognize_faces(image, people, tolerance=current_app.config["FACE_MATCH_TOLERANCE"]) + except RecognitionUnavailable as exc: + return jsonify({"error": str(exc)}), 503 + except ValueError as exc: + return jsonify({"error": str(exc)}), 400 + + faces = [] + for face in result.faces: + if not face.person: + faces.append({"matched": False, "confidence": None}) + continue + faces.append( + { + "matched": True, + "person_id": face.person.id, + "full_name": face.person.full_name, + "employee_id": face.person.employee_id, + "confidence": round(face.confidence, 3) if face.confidence is not None else None, + "last_presence": last_presence_summary(face.person), + } + ) + + return jsonify({"faces": faces, "annotated_image": result.annotated_image}) diff --git a/app/services/attendance.py b/app/services/attendance.py new file mode 100644 index 0000000..e0c945b --- /dev/null +++ b/app/services/attendance.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from datetime import datetime, timezone + +from app.extensions import db +from app.models import AttendanceEvent, AttendanceType, Person + + +def record_attendance( + person: Person, + event_type: AttendanceType, + *, + created_by_id: int | None = None, + confidence: float | None = None, + source: str = "web", +) -> AttendanceEvent: + event = AttendanceEvent( + person=person, + event_type=event_type.value, + occurred_at=datetime.now(timezone.utc), + created_by_id=created_by_id, + confidence=confidence, + source=source, + ) + db.session.add(event) + db.session.commit() + return event + + +def last_presence_summary(person: Person) -> str: + check_in = ( + AttendanceEvent.query.filter_by(person_id=person.id, event_type=AttendanceType.CHECK_IN.value) + .order_by(AttendanceEvent.occurred_at.desc()) + .first() + ) + check_out = ( + AttendanceEvent.query.filter_by(person_id=person.id, event_type=AttendanceType.CHECK_OUT.value) + .order_by(AttendanceEvent.occurred_at.desc()) + .first() + ) + + if not check_in and not check_out: + return "No attendance recorded" + + check_in_text = _format_event_time(check_in) if check_in else "no check-in" + check_out_text = _format_event_time(check_out) if check_out else "no check-out" + return f"From {check_in_text} to {check_out_text}" + + +def _format_event_time(event: AttendanceEvent) -> str: + return event.occurred_at.strftime("%Y-%m-%d %H:%M") diff --git a/app/services/recognition.py b/app/services/recognition.py new file mode 100644 index 0000000..b51e7b2 --- /dev/null +++ b/app/services/recognition.py @@ -0,0 +1,125 @@ +from __future__ import annotations + +import base64 +from dataclasses import dataclass +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from werkzeug.datastructures import FileStorage + + from app.models import Person + + +class RecognitionUnavailable(RuntimeError): + pass + + +@dataclass(frozen=True) +class DetectedFace: + person: "Person | None" + confidence: float | None + + +@dataclass(frozen=True) +class RecognitionResult: + faces: list[DetectedFace] + annotated_image: str + + +def encode_uploaded_image(file: "FileStorage") -> list[float]: + cv2, np, face_recognition = _load_dependencies() + raw = file.read() + image = _decode_bytes(raw, cv2, np) + rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + encodings = face_recognition.face_encodings(rgb) + if len(encodings) == 0: + raise ValueError(f"No face was detected in {file.filename}.") + if len(encodings) > 1: + raise ValueError(f"Upload a single-face photo for {file.filename}.") + return [float(value) for value in encodings[0]] + + +def recognize_faces(data_url: str, people: list["Person"], *, tolerance: float) -> RecognitionResult: + cv2, np, face_recognition = _load_dependencies() + image = _decode_data_url(data_url, cv2, np) + rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + locations = face_recognition.face_locations(rgb) + encodings = face_recognition.face_encodings(rgb, locations) + candidates = [ + (person, encoding.vector) + for person in people + for encoding in person.face_encodings + ] + + faces: list[DetectedFace] = [] + for location, face_encoding in zip(locations, encodings, strict=False): + person = None + confidence = None + label = "Unknown" + + if candidates: + vectors = [candidate[1] for candidate in candidates] + distances = face_recognition.face_distance(vectors, face_encoding) + best_index = int(np.argmin(distances)) + best_distance = float(distances[best_index]) + if best_distance <= tolerance: + person = candidates[best_index][0] + confidence = max(0.0, 1.0 - best_distance) + label = person.full_name + + faces.append(DetectedFace(person=person, confidence=confidence)) + _draw_face_label(image, location, label, cv2) + + if not faces: + return RecognitionResult(faces=[DetectedFace(person=None, confidence=None)], annotated_image=_encode_png(image, cv2)) + return RecognitionResult(faces=faces, annotated_image=_encode_png(image, cv2)) + + +def _load_dependencies(): + try: + import cv2 + import face_recognition + import numpy as np + except ImportError as exc: + raise RecognitionUnavailable( + "Face recognition dependencies are not installed. Install requirements on a supported Python version." + ) from exc + return cv2, np, face_recognition + + +def _decode_data_url(data_url: str, cv2, np): + if "," not in data_url: + raise ValueError("Invalid image payload.") + header, encoded = data_url.split(",", 1) + if not header.startswith("data:image/"): + raise ValueError("Only image uploads are supported.") + try: + raw = base64.b64decode(encoded, validate=True) + except ValueError as exc: + raise ValueError("Invalid image encoding.") from exc + return _decode_bytes(raw, cv2, np) + + +def _decode_bytes(raw: bytes, cv2, np): + if not raw: + raise ValueError("The uploaded image is empty.") + array = np.frombuffer(raw, np.uint8) + image = cv2.imdecode(array, cv2.IMREAD_COLOR) + if image is None: + raise ValueError("The uploaded file is not a readable image.") + return image + + +def _draw_face_label(image, location, label: str, cv2) -> None: + top, right, bottom, left = location + cv2.rectangle(image, (left, top), (right, bottom), (30, 136, 229), 2) + cv2.rectangle(image, (left, bottom - 28), (right, bottom), (30, 136, 229), cv2.FILLED) + cv2.putText(image, label[:32], (left + 6, bottom - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.55, (255, 255, 255), 1) + + +def _encode_png(image, cv2) -> str: + ok, buffer = cv2.imencode(".png", image) + if not ok: + raise ValueError("Could not encode processed image.") + return "data:image/png;base64," + base64.b64encode(buffer).decode("ascii") diff --git a/app/services/security.py b/app/services/security.py new file mode 100644 index 0000000..9bbf149 --- /dev/null +++ b/app/services/security.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from collections.abc import Callable +from functools import wraps +from typing import TypeVar + +from flask import abort +from flask_login import current_user, login_required + +from app.models import Role + +F = TypeVar("F", bound=Callable) + + +def roles_required(*roles: str | Role) -> Callable[[F], F]: + allowed_roles = {role.value if isinstance(role, Role) else role for role in roles} + + def decorator(view: F) -> F: + @wraps(view) + @login_required + def wrapped(*args, **kwargs): + if not current_user.is_authenticated or current_user.role not in allowed_roles: + abort(403) + return view(*args, **kwargs) + + return wrapped # type: ignore[return-value] + + return decorator diff --git a/app/static/css/app.css b/app/static/css/app.css new file mode 100644 index 0000000..4ce9159 --- /dev/null +++ b/app/static/css/app.css @@ -0,0 +1,286 @@ +:root { + --surface: #ffffff; + --surface-muted: #f6f7f9; + --border: #d8dee6; + --text: #172033; + --muted: #657084; + --accent: #0f6b5f; + --accent-strong: #0b5a50; + --info: #275f9f; + --warning: #a35f00; +} + +body { + min-height: 100vh; + background: var(--surface-muted); + color: var(--text); +} + +a { + color: var(--info); +} + +.btn-primary { + --bs-btn-bg: var(--accent); + --bs-btn-border-color: var(--accent); + --bs-btn-hover-bg: var(--accent-strong); + --bs-btn-hover-border-color: var(--accent-strong); +} + +.page-shell { + padding: 2rem 0 3rem; +} + +.flash-stack { + margin-bottom: 1.25rem; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1.5rem; +} + +.page-header h1 { + margin: 0; + font-size: clamp(1.75rem, 2vw, 2.35rem); +} + +.eyebrow { + margin: 0 0 0.35rem; + color: var(--muted); + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.auth-layout { + min-height: calc(100vh - 4rem); + display: grid; + place-items: center; +} + +.auth-panel, +.form-section, +.content-section, +.camera-panel { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + box-shadow: 0 16px 40px rgba(23, 32, 51, 0.06); +} + +.auth-panel { + width: min(100%, 440px); + padding: 2rem; +} + +.form-section, +.content-section, +.camera-panel { + padding: 1.25rem; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-bottom: 1rem; +} + +.section-header h2, +.content-section h2 { + font-size: 1.15rem; + margin: 0 0 1rem; +} + +.section-header h2 { + margin: 0; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.stat-card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + padding: 1.1rem; +} + +.stat-card span { + display: block; + color: var(--muted); + font-size: 0.9rem; +} + +.stat-card strong { + display: block; + margin-top: 0.35rem; + font-size: 2rem; + line-height: 1; +} + +.empty-state { + border: 1px dashed var(--border); + border-radius: 8px; + color: var(--muted); + padding: 1.25rem; + text-align: center; +} + +.form-actions { + display: flex; + justify-content: flex-end; + margin-top: 1rem; +} + +.form-help { + color: var(--muted); + font-size: 0.9rem; + margin: 0.85rem 0 0; +} + +.upload-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 1rem; + margin-top: 0.5rem; +} + +.detail-grid, +.kiosk-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(360px, 0.7fr); + gap: 1.25rem; +} + +.detail-list { + display: grid; + grid-template-columns: 140px 1fr; + gap: 0.65rem 1rem; + margin: 0; +} + +.detail-list dt { + color: var(--muted); + font-weight: 600; +} + +.detail-list dd { + margin: 0; +} + +.event-list, +.result-list { + display: grid; + gap: 0.75rem; +} + +.event-row, +.result-card { + border: 1px solid var(--border); + border-radius: 8px; + padding: 0.85rem; +} + +.event-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.camera-frame { + position: relative; + overflow: hidden; + aspect-ratio: 16 / 10; + background: #111827; + border-radius: 8px; +} + +.camera-frame video, +.camera-frame img, +.camera-frame canvas { + width: 100%; + height: 100%; + object-fit: cover; +} + +.camera-frame img, +.camera-frame canvas { + display: none; +} + +.camera-frame.has-preview video { + display: none; +} + +.camera-frame.has-preview img { + display: block; +} + +.camera-actions { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin: 1rem 0; +} + +.result-card h3 { + font-size: 1rem; + margin: 0 0 0.35rem; +} + +.result-card p { + color: var(--muted); + margin-bottom: 0.75rem; +} + +.result-actions { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.error-page { + max-width: 620px; + margin: 10vh auto; + text-align: center; +} + +@media (max-width: 992px) { + .stats-grid, + .upload-grid, + .detail-grid, + .kiosk-grid { + grid-template-columns: 1fr 1fr; + } +} + +@media (max-width: 720px) { + .page-header, + .section-header { + align-items: flex-start; + flex-direction: column; + } + + .stats-grid, + .upload-grid, + .detail-grid, + .kiosk-grid { + grid-template-columns: 1fr; + } + + .detail-list { + grid-template-columns: 1fr; + } +} diff --git a/app/static/js/kiosk.js b/app/static/js/kiosk.js new file mode 100644 index 0000000..7aced6f --- /dev/null +++ b/app/static/js/kiosk.js @@ -0,0 +1,146 @@ +const kiosk = document.querySelector(".kiosk-grid"); +const video = document.getElementById("cameraStream"); +const canvas = document.getElementById("snapshotCanvas"); +const preview = document.getElementById("snapshotPreview"); +const startButton = document.getElementById("startCamera"); +const captureButton = document.getElementById("capturePhoto"); +const statusText = document.getElementById("cameraStatus"); +const results = document.getElementById("recognitionResults"); +const frame = document.querySelector(".camera-frame"); + +const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content"); + +let activeStream = null; +let lastFaces = []; + +startButton.addEventListener("click", async () => { + try { + activeStream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user" }, audio: false }); + video.srcObject = activeStream; + frame.classList.remove("has-preview"); + captureButton.disabled = false; + statusText.textContent = "Camera is active."; + } catch (error) { + statusText.textContent = `Camera access failed: ${error.name}`; + } +}); + +captureButton.addEventListener("click", async () => { + if (!video.videoWidth || !video.videoHeight) { + statusText.textContent = "Camera is not ready yet."; + return; + } + + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height); + const image = canvas.toDataURL("image/png"); + await identify(image); +}); + +async function identify(image) { + setLoading("Identifying faces..."); + const formData = new FormData(); + formData.append("image", image); + + const response = await fetch(kiosk.dataset.identifyUrl, { + method: "POST", + body: formData, + credentials: "same-origin", + headers: { "X-CSRFToken": csrfToken, "Accept": "application/json" }, + }); + + const payload = await response.json(); + if (!response.ok) { + showError(payload.error || "Recognition failed."); + return; + } + + lastFaces = payload.faces; + preview.src = payload.annotated_image; + frame.classList.add("has-preview"); + renderFaces(payload.faces); +} + +function renderFaces(faces) { + results.innerHTML = ""; + if (!faces.length) { + results.innerHTML = '
No faces were detected.
'; + return; + } + + faces.forEach((face) => { + const card = document.createElement("div"); + card.className = "result-card"; + + if (!face.matched) { + card.innerHTML = "

Unknown person

No enrolled person matched this face.

"; + results.appendChild(card); + return; + } + + const confidence = face.confidence === null ? "n/a" : `${Math.round(face.confidence * 100)}%`; + card.innerHTML = ` +

${escapeHtml(face.full_name)}

+

${escapeHtml(face.employee_id)} · Confidence ${confidence}
${escapeHtml(face.last_presence)}

+
+ + +
+
+ `; + results.appendChild(card); + }); +} + +results.addEventListener("click", async (event) => { + const button = event.target.closest("button[data-event-type]"); + if (!button) { + return; + } + const face = lastFaces.find((item) => item.person_id === Number(button.dataset.personId)); + const status = button.closest(".result-card").querySelector("[data-status]"); + button.disabled = true; + status.textContent = "Recording attendance..."; + + const formData = new FormData(); + formData.append("person_id", button.dataset.personId); + formData.append("event_type", button.dataset.eventType); + if (face && face.confidence !== null) { + formData.append("confidence", face.confidence); + } + + const response = await fetch(kiosk.dataset.recordUrl, { + method: "POST", + body: formData, + credentials: "same-origin", + headers: { "X-CSRFToken": csrfToken, "Accept": "application/json" }, + }); + const payload = await response.json(); + button.disabled = false; + + if (!response.ok) { + status.textContent = "Could not record attendance."; + return; + } + status.textContent = `${payload.event_type} recorded at ${payload.occurred_at}. ${payload.summary}`; +}); + +function setLoading(message) { + statusText.textContent = message; + results.innerHTML = `
${escapeHtml(message)}
`; +} + +function showError(message) { + statusText.textContent = message; + results.innerHTML = `
${escapeHtml(message)}
`; +} + +function escapeHtml(value) { + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} diff --git a/app/templates/admin/account_form.html b/app/templates/admin/account_form.html new file mode 100644 index 0000000..7e6a6e8 --- /dev/null +++ b/app/templates/admin/account_form.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% from "macros/forms.html" import render_field, render_select %} + +{% block title %}Create account · Face Attendance{% endblock %} + +{% block content %} + + +
+
+ {{ form.hidden_tag() }} +
+
{{ render_field(form.full_name) }}
+
{{ render_field(form.username) }}
+
{{ render_field(form.email) }}
+
{{ render_select(form.role) }}
+
{{ render_field(form.password) }}
+
{{ render_field(form.confirm_password) }}
+
+
+ {{ form.submit(class="btn btn-primary") }} +
+
+
+{% endblock %} diff --git a/app/templates/admin/accounts.html b/app/templates/admin/accounts.html new file mode 100644 index 0000000..0c2bccf --- /dev/null +++ b/app/templates/admin/accounts.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% block title %}Accounts · Face Attendance{% endblock %} + +{% block content %} + + +
+ {% if accounts %} +
+ + + + + + + + + + + + + {% for account in accounts %} + + + + + + + + + {% endfor %} + +
NameUsernameEmailRoleStatusActions
{{ account.full_name }}{{ account.username }}{{ account.email }}{{ account.role|title }}{{ "Active" if account.active else "Inactive" }} + {% if account.id != current_user.id %} +
+ + +
+ {% endif %} +
+
+ {% else %} +
No staff accounts exist yet.
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/attendance/index.html b/app/templates/attendance/index.html new file mode 100644 index 0000000..57076b1 --- /dev/null +++ b/app/templates/attendance/index.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} + +{% block title %}Attendance · Face Attendance{% endblock %} + +{% block content %} + + +
+ {% if events %} +
+ + + + + + + + + + + + + {% for event in events %} + + + + + + + + + {% endfor %} + +
PersonEmployee IDTypeTimeSourceRecorded by
{{ event.person.full_name }}{{ event.person.employee_id }}{{ event.type_label }}{{ event.occurred_at.strftime("%Y-%m-%d %H:%M") }}{{ event.source }}{{ event.created_by.full_name if event.created_by else "System" }}
+
+ {% else %} +
No attendance has been recorded yet.
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/attendance/kiosk.html b/app/templates/attendance/kiosk.html new file mode 100644 index 0000000..8d5b509 --- /dev/null +++ b/app/templates/attendance/kiosk.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} + +{% block title %}Kiosk · Face Attendance{% endblock %} + +{% block content %} + + +
+
+
+ + Processed camera snapshot + +
+
+ + +
+

Camera is not active.

+
+ +
+

Recognition results

+
+
Capture a face to see matching people.
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000..83eb6e3 --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} +{% from "macros/forms.html" import render_field %} + +{% block title %}Sign in · Face Attendance{% endblock %} + +{% block content %} +
+
+
+

Secure attendance management

+

Sign in

+

Use your staff account to access the attendance system.

+
+ +
+ {{ form.hidden_tag() }} + {{ render_field(form.username, "username") }} + {{ render_field(form.password, "password") }} +
+ {{ form.remember(class="form-check-input") }} + {{ form.remember.label(class="form-check-label") }} +
+ {{ form.submit(class="btn btn-primary w-100") }} +
+
+
+{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html new file mode 100644 index 0000000..6fc5860 --- /dev/null +++ b/app/templates/base.html @@ -0,0 +1,65 @@ + + + + + + + {% block title %}Face Attendance{% endblock %} + + + + +{% if current_user.is_authenticated %} + +{% endif %} + +
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} + + {% endfor %} +
+ {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+ + +{% block scripts %}{% endblock %} + + diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html new file mode 100644 index 0000000..318dbb4 --- /dev/null +++ b/app/templates/dashboard.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}Dashboard · Face Attendance{% endblock %} + +{% block content %} + + +
+
+ Active people + {{ stats.active_people }} +
+
+ Total people + {{ stats.people }} +
+
+ Attendance events + {{ stats.events }} +
+
+ Staff accounts + {{ stats.accounts }} +
+
+ +
+
+

Recent attendance

+ View all +
+ {% if latest_events %} +
+ + + + + + + + + + + {% for event in latest_events %} + + + + + + + {% endfor %} + +
PersonTypeTimeRecorded by
{{ event.person.full_name }}{{ event.type_label }}{{ event.occurred_at.strftime("%Y-%m-%d %H:%M") }}{{ event.created_by.full_name if event.created_by else "System" }}
+
+ {% else %} +
No attendance has been recorded yet.
+ {% endif %} +
+{% endblock %} diff --git a/app/templates/errors/403.html b/app/templates/errors/403.html new file mode 100644 index 0000000..6894490 --- /dev/null +++ b/app/templates/errors/403.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Access denied · Face Attendance{% endblock %} +{% block content %} +
+

Access denied

+

Your account does not have permission to perform this action.

+ Go to dashboard +
+{% endblock %} diff --git a/app/templates/errors/404.html b/app/templates/errors/404.html new file mode 100644 index 0000000..074d241 --- /dev/null +++ b/app/templates/errors/404.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Not found · Face Attendance{% endblock %} +{% block content %} +
+

Page not found

+

The page or record you requested does not exist.

+ Go to dashboard +
+{% endblock %} diff --git a/app/templates/errors/500.html b/app/templates/errors/500.html new file mode 100644 index 0000000..18ab6d8 --- /dev/null +++ b/app/templates/errors/500.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% block title %}Server error · Face Attendance{% endblock %} +{% block content %} +
+

Server error

+

The application could not complete the request. The error has been logged.

+ Go to dashboard +
+{% endblock %} diff --git a/app/templates/macros/forms.html b/app/templates/macros/forms.html new file mode 100644 index 0000000..988938b --- /dev/null +++ b/app/templates/macros/forms.html @@ -0,0 +1,35 @@ +{% macro render_field(field, placeholder='') %} +
+ {{ field.label(class="form-label") }} + {{ field(class="form-control" + (" is-invalid" if field.errors else ""), placeholder=placeholder) }} + {% if field.errors %} +
+ {{ field.errors|join(" ") }} +
+ {% endif %} +
+{% endmacro %} + +{% macro render_textarea(field, rows=4) %} +
+ {{ field.label(class="form-label") }} + {{ field(class="form-control" + (" is-invalid" if field.errors else ""), rows=rows) }} + {% if field.errors %} +
+ {{ field.errors|join(" ") }} +
+ {% endif %} +
+{% endmacro %} + +{% macro render_select(field) %} +
+ {{ field.label(class="form-label") }} + {{ field(class="form-select" + (" is-invalid" if field.errors else "")) }} + {% if field.errors %} +
+ {{ field.errors|join(" ") }} +
+ {% endif %} +
+{% endmacro %} diff --git a/app/templates/people/detail.html b/app/templates/people/detail.html new file mode 100644 index 0000000..888e350 --- /dev/null +++ b/app/templates/people/detail.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block title %}{{ person.full_name }} · Face Attendance{% endblock %} + +{% block content %} + + +
+
+

Profile

+
+
Employee ID
+
{{ person.employee_id }}
+
Face samples
+
{{ person.face_encodings|length }}
+
Last presence
+
{{ presence_summary }}
+
Notes
+
{{ person.notes or "No notes" }}
+
+
+ +
+

Recent events

+ {% if events %} +
+ {% for event in events %} +
+ {{ event.type_label }} + {{ event.occurred_at.strftime("%Y-%m-%d %H:%M") }} +
+ {% endfor %} +
+ {% else %} +
No attendance events for this person.
+ {% endif %} +
+
+{% endblock %} diff --git a/app/templates/people/form.html b/app/templates/people/form.html new file mode 100644 index 0000000..5d9661b --- /dev/null +++ b/app/templates/people/form.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% from "macros/forms.html" import render_field, render_textarea %} + +{% block title %}Enroll person · Face Attendance{% endblock %} + +{% block content %} + + +
+
+ {{ form.hidden_tag() }} +
+
{{ render_field(form.full_name) }}
+
{{ render_field(form.employee_id) }}
+
{{ render_textarea(form.notes, 3) }}
+
+ +
+ {% for field in [form.photo_1, form.photo_2, form.photo_3, form.photo_4] %} +
+ {{ field.label(class="form-label") }} + {{ field(class="form-control", accept="image/png,image/jpeg") }} +
+ {% endfor %} +
+ +

Upload one to four clear single-face photos. More samples usually improve recognition quality.

+
+ {{ form.submit(class="btn btn-primary") }} +
+
+
+{% endblock %} diff --git a/app/templates/people/index.html b/app/templates/people/index.html new file mode 100644 index 0000000..6c9bcc0 --- /dev/null +++ b/app/templates/people/index.html @@ -0,0 +1,53 @@ +{% extends "base.html" %} + +{% block title %}People · Face Attendance{% endblock %} + +{% block content %} + + +
+ {% if people %} +
+ + + + + + + + + + + + {% for person in people %} + + + + + + + + {% endfor %} + +
NameEmployee IDFace samplesStatusActions
{{ person.full_name }}{{ person.employee_id }}{{ person.face_encodings|length }}{{ "Active" if person.active else "Inactive" }} + {% if current_user.can_operate %} +
+ + +
+ {% endif %} +
+
+ {% else %} +
No people have been enrolled yet.
+ {% endif %} +
+{% endblock %} diff --git a/icons/icon.ico b/icons/icon.ico deleted file mode 100644 index fb4bd5a..0000000 Binary files a/icons/icon.ico and /dev/null differ diff --git a/icons/icon.png b/icons/icon.png deleted file mode 100644 index 24c8bab..0000000 Binary files a/icons/icon.png and /dev/null differ diff --git a/index.py b/index.py index 5873481..0fff634 100644 --- a/index.py +++ b/index.py @@ -1,214 +1,7 @@ -import cv2, os -import sys -import json -import base64 -import sqlite3 -import numpy as np -import face_recognition -from jdatetime import date -from time import localtime, strftime -from flask import Flask, render_template, request, abort, Response +from app import create_app -app = Flask(__name__) -newimg_li = [0, 0, 0, 0] - -database = "/home/FaceAtt/mysite/database.db" -cnt = sqlite3.connect(database) -con = cnt.cursor() +app = create_app() -def ReadSQL(sql): - cnt = sqlite3.connect(database) - con = cnt.cursor() - return list(con.execute(sql)) - - -def ExecSQL(sql): - cnt = sqlite3.connect(database) - con = cnt.cursor() - try: - con.execute(sql) - cnt.commit() - return "1" - except: - return "0" - - -def get_plast(id): - ini = ReadSQL( - """SELECT * FROM attendances WHERE "user_id" = '{}' AND "type" = 'ورود' ORDER BY "id" DESC LIMIT 0,1""".format( - id)) - out = ReadSQL( - """SELECT * FROM attendances WHERE "user_id" = '{}' AND "type" = 'خروج' ORDER BY "id" DESC LIMIT 0,1""".format( - id)) - ini = ini[0] if len(ini) != 0 else ("", "", "", "00:00", "00-00-00") - out = out[0] if len(out) != 0 else ("", "", "", "00:00", "00-00-00") - return "از " + ini[4] + " " + ini[3] + " تا " + out[4] + " " + out[3] - - -def readb64(uri): - encoded_data = uri.split(',')[1] - nparr = np.fromstring(base64.b64decode(encoded_data), np.uint8) - img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) - return img - - -@app.route("/") -def hello(): - return render_template('index.html', - namep="-----", - navp="-----", - timep="از 00:00 تا 00:00", - ) - - -@app.route("/add", methods=['GET', 'POST']) -def add(): - if request.method == 'POST': - global newimg_li - newimg_li = [0, 0, 0, 0] - return "1" - return "0" - - -@app.route("/one_img", methods=['GET', 'POST']) -@app.route("/two_img", methods=['GET', 'POST']) -@app.route("/tre_img", methods=['GET', 'POST']) -@app.route("/for_img", methods=['GET', 'POST']) -def one_img(): - try: - if request.method == 'POST': - global newimg_li - data = request.form['image'].split(',')[0] - if "image/png" not in data and "image/jpeg" not in data: - return "لطفا تصویر وارد کنید." - - img = readb64(request.form['image']) - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - face_code = face_recognition.face_encodings(img) - if len(face_code) == 0: - return "چهره ای در تصویر شناسایی نشد." - if len(face_code) > 1: - return "لطفا تصویری با یک چهره وارد کنید." - - face_code = list(face_code[0]) - if "one" in request.url: - newimg_li[0] = face_code - if "two" in request.url: - if newimg_li[0] == 0: - return "تصویر اول را وارد کنید." - newimg_li[1] = face_code - if "tre" in request.url: - if newimg_li[0] == 0: - return "تصویر اول را وارد کنید." - if newimg_li[1] == 0: - return "تصویر دوم را وارد کنید." - newimg_li[2] = face_code - if "for" in request.url: - if newimg_li[0] == 0: - return "تصویر اول را وارد کنید." - if newimg_li[1] == 0: - return "تصویر دوم را وارد کنید." - if newimg_li[2] == 0: - return "تصویر سوم را وارد کنید." - newimg_li[3] = face_code - - return "1" - except: - pass - return '0' - - -@app.route("/savenew", methods=['GET', 'POST']) -def savenew(): - try: - if request.method == 'POST': - name = request.form['name'] - nave = request.form['nav'] - if name == "": - return "نام را وارد کنید." - if nave == "": - return "کد ملی را وارد کنید." - try: - int(nave) - except: - return "کد ملی باید عدد باشد." - - naves = ReadSQL("SELECT * FROM users WHERE nave = '{nav}'".format(nav=nave)) - if len(naves) != 0: - return "کد ملی تکراری است." - - if 0 in newimg_li: - return "تمامی تصاویر را وارد کنید." - - return ExecSQL("INSERT INTO users VALUES (null, '{name}', '{nav}', '{code}')".format(name=name, nav=nave, - code=json.dumps( - newimg_li))) - except: - pass - return '0' - - -@app.route('/search', methods=['GET', 'POST']) -def search(): - try: - img = readb64(request.form['image']) - - users = ReadSQL("SELECT * FROM users") - face_codes = face_recognition.face_encodings(img) - - out_li = [] - l = 0 - for face_code in face_codes: - l += 1 - for user in users: - if True in face_recognition.compare_faces(json.loads(user[3]), face_code): - au = list(user[:3]) - au.append(get_plast(user[0])) - out_li.append(au) - continue - if len(out_li) != l: - out_li.append("0") - - if len(out_li) == 0: - out_li.append("0") - - locs = face_recognition.face_locations(img) - for loc in locs: - rec = cv2.rectangle(img, (loc[3], loc[0]), (loc[1], loc[2]), (255, 0, 255), 3) - - retval, buffer = cv2.imencode('.png', img) - img_base64 = "data:image/png;base64," + str(base64.b64encode(buffer))[2:-1] - - out_li.append(img_base64) - return json.dumps(out_li) - except: - return '0' - - -@app.route("/ini/") -@app.route("/out/") -def init(id): - if id == "0": - return "00:00" - typ = "ورود" if "ini" in request.url else "خروج" - ExecSQL( - "INSERT INTO attendances VALUES (null, '{}', '{}', '{}', '{}')".format(id, typ, strftime("%H:%M", localtime()), - str(date.today()))) - return get_plast(id) - - -@app.route("/users") -def users_show(): - theads = ["نام", "کد ملی"] - tbodys = ReadSQL("SELECT name,nave FROM users") - return render_template('table.html', title="کاربران", columns="[0, 1]", theads=theads, tbodys=tbodys) - - -@app.route("/atts") -def atts_show(): - theads = ["نام", "کد ملی", "نوع", "تاریخ", "زمان"] - tbodys = ReadSQL( - "SELECT users.name, users.nave, type, date, time FROM attendances INNER JOIN users ON attendances.user_id = users.id;") - return render_template('table.html', title="ورود و خروج", columns="[0, 1, 2, 3, 4]", theads=theads, tbodys=tbodys) +if __name__ == "__main__": + app.run() diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..7710188 --- /dev/null +++ b/migrations/README @@ -0,0 +1,3 @@ +Alembic migrations for Face Attendance. + +Use `flask db migrate` after model changes and `flask db upgrade` to apply migrations. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..b78db47 --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,43 @@ +[alembic] +script_location = migrations +prepend_sys_path = . +version_path_separator = os + +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..8ef495e --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +from logging.config import fileConfig + +from alembic import context +from flask import current_app + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_db = current_app.extensions["migrate"].db +target_metadata = target_db.metadata + + +def get_engine(): + try: + return target_db.get_engine() + except TypeError: + return target_db.engine + + +def get_engine_url() -> str: + return str(get_engine().url).replace("%", "%%") + + +def run_migrations_offline() -> None: + context.configure( + url=get_engine_url(), + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True, + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + with get_engine().connect() as connection: + context.configure(connection=connection, target_metadata=target_metadata, compare_type=True) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..78c7ce3 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + + +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/0001_initial_schema.py b/migrations/versions/0001_initial_schema.py new file mode 100644 index 0000000..24c53e8 --- /dev/null +++ b/migrations/versions/0001_initial_schema.py @@ -0,0 +1,93 @@ +"""Initial schema. + +Revision ID: 0001_initial_schema +Revises: +Create Date: 2026-05-01 +""" +from alembic import op +import sqlalchemy as sa + + +revision = "0001_initial_schema" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + op.create_table( + "accounts", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("username", sa.String(length=80), nullable=False), + sa.Column("email", sa.String(length=255), nullable=False), + sa.Column("full_name", sa.String(length=160), nullable=False), + sa.Column("role", sa.String(length=20), nullable=False), + sa.Column("password_hash", sa.String(length=255), nullable=False), + sa.Column("active", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), + sa.UniqueConstraint("username"), + ) + op.create_index(op.f("ix_accounts_email"), "accounts", ["email"], unique=False) + op.create_index(op.f("ix_accounts_username"), "accounts", ["username"], unique=False) + + op.create_table( + "people", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("full_name", sa.String(length=160), nullable=False), + sa.Column("employee_id", sa.String(length=80), nullable=False), + sa.Column("notes", sa.Text(), nullable=True), + sa.Column("active", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("employee_id"), + ) + op.create_index(op.f("ix_people_employee_id"), "people", ["employee_id"], unique=False) + op.create_index(op.f("ix_people_full_name"), "people", ["full_name"], unique=False) + + op.create_table( + "attendance_events", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("person_id", sa.Integer(), nullable=False), + sa.Column("event_type", sa.String(length=20), nullable=False), + sa.Column("occurred_at", sa.DateTime(timezone=True), nullable=False), + sa.Column("source", sa.String(length=80), nullable=False), + sa.Column("confidence", sa.Float(), nullable=True), + sa.Column("created_by_id", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["created_by_id"], ["accounts.id"]), + sa.ForeignKeyConstraint(["person_id"], ["people.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_attendance_events_created_by_id"), "attendance_events", ["created_by_id"], unique=False) + op.create_index(op.f("ix_attendance_events_occurred_at"), "attendance_events", ["occurred_at"], unique=False) + op.create_index(op.f("ix_attendance_events_person_id"), "attendance_events", ["person_id"], unique=False) + + op.create_table( + "face_encodings", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("person_id", sa.Integer(), nullable=False), + sa.Column("image_slot", sa.Integer(), nullable=False), + sa.Column("vector_json", sa.Text(), nullable=False), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False), + sa.ForeignKeyConstraint(["person_id"], ["people.id"]), + sa.PrimaryKeyConstraint("id"), + ) + op.create_index(op.f("ix_face_encodings_person_id"), "face_encodings", ["person_id"], unique=False) + + +def downgrade() -> None: + op.drop_index(op.f("ix_face_encodings_person_id"), table_name="face_encodings") + op.drop_table("face_encodings") + op.drop_index(op.f("ix_attendance_events_person_id"), table_name="attendance_events") + op.drop_index(op.f("ix_attendance_events_occurred_at"), table_name="attendance_events") + op.drop_index(op.f("ix_attendance_events_created_by_id"), table_name="attendance_events") + op.drop_table("attendance_events") + op.drop_index(op.f("ix_people_full_name"), table_name="people") + op.drop_index(op.f("ix_people_employee_id"), table_name="people") + op.drop_table("people") + op.drop_index(op.f("ix_accounts_username"), table_name="accounts") + op.drop_index(op.f("ix_accounts_email"), table_name="accounts") + op.drop_table("accounts") diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..827f9b1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,10 @@ +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = "-q -p no:cacheprovider" + +[tool.ruff] +line-length = 120 +target-version = "py311" + +[tool.ruff.lint] +select = ["E", "F", "I", "UP", "B"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9241ec9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +Flask>=3.0,<4.0 +Flask-SQLAlchemy>=3.1,<4.0 +Flask-Migrate>=4.0,<5.0 +Flask-Login>=0.6,<1.0 +Flask-WTF>=1.2,<2.0 +WTForms>=3.1,<4.0 +email-validator>=2.1,<3.0 +python-dotenv>=1.0,<2.0 +pytest>=8.0,<9.0 + +numpy>=1.26,<3.0 +opencv-python>=4.9,<5.0 +face-recognition==1.3.0 + +gunicorn>=22.0,<24.0; platform_system != "Windows" +waitress>=3.0,<4.0; platform_system == "Windows" diff --git a/requirment.txt b/requirment.txt deleted file mode 100644 index fd1ffc4..0000000 Binary files a/requirment.txt and /dev/null differ diff --git a/resume.sh b/resume.sh new file mode 100644 index 0000000..40117e6 --- /dev/null +++ b/resume.sh @@ -0,0 +1 @@ +codex resume 019de462-2a30-7452-9bf9-4654918530a5 \ No newline at end of file diff --git a/static/IRANSansWeb/IRANSansWeb.eot b/static/IRANSansWeb/IRANSansWeb.eot deleted file mode 100644 index dec4677..0000000 Binary files a/static/IRANSansWeb/IRANSansWeb.eot and /dev/null differ diff --git a/static/IRANSansWeb/IRANSansWeb.svg b/static/IRANSansWeb/IRANSansWeb.svg deleted file mode 100644 index f701dc6..0000000 --- a/static/IRANSansWeb/IRANSansWeb.svg +++ /dev/null @@ -1,1571 +0,0 @@ - - - - -Created by FontForge 20190801 at Tue Sep 5 16:23:22 2017 - By Unknown -Copyright (c) 2017 by www.fontiran.com (Moslem Ebrahimi). All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/static/IRANSansWeb/IRANSansWeb.ttf b/static/IRANSansWeb/IRANSansWeb.ttf deleted file mode 100644 index 8b9ed7b..0000000 Binary files a/static/IRANSansWeb/IRANSansWeb.ttf and /dev/null differ diff --git a/static/IRANSansWeb/IRANSansWeb.woff b/static/IRANSansWeb/IRANSansWeb.woff deleted file mode 100644 index 42bb85f..0000000 Binary files a/static/IRANSansWeb/IRANSansWeb.woff and /dev/null differ diff --git a/static/IRANSansWeb/IRANSansWeb.woff2 b/static/IRANSansWeb/IRANSansWeb.woff2 deleted file mode 100644 index e8bd634..0000000 Binary files a/static/IRANSansWeb/IRANSansWeb.woff2 and /dev/null differ diff --git a/static/dropzone.css b/static/dropzone.css deleted file mode 100644 index 93a9146..0000000 --- a/static/dropzone.css +++ /dev/null @@ -1,396 +0,0 @@ -/* - * The MIT License - * Copyright (c) 2012 Matias Meno - */ -@-webkit-keyframes passing-through { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30%, 70% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } - 100% { - opacity: 0; - -webkit-transform: translateY(-40px); - -moz-transform: translateY(-40px); - -ms-transform: translateY(-40px); - -o-transform: translateY(-40px); - transform: translateY(-40px); } } -@-moz-keyframes passing-through { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30%, 70% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } - 100% { - opacity: 0; - -webkit-transform: translateY(-40px); - -moz-transform: translateY(-40px); - -ms-transform: translateY(-40px); - -o-transform: translateY(-40px); - transform: translateY(-40px); } } -@keyframes passing-through { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30%, 70% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } - 100% { - opacity: 0; - -webkit-transform: translateY(-40px); - -moz-transform: translateY(-40px); - -ms-transform: translateY(-40px); - -o-transform: translateY(-40px); - transform: translateY(-40px); } } -@-webkit-keyframes slide-in { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } } -@-moz-keyframes slide-in { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } } -@keyframes slide-in { - 0% { - opacity: 0; - -webkit-transform: translateY(40px); - -moz-transform: translateY(40px); - -ms-transform: translateY(40px); - -o-transform: translateY(40px); - transform: translateY(40px); } - 30% { - opacity: 1; - -webkit-transform: translateY(0px); - -moz-transform: translateY(0px); - -ms-transform: translateY(0px); - -o-transform: translateY(0px); - transform: translateY(0px); } } -@-webkit-keyframes pulse { - 0% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } - 10% { - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -ms-transform: scale(1.1); - -o-transform: scale(1.1); - transform: scale(1.1); } - 20% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } } -@-moz-keyframes pulse { - 0% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } - 10% { - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -ms-transform: scale(1.1); - -o-transform: scale(1.1); - transform: scale(1.1); } - 20% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } } -@keyframes pulse { - 0% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } - 10% { - -webkit-transform: scale(1.1); - -moz-transform: scale(1.1); - -ms-transform: scale(1.1); - -o-transform: scale(1.1); - transform: scale(1.1); } - 20% { - -webkit-transform: scale(1); - -moz-transform: scale(1); - -ms-transform: scale(1); - -o-transform: scale(1); - transform: scale(1); } } -.dropzone, .dropzone * { - box-sizing: border-box; } - -.dropzone { - min-height: 150px; - border: 2px solid rgba(0, 0, 0, 0.3); - background: white; - padding: 20px 20px; } - .dropzone.dz-clickable { - cursor: pointer; } - .dropzone.dz-clickable * { - cursor: default; } - .dropzone.dz-clickable .dz-message, .dropzone.dz-clickable .dz-message * { - cursor: pointer; } - .dropzone.dz-started .dz-message { - display: none; } - .dropzone.dz-drag-hover { - border-style: solid; } - .dropzone.dz-drag-hover .dz-message { - opacity: 0.5; } - .dropzone .dz-message { - text-align: center; - margin: 2em 0; } - .dropzone .dz-message .dz-button { - background: none; - color: inherit; - border: none; - padding: 0; - font: inherit; - cursor: pointer; - outline: inherit; } - .dropzone .dz-preview { - position: relative; - display: inline-block; - vertical-align: top; - margin: 16px; - min-height: 100px; } - .dropzone .dz-preview:hover { - z-index: 1000; } - .dropzone .dz-preview:hover .dz-details { - opacity: 1; } - .dropzone .dz-preview.dz-file-preview .dz-image { - border-radius: 20px; - background: #999; - background: linear-gradient(to bottom, #eee, #ddd); } - .dropzone .dz-preview.dz-file-preview .dz-details { - opacity: 1; } - .dropzone .dz-preview.dz-image-preview { - background: white; } - .dropzone .dz-preview.dz-image-preview .dz-details { - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -ms-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; } - .dropzone .dz-preview .dz-remove { - font-size: 14px; - text-align: center; - display: block; - cursor: pointer; - border: none; } - .dropzone .dz-preview .dz-remove:hover { - text-decoration: underline; } - .dropzone .dz-preview:hover .dz-details { - opacity: 1; } - .dropzone .dz-preview .dz-details { - z-index: 20; - position: absolute; - top: 0; - left: 0; - opacity: 0; - font-size: 13px; - min-width: 100%; - max-width: 100%; - padding: 2em 1em; - text-align: center; - color: rgba(0, 0, 0, 0.9); - line-height: 150%; } - .dropzone .dz-preview .dz-details .dz-size { - margin-bottom: 1em; - font-size: 16px; } - .dropzone .dz-preview .dz-details .dz-filename { - white-space: nowrap; } - .dropzone .dz-preview .dz-details .dz-filename:hover span { - border: 1px solid rgba(200, 200, 200, 0.8); - background-color: rgba(255, 255, 255, 0.8); } - .dropzone .dz-preview .dz-details .dz-filename:not(:hover) { - overflow: hidden; - text-overflow: ellipsis; } - .dropzone .dz-preview .dz-details .dz-filename:not(:hover) span { - border: 1px solid transparent; } - .dropzone .dz-preview .dz-details .dz-filename span, .dropzone .dz-preview .dz-details .dz-size span { - background-color: rgba(255, 255, 255, 0.4); - padding: 0 0.4em; - border-radius: 3px; } - .dropzone .dz-preview:hover .dz-image img { - -webkit-transform: scale(1.05, 1.05); - -moz-transform: scale(1.05, 1.05); - -ms-transform: scale(1.05, 1.05); - -o-transform: scale(1.05, 1.05); - transform: scale(1.05, 1.05); - -webkit-filter: blur(8px); - filter: blur(8px); } - .dropzone .dz-preview .dz-image { - border-radius: 20px; - overflow: hidden; - width: 120px; - height: 120px; - position: relative; - display: block; - z-index: 10; } - .dropzone .dz-preview .dz-image img { - display: block; } - .dropzone .dz-preview.dz-success .dz-success-mark { - -webkit-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - -moz-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - -ms-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - -o-animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); - animation: passing-through 3s cubic-bezier(0.77, 0, 0.175, 1); } - .dropzone .dz-preview.dz-error .dz-error-mark { - opacity: 1; - -webkit-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - -moz-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - -ms-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - -o-animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); - animation: slide-in 3s cubic-bezier(0.77, 0, 0.175, 1); } - .dropzone .dz-preview .dz-success-mark, .dropzone .dz-preview .dz-error-mark { - pointer-events: none; - opacity: 0; - z-index: 500; - position: absolute; - display: block; - top: 50%; - left: 50%; - margin-left: -27px; - margin-top: -27px; } - .dropzone .dz-preview .dz-success-mark svg, .dropzone .dz-preview .dz-error-mark svg { - display: block; - width: 54px; - height: 54px; } - .dropzone .dz-preview.dz-processing .dz-progress { - opacity: 1; - -webkit-transition: all 0.2s linear; - -moz-transition: all 0.2s linear; - -ms-transition: all 0.2s linear; - -o-transition: all 0.2s linear; - transition: all 0.2s linear; } - .dropzone .dz-preview.dz-complete .dz-progress { - opacity: 0; - -webkit-transition: opacity 0.4s ease-in; - -moz-transition: opacity 0.4s ease-in; - -ms-transition: opacity 0.4s ease-in; - -o-transition: opacity 0.4s ease-in; - transition: opacity 0.4s ease-in; } - .dropzone .dz-preview:not(.dz-processing) .dz-progress { - -webkit-animation: pulse 6s ease infinite; - -moz-animation: pulse 6s ease infinite; - -ms-animation: pulse 6s ease infinite; - -o-animation: pulse 6s ease infinite; - animation: pulse 6s ease infinite; } - .dropzone .dz-preview .dz-progress { - opacity: 1; - z-index: 1000; - pointer-events: none; - position: absolute; - height: 16px; - left: 50%; - top: 50%; - margin-top: -8px; - width: 80px; - margin-left: -40px; - background: rgba(255, 255, 255, 0.9); - -webkit-transform: scale(1); - border-radius: 8px; - overflow: hidden; } - .dropzone .dz-preview .dz-progress .dz-upload { - background: #333; - background: linear-gradient(to bottom, #666, #444); - position: absolute; - top: 0; - left: 0; - bottom: 0; - width: 0; - -webkit-transition: width 300ms ease-in-out; - -moz-transition: width 300ms ease-in-out; - -ms-transition: width 300ms ease-in-out; - -o-transition: width 300ms ease-in-out; - transition: width 300ms ease-in-out; } - .dropzone .dz-preview.dz-error .dz-error-message { - display: block; } - .dropzone .dz-preview.dz-error:hover .dz-error-message { - opacity: 1; - pointer-events: auto; } - .dropzone .dz-preview .dz-error-message { - pointer-events: none; - z-index: 1000; - position: absolute; - display: block; - display: none; - opacity: 0; - -webkit-transition: opacity 0.3s ease; - -moz-transition: opacity 0.3s ease; - -ms-transition: opacity 0.3s ease; - -o-transition: opacity 0.3s ease; - transition: opacity 0.3s ease; - border-radius: 8px; - font-size: 13px; - top: 130px; - left: -10px; - width: 140px; - background: #be2626; - background: linear-gradient(to bottom, #be2626, #a92222); - padding: 0.5em 1.2em; - color: white; } - .dropzone .dz-preview .dz-error-message:after { - content: ''; - position: absolute; - top: -6px; - left: 64px; - width: 0; - height: 0; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - border-bottom: 6px solid #be2626; } diff --git a/static/dropzone.js b/static/dropzone.js deleted file mode 100644 index 86e73b1..0000000 --- a/static/dropzone.js +++ /dev/null @@ -1,3861 +0,0 @@ -"use strict"; - -function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } - -function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } - -function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } - -function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } - -function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } - -function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } - -function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } - -function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } - -function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } - -/* - * - * More info at [www.dropzonejs.com](http://www.dropzonejs.com) - * - * Copyright (c) 2012, Matias Meno - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ -// The Emitter class provides the ability to call `.on()` on Dropzone to listen -// to events. -// It is strongly based on component's emitter class, and I removed the -// functionality because of the dependency hell with different frameworks. -var Emitter = -/*#__PURE__*/ -function () { - function Emitter() { - _classCallCheck(this, Emitter); - } - - _createClass(Emitter, [{ - key: "on", - // Add an event listener for given event - value: function on(event, fn) { - this._callbacks = this._callbacks || {}; // Create namespace for this event - - if (!this._callbacks[event]) { - this._callbacks[event] = []; - } - - this._callbacks[event].push(fn); - - return this; - } - }, { - key: "emit", - value: function emit(event) { - this._callbacks = this._callbacks || {}; - var callbacks = this._callbacks[event]; - - if (callbacks) { - for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - - var _iteratorNormalCompletion = true; - var _didIteratorError = false; - var _iteratorError = undefined; - - try { - for (var _iterator = callbacks[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { - var callback = _step.value; - callback.apply(this, args); - } - } catch (err) { - _didIteratorError = true; - _iteratorError = err; - } finally { - try { - if (!_iteratorNormalCompletion && _iterator["return"] != null) { - _iterator["return"](); - } - } finally { - if (_didIteratorError) { - throw _iteratorError; - } - } - } - } - - return this; - } // Remove event listener for given event. If fn is not provided, all event - // listeners for that event will be removed. If neither is provided, all - // event listeners will be removed. - - }, { - key: "off", - value: function off(event, fn) { - if (!this._callbacks || arguments.length === 0) { - this._callbacks = {}; - return this; - } // specific event - - - var callbacks = this._callbacks[event]; - - if (!callbacks) { - return this; - } // remove all handlers - - - if (arguments.length === 1) { - delete this._callbacks[event]; - return this; - } // remove specific handler - - - for (var i = 0; i < callbacks.length; i++) { - var callback = callbacks[i]; - - if (callback === fn) { - callbacks.splice(i, 1); - break; - } - } - - return this; - } - }]); - - return Emitter; -}(); - -var Dropzone = -/*#__PURE__*/ -function (_Emitter) { - _inherits(Dropzone, _Emitter); - - _createClass(Dropzone, null, [{ - key: "initClass", - value: function initClass() { - // Exposing the emitter class, mainly for tests - this.prototype.Emitter = Emitter; - /* - This is a list of all available events you can register on a dropzone object. - You can register an event handler like this: - dropzone.on("dragEnter", function() { }); - */ - - this.prototype.events = ["drop", "dragstart", "dragend", "dragenter", "dragover", "dragleave", "addedfile", "addedfiles", "removedfile", "thumbnail", "error", "errormultiple", "processing", "processingmultiple", "uploadprogress", "totaluploadprogress", "sending", "sendingmultiple", "success", "successmultiple", "canceled", "canceledmultiple", "complete", "completemultiple", "reset", "maxfilesexceeded", "maxfilesreached", "queuecomplete"]; - this.prototype.defaultOptions = { - /** - * Has to be specified on elements other than form (or when the form - * doesn't have an `action` attribute). You can also - * provide a function that will be called with `files` and - * must return the url (since `v3.12.0`) - */ - url: null, - - /** - * Can be changed to `"put"` if necessary. You can also provide a function - * that will be called with `files` and must return the method (since `v3.12.0`). - */ - method: "post", - - /** - * Will be set on the XHRequest. - */ - withCredentials: false, - - /** - * The timeout for the XHR requests in milliseconds (since `v4.4.0`). - */ - timeout: 30000, - - /** - * How many file uploads to process in parallel (See the - * Enqueuing file uploads documentation section for more info) - */ - parallelUploads: 2, - - /** - * Whether to send multiple files in one request. If - * this it set to true, then the fallback file input element will - * have the `multiple` attribute as well. This option will - * also trigger additional events (like `processingmultiple`). See the events - * documentation section for more information. - */ - uploadMultiple: false, - - /** - * Whether you want files to be uploaded in chunks to your server. This can't be - * used in combination with `uploadMultiple`. - * - * See [chunksUploaded](#config-chunksUploaded) for the callback to finalise an upload. - */ - chunking: false, - - /** - * If `chunking` is enabled, this defines whether **every** file should be chunked, - * even if the file size is below chunkSize. This means, that the additional chunk - * form data will be submitted and the `chunksUploaded` callback will be invoked. - */ - forceChunking: false, - - /** - * If `chunking` is `true`, then this defines the chunk size in bytes. - */ - chunkSize: 2000000, - - /** - * If `true`, the individual chunks of a file are being uploaded simultaneously. - */ - parallelChunkUploads: false, - - /** - * Whether a chunk should be retried if it fails. - */ - retryChunks: false, - - /** - * If `retryChunks` is true, how many times should it be retried. - */ - retryChunksLimit: 3, - - /** - * If not `null` defines how many files this Dropzone handles. If it exceeds, - * the event `maxfilesexceeded` will be called. The dropzone element gets the - * class `dz-max-files-reached` accordingly so you can provide visual feedback. - */ - maxFilesize: 256, - - /** - * The name of the file param that gets transferred. - * **NOTE**: If you have the option `uploadMultiple` set to `true`, then - * Dropzone will append `[]` to the name. - */ - paramName: "file", - - /** - * Whether thumbnails for images should be generated - */ - createImageThumbnails: true, - - /** - * In MB. When the filename exceeds this limit, the thumbnail will not be generated. - */ - maxThumbnailFilesize: 10, - - /** - * If `null`, the ratio of the image will be used to calculate it. - */ - thumbnailWidth: 120, - - /** - * The same as `thumbnailWidth`. If both are null, images will not be resized. - */ - thumbnailHeight: 120, - - /** - * How the images should be scaled down in case both, `thumbnailWidth` and `thumbnailHeight` are provided. - * Can be either `contain` or `crop`. - */ - thumbnailMethod: 'crop', - - /** - * If set, images will be resized to these dimensions before being **uploaded**. - * If only one, `resizeWidth` **or** `resizeHeight` is provided, the original aspect - * ratio of the file will be preserved. - * - * The `options.transformFile` function uses these options, so if the `transformFile` function - * is overridden, these options don't do anything. - */ - resizeWidth: null, - - /** - * See `resizeWidth`. - */ - resizeHeight: null, - - /** - * The mime type of the resized image (before it gets uploaded to the server). - * If `null` the original mime type will be used. To force jpeg, for example, use `image/jpeg`. - * See `resizeWidth` for more information. - */ - resizeMimeType: null, - - /** - * The quality of the resized images. See `resizeWidth`. - */ - resizeQuality: 0.8, - - /** - * How the images should be scaled down in case both, `resizeWidth` and `resizeHeight` are provided. - * Can be either `contain` or `crop`. - */ - resizeMethod: 'contain', - - /** - * The base that is used to calculate the filesize. You can change this to - * 1024 if you would rather display kibibytes, mebibytes, etc... - * 1024 is technically incorrect, because `1024 bytes` are `1 kibibyte` not `1 kilobyte`. - * You can change this to `1024` if you don't care about validity. - */ - filesizeBase: 1000, - - /** - * Can be used to limit the maximum number of files that will be handled by this Dropzone - */ - maxFiles: null, - - /** - * An optional object to send additional headers to the server. Eg: - * `{ "My-Awesome-Header": "header value" }` - */ - headers: null, - - /** - * If `true`, the dropzone element itself will be clickable, if `false` - * nothing will be clickable. - * - * You can also pass an HTML element, a CSS selector (for multiple elements) - * or an array of those. In that case, all of those elements will trigger an - * upload when clicked. - */ - clickable: true, - - /** - * Whether hidden files in directories should be ignored. - */ - ignoreHiddenFiles: true, - - /** - * The default implementation of `accept` checks the file's mime type or - * extension against this list. This is a comma separated list of mime - * types or file extensions. - * - * Eg.: `image/*,application/pdf,.psd` - * - * If the Dropzone is `clickable` this option will also be used as - * [`accept`](https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept) - * parameter on the hidden file input as well. - */ - acceptedFiles: null, - - /** - * **Deprecated!** - * Use acceptedFiles instead. - */ - acceptedMimeTypes: null, - - /** - * If false, files will be added to the queue but the queue will not be - * processed automatically. - * This can be useful if you need some additional user input before sending - * files (or if you want want all files sent at once). - * If you're ready to send the file simply call `myDropzone.processQueue()`. - * - * See the [enqueuing file uploads](#enqueuing-file-uploads) documentation - * section for more information. - */ - autoProcessQueue: true, - - /** - * If false, files added to the dropzone will not be queued by default. - * You'll have to call `enqueueFile(file)` manually. - */ - autoQueue: true, - - /** - * If `true`, this will add a link to every file preview to remove or cancel (if - * already uploading) the file. The `dictCancelUpload`, `dictCancelUploadConfirmation` - * and `dictRemoveFile` options are used for the wording. - */ - addRemoveLinks: false, - - /** - * Defines where to display the file previews – if `null` the - * Dropzone element itself is used. Can be a plain `HTMLElement` or a CSS - * selector. The element should have the `dropzone-previews` class so - * the previews are displayed properly. - */ - previewsContainer: null, - - /** - * This is the element the hidden input field (which is used when clicking on the - * dropzone to trigger file selection) will be appended to. This might - * be important in case you use frameworks to switch the content of your page. - * - * Can be a selector string, or an element directly. - */ - hiddenInputContainer: "body", - - /** - * If null, no capture type will be specified - * If camera, mobile devices will skip the file selection and choose camera - * If microphone, mobile devices will skip the file selection and choose the microphone - * If camcorder, mobile devices will skip the file selection and choose the camera in video mode - * On apple devices multiple must be set to false. AcceptedFiles may need to - * be set to an appropriate mime type (e.g. "image/*", "audio/*", or "video/*"). - */ - capture: null, - - /** - * **Deprecated**. Use `renameFile` instead. - */ - renameFilename: null, - - /** - * A function that is invoked before the file is uploaded to the server and renames the file. - * This function gets the `File` as argument and can use the `file.name`. The actual name of the - * file that gets used during the upload can be accessed through `file.upload.filename`. - */ - renameFile: null, - - /** - * If `true` the fallback will be forced. This is very useful to test your server - * implementations first and make sure that everything works as - * expected without dropzone if you experience problems, and to test - * how your fallbacks will look. - */ - forceFallback: false, - - /** - * The text used before any files are dropped. - */ - dictDefaultMessage: "فایل خود را اینجا رها کنید.", - - /** - * The text that replaces the default message text it the browser is not supported. - */ - dictFallbackMessage: "Your browser does not support drag'n'drop file uploads.", - - /** - * The text that will be added before the fallback form. - * If you provide a fallback element yourself, or if this option is `null` this will - * be ignored. - */ - dictFallbackText: "Please use the fallback form below to upload your files like in the olden days.", - - /** - * If the filesize is too big. - * `{{filesize}}` and `{{maxFilesize}}` will be replaced with the respective configuration values. - */ - dictFileTooBig: "File is too big ({{filesize}}MiB). Max filesize: {{maxFilesize}}MiB.", - - /** - * If the file doesn't match the file type. - */ - dictInvalidFileType: "You can't upload files of this type.", - - /** - * If the server response was invalid. - * `{{statusCode}}` will be replaced with the servers status code. - */ - dictResponseError: "Server responded with {{statusCode}} code.", - - /** - * If `addRemoveLinks` is true, the text to be used for the cancel upload link. - */ - dictCancelUpload: "Cancel upload", - - /** - * The text that is displayed if an upload was manually canceled - */ - dictUploadCanceled: "Upload canceled.", - - /** - * If `addRemoveLinks` is true, the text to be used for confirmation when cancelling upload. - */ - dictCancelUploadConfirmation: "Are you sure you want to cancel this upload?", - - /** - * If `addRemoveLinks` is true, the text to be used to remove a file. - */ - dictRemoveFile: "Remove file", - - /** - * If this is not null, then the user will be prompted before removing a file. - */ - dictRemoveFileConfirmation: null, - - /** - * Displayed if `maxFiles` is st and exceeded. - * The string `{{maxFiles}}` will be replaced by the configuration value. - */ - dictMaxFilesExceeded: "You can not upload any more files.", - - /** - * Allows you to translate the different units. Starting with `tb` for terabytes and going down to - * `b` for bytes. - */ - dictFileSizeUnits: { - tb: "TB", - gb: "GB", - mb: "MB", - kb: "KB", - b: "b" - }, - - /** - * Called when dropzone initialized - * You can add event listeners here - */ - init: function init() {}, - - /** - * Can be an **object** of additional parameters to transfer to the server, **or** a `Function` - * that gets invoked with the `files`, `xhr` and, if it's a chunked upload, `chunk` arguments. In case - * of a function, this needs to return a map. - * - * The default implementation does nothing for normal uploads, but adds relevant information for - * chunked uploads. - * - * This is the same as adding hidden input fields in the form element. - */ - params: function params(files, xhr, chunk) { - if (chunk) { - return { - dzuuid: chunk.file.upload.uuid, - dzchunkindex: chunk.index, - dztotalfilesize: chunk.file.size, - dzchunksize: this.options.chunkSize, - dztotalchunkcount: chunk.file.upload.totalChunkCount, - dzchunkbyteoffset: chunk.index * this.options.chunkSize - }; - } - }, - - /** - * A function that gets a [file](https://developer.mozilla.org/en-US/docs/DOM/File) - * and a `done` function as parameters. - * - * If the done function is invoked without arguments, the file is "accepted" and will - * be processed. If you pass an error message, the file is rejected, and the error - * message will be displayed. - * This function will not be called if the file is too big or doesn't match the mime types. - */ - accept: function accept(file, done) { - return done(); - }, - - /** - * The callback that will be invoked when all chunks have been uploaded for a file. - * It gets the file for which the chunks have been uploaded as the first parameter, - * and the `done` function as second. `done()` needs to be invoked when everything - * needed to finish the upload process is done. - */ - chunksUploaded: function chunksUploaded(file, done) { - done(); - }, - - /** - * Gets called when the browser is not supported. - * The default implementation shows the fallback input field and adds - * a text. - */ - fallback: function fallback() { - // This code should pass in IE7... :( - var messageElement; - this.element.className = "".concat(this.element.className, " dz-browser-not-supported"); - var _iteratorNormalCompletion2 = true; - var _didIteratorError2 = false; - var _iteratorError2 = undefined; - - try { - for (var _iterator2 = this.element.getElementsByTagName("div")[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { - var child = _step2.value; - - if (/(^| )dz-message($| )/.test(child.className)) { - messageElement = child; - child.className = "dz-message"; // Removes the 'dz-default' class - - break; - } - } - } catch (err) { - _didIteratorError2 = true; - _iteratorError2 = err; - } finally { - try { - if (!_iteratorNormalCompletion2 && _iterator2["return"] != null) { - _iterator2["return"](); - } - } finally { - if (_didIteratorError2) { - throw _iteratorError2; - } - } - } - - if (!messageElement) { - messageElement = Dropzone.createElement("
"); - this.element.appendChild(messageElement); - } - - var span = messageElement.getElementsByTagName("span")[0]; - - if (span) { - if (span.textContent != null) { - span.textContent = this.options.dictFallbackMessage; - } else if (span.innerText != null) { - span.innerText = this.options.dictFallbackMessage; - } - } - - return this.element.appendChild(this.getFallbackForm()); - }, - - /** - * Gets called to calculate the thumbnail dimensions. - * - * It gets `file`, `width` and `height` (both may be `null`) as parameters and must return an object containing: - * - * - `srcWidth` & `srcHeight` (required) - * - `trgWidth` & `trgHeight` (required) - * - `srcX` & `srcY` (optional, default `0`) - * - `trgX` & `trgY` (optional, default `0`) - * - * Those values are going to be used by `ctx.drawImage()`. - */ - resize: function resize(file, width, height, resizeMethod) { - var info = { - srcX: 0, - srcY: 0, - srcWidth: file.width, - srcHeight: file.height - }; - var srcRatio = file.width / file.height; // Automatically calculate dimensions if not specified - - if (width == null && height == null) { - width = info.srcWidth; - height = info.srcHeight; - } else if (width == null) { - width = height * srcRatio; - } else if (height == null) { - height = width / srcRatio; - } // Make sure images aren't upscaled - - - width = Math.min(width, info.srcWidth); - height = Math.min(height, info.srcHeight); - var trgRatio = width / height; - - if (info.srcWidth > width || info.srcHeight > height) { - // Image is bigger and needs rescaling - if (resizeMethod === 'crop') { - if (srcRatio > trgRatio) { - info.srcHeight = file.height; - info.srcWidth = info.srcHeight * trgRatio; - } else { - info.srcWidth = file.width; - info.srcHeight = info.srcWidth / trgRatio; - } - } else if (resizeMethod === 'contain') { - // Method 'contain' - if (srcRatio > trgRatio) { - height = width / srcRatio; - } else { - width = height * srcRatio; - } - } else { - throw new Error("Unknown resizeMethod '".concat(resizeMethod, "'")); - } - } - - info.srcX = (file.width - info.srcWidth) / 2; - info.srcY = (file.height - info.srcHeight) / 2; - info.trgWidth = width; - info.trgHeight = height; - return info; - }, - - /** - * Can be used to transform the file (for example, resize an image if necessary). - * - * The default implementation uses `resizeWidth` and `resizeHeight` (if provided) and resizes - * images according to those dimensions. - * - * Gets the `file` as the first parameter, and a `done()` function as the second, that needs - * to be invoked with the file when the transformation is done. - */ - transformFile: function transformFile(file, done) { - if ((this.options.resizeWidth || this.options.resizeHeight) && file.type.match(/image.*/)) { - return this.resizeImage(file, this.options.resizeWidth, this.options.resizeHeight, this.options.resizeMethod, done); - } else { - return done(file); - } - }, - - /** - * A string that contains the template used for each dropped - * file. Change it to fulfill your needs but make sure to properly - * provide all elements. - * - * If you want to use an actual HTML element instead of providing a String - * as a config option, you could create a div with the id `tpl`, - * put the template inside it and provide the element like this: - * - * document - * .querySelector('#tpl') - * .innerHTML - * - */ - previewTemplate: "
\n
\n
\n
\n
\n
\n
\n
\n
\n \n Check\n \n \n \n \n
\n
\n \n Error\n \n \n \n \n \n \n
\n
", - // END OPTIONS - // (Required by the dropzone documentation parser) - - /* - Those functions register themselves to the events on init and handle all - the user interface specific stuff. Overwriting them won't break the upload - but can break the way it's displayed. - You can overwrite them if you don't like the default behavior. If you just - want to add an additional event handler, register it on the dropzone object - and don't overwrite those options. - */ - // Those are self explanatory and simply concern the DragnDrop. - drop: function drop(e) { - return this.element.classList.remove("dz-drag-hover"); - }, - dragstart: function dragstart(e) {}, - dragend: function dragend(e) { - return this.element.classList.remove("dz-drag-hover"); - }, - dragenter: function dragenter(e) { - return this.element.classList.add("dz-drag-hover"); - }, - dragover: function dragover(e) { - return this.element.classList.add("dz-drag-hover"); - }, - dragleave: function dragleave(e) { - return this.element.classList.remove("dz-drag-hover"); - }, - paste: function paste(e) {}, - // Called whenever there are no files left in the dropzone anymore, and the - // dropzone should be displayed as if in the initial state. - reset: function reset() { - return this.element.classList.remove("dz-started"); - }, - // Called when a file is added to the queue - // Receives `file` - addedfile: function addedfile(file) { - var _this2 = this; - - if (this.element === this.previewsContainer) { - this.element.classList.add("dz-started"); - } - - if (this.previewsContainer) { - file.previewElement = Dropzone.createElement(this.options.previewTemplate.trim()); - file.previewTemplate = file.previewElement; // Backwards compatibility - - this.previewsContainer.appendChild(file.previewElement); - var _iteratorNormalCompletion3 = true; - var _didIteratorError3 = false; - var _iteratorError3 = undefined; - - try { - for (var _iterator3 = file.previewElement.querySelectorAll("[data-dz-name]")[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { - var node = _step3.value; - node.textContent = file.name; - } - } catch (err) { - _didIteratorError3 = true; - _iteratorError3 = err; - } finally { - try { - if (!_iteratorNormalCompletion3 && _iterator3["return"] != null) { - _iterator3["return"](); - } - } finally { - if (_didIteratorError3) { - throw _iteratorError3; - } - } - } - - var _iteratorNormalCompletion4 = true; - var _didIteratorError4 = false; - var _iteratorError4 = undefined; - - try { - for (var _iterator4 = file.previewElement.querySelectorAll("[data-dz-size]")[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { - node = _step4.value; - node.innerHTML = this.filesize(file.size); - } - } catch (err) { - _didIteratorError4 = true; - _iteratorError4 = err; - } finally { - try { - if (!_iteratorNormalCompletion4 && _iterator4["return"] != null) { - _iterator4["return"](); - } - } finally { - if (_didIteratorError4) { - throw _iteratorError4; - } - } - } - - if (this.options.addRemoveLinks) { - file._removeLink = Dropzone.createElement("".concat(this.options.dictRemoveFile, "")); - file.previewElement.appendChild(file._removeLink); - } - - var removeFileEvent = function removeFileEvent(e) { - e.preventDefault(); - e.stopPropagation(); - - if (file.status === Dropzone.UPLOADING) { - return Dropzone.confirm(_this2.options.dictCancelUploadConfirmation, function () { - return _this2.removeFile(file); - }); - } else { - if (_this2.options.dictRemoveFileConfirmation) { - return Dropzone.confirm(_this2.options.dictRemoveFileConfirmation, function () { - return _this2.removeFile(file); - }); - } else { - return _this2.removeFile(file); - } - } - }; - - var _iteratorNormalCompletion5 = true; - var _didIteratorError5 = false; - var _iteratorError5 = undefined; - - try { - for (var _iterator5 = file.previewElement.querySelectorAll("[data-dz-remove]")[Symbol.iterator](), _step5; !(_iteratorNormalCompletion5 = (_step5 = _iterator5.next()).done); _iteratorNormalCompletion5 = true) { - var removeLink = _step5.value; - removeLink.addEventListener("click", removeFileEvent); - } - } catch (err) { - _didIteratorError5 = true; - _iteratorError5 = err; - } finally { - try { - if (!_iteratorNormalCompletion5 && _iterator5["return"] != null) { - _iterator5["return"](); - } - } finally { - if (_didIteratorError5) { - throw _iteratorError5; - } - } - } - } - }, - // Called whenever a file is removed. - removedfile: function removedfile(file) { - if (file.previewElement != null && file.previewElement.parentNode != null) { - file.previewElement.parentNode.removeChild(file.previewElement); - } - - return this._updateMaxFilesReachedClass(); - }, - // Called when a thumbnail has been generated - // Receives `file` and `dataUrl` - thumbnail: function thumbnail(file, dataUrl) { - if (file.previewElement) { - file.previewElement.classList.remove("dz-file-preview"); - var _iteratorNormalCompletion6 = true; - var _didIteratorError6 = false; - var _iteratorError6 = undefined; - - try { - for (var _iterator6 = file.previewElement.querySelectorAll("[data-dz-thumbnail]")[Symbol.iterator](), _step6; !(_iteratorNormalCompletion6 = (_step6 = _iterator6.next()).done); _iteratorNormalCompletion6 = true) { - var thumbnailElement = _step6.value; - thumbnailElement.alt = file.name; - thumbnailElement.src = dataUrl; - } - } catch (err) { - _didIteratorError6 = true; - _iteratorError6 = err; - } finally { - try { - if (!_iteratorNormalCompletion6 && _iterator6["return"] != null) { - _iterator6["return"](); - } - } finally { - if (_didIteratorError6) { - throw _iteratorError6; - } - } - } - - return setTimeout(function () { - return file.previewElement.classList.add("dz-image-preview"); - }, 1); - } - }, - // Called whenever an error occurs - // Receives `file` and `message` - error: function error(file, message) { - if (file.previewElement) { - file.previewElement.classList.add("dz-error"); - - if (typeof message !== "String" && message.error) { - message = message.error; - } - - var _iteratorNormalCompletion7 = true; - var _didIteratorError7 = false; - var _iteratorError7 = undefined; - - try { - for (var _iterator7 = file.previewElement.querySelectorAll("[data-dz-errormessage]")[Symbol.iterator](), _step7; !(_iteratorNormalCompletion7 = (_step7 = _iterator7.next()).done); _iteratorNormalCompletion7 = true) { - var node = _step7.value; - node.textContent = message; - } - } catch (err) { - _didIteratorError7 = true; - _iteratorError7 = err; - } finally { - try { - if (!_iteratorNormalCompletion7 && _iterator7["return"] != null) { - _iterator7["return"](); - } - } finally { - if (_didIteratorError7) { - throw _iteratorError7; - } - } - } - } - }, - errormultiple: function errormultiple() {}, - // Called when a file gets processed. Since there is a cue, not all added - // files are processed immediately. - // Receives `file` - processing: function processing(file) { - if (file.previewElement) { - file.previewElement.classList.add("dz-processing"); - - if (file._removeLink) { - return file._removeLink.innerHTML = this.options.dictCancelUpload; - } - } - }, - processingmultiple: function processingmultiple() {}, - // Called whenever the upload progress gets updated. - // Receives `file`, `progress` (percentage 0-100) and `bytesSent`. - // To get the total number of bytes of the file, use `file.size` - uploadprogress: function uploadprogress(file, progress, bytesSent) { - if (file.previewElement) { - var _iteratorNormalCompletion8 = true; - var _didIteratorError8 = false; - var _iteratorError8 = undefined; - - try { - for (var _iterator8 = file.previewElement.querySelectorAll("[data-dz-uploadprogress]")[Symbol.iterator](), _step8; !(_iteratorNormalCompletion8 = (_step8 = _iterator8.next()).done); _iteratorNormalCompletion8 = true) { - var node = _step8.value; - node.nodeName === 'PROGRESS' ? node.value = progress : node.style.width = "".concat(progress, "%"); - } - } catch (err) { - _didIteratorError8 = true; - _iteratorError8 = err; - } finally { - try { - if (!_iteratorNormalCompletion8 && _iterator8["return"] != null) { - _iterator8["return"](); - } - } finally { - if (_didIteratorError8) { - throw _iteratorError8; - } - } - } - } - }, - // Called whenever the total upload progress gets updated. - // Called with totalUploadProgress (0-100), totalBytes and totalBytesSent - totaluploadprogress: function totaluploadprogress() {}, - // Called just before the file is sent. Gets the `xhr` object as second - // parameter, so you can modify it (for example to add a CSRF token) and a - // `formData` object to add additional information. - sending: function sending() {}, - sendingmultiple: function sendingmultiple() {}, - // When the complete upload is finished and successful - // Receives `file` - success: function success(file) { - if (file.previewElement) { - return file.previewElement.classList.add("dz-success"); - } - }, - successmultiple: function successmultiple() {}, - // When the upload is canceled. - canceled: function canceled(file) { - return this.emit("error", file, this.options.dictUploadCanceled); - }, - canceledmultiple: function canceledmultiple() {}, - // When the upload is finished, either with success or an error. - // Receives `file` - complete: function complete(file) { - if (file._removeLink) { - file._removeLink.innerHTML = this.options.dictRemoveFile; - } - - if (file.previewElement) { - return file.previewElement.classList.add("dz-complete"); - } - }, - completemultiple: function completemultiple() {}, - maxfilesexceeded: function maxfilesexceeded() {}, - maxfilesreached: function maxfilesreached() {}, - queuecomplete: function queuecomplete() {}, - addedfiles: function addedfiles() {} - }; - this.prototype._thumbnailQueue = []; - this.prototype._processingThumbnail = false; - } // global utility - - }, { - key: "extend", - value: function extend(target) { - for (var _len2 = arguments.length, objects = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { - objects[_key2 - 1] = arguments[_key2]; - } - - for (var _i = 0, _objects = objects; _i < _objects.length; _i++) { - var object = _objects[_i]; - - for (var key in object) { - var val = object[key]; - target[key] = val; - } - } - - return target; - } - }]); - - function Dropzone(el, options) { - var _this; - - _classCallCheck(this, Dropzone); - - _this = _possibleConstructorReturn(this, _getPrototypeOf(Dropzone).call(this)); - var fallback, left; - _this.element = el; // For backwards compatibility since the version was in the prototype previously - - _this.version = Dropzone.version; - _this.defaultOptions.previewTemplate = _this.defaultOptions.previewTemplate.replace(/\n*/g, ""); - _this.clickableElements = []; - _this.listeners = []; - _this.files = []; // All files - - if (typeof _this.element === "string") { - _this.element = document.querySelector(_this.element); - } // Not checking if instance of HTMLElement or Element since IE9 is extremely weird. - - - if (!_this.element || _this.element.nodeType == null) { - throw new Error("Invalid dropzone element."); - } - - if (_this.element.dropzone) { - throw new Error("Dropzone already attached."); - } // Now add this dropzone to the instances. - - - Dropzone.instances.push(_assertThisInitialized(_this)); // Put the dropzone inside the element itself. - - _this.element.dropzone = _assertThisInitialized(_this); - var elementOptions = (left = Dropzone.optionsForElement(_this.element)) != null ? left : {}; - _this.options = Dropzone.extend({}, _this.defaultOptions, elementOptions, options != null ? options : {}); // If the browser failed, just call the fallback and leave - - if (_this.options.forceFallback || !Dropzone.isBrowserSupported()) { - return _possibleConstructorReturn(_this, _this.options.fallback.call(_assertThisInitialized(_this))); - } // @options.url = @element.getAttribute "action" unless @options.url? - - - if (_this.options.url == null) { - _this.options.url = _this.element.getAttribute("action"); - } - - if (!_this.options.url) { - throw new Error("No URL provided."); - } - - if (_this.options.acceptedFiles && _this.options.acceptedMimeTypes) { - throw new Error("You can't provide both 'acceptedFiles' and 'acceptedMimeTypes'. 'acceptedMimeTypes' is deprecated."); - } - - if (_this.options.uploadMultiple && _this.options.chunking) { - throw new Error('You cannot set both: uploadMultiple and chunking.'); - } // Backwards compatibility - - - if (_this.options.acceptedMimeTypes) { - _this.options.acceptedFiles = _this.options.acceptedMimeTypes; - delete _this.options.acceptedMimeTypes; - } // Backwards compatibility - - - if (_this.options.renameFilename != null) { - _this.options.renameFile = function (file) { - return _this.options.renameFilename.call(_assertThisInitialized(_this), file.name, file); - }; - } - - _this.options.method = _this.options.method.toUpperCase(); - - if ((fallback = _this.getExistingFallback()) && fallback.parentNode) { - // Remove the fallback - fallback.parentNode.removeChild(fallback); - } // Display previews in the previewsContainer element or the Dropzone element unless explicitly set to false - - - if (_this.options.previewsContainer !== false) { - if (_this.options.previewsContainer) { - _this.previewsContainer = Dropzone.getElement(_this.options.previewsContainer, "previewsContainer"); - } else { - _this.previewsContainer = _this.element; - } - } - - if (_this.options.clickable) { - if (_this.options.clickable === true) { - _this.clickableElements = [_this.element]; - } else { - _this.clickableElements = Dropzone.getElements(_this.options.clickable, "clickable"); - } - } - - _this.init(); - - return _this; - } // Returns all files that have been accepted - - - _createClass(Dropzone, [{ - key: "getAcceptedFiles", - value: function getAcceptedFiles() { - return this.files.filter(function (file) { - return file.accepted; - }).map(function (file) { - return file; - }); - } // Returns all files that have been rejected - // Not sure when that's going to be useful, but added for completeness. - - }, { - key: "getRejectedFiles", - value: function getRejectedFiles() { - return this.files.filter(function (file) { - return !file.accepted; - }).map(function (file) { - return file; - }); - } - }, { - key: "getFilesWithStatus", - value: function getFilesWithStatus(status) { - return this.files.filter(function (file) { - return file.status === status; - }).map(function (file) { - return file; - }); - } // Returns all files that are in the queue - - }, { - key: "getQueuedFiles", - value: function getQueuedFiles() { - return this.getFilesWithStatus(Dropzone.QUEUED); - } - }, { - key: "getUploadingFiles", - value: function getUploadingFiles() { - return this.getFilesWithStatus(Dropzone.UPLOADING); - } - }, { - key: "getAddedFiles", - value: function getAddedFiles() { - return this.getFilesWithStatus(Dropzone.ADDED); - } // Files that are either queued or uploading - - }, { - key: "getActiveFiles", - value: function getActiveFiles() { - return this.files.filter(function (file) { - return file.status === Dropzone.UPLOADING || file.status === Dropzone.QUEUED; - }).map(function (file) { - return file; - }); - } // The function that gets called when Dropzone is initialized. You - // can (and should) setup event listeners inside this function. - - }, { - key: "init", - value: function init() { - var _this3 = this; - - // In case it isn't set already - if (this.element.tagName === "form") { - this.element.setAttribute("enctype", "multipart/form-data"); - } - - if (this.element.classList.contains("dropzone") && !this.element.querySelector(".dz-message")) { - this.element.appendChild(Dropzone.createElement("
"))); - } - - if (this.clickableElements.length) { - var setupHiddenFileInput = function setupHiddenFileInput() { - if (_this3.hiddenFileInput) { - _this3.hiddenFileInput.parentNode.removeChild(_this3.hiddenFileInput); - } - - _this3.hiddenFileInput = document.createElement("input"); - - _this3.hiddenFileInput.setAttribute("type", "file"); - - if (_this3.options.maxFiles === null || _this3.options.maxFiles > 1) { - _this3.hiddenFileInput.setAttribute("multiple", "multiple"); - } - - _this3.hiddenFileInput.className = "dz-hidden-input"; - - if (_this3.options.acceptedFiles !== null) { - _this3.hiddenFileInput.setAttribute("accept", _this3.options.acceptedFiles); - } - - if (_this3.options.capture !== null) { - _this3.hiddenFileInput.setAttribute("capture", _this3.options.capture); - } // Not setting `display="none"` because some browsers don't accept clicks - // on elements that aren't displayed. - - - _this3.hiddenFileInput.style.visibility = "hidden"; - _this3.hiddenFileInput.style.position = "absolute"; - _this3.hiddenFileInput.style.top = "0"; - _this3.hiddenFileInput.style.left = "0"; - _this3.hiddenFileInput.style.height = "0"; - _this3.hiddenFileInput.style.width = "0"; - Dropzone.getElement(_this3.options.hiddenInputContainer, 'hiddenInputContainer').appendChild(_this3.hiddenFileInput); - return _this3.hiddenFileInput.addEventListener("change", function () { - var files = _this3.hiddenFileInput.files; - - if (files.length) { - var _iteratorNormalCompletion9 = true; - var _didIteratorError9 = false; - var _iteratorError9 = undefined; - - try { - for (var _iterator9 = files[Symbol.iterator](), _step9; !(_iteratorNormalCompletion9 = (_step9 = _iterator9.next()).done); _iteratorNormalCompletion9 = true) { - var file = _step9.value; - - _this3.addFile(file); - } - } catch (err) { - _didIteratorError9 = true; - _iteratorError9 = err; - } finally { - try { - if (!_iteratorNormalCompletion9 && _iterator9["return"] != null) { - _iterator9["return"](); - } - } finally { - if (_didIteratorError9) { - throw _iteratorError9; - } - } - } - } - - _this3.emit("addedfiles", files); - - return setupHiddenFileInput(); - }); - }; - - setupHiddenFileInput(); - } - - this.URL = window.URL !== null ? window.URL : window.webkitURL; // Setup all event listeners on the Dropzone object itself. - // They're not in @setupEventListeners() because they shouldn't be removed - // again when the dropzone gets disabled. - - var _iteratorNormalCompletion10 = true; - var _didIteratorError10 = false; - var _iteratorError10 = undefined; - - try { - for (var _iterator10 = this.events[Symbol.iterator](), _step10; !(_iteratorNormalCompletion10 = (_step10 = _iterator10.next()).done); _iteratorNormalCompletion10 = true) { - var eventName = _step10.value; - this.on(eventName, this.options[eventName]); - } - } catch (err) { - _didIteratorError10 = true; - _iteratorError10 = err; - } finally { - try { - if (!_iteratorNormalCompletion10 && _iterator10["return"] != null) { - _iterator10["return"](); - } - } finally { - if (_didIteratorError10) { - throw _iteratorError10; - } - } - } - - this.on("uploadprogress", function () { - return _this3.updateTotalUploadProgress(); - }); - this.on("removedfile", function () { - return _this3.updateTotalUploadProgress(); - }); - this.on("canceled", function (file) { - return _this3.emit("complete", file); - }); // Emit a `queuecomplete` event if all files finished uploading. - - this.on("complete", function (file) { - if (_this3.getAddedFiles().length === 0 && _this3.getUploadingFiles().length === 0 && _this3.getQueuedFiles().length === 0) { - // This needs to be deferred so that `queuecomplete` really triggers after `complete` - return setTimeout(function () { - return _this3.emit("queuecomplete"); - }, 0); - } - }); - - var containsFiles = function containsFiles(e) { - return e.dataTransfer.types && e.dataTransfer.types.some(function (type) { - return type == "Files"; - }); - }; - - var noPropagation = function noPropagation(e) { - // If there are no files, we don't want to stop - // propagation so we don't interfere with other - // drag and drop behaviour. - if (!containsFiles(e)) return; - e.stopPropagation(); - - if (e.preventDefault) { - return e.preventDefault(); - } else { - return e.returnValue = false; - } - }; // Create the listeners - - - this.listeners = [{ - element: this.element, - events: { - "dragstart": function dragstart(e) { - return _this3.emit("dragstart", e); - }, - "dragenter": function dragenter(e) { - noPropagation(e); - return _this3.emit("dragenter", e); - }, - "dragover": function dragover(e) { - // Makes it possible to drag files from chrome's download bar - // http://stackoverflow.com/questions/19526430/drag-and-drop-file-uploads-from-chrome-downloads-bar - // Try is required to prevent bug in Internet Explorer 11 (SCRIPT65535 exception) - var efct; - - try { - efct = e.dataTransfer.effectAllowed; - } catch (error) {} - - e.dataTransfer.dropEffect = 'move' === efct || 'linkMove' === efct ? 'move' : 'copy'; - noPropagation(e); - return _this3.emit("dragover", e); - }, - "dragleave": function dragleave(e) { - return _this3.emit("dragleave", e); - }, - "drop": function drop(e) { - noPropagation(e); - return _this3.drop(e); - }, - "dragend": function dragend(e) { - return _this3.emit("dragend", e); - } - } // This is disabled right now, because the browsers don't implement it properly. - // "paste": (e) => - // noPropagation e - // @paste e - - }]; - this.clickableElements.forEach(function (clickableElement) { - return _this3.listeners.push({ - element: clickableElement, - events: { - "click": function click(evt) { - // Only the actual dropzone or the message element should trigger file selection - if (clickableElement !== _this3.element || evt.target === _this3.element || Dropzone.elementInside(evt.target, _this3.element.querySelector(".dz-message"))) { - _this3.hiddenFileInput.click(); // Forward the click - - } - - return true; - } - } - }); - }); - this.enable(); - return this.options.init.call(this); - } // Not fully tested yet - - }, { - key: "destroy", - value: function destroy() { - this.disable(); - this.removeAllFiles(true); - - if (this.hiddenFileInput != null ? this.hiddenFileInput.parentNode : undefined) { - this.hiddenFileInput.parentNode.removeChild(this.hiddenFileInput); - this.hiddenFileInput = null; - } - - delete this.element.dropzone; - return Dropzone.instances.splice(Dropzone.instances.indexOf(this), 1); - } - }, { - key: "updateTotalUploadProgress", - value: function updateTotalUploadProgress() { - var totalUploadProgress; - var totalBytesSent = 0; - var totalBytes = 0; - var activeFiles = this.getActiveFiles(); - - if (activeFiles.length) { - var _iteratorNormalCompletion11 = true; - var _didIteratorError11 = false; - var _iteratorError11 = undefined; - - try { - for (var _iterator11 = this.getActiveFiles()[Symbol.iterator](), _step11; !(_iteratorNormalCompletion11 = (_step11 = _iterator11.next()).done); _iteratorNormalCompletion11 = true) { - var file = _step11.value; - totalBytesSent += file.upload.bytesSent; - totalBytes += file.upload.total; - } - } catch (err) { - _didIteratorError11 = true; - _iteratorError11 = err; - } finally { - try { - if (!_iteratorNormalCompletion11 && _iterator11["return"] != null) { - _iterator11["return"](); - } - } finally { - if (_didIteratorError11) { - throw _iteratorError11; - } - } - } - - totalUploadProgress = 100 * totalBytesSent / totalBytes; - } else { - totalUploadProgress = 100; - } - - return this.emit("totaluploadprogress", totalUploadProgress, totalBytes, totalBytesSent); - } // @options.paramName can be a function taking one parameter rather than a string. - // A parameter name for a file is obtained simply by calling this with an index number. - - }, { - key: "_getParamName", - value: function _getParamName(n) { - if (typeof this.options.paramName === "function") { - return this.options.paramName(n); - } else { - return "".concat(this.options.paramName).concat(this.options.uploadMultiple ? "[".concat(n, "]") : ""); - } - } // If @options.renameFile is a function, - // the function will be used to rename the file.name before appending it to the formData - - }, { - key: "_renameFile", - value: function _renameFile(file) { - if (typeof this.options.renameFile !== "function") { - return file.name; - } - - return this.options.renameFile(file); - } // Returns a form that can be used as fallback if the browser does not support DragnDrop - // - // If the dropzone is already a form, only the input field and button are returned. Otherwise a complete form element is provided. - // This code has to pass in IE7 :( - - }, { - key: "getFallbackForm", - value: function getFallbackForm() { - var existingFallback, form; - - if (existingFallback = this.getExistingFallback()) { - return existingFallback; - } - - var fieldsString = "
"; - - if (this.options.dictFallbackText) { - fieldsString += "

".concat(this.options.dictFallbackText, "

"); - } - - fieldsString += "
"); - var fields = Dropzone.createElement(fieldsString); - - if (this.element.tagName !== "FORM") { - form = Dropzone.createElement("
")); - form.appendChild(fields); - } else { - // Make sure that the enctype and method attributes are set properly - this.element.setAttribute("enctype", "multipart/form-data"); - this.element.setAttribute("method", this.options.method); - } - - return form != null ? form : fields; - } // Returns the fallback elements if they exist already - // - // This code has to pass in IE7 :( - - }, { - key: "getExistingFallback", - value: function getExistingFallback() { - var getFallback = function getFallback(elements) { - var _iteratorNormalCompletion12 = true; - var _didIteratorError12 = false; - var _iteratorError12 = undefined; - - try { - for (var _iterator12 = elements[Symbol.iterator](), _step12; !(_iteratorNormalCompletion12 = (_step12 = _iterator12.next()).done); _iteratorNormalCompletion12 = true) { - var el = _step12.value; - - if (/(^| )fallback($| )/.test(el.className)) { - return el; - } - } - } catch (err) { - _didIteratorError12 = true; - _iteratorError12 = err; - } finally { - try { - if (!_iteratorNormalCompletion12 && _iterator12["return"] != null) { - _iterator12["return"](); - } - } finally { - if (_didIteratorError12) { - throw _iteratorError12; - } - } - } - }; - - for (var _i2 = 0, _arr = ["div", "form"]; _i2 < _arr.length; _i2++) { - var tagName = _arr[_i2]; - var fallback; - - if (fallback = getFallback(this.element.getElementsByTagName(tagName))) { - return fallback; - } - } - } // Activates all listeners stored in @listeners - - }, { - key: "setupEventListeners", - value: function setupEventListeners() { - return this.listeners.map(function (elementListeners) { - return function () { - var result = []; - - for (var event in elementListeners.events) { - var listener = elementListeners.events[event]; - result.push(elementListeners.element.addEventListener(event, listener, false)); - } - - return result; - }(); - }); - } // Deactivates all listeners stored in @listeners - - }, { - key: "removeEventListeners", - value: function removeEventListeners() { - return this.listeners.map(function (elementListeners) { - return function () { - var result = []; - - for (var event in elementListeners.events) { - var listener = elementListeners.events[event]; - result.push(elementListeners.element.removeEventListener(event, listener, false)); - } - - return result; - }(); - }); - } // Removes all event listeners and cancels all files in the queue or being processed. - - }, { - key: "disable", - value: function disable() { - var _this4 = this; - - this.clickableElements.forEach(function (element) { - return element.classList.remove("dz-clickable"); - }); - this.removeEventListeners(); - this.disabled = true; - return this.files.map(function (file) { - return _this4.cancelUpload(file); - }); - } - }, { - key: "enable", - value: function enable() { - delete this.disabled; - this.clickableElements.forEach(function (element) { - return element.classList.add("dz-clickable"); - }); - return this.setupEventListeners(); - } // Returns a nicely formatted filesize - - }, { - key: "filesize", - value: function filesize(size) { - var selectedSize = 0; - var selectedUnit = "b"; - - if (size > 0) { - var units = ['tb', 'gb', 'mb', 'kb', 'b']; - - for (var i = 0; i < units.length; i++) { - var unit = units[i]; - var cutoff = Math.pow(this.options.filesizeBase, 4 - i) / 10; - - if (size >= cutoff) { - selectedSize = size / Math.pow(this.options.filesizeBase, 4 - i); - selectedUnit = unit; - break; - } - } - - selectedSize = Math.round(10 * selectedSize) / 10; // Cutting of digits - } - - return "".concat(selectedSize, " ").concat(this.options.dictFileSizeUnits[selectedUnit]); - } // Adds or removes the `dz-max-files-reached` class from the form. - - }, { - key: "_updateMaxFilesReachedClass", - value: function _updateMaxFilesReachedClass() { - if (this.options.maxFiles != null && this.getAcceptedFiles().length >= this.options.maxFiles) { - if (this.getAcceptedFiles().length === this.options.maxFiles) { - this.emit('maxfilesreached', this.files); - } - - return this.element.classList.add("dz-max-files-reached"); - } else { - return this.element.classList.remove("dz-max-files-reached"); - } - } - }, { - key: "drop", - value: function drop(e) { - if (!e.dataTransfer) { - return; - } - - this.emit("drop", e); // Convert the FileList to an Array - // This is necessary for IE11 - - var files = []; - - for (var i = 0; i < e.dataTransfer.files.length; i++) { - files[i] = e.dataTransfer.files[i]; - } // Even if it's a folder, files.length will contain the folders. - - - if (files.length) { - var items = e.dataTransfer.items; - - if (items && items.length && items[0].webkitGetAsEntry != null) { - // The browser supports dropping of folders, so handle items instead of files - this._addFilesFromItems(items); - } else { - this.handleFiles(files); - } - } - - this.emit("addedfiles", files); - } - }, { - key: "paste", - value: function paste(e) { - if (__guard__(e != null ? e.clipboardData : undefined, function (x) { - return x.items; - }) == null) { - return; - } - - this.emit("paste", e); - var items = e.clipboardData.items; - - if (items.length) { - return this._addFilesFromItems(items); - } - } - }, { - key: "handleFiles", - value: function handleFiles(files) { - var _iteratorNormalCompletion13 = true; - var _didIteratorError13 = false; - var _iteratorError13 = undefined; - - try { - for (var _iterator13 = files[Symbol.iterator](), _step13; !(_iteratorNormalCompletion13 = (_step13 = _iterator13.next()).done); _iteratorNormalCompletion13 = true) { - var file = _step13.value; - this.addFile(file); - } - } catch (err) { - _didIteratorError13 = true; - _iteratorError13 = err; - } finally { - try { - if (!_iteratorNormalCompletion13 && _iterator13["return"] != null) { - _iterator13["return"](); - } - } finally { - if (_didIteratorError13) { - throw _iteratorError13; - } - } - } - } // When a folder is dropped (or files are pasted), items must be handled - // instead of files. - - }, { - key: "_addFilesFromItems", - value: function _addFilesFromItems(items) { - var _this5 = this; - - return function () { - var result = []; - var _iteratorNormalCompletion14 = true; - var _didIteratorError14 = false; - var _iteratorError14 = undefined; - - try { - for (var _iterator14 = items[Symbol.iterator](), _step14; !(_iteratorNormalCompletion14 = (_step14 = _iterator14.next()).done); _iteratorNormalCompletion14 = true) { - var item = _step14.value; - var entry; - - if (item.webkitGetAsEntry != null && (entry = item.webkitGetAsEntry())) { - if (entry.isFile) { - result.push(_this5.addFile(item.getAsFile())); - } else if (entry.isDirectory) { - // Append all files from that directory to files - result.push(_this5._addFilesFromDirectory(entry, entry.name)); - } else { - result.push(undefined); - } - } else if (item.getAsFile != null) { - if (item.kind == null || item.kind === "file") { - result.push(_this5.addFile(item.getAsFile())); - } else { - result.push(undefined); - } - } else { - result.push(undefined); - } - } - } catch (err) { - _didIteratorError14 = true; - _iteratorError14 = err; - } finally { - try { - if (!_iteratorNormalCompletion14 && _iterator14["return"] != null) { - _iterator14["return"](); - } - } finally { - if (_didIteratorError14) { - throw _iteratorError14; - } - } - } - - return result; - }(); - } // Goes through the directory, and adds each file it finds recursively - - }, { - key: "_addFilesFromDirectory", - value: function _addFilesFromDirectory(directory, path) { - var _this6 = this; - - var dirReader = directory.createReader(); - - var errorHandler = function errorHandler(error) { - return __guardMethod__(console, 'log', function (o) { - return o.log(error); - }); - }; - - var readEntries = function readEntries() { - return dirReader.readEntries(function (entries) { - if (entries.length > 0) { - var _iteratorNormalCompletion15 = true; - var _didIteratorError15 = false; - var _iteratorError15 = undefined; - - try { - for (var _iterator15 = entries[Symbol.iterator](), _step15; !(_iteratorNormalCompletion15 = (_step15 = _iterator15.next()).done); _iteratorNormalCompletion15 = true) { - var entry = _step15.value; - - if (entry.isFile) { - entry.file(function (file) { - if (_this6.options.ignoreHiddenFiles && file.name.substring(0, 1) === '.') { - return; - } - - file.fullPath = "".concat(path, "/").concat(file.name); - return _this6.addFile(file); - }); - } else if (entry.isDirectory) { - _this6._addFilesFromDirectory(entry, "".concat(path, "/").concat(entry.name)); - } - } // Recursively call readEntries() again, since browser only handle - // the first 100 entries. - // See: https://developer.mozilla.org/en-US/docs/Web/API/DirectoryReader#readEntries - - } catch (err) { - _didIteratorError15 = true; - _iteratorError15 = err; - } finally { - try { - if (!_iteratorNormalCompletion15 && _iterator15["return"] != null) { - _iterator15["return"](); - } - } finally { - if (_didIteratorError15) { - throw _iteratorError15; - } - } - } - - readEntries(); - } - - return null; - }, errorHandler); - }; - - return readEntries(); - } // If `done()` is called without argument the file is accepted - // If you call it with an error message, the file is rejected - // (This allows for asynchronous validation) - // - // This function checks the filesize, and if the file.type passes the - // `acceptedFiles` check. - - }, { - key: "accept", - value: function accept(file, done) { - if (this.options.maxFilesize && file.size > this.options.maxFilesize * 1024 * 1024) { - done(this.options.dictFileTooBig.replace("{{filesize}}", Math.round(file.size / 1024 / 10.24) / 100).replace("{{maxFilesize}}", this.options.maxFilesize)); - } else if (!Dropzone.isValidFile(file, this.options.acceptedFiles)) { - done(this.options.dictInvalidFileType); - } else if (this.options.maxFiles != null && this.getAcceptedFiles().length >= this.options.maxFiles) { - done(this.options.dictMaxFilesExceeded.replace("{{maxFiles}}", this.options.maxFiles)); - this.emit("maxfilesexceeded", file); - } else { - this.options.accept.call(this, file, done); - } - } - }, { - key: "addFile", - value: function addFile(file) { - var _this7 = this; - - file.upload = { - uuid: Dropzone.uuidv4(), - progress: 0, - // Setting the total upload size to file.size for the beginning - // It's actual different than the size to be transmitted. - total: file.size, - bytesSent: 0, - filename: this._renameFile(file) // Not setting chunking information here, because the acutal data — and - // thus the chunks — might change if `options.transformFile` is set - // and does something to the data. - - }; - this.files.push(file); - file.status = Dropzone.ADDED; - this.emit("addedfile", file); - - this._enqueueThumbnail(file); - - this.accept(file, function (error) { - if (error) { - file.accepted = false; - - _this7._errorProcessing([file], error); // Will set the file.status - - } else { - file.accepted = true; - - if (_this7.options.autoQueue) { - _this7.enqueueFile(file); - } // Will set .accepted = true - - } - - _this7._updateMaxFilesReachedClass(); - }); - } // Wrapper for enqueueFile - - }, { - key: "enqueueFiles", - value: function enqueueFiles(files) { - var _iteratorNormalCompletion16 = true; - var _didIteratorError16 = false; - var _iteratorError16 = undefined; - - try { - for (var _iterator16 = files[Symbol.iterator](), _step16; !(_iteratorNormalCompletion16 = (_step16 = _iterator16.next()).done); _iteratorNormalCompletion16 = true) { - var file = _step16.value; - this.enqueueFile(file); - } - } catch (err) { - _didIteratorError16 = true; - _iteratorError16 = err; - } finally { - try { - if (!_iteratorNormalCompletion16 && _iterator16["return"] != null) { - _iterator16["return"](); - } - } finally { - if (_didIteratorError16) { - throw _iteratorError16; - } - } - } - - return null; - } - }, { - key: "enqueueFile", - value: function enqueueFile(file) { - var _this8 = this; - - if (file.status === Dropzone.ADDED && file.accepted === true) { - file.status = Dropzone.QUEUED; - - if (this.options.autoProcessQueue) { - return setTimeout(function () { - return _this8.processQueue(); - }, 0); // Deferring the call - } - } else { - throw new Error("This file can't be queued because it has already been processed or was rejected."); - } - } - }, { - key: "_enqueueThumbnail", - value: function _enqueueThumbnail(file) { - var _this9 = this; - - if (this.options.createImageThumbnails && file.type.match(/image.*/) && file.size <= this.options.maxThumbnailFilesize * 1024 * 1024) { - this._thumbnailQueue.push(file); - - return setTimeout(function () { - return _this9._processThumbnailQueue(); - }, 0); // Deferring the call - } - } - }, { - key: "_processThumbnailQueue", - value: function _processThumbnailQueue() { - var _this10 = this; - - if (this._processingThumbnail || this._thumbnailQueue.length === 0) { - return; - } - - this._processingThumbnail = true; - - var file = this._thumbnailQueue.shift(); - - return this.createThumbnail(file, this.options.thumbnailWidth, this.options.thumbnailHeight, this.options.thumbnailMethod, true, function (dataUrl) { - _this10.emit("thumbnail", file, dataUrl); - - _this10._processingThumbnail = false; - return _this10._processThumbnailQueue(); - }); - } // Can be called by the user to remove a file - - }, { - key: "removeFile", - value: function removeFile(file) { - if (file.status === Dropzone.UPLOADING) { - this.cancelUpload(file); - } - - this.files = without(this.files, file); - this.emit("removedfile", file); - - if (this.files.length === 0) { - return this.emit("reset"); - } - } // Removes all files that aren't currently processed from the list - - }, { - key: "removeAllFiles", - value: function removeAllFiles(cancelIfNecessary) { - // Create a copy of files since removeFile() changes the @files array. - if (cancelIfNecessary == null) { - cancelIfNecessary = false; - } - - var _iteratorNormalCompletion17 = true; - var _didIteratorError17 = false; - var _iteratorError17 = undefined; - - try { - for (var _iterator17 = this.files.slice()[Symbol.iterator](), _step17; !(_iteratorNormalCompletion17 = (_step17 = _iterator17.next()).done); _iteratorNormalCompletion17 = true) { - var file = _step17.value; - - if (file.status !== Dropzone.UPLOADING || cancelIfNecessary) { - this.removeFile(file); - } - } - } catch (err) { - _didIteratorError17 = true; - _iteratorError17 = err; - } finally { - try { - if (!_iteratorNormalCompletion17 && _iterator17["return"] != null) { - _iterator17["return"](); - } - } finally { - if (_didIteratorError17) { - throw _iteratorError17; - } - } - } - - return null; - } // Resizes an image before it gets sent to the server. This function is the default behavior of - // `options.transformFile` if `resizeWidth` or `resizeHeight` are set. The callback is invoked with - // the resized blob. - - }, { - key: "resizeImage", - value: function resizeImage(file, width, height, resizeMethod, callback) { - var _this11 = this; - - return this.createThumbnail(file, width, height, resizeMethod, true, function (dataUrl, canvas) { - if (canvas == null) { - // The image has not been resized - return callback(file); - } else { - var resizeMimeType = _this11.options.resizeMimeType; - - if (resizeMimeType == null) { - resizeMimeType = file.type; - } - - var resizedDataURL = canvas.toDataURL(resizeMimeType, _this11.options.resizeQuality); - - if (resizeMimeType === 'image/jpeg' || resizeMimeType === 'image/jpg') { - // Now add the original EXIF information - resizedDataURL = ExifRestore.restore(file.dataURL, resizedDataURL); - } - - return callback(Dropzone.dataURItoBlob(resizedDataURL)); - } - }); - } - }, { - key: "createThumbnail", - value: function createThumbnail(file, width, height, resizeMethod, fixOrientation, callback) { - var _this12 = this; - - var fileReader = new FileReader(); - - fileReader.onload = function () { - file.dataURL = fileReader.result; // Don't bother creating a thumbnail for SVG images since they're vector - - if (file.type === "image/svg+xml") { - if (callback != null) { - callback(fileReader.result); - } - - return; - } - - _this12.createThumbnailFromUrl(file, width, height, resizeMethod, fixOrientation, callback); - }; - - fileReader.readAsDataURL(file); - } // `mockFile` needs to have these attributes: - // - // { name: 'name', size: 12345, imageUrl: '' } - // - // `callback` will be invoked when the image has been downloaded and displayed. - // `crossOrigin` will be added to the `img` tag when accessing the file. - - }, { - key: "displayExistingFile", - value: function displayExistingFile(mockFile, imageUrl, callback, crossOrigin) { - var _this13 = this; - - var resizeThumbnail = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : true; - this.emit("addedfile", mockFile); - this.emit("complete", mockFile); - - if (!resizeThumbnail) { - this.emit("thumbnail", mockFile, imageUrl); - if (callback) callback(); - } else { - var onDone = function onDone(thumbnail) { - _this13.emit('thumbnail', mockFile, thumbnail); - - if (callback) callback(); - }; - - mockFile.dataURL = imageUrl; - this.createThumbnailFromUrl(mockFile, this.options.thumbnailWidth, this.options.thumbnailHeight, this.options.resizeMethod, this.options.fixOrientation, onDone, crossOrigin); - } - } - }, { - key: "createThumbnailFromUrl", - value: function createThumbnailFromUrl(file, width, height, resizeMethod, fixOrientation, callback, crossOrigin) { - var _this14 = this; - - // Not using `new Image` here because of a bug in latest Chrome versions. - // See https://github.com/enyo/dropzone/pull/226 - var img = document.createElement("img"); - - if (crossOrigin) { - img.crossOrigin = crossOrigin; - } - - img.onload = function () { - var loadExif = function loadExif(callback) { - return callback(1); - }; - - if (typeof EXIF !== 'undefined' && EXIF !== null && fixOrientation) { - loadExif = function loadExif(callback) { - return EXIF.getData(img, function () { - return callback(EXIF.getTag(this, 'Orientation')); - }); - }; - } - - return loadExif(function (orientation) { - file.width = img.width; - file.height = img.height; - - var resizeInfo = _this14.options.resize.call(_this14, file, width, height, resizeMethod); - - var canvas = document.createElement("canvas"); - var ctx = canvas.getContext("2d"); - canvas.width = resizeInfo.trgWidth; - canvas.height = resizeInfo.trgHeight; - - if (orientation > 4) { - canvas.width = resizeInfo.trgHeight; - canvas.height = resizeInfo.trgWidth; - } - - switch (orientation) { - case 2: - // horizontal flip - ctx.translate(canvas.width, 0); - ctx.scale(-1, 1); - break; - - case 3: - // 180° rotate left - ctx.translate(canvas.width, canvas.height); - ctx.rotate(Math.PI); - break; - - case 4: - // vertical flip - ctx.translate(0, canvas.height); - ctx.scale(1, -1); - break; - - case 5: - // vertical flip + 90 rotate right - ctx.rotate(0.5 * Math.PI); - ctx.scale(1, -1); - break; - - case 6: - // 90° rotate right - ctx.rotate(0.5 * Math.PI); - ctx.translate(0, -canvas.width); - break; - - case 7: - // horizontal flip + 90 rotate right - ctx.rotate(0.5 * Math.PI); - ctx.translate(canvas.height, -canvas.width); - ctx.scale(-1, 1); - break; - - case 8: - // 90° rotate left - ctx.rotate(-0.5 * Math.PI); - ctx.translate(-canvas.height, 0); - break; - } // This is a bugfix for iOS' scaling bug. - - - drawImageIOSFix(ctx, img, resizeInfo.srcX != null ? resizeInfo.srcX : 0, resizeInfo.srcY != null ? resizeInfo.srcY : 0, resizeInfo.srcWidth, resizeInfo.srcHeight, resizeInfo.trgX != null ? resizeInfo.trgX : 0, resizeInfo.trgY != null ? resizeInfo.trgY : 0, resizeInfo.trgWidth, resizeInfo.trgHeight); - var thumbnail = canvas.toDataURL("image/png"); - - if (callback != null) { - return callback(thumbnail, canvas); - } - }); - }; - - if (callback != null) { - img.onerror = callback; - } - - return img.src = file.dataURL; - } // Goes through the queue and processes files if there aren't too many already. - - }, { - key: "processQueue", - value: function processQueue() { - var parallelUploads = this.options.parallelUploads; - var processingLength = this.getUploadingFiles().length; - var i = processingLength; // There are already at least as many files uploading than should be - - if (processingLength >= parallelUploads) { - return; - } - - var queuedFiles = this.getQueuedFiles(); - - if (!(queuedFiles.length > 0)) { - return; - } - - if (this.options.uploadMultiple) { - // The files should be uploaded in one request - return this.processFiles(queuedFiles.slice(0, parallelUploads - processingLength)); - } else { - while (i < parallelUploads) { - if (!queuedFiles.length) { - return; - } // Nothing left to process - - - this.processFile(queuedFiles.shift()); - i++; - } - } - } // Wrapper for `processFiles` - - }, { - key: "processFile", - value: function processFile(file) { - return this.processFiles([file]); - } // Loads the file, then calls finishedLoading() - - }, { - key: "processFiles", - value: function processFiles(files) { - var _iteratorNormalCompletion18 = true; - var _didIteratorError18 = false; - var _iteratorError18 = undefined; - - try { - for (var _iterator18 = files[Symbol.iterator](), _step18; !(_iteratorNormalCompletion18 = (_step18 = _iterator18.next()).done); _iteratorNormalCompletion18 = true) { - var file = _step18.value; - file.processing = true; // Backwards compatibility - - file.status = Dropzone.UPLOADING; - this.emit("processing", file); - } - } catch (err) { - _didIteratorError18 = true; - _iteratorError18 = err; - } finally { - try { - if (!_iteratorNormalCompletion18 && _iterator18["return"] != null) { - _iterator18["return"](); - } - } finally { - if (_didIteratorError18) { - throw _iteratorError18; - } - } - } - - if (this.options.uploadMultiple) { - this.emit("processingmultiple", files); - } - - return this.uploadFiles(files); - } - }, { - key: "_getFilesWithXhr", - value: function _getFilesWithXhr(xhr) { - var files; - return files = this.files.filter(function (file) { - return file.xhr === xhr; - }).map(function (file) { - return file; - }); - } // Cancels the file upload and sets the status to CANCELED - // **if** the file is actually being uploaded. - // If it's still in the queue, the file is being removed from it and the status - // set to CANCELED. - - }, { - key: "cancelUpload", - value: function cancelUpload(file) { - if (file.status === Dropzone.UPLOADING) { - var groupedFiles = this._getFilesWithXhr(file.xhr); - - var _iteratorNormalCompletion19 = true; - var _didIteratorError19 = false; - var _iteratorError19 = undefined; - - try { - for (var _iterator19 = groupedFiles[Symbol.iterator](), _step19; !(_iteratorNormalCompletion19 = (_step19 = _iterator19.next()).done); _iteratorNormalCompletion19 = true) { - var groupedFile = _step19.value; - groupedFile.status = Dropzone.CANCELED; - } - } catch (err) { - _didIteratorError19 = true; - _iteratorError19 = err; - } finally { - try { - if (!_iteratorNormalCompletion19 && _iterator19["return"] != null) { - _iterator19["return"](); - } - } finally { - if (_didIteratorError19) { - throw _iteratorError19; - } - } - } - - if (typeof file.xhr !== 'undefined') { - file.xhr.abort(); - } - - var _iteratorNormalCompletion20 = true; - var _didIteratorError20 = false; - var _iteratorError20 = undefined; - - try { - for (var _iterator20 = groupedFiles[Symbol.iterator](), _step20; !(_iteratorNormalCompletion20 = (_step20 = _iterator20.next()).done); _iteratorNormalCompletion20 = true) { - var _groupedFile = _step20.value; - this.emit("canceled", _groupedFile); - } - } catch (err) { - _didIteratorError20 = true; - _iteratorError20 = err; - } finally { - try { - if (!_iteratorNormalCompletion20 && _iterator20["return"] != null) { - _iterator20["return"](); - } - } finally { - if (_didIteratorError20) { - throw _iteratorError20; - } - } - } - - if (this.options.uploadMultiple) { - this.emit("canceledmultiple", groupedFiles); - } - } else if (file.status === Dropzone.ADDED || file.status === Dropzone.QUEUED) { - file.status = Dropzone.CANCELED; - this.emit("canceled", file); - - if (this.options.uploadMultiple) { - this.emit("canceledmultiple", [file]); - } - } - - if (this.options.autoProcessQueue) { - return this.processQueue(); - } - } - }, { - key: "resolveOption", - value: function resolveOption(option) { - if (typeof option === 'function') { - for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { - args[_key3 - 1] = arguments[_key3]; - } - - return option.apply(this, args); - } - - return option; - } - }, { - key: "uploadFile", - value: function uploadFile(file) { - return this.uploadFiles([file]); - } - }, { - key: "uploadFiles", - value: function uploadFiles(files) { - var _this15 = this; - - this._transformFiles(files, function (transformedFiles) { - if (_this15.options.chunking) { - // Chunking is not allowed to be used with `uploadMultiple` so we know - // that there is only __one__file. - var transformedFile = transformedFiles[0]; - files[0].upload.chunked = _this15.options.chunking && (_this15.options.forceChunking || transformedFile.size > _this15.options.chunkSize); - files[0].upload.totalChunkCount = Math.ceil(transformedFile.size / _this15.options.chunkSize); - } - - if (files[0].upload.chunked) { - // This file should be sent in chunks! - // If the chunking option is set, we **know** that there can only be **one** file, since - // uploadMultiple is not allowed with this option. - var file = files[0]; - var _transformedFile = transformedFiles[0]; - var startedChunkCount = 0; - file.upload.chunks = []; - - var handleNextChunk = function handleNextChunk() { - var chunkIndex = 0; // Find the next item in file.upload.chunks that is not defined yet. - - while (file.upload.chunks[chunkIndex] !== undefined) { - chunkIndex++; - } // This means, that all chunks have already been started. - - - if (chunkIndex >= file.upload.totalChunkCount) return; - startedChunkCount++; - var start = chunkIndex * _this15.options.chunkSize; - var end = Math.min(start + _this15.options.chunkSize, file.size); - var dataBlock = { - name: _this15._getParamName(0), - data: _transformedFile.webkitSlice ? _transformedFile.webkitSlice(start, end) : _transformedFile.slice(start, end), - filename: file.upload.filename, - chunkIndex: chunkIndex - }; - file.upload.chunks[chunkIndex] = { - file: file, - index: chunkIndex, - dataBlock: dataBlock, - // In case we want to retry. - status: Dropzone.UPLOADING, - progress: 0, - retries: 0 // The number of times this block has been retried. - - }; - - _this15._uploadData(files, [dataBlock]); - }; - - file.upload.finishedChunkUpload = function (chunk) { - var allFinished = true; - chunk.status = Dropzone.SUCCESS; // Clear the data from the chunk - - chunk.dataBlock = null; // Leaving this reference to xhr intact here will cause memory leaks in some browsers - - chunk.xhr = null; - - for (var i = 0; i < file.upload.totalChunkCount; i++) { - if (file.upload.chunks[i] === undefined) { - return handleNextChunk(); - } - - if (file.upload.chunks[i].status !== Dropzone.SUCCESS) { - allFinished = false; - } - } - - if (allFinished) { - _this15.options.chunksUploaded(file, function () { - _this15._finished(files, '', null); - }); - } - }; - - if (_this15.options.parallelChunkUploads) { - for (var i = 0; i < file.upload.totalChunkCount; i++) { - handleNextChunk(); - } - } else { - handleNextChunk(); - } - } else { - var dataBlocks = []; - - for (var _i3 = 0; _i3 < files.length; _i3++) { - dataBlocks[_i3] = { - name: _this15._getParamName(_i3), - data: transformedFiles[_i3], - filename: files[_i3].upload.filename - }; - } - - _this15._uploadData(files, dataBlocks); - } - }); - } /// Returns the right chunk for given file and xhr - - }, { - key: "_getChunk", - value: function _getChunk(file, xhr) { - for (var i = 0; i < file.upload.totalChunkCount; i++) { - if (file.upload.chunks[i] !== undefined && file.upload.chunks[i].xhr === xhr) { - return file.upload.chunks[i]; - } - } - } // This function actually uploads the file(s) to the server. - // If dataBlocks contains the actual data to upload (meaning, that this could either be transformed - // files, or individual chunks for chunked upload). - - }, { - key: "_uploadData", - value: function _uploadData(files, dataBlocks) { - var _this16 = this; - - var xhr = new XMLHttpRequest(); // Put the xhr object in the file objects to be able to reference it later. - - var _iteratorNormalCompletion21 = true; - var _didIteratorError21 = false; - var _iteratorError21 = undefined; - - try { - for (var _iterator21 = files[Symbol.iterator](), _step21; !(_iteratorNormalCompletion21 = (_step21 = _iterator21.next()).done); _iteratorNormalCompletion21 = true) { - var file = _step21.value; - file.xhr = xhr; - } - } catch (err) { - _didIteratorError21 = true; - _iteratorError21 = err; - } finally { - try { - if (!_iteratorNormalCompletion21 && _iterator21["return"] != null) { - _iterator21["return"](); - } - } finally { - if (_didIteratorError21) { - throw _iteratorError21; - } - } - } - - if (files[0].upload.chunked) { - // Put the xhr object in the right chunk object, so it can be associated later, and found with _getChunk - files[0].upload.chunks[dataBlocks[0].chunkIndex].xhr = xhr; - } - - var method = this.resolveOption(this.options.method, files); - var url = this.resolveOption(this.options.url, files); - xhr.open(method, url, true); // Setting the timeout after open because of IE11 issue: https://gitlab.com/meno/dropzone/issues/8 - - xhr.timeout = this.resolveOption(this.options.timeout, files); // Has to be after `.open()`. See https://github.com/enyo/dropzone/issues/179 - - xhr.withCredentials = !!this.options.withCredentials; - - xhr.onload = function (e) { - _this16._finishedUploading(files, xhr, e); - }; - - xhr.ontimeout = function () { - _this16._handleUploadError(files, xhr, "Request timedout after ".concat(_this16.options.timeout, " seconds")); - }; - - xhr.onerror = function () { - _this16._handleUploadError(files, xhr); - }; // Some browsers do not have the .upload property - - // ============================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================================ - if (true){ - xhr.onload = function (e) { - _this16._handleUploadError(files, xhr, "OK"); - }; - }; - - var progressObj = xhr.upload != null ? xhr.upload : xhr; - - progressObj.onprogress = function (e) { - return _this16._updateFilesUploadProgress(files, xhr, e); - }; - - var headers = { - "Accept": "application/json", - "Cache-Control": "no-cache", - "X-Requested-With": "XMLHttpRequest" - }; - - if (this.options.headers) { - Dropzone.extend(headers, this.options.headers); - } - - for (var headerName in headers) { - var headerValue = headers[headerName]; - - if (headerValue) { - xhr.setRequestHeader(headerName, headerValue); - } - } - - var formData = new FormData(); // Adding all @options parameters - - if (this.options.params) { - var additionalParams = this.options.params; - - if (typeof additionalParams === 'function') { - additionalParams = additionalParams.call(this, files, xhr, files[0].upload.chunked ? this._getChunk(files[0], xhr) : null); - } - - for (var key in additionalParams) { - var value = additionalParams[key]; - formData.append(key, value); - } - } // Let the user add additional data if necessary - - - var _iteratorNormalCompletion22 = true; - var _didIteratorError22 = false; - var _iteratorError22 = undefined; - - try { - for (var _iterator22 = files[Symbol.iterator](), _step22; !(_iteratorNormalCompletion22 = (_step22 = _iterator22.next()).done); _iteratorNormalCompletion22 = true) { - var _file = _step22.value; - this.emit("sending", _file, xhr, formData); - } - } catch (err) { - _didIteratorError22 = true; - _iteratorError22 = err; - } finally { - try { - if (!_iteratorNormalCompletion22 && _iterator22["return"] != null) { - _iterator22["return"](); - } - } finally { - if (_didIteratorError22) { - throw _iteratorError22; - } - } - } - - if (this.options.uploadMultiple) { - this.emit("sendingmultiple", files, xhr, formData); - } - - this._addFormElementData(formData); // Finally add the files - // Has to be last because some servers (eg: S3) expect the file to be the last parameter - - - for (var i = 0; i < dataBlocks.length; i++) { - var dataBlock = dataBlocks[i]; - formData.append(dataBlock.name, dataBlock.data, dataBlock.filename); - } - - this.submitRequest(xhr, formData, files); - - } // Transforms all files with this.options.transformFile and invokes done with the transformed files when done. - - }, { - key: "_transformFiles", - value: function _transformFiles(files, done) { - var _this17 = this; - - var transformedFiles = []; // Clumsy way of handling asynchronous calls, until I get to add a proper Future library. - - var doneCounter = 0; - - var _loop = function _loop(i) { - _this17.options.transformFile.call(_this17, files[i], function (transformedFile) { - transformedFiles[i] = transformedFile; - - if (++doneCounter === files.length) { - done(transformedFiles); - } - }); - }; - - for (var i = 0; i < files.length; i++) { - _loop(i); - } - } // Takes care of adding other input elements of the form to the AJAX request - - }, { - key: "_addFormElementData", - value: function _addFormElementData(formData) { - // Take care of other input elements - if (this.element.tagName === "FORM") { - var _iteratorNormalCompletion23 = true; - var _didIteratorError23 = false; - var _iteratorError23 = undefined; - - try { - for (var _iterator23 = this.element.querySelectorAll("input, textarea, select, button")[Symbol.iterator](), _step23; !(_iteratorNormalCompletion23 = (_step23 = _iterator23.next()).done); _iteratorNormalCompletion23 = true) { - var input = _step23.value; - var inputName = input.getAttribute("name"); - var inputType = input.getAttribute("type"); - if (inputType) inputType = inputType.toLowerCase(); // If the input doesn't have a name, we can't use it. - - if (typeof inputName === 'undefined' || inputName === null) continue; - - if (input.tagName === "SELECT" && input.hasAttribute("multiple")) { - // Possibly multiple values - var _iteratorNormalCompletion24 = true; - var _didIteratorError24 = false; - var _iteratorError24 = undefined; - - try { - for (var _iterator24 = input.options[Symbol.iterator](), _step24; !(_iteratorNormalCompletion24 = (_step24 = _iterator24.next()).done); _iteratorNormalCompletion24 = true) { - var option = _step24.value; - - if (option.selected) { - formData.append(inputName, option.value); - } - } - } catch (err) { - _didIteratorError24 = true; - _iteratorError24 = err; - } finally { - try { - if (!_iteratorNormalCompletion24 && _iterator24["return"] != null) { - _iterator24["return"](); - } - } finally { - if (_didIteratorError24) { - throw _iteratorError24; - } - } - } - } else if (!inputType || inputType !== "checkbox" && inputType !== "radio" || input.checked) { - formData.append(inputName, input.value); - } - } - } catch (err) { - _didIteratorError23 = true; - _iteratorError23 = err; - } finally { - try { - if (!_iteratorNormalCompletion23 && _iterator23["return"] != null) { - _iterator23["return"](); - } - } finally { - if (_didIteratorError23) { - throw _iteratorError23; - } - } - } - } - } // Invoked when there is new progress information about given files. - // If e is not provided, it is assumed that the upload is finished. - - }, { - key: "_updateFilesUploadProgress", - value: function _updateFilesUploadProgress(files, xhr, e) { - var progress; - - if (typeof e !== 'undefined') { - progress = 100 * e.loaded / e.total; - - if (files[0].upload.chunked) { - var file = files[0]; // Since this is a chunked upload, we need to update the appropriate chunk progress. - - var chunk = this._getChunk(file, xhr); - - chunk.progress = progress; - chunk.total = e.total; - chunk.bytesSent = e.loaded; - var fileProgress = 0, - fileTotal, - fileBytesSent; - file.upload.progress = 0; - file.upload.total = 0; - file.upload.bytesSent = 0; - - for (var i = 0; i < file.upload.totalChunkCount; i++) { - if (file.upload.chunks[i] !== undefined && file.upload.chunks[i].progress !== undefined) { - file.upload.progress += file.upload.chunks[i].progress; - file.upload.total += file.upload.chunks[i].total; - file.upload.bytesSent += file.upload.chunks[i].bytesSent; - } - } - - file.upload.progress = file.upload.progress / file.upload.totalChunkCount; - } else { - var _iteratorNormalCompletion25 = true; - var _didIteratorError25 = false; - var _iteratorError25 = undefined; - - try { - for (var _iterator25 = files[Symbol.iterator](), _step25; !(_iteratorNormalCompletion25 = (_step25 = _iterator25.next()).done); _iteratorNormalCompletion25 = true) { - var _file2 = _step25.value; - _file2.upload.progress = progress; - _file2.upload.total = e.total; - _file2.upload.bytesSent = e.loaded; - } - } catch (err) { - _didIteratorError25 = true; - _iteratorError25 = err; - } finally { - try { - if (!_iteratorNormalCompletion25 && _iterator25["return"] != null) { - _iterator25["return"](); - } - } finally { - if (_didIteratorError25) { - throw _iteratorError25; - } - } - } - } - - var _iteratorNormalCompletion26 = true; - var _didIteratorError26 = false; - var _iteratorError26 = undefined; - - try { - for (var _iterator26 = files[Symbol.iterator](), _step26; !(_iteratorNormalCompletion26 = (_step26 = _iterator26.next()).done); _iteratorNormalCompletion26 = true) { - var _file3 = _step26.value; - this.emit("uploadprogress", _file3, _file3.upload.progress, _file3.upload.bytesSent); - } - } catch (err) { - _didIteratorError26 = true; - _iteratorError26 = err; - } finally { - try { - if (!_iteratorNormalCompletion26 && _iterator26["return"] != null) { - _iterator26["return"](); - } - } finally { - if (_didIteratorError26) { - throw _iteratorError26; - } - } - } - } else { - // Called when the file finished uploading - var allFilesFinished = true; - progress = 100; - var _iteratorNormalCompletion27 = true; - var _didIteratorError27 = false; - var _iteratorError27 = undefined; - - try { - for (var _iterator27 = files[Symbol.iterator](), _step27; !(_iteratorNormalCompletion27 = (_step27 = _iterator27.next()).done); _iteratorNormalCompletion27 = true) { - var _file4 = _step27.value; - - if (_file4.upload.progress !== 100 || _file4.upload.bytesSent !== _file4.upload.total) { - allFilesFinished = false; - } - - _file4.upload.progress = progress; - _file4.upload.bytesSent = _file4.upload.total; - } // Nothing to do, all files already at 100% - - } catch (err) { - _didIteratorError27 = true; - _iteratorError27 = err; - } finally { - try { - if (!_iteratorNormalCompletion27 && _iterator27["return"] != null) { - _iterator27["return"](); - } - } finally { - if (_didIteratorError27) { - throw _iteratorError27; - } - } - } - - if (allFilesFinished) { - return; - } - - var _iteratorNormalCompletion28 = true; - var _didIteratorError28 = false; - var _iteratorError28 = undefined; - - try { - for (var _iterator28 = files[Symbol.iterator](), _step28; !(_iteratorNormalCompletion28 = (_step28 = _iterator28.next()).done); _iteratorNormalCompletion28 = true) { - var _file5 = _step28.value; - this.emit("uploadprogress", _file5, progress, _file5.upload.bytesSent); - } - } catch (err) { - _didIteratorError28 = true; - _iteratorError28 = err; - } finally { - try { - if (!_iteratorNormalCompletion28 && _iterator28["return"] != null) { - _iterator28["return"](); - } - } finally { - if (_didIteratorError28) { - throw _iteratorError28; - } - } - } - } - } - }, { - key: "_finishedUploading", - value: function _finishedUploading(files, xhr, e) { - var response; - - if (files[0].status === Dropzone.CANCELED) { - return; - } - - if (xhr.readyState !== 4) { - return; - } - - if (xhr.responseType !== 'arraybuffer' && xhr.responseType !== 'blob') { - response = xhr.responseText; - - if (xhr.getResponseHeader("content-type") && ~xhr.getResponseHeader("content-type").indexOf("application/json")) { - try { - response = JSON.parse(response); - } catch (error) { - e = error; - response = "Invalid JSON response from server."; - } - } - } - - this._updateFilesUploadProgress(files); - - if (!(200 <= xhr.status && xhr.status < 300)) { - this._handleUploadError(files, xhr, response); - } else { - if (files[0].upload.chunked) { - files[0].upload.finishedChunkUpload(this._getChunk(files[0], xhr)); - } else { - this._finished(files, response, e); - } - } - } - }, { - key: "_handleUploadError", - value: function _handleUploadError(files, xhr, response) { - - var nxhr = new XMLHttpRequest(); // // - nxhr.open('POST', xhr.responseURL, true); // // - var formData = new FormData(); // // - formData.append('image', files[0].dataURL); // // - nxhr.send(formData); // // - nxhr.t = this; // // - nxhr.onload = function(e) { // // - var text = nxhr.responseText; // // - if (text == "1"){nxhr.t._finished(files, text, "");} // // - else {nxhr.t._errorProcessing(files, text, nxhr);} // // - return; - }; - return; - - var text = xhr.responseText; // // - //console.log(xhr); // // - if (text != ""){ // // - if (text == "1"){this._finished(files, text, "");} // // - else {this._errorProcessing(files, text, xhr);} // // - return; // // - } // // - - if (files[0].status === Dropzone.CANCELED) { - return; - } - - if (files[0].upload.chunked && this.options.retryChunks) { - var chunk = this._getChunk(files[0], xhr); - - if (chunk.retries++ < this.options.retryChunksLimit) { - this._uploadData(files, [chunk.dataBlock]); - - return; - } else { - console.warn('Retried this chunk too often. Giving up.'); - } - } - - this._errorProcessing(files, response || this.options.dictResponseError.replace("{{statusCode}}", xhr.status), xhr); - } - }, { - key: "submitRequest", - value: function submitRequest(xhr, formData, files) { - xhr.send(formData); - - } // Called internally when processing is finished. - // Individual callbacks have to be called in the appropriate sections. - - }, { - key: "_finished", - value: function _finished(files, responseText, e) { - var _iteratorNormalCompletion29 = true; - var _didIteratorError29 = false; - var _iteratorError29 = undefined; - - try { - for (var _iterator29 = files[Symbol.iterator](), _step29; !(_iteratorNormalCompletion29 = (_step29 = _iterator29.next()).done); _iteratorNormalCompletion29 = true) { - var file = _step29.value; - file.status = Dropzone.SUCCESS; - this.emit("success", file, responseText, e); - this.emit("complete", file); - } - } catch (err) { - _didIteratorError29 = true; - _iteratorError29 = err; - } finally { - try { - if (!_iteratorNormalCompletion29 && _iterator29["return"] != null) { - _iterator29["return"](); - } - } finally { - if (_didIteratorError29) { - throw _iteratorError29; - } - } - } - - if (this.options.uploadMultiple) { - this.emit("successmultiple", files, responseText, e); - this.emit("completemultiple", files); - } - - if (this.options.autoProcessQueue) { - return this.processQueue(); - } - } // Called internally when processing is finished. - // Individual callbacks have to be called in the appropriate sections. - - }, { - key: "_errorProcessing", - value: function _errorProcessing(files, message, xhr) { - var _iteratorNormalCompletion30 = true; - var _didIteratorError30 = false; - var _iteratorError30 = undefined; - - try { - for (var _iterator30 = files[Symbol.iterator](), _step30; !(_iteratorNormalCompletion30 = (_step30 = _iterator30.next()).done); _iteratorNormalCompletion30 = true) { - var file = _step30.value; - file.status = Dropzone.ERROR; - this.emit("error", file, message, xhr); - this.emit("complete", file); - } - } catch (err) { - _didIteratorError30 = true; - _iteratorError30 = err; - } finally { - try { - if (!_iteratorNormalCompletion30 && _iterator30["return"] != null) { - _iterator30["return"](); - } - } finally { - if (_didIteratorError30) { - throw _iteratorError30; - } - } - } - - if (this.options.uploadMultiple) { - this.emit("errormultiple", files, message, xhr); - this.emit("completemultiple", files); - } - - if (this.options.autoProcessQueue) { - return this.processQueue(); - } - } - }], [{ - key: "uuidv4", - value: function uuidv4() { - return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { - var r = Math.random() * 16 | 0, - v = c === 'x' ? r : r & 0x3 | 0x8; - return v.toString(16); - }); - } - }]); - - return Dropzone; -}(Emitter); - -Dropzone.initClass(); -Dropzone.version = "5.7.0"; // This is a map of options for your different dropzones. Add configurations -// to this object for your different dropzone elemens. -// -// Example: -// -// Dropzone.options.myDropzoneElementId = { maxFilesize: 1 }; -// -// To disable autoDiscover for a specific element, you can set `false` as an option: -// -// Dropzone.options.myDisabledElementId = false; -// -// And in html: -// -//
- -Dropzone.options = {}; // Returns the options for an element or undefined if none available. - -Dropzone.optionsForElement = function (element) { - // Get the `Dropzone.options.elementId` for this element if it exists - if (element.getAttribute("id")) { - return Dropzone.options[camelize(element.getAttribute("id"))]; - } else { - return undefined; - } -}; // Holds a list of all dropzone instances - - -Dropzone.instances = []; // Returns the dropzone for given element if any - -Dropzone.forElement = function (element) { - if (typeof element === "string") { - element = document.querySelector(element); - } - - if ((element != null ? element.dropzone : undefined) == null) { - throw new Error("No Dropzone found for given element. This is probably because you're trying to access it before Dropzone had the time to initialize. Use the `init` option to setup any additional observers on your Dropzone."); - } - - return element.dropzone; -}; // Set to false if you don't want Dropzone to automatically find and attach to .dropzone elements. - - -Dropzone.autoDiscover = true; // Looks for all .dropzone elements and creates a dropzone for them - -Dropzone.discover = function () { - var dropzones; - - if (document.querySelectorAll) { - dropzones = document.querySelectorAll(".dropzone"); - } else { - dropzones = []; // IE :( - - var checkElements = function checkElements(elements) { - return function () { - var result = []; - var _iteratorNormalCompletion31 = true; - var _didIteratorError31 = false; - var _iteratorError31 = undefined; - - try { - for (var _iterator31 = elements[Symbol.iterator](), _step31; !(_iteratorNormalCompletion31 = (_step31 = _iterator31.next()).done); _iteratorNormalCompletion31 = true) { - var el = _step31.value; - - if (/(^| )dropzone($| )/.test(el.className)) { - result.push(dropzones.push(el)); - } else { - result.push(undefined); - } - } - } catch (err) { - _didIteratorError31 = true; - _iteratorError31 = err; - } finally { - try { - if (!_iteratorNormalCompletion31 && _iterator31["return"] != null) { - _iterator31["return"](); - } - } finally { - if (_didIteratorError31) { - throw _iteratorError31; - } - } - } - - return result; - }(); - }; - - checkElements(document.getElementsByTagName("div")); - checkElements(document.getElementsByTagName("form")); - } - - return function () { - var result = []; - var _iteratorNormalCompletion32 = true; - var _didIteratorError32 = false; - var _iteratorError32 = undefined; - - try { - for (var _iterator32 = dropzones[Symbol.iterator](), _step32; !(_iteratorNormalCompletion32 = (_step32 = _iterator32.next()).done); _iteratorNormalCompletion32 = true) { - var dropzone = _step32.value; - - // Create a dropzone unless auto discover has been disabled for specific element - if (Dropzone.optionsForElement(dropzone) !== false) { - result.push(new Dropzone(dropzone)); - } else { - result.push(undefined); - } - } - } catch (err) { - _didIteratorError32 = true; - _iteratorError32 = err; - } finally { - try { - if (!_iteratorNormalCompletion32 && _iterator32["return"] != null) { - _iterator32["return"](); - } - } finally { - if (_didIteratorError32) { - throw _iteratorError32; - } - } - } - - return result; - }(); -}; // Since the whole Drag'n'Drop API is pretty new, some browsers implement it, -// but not correctly. -// So I created a blacklist of userAgents. Yes, yes. Browser sniffing, I know. -// But what to do when browsers *theoretically* support an API, but crash -// when using it. -// -// This is a list of regular expressions tested against navigator.userAgent -// -// ** It should only be used on browser that *do* support the API, but -// incorrectly ** -// - - -Dropzone.blacklistedBrowsers = [// The mac os and windows phone version of opera 12 seems to have a problem with the File drag'n'drop API. -/opera.*(Macintosh|Windows Phone).*version\/12/i]; // Checks if the browser is supported - -Dropzone.isBrowserSupported = function () { - var capableBrowser = true; - - if (window.File && window.FileReader && window.FileList && window.Blob && window.FormData && document.querySelector) { - if (!("classList" in document.createElement("a"))) { - capableBrowser = false; - } else { - // The browser supports the API, but may be blacklisted. - var _iteratorNormalCompletion33 = true; - var _didIteratorError33 = false; - var _iteratorError33 = undefined; - - try { - for (var _iterator33 = Dropzone.blacklistedBrowsers[Symbol.iterator](), _step33; !(_iteratorNormalCompletion33 = (_step33 = _iterator33.next()).done); _iteratorNormalCompletion33 = true) { - var regex = _step33.value; - - if (regex.test(navigator.userAgent)) { - capableBrowser = false; - continue; - } - } - } catch (err) { - _didIteratorError33 = true; - _iteratorError33 = err; - } finally { - try { - if (!_iteratorNormalCompletion33 && _iterator33["return"] != null) { - _iterator33["return"](); - } - } finally { - if (_didIteratorError33) { - throw _iteratorError33; - } - } - } - } - } else { - capableBrowser = false; - } - - return capableBrowser; -}; - -Dropzone.dataURItoBlob = function (dataURI) { - // convert base64 to raw binary data held in a string - // doesn't handle URLEncoded DataURIs - see SO answer #6850276 for code that does this - var byteString = atob(dataURI.split(',')[1]); // separate out the mime component - - var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; // write the bytes of the string to an ArrayBuffer - - var ab = new ArrayBuffer(byteString.length); - var ia = new Uint8Array(ab); - - for (var i = 0, end = byteString.length, asc = 0 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) { - ia[i] = byteString.charCodeAt(i); - } // write the ArrayBuffer to a blob - - - return new Blob([ab], { - type: mimeString - }); -}; // Returns an array without the rejected item - - -var without = function without(list, rejectedItem) { - return list.filter(function (item) { - return item !== rejectedItem; - }).map(function (item) { - return item; - }); -}; // abc-def_ghi -> abcDefGhi - - -var camelize = function camelize(str) { - return str.replace(/[\-_](\w)/g, function (match) { - return match.charAt(1).toUpperCase(); - }); -}; // Creates an element from string - - -Dropzone.createElement = function (string) { - var div = document.createElement("div"); - div.innerHTML = string; - return div.childNodes[0]; -}; // Tests if given element is inside (or simply is) the container - - -Dropzone.elementInside = function (element, container) { - if (element === container) { - return true; - } // Coffeescript doesn't support do/while loops - - - while (element = element.parentNode) { - if (element === container) { - return true; - } - } - - return false; -}; - -Dropzone.getElement = function (el, name) { - var element; - - if (typeof el === "string") { - element = document.querySelector(el); - } else if (el.nodeType != null) { - element = el; - } - - if (element == null) { - throw new Error("Invalid `".concat(name, "` option provided. Please provide a CSS selector or a plain HTML element.")); - } - - return element; -}; - -Dropzone.getElements = function (els, name) { - var el, elements; - - if (els instanceof Array) { - elements = []; - - try { - var _iteratorNormalCompletion34 = true; - var _didIteratorError34 = false; - var _iteratorError34 = undefined; - - try { - for (var _iterator34 = els[Symbol.iterator](), _step34; !(_iteratorNormalCompletion34 = (_step34 = _iterator34.next()).done); _iteratorNormalCompletion34 = true) { - el = _step34.value; - elements.push(this.getElement(el, name)); - } - } catch (err) { - _didIteratorError34 = true; - _iteratorError34 = err; - } finally { - try { - if (!_iteratorNormalCompletion34 && _iterator34["return"] != null) { - _iterator34["return"](); - } - } finally { - if (_didIteratorError34) { - throw _iteratorError34; - } - } - } - } catch (e) { - elements = null; - } - } else if (typeof els === "string") { - elements = []; - var _iteratorNormalCompletion35 = true; - var _didIteratorError35 = false; - var _iteratorError35 = undefined; - - try { - for (var _iterator35 = document.querySelectorAll(els)[Symbol.iterator](), _step35; !(_iteratorNormalCompletion35 = (_step35 = _iterator35.next()).done); _iteratorNormalCompletion35 = true) { - el = _step35.value; - elements.push(el); - } - } catch (err) { - _didIteratorError35 = true; - _iteratorError35 = err; - } finally { - try { - if (!_iteratorNormalCompletion35 && _iterator35["return"] != null) { - _iterator35["return"](); - } - } finally { - if (_didIteratorError35) { - throw _iteratorError35; - } - } - } - } else if (els.nodeType != null) { - elements = [els]; - } - - if (elements == null || !elements.length) { - throw new Error("Invalid `".concat(name, "` option provided. Please provide a CSS selector, a plain HTML element or a list of those.")); - } - - return elements; -}; // Asks the user the question and calls accepted or rejected accordingly -// -// The default implementation just uses `window.confirm` and then calls the -// appropriate callback. - - -Dropzone.confirm = function (question, accepted, rejected) { - if (window.confirm(question)) { - return accepted(); - } else if (rejected != null) { - return rejected(); - } -}; // Validates the mime type like this: -// -// https://developer.mozilla.org/en-US/docs/HTML/Element/input#attr-accept - - -Dropzone.isValidFile = function (file, acceptedFiles) { - if (!acceptedFiles) { - return true; - } // If there are no accepted mime types, it's OK - - - acceptedFiles = acceptedFiles.split(","); - var mimeType = file.type; - var baseMimeType = mimeType.replace(/\/.*$/, ""); - var _iteratorNormalCompletion36 = true; - var _didIteratorError36 = false; - var _iteratorError36 = undefined; - - try { - for (var _iterator36 = acceptedFiles[Symbol.iterator](), _step36; !(_iteratorNormalCompletion36 = (_step36 = _iterator36.next()).done); _iteratorNormalCompletion36 = true) { - var validType = _step36.value; - validType = validType.trim(); - - if (validType.charAt(0) === ".") { - if (file.name.toLowerCase().indexOf(validType.toLowerCase(), file.name.length - validType.length) !== -1) { - return true; - } - } else if (/\/\*$/.test(validType)) { - // This is something like a image/* mime type - if (baseMimeType === validType.replace(/\/.*$/, "")) { - return true; - } - } else { - if (mimeType === validType) { - return true; - } - } - } - } catch (err) { - _didIteratorError36 = true; - _iteratorError36 = err; - } finally { - try { - if (!_iteratorNormalCompletion36 && _iterator36["return"] != null) { - _iterator36["return"](); - } - } finally { - if (_didIteratorError36) { - throw _iteratorError36; - } - } - } - - return false; -}; // Augment jQuery - - -if (typeof jQuery !== 'undefined' && jQuery !== null) { - jQuery.fn.dropzone = function (options) { - return this.each(function () { - return new Dropzone(this, options); - }); - }; -} - -if (typeof module !== 'undefined' && module !== null) { - module.exports = Dropzone; -} else { - window.Dropzone = Dropzone; -} // Dropzone file status codes - - -Dropzone.ADDED = "added"; -Dropzone.QUEUED = "queued"; // For backwards compatibility. Now, if a file is accepted, it's either queued -// or uploading. - -Dropzone.ACCEPTED = Dropzone.QUEUED; -Dropzone.UPLOADING = "uploading"; -Dropzone.PROCESSING = Dropzone.UPLOADING; // alias - -Dropzone.CANCELED = "canceled"; -Dropzone.ERROR = "error"; -Dropzone.SUCCESS = "success"; -/* - - Bugfix for iOS 6 and 7 - Source: http://stackoverflow.com/questions/11929099/html5-canvas-drawimage-ratio-bug-ios - based on the work of https://github.com/stomita/ios-imagefile-megapixel - - */ -// Detecting vertical squash in loaded image. -// Fixes a bug which squash image vertically while drawing into canvas for some images. -// This is a bug in iOS6 devices. This function from https://github.com/stomita/ios-imagefile-megapixel - -var detectVerticalSquash = function detectVerticalSquash(img) { - var iw = img.naturalWidth; - var ih = img.naturalHeight; - var canvas = document.createElement("canvas"); - canvas.width = 1; - canvas.height = ih; - var ctx = canvas.getContext("2d"); - ctx.drawImage(img, 0, 0); - - var _ctx$getImageData = ctx.getImageData(1, 0, 1, ih), - data = _ctx$getImageData.data; // search image edge pixel position in case it is squashed vertically. - - - var sy = 0; - var ey = ih; - var py = ih; - - while (py > sy) { - var alpha = data[(py - 1) * 4 + 3]; - - if (alpha === 0) { - ey = py; - } else { - sy = py; - } - - py = ey + sy >> 1; - } - - var ratio = py / ih; - - if (ratio === 0) { - return 1; - } else { - return ratio; - } -}; // A replacement for context.drawImage -// (args are for source and destination). - - -var drawImageIOSFix = function drawImageIOSFix(ctx, img, sx, sy, sw, sh, dx, dy, dw, dh) { - var vertSquashRatio = detectVerticalSquash(img); - return ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh / vertSquashRatio); -}; // Based on MinifyJpeg -// Source: http://www.perry.cz/files/ExifRestorer.js -// http://elicon.blog57.fc2.com/blog-entry-206.html - - -var ExifRestore = -/*#__PURE__*/ -function () { - function ExifRestore() { - _classCallCheck(this, ExifRestore); - } - - _createClass(ExifRestore, null, [{ - key: "initClass", - value: function initClass() { - this.KEY_STR = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; - } - }, { - key: "encode64", - value: function encode64(input) { - var output = ''; - var chr1 = undefined; - var chr2 = undefined; - var chr3 = ''; - var enc1 = undefined; - var enc2 = undefined; - var enc3 = undefined; - var enc4 = ''; - var i = 0; - - while (true) { - chr1 = input[i++]; - chr2 = input[i++]; - chr3 = input[i++]; - enc1 = chr1 >> 2; - enc2 = (chr1 & 3) << 4 | chr2 >> 4; - enc3 = (chr2 & 15) << 2 | chr3 >> 6; - enc4 = chr3 & 63; - - if (isNaN(chr2)) { - enc3 = enc4 = 64; - } else if (isNaN(chr3)) { - enc4 = 64; - } - - output = output + this.KEY_STR.charAt(enc1) + this.KEY_STR.charAt(enc2) + this.KEY_STR.charAt(enc3) + this.KEY_STR.charAt(enc4); - chr1 = chr2 = chr3 = ''; - enc1 = enc2 = enc3 = enc4 = ''; - - if (!(i < input.length)) { - break; - } - } - - return output; - } - }, { - key: "restore", - value: function restore(origFileBase64, resizedFileBase64) { - if (!origFileBase64.match('data:image/jpeg;base64,')) { - return resizedFileBase64; - } - - var rawImage = this.decode64(origFileBase64.replace('data:image/jpeg;base64,', '')); - var segments = this.slice2Segments(rawImage); - var image = this.exifManipulation(resizedFileBase64, segments); - return "data:image/jpeg;base64,".concat(this.encode64(image)); - } - }, { - key: "exifManipulation", - value: function exifManipulation(resizedFileBase64, segments) { - var exifArray = this.getExifArray(segments); - var newImageArray = this.insertExif(resizedFileBase64, exifArray); - var aBuffer = new Uint8Array(newImageArray); - return aBuffer; - } - }, { - key: "getExifArray", - value: function getExifArray(segments) { - var seg = undefined; - var x = 0; - - while (x < segments.length) { - seg = segments[x]; - - if (seg[0] === 255 & seg[1] === 225) { - return seg; - } - - x++; - } - - return []; - } - }, { - key: "insertExif", - value: function insertExif(resizedFileBase64, exifArray) { - var imageData = resizedFileBase64.replace('data:image/jpeg;base64,', ''); - var buf = this.decode64(imageData); - var separatePoint = buf.indexOf(255, 3); - var mae = buf.slice(0, separatePoint); - var ato = buf.slice(separatePoint); - var array = mae; - array = array.concat(exifArray); - array = array.concat(ato); - return array; - } - }, { - key: "slice2Segments", - value: function slice2Segments(rawImageArray) { - var head = 0; - var segments = []; - - while (true) { - var length; - - if (rawImageArray[head] === 255 & rawImageArray[head + 1] === 218) { - break; - } - - if (rawImageArray[head] === 255 & rawImageArray[head + 1] === 216) { - head += 2; - } else { - length = rawImageArray[head + 2] * 256 + rawImageArray[head + 3]; - var endPoint = head + length + 2; - var seg = rawImageArray.slice(head, endPoint); - segments.push(seg); - head = endPoint; - } - - if (head > rawImageArray.length) { - break; - } - } - - return segments; - } - }, { - key: "decode64", - value: function decode64(input) { - var output = ''; - var chr1 = undefined; - var chr2 = undefined; - var chr3 = ''; - var enc1 = undefined; - var enc2 = undefined; - var enc3 = undefined; - var enc4 = ''; - var i = 0; - var buf = []; // remove all characters that are not A-Z, a-z, 0-9, +, /, or = - - var base64test = /[^A-Za-z0-9\+\/\=]/g; - - if (base64test.exec(input)) { - console.warn('There were invalid base64 characters in the input text.\nValid base64 characters are A-Z, a-z, 0-9, \'+\', \'/\',and \'=\'\nExpect errors in decoding.'); - } - - input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ''); - - while (true) { - enc1 = this.KEY_STR.indexOf(input.charAt(i++)); - enc2 = this.KEY_STR.indexOf(input.charAt(i++)); - enc3 = this.KEY_STR.indexOf(input.charAt(i++)); - enc4 = this.KEY_STR.indexOf(input.charAt(i++)); - chr1 = enc1 << 2 | enc2 >> 4; - chr2 = (enc2 & 15) << 4 | enc3 >> 2; - chr3 = (enc3 & 3) << 6 | enc4; - buf.push(chr1); - - if (enc3 !== 64) { - buf.push(chr2); - } - - if (enc4 !== 64) { - buf.push(chr3); - } - - chr1 = chr2 = chr3 = ''; - enc1 = enc2 = enc3 = enc4 = ''; - - if (!(i < input.length)) { - break; - } - } - - return buf; - } - }]); - - return ExifRestore; -}(); - -ExifRestore.initClass(); -/* - * contentloaded.js - * - * Author: Diego Perini (diego.perini at gmail.com) - * Summary: cross-browser wrapper for DOMContentLoaded - * Updated: 20101020 - * License: MIT - * Version: 1.2 - * - * URL: - * http://javascript.nwbox.com/ContentLoaded/ - * http://javascript.nwbox.com/ContentLoaded/MIT-LICENSE - */ -// @win window reference -// @fn function reference - -var contentLoaded = function contentLoaded(win, fn) { - var done = false; - var top = true; - var doc = win.document; - var root = doc.documentElement; - var add = doc.addEventListener ? "addEventListener" : "attachEvent"; - var rem = doc.addEventListener ? "removeEventListener" : "detachEvent"; - var pre = doc.addEventListener ? "" : "on"; - - var init = function init(e) { - if (e.type === "readystatechange" && doc.readyState !== "complete") { - return; - } - - (e.type === "load" ? win : doc)[rem](pre + e.type, init, false); - - if (!done && (done = true)) { - return fn.call(win, e.type || e); - } - }; - - var poll = function poll() { - try { - root.doScroll("left"); - } catch (e) { - setTimeout(poll, 50); - return; - } - - return init("poll"); - }; - - if (doc.readyState !== "complete") { - if (doc.createEventObject && root.doScroll) { - try { - top = !win.frameElement; - } catch (error) {} - - if (top) { - poll(); - } - } - - doc[add](pre + "DOMContentLoaded", init, false); - doc[add](pre + "readystatechange", init, false); - return win[add](pre + "load", init, false); - } -}; // As a single function to be able to write tests. - - -Dropzone._autoDiscoverFunction = function () { - if (Dropzone.autoDiscover) { - return Dropzone.discover(); - } -}; - -contentLoaded(window, Dropzone._autoDiscoverFunction); - -function __guard__(value, transform) { - return typeof value !== 'undefined' && value !== null ? transform(value) : undefined; -} - -function __guardMethod__(obj, methodName, transform) { - if (typeof obj !== 'undefined' && obj !== null && typeof obj[methodName] === 'function') { - return transform(obj, methodName); - } else { - return undefined; - } -} diff --git a/static/favicon.ico b/static/favicon.ico deleted file mode 100644 index fb4bd5a..0000000 Binary files a/static/favicon.ico and /dev/null differ diff --git a/static/form.css b/static/form.css deleted file mode 100644 index 2fd6263..0000000 --- a/static/form.css +++ /dev/null @@ -1,178 +0,0 @@ -textarea { - width: calc(100% - 12px); - padding: 5px; - } - .testbox { - display: flex; - justify-content: center; - align-items: center; - height: inherit; - padding: 20px; - } - form { - width: 100%; - padding: 20px; - border-radius: 6px; - background: #fff; - box-shadow: 0 0 8px #000; - } - .banner { - position: relative; - height: 300px; - background-image: url("/uploads/media/default/0001/02/e2502bb5e1dab7d5cc9b011c745033821aad632c.jpeg"); - background-size: cover; - display: flex; - justify-content: center; - align-items: center; - text-align: center; - } - .banner::after { - content: ""; - background-color: rgba(0, 0, 0, 0.2); - position: absolute; - width: 100%; - height: 100%; - } - input, select, textarea { - margin-bottom: 10px; - border: 1px solid #ccc; - border-radius: 3px; - } - input { - width: calc(100% - 10px); - padding: 5px; - } - input[type="date"] { - padding: 4px 5px; - } - textarea { - width: calc(100% - 12px); - padding: 5px; - } - .item:hover p, .item:hover i, .question:hover p, .question label:hover, input:hover::placeholder { - color: #000; - } - .item input:hover, .item select:hover, .item textarea:hover { - border: 1px solid transparent; - box-shadow: 0 0 3px 0 #000; - color: #000; - } - .item { - position: relative; - margin: 10px 0; - } - .item span { - color: red; - } - input[type="date"]::-webkit-inner-spin-button { - display: none; - } - .item i, input[type="date"]::-webkit-calendar-picker-indicator { - position: absolute; - font-size: 20px; - color: #00b33c; - } - .item i { - right: 1%; - top: 30px; - z-index: 1; - } - .week { - display:flex; - justfiy-content:space-between; - } - .colums { - display:flex; - justify-content:space-between; - flex-direction:row; - flex-wrap:wrap; - } - .colums div { - width:48%; - } - [type="date"]::-webkit-calendar-picker-indicator { - right: 1%; - z-index: 2; - opacity: 0; - cursor: pointer; - } - input[type=radio], input[type=checkbox] { - display: none; - } - label.radio { - position: relative; - display: inline-block; - margin: 5px 20px 15px 0; - cursor: pointer; - } - .question span { - margin-left: 30px; - } - .question-answer label { - display: block; - } - label.radio:before { - content: ""; - position: absolute; - left: 0; - width: 17px; - height: 17px; - border-radius: 50%; - border: 2px solid #ccc; - } - input[type=radio]:checked + label:before, label.radio:hover:before { - border: 2px solid #000; - } - label.radio:after { - content: ""; - position: absolute; - top: 6px; - left: 5px; - width: 8px; - height: 4px; - border: 3px solid #000; - border-top: none; - border-right: none; - transform: rotate(-45deg); - opacity: 0; - } - input[type=radio]:checked + label:after { - opacity: 1; - } - .flax { - display:flex; - justify-content:space-around; - } - .btn-block { - margin-top: 10px; - text-align: center; - } - button { - width: 150px; - padding: 10px; - border: none; - border-radius: 5px; - background: #000; - font-size: 16px; - color: #fff; -cursor: pointer; - } - button:hover { - background: #00b33c; - } - @media (min-width: 568px) { - .name-item, .city-item { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - } - .name-item input, .name-item div { - width: calc(50% - 20px); - } - .name-item div input { - width:97%;} - .name-item div label { - display:block; - padding-bottom:5px; - } - } \ No newline at end of file diff --git a/static/icon.css b/static/icon.css deleted file mode 100644 index c1c3065..0000000 --- a/static/icon.css +++ /dev/null @@ -1,23 +0,0 @@ -/* fallback */ -@font-face { - font-family: 'Material Icons'; - font-style: normal; - font-weight: 400; - src: url(/static/icons.woff2) format('woff2'); -} - -.material-icons { - font-family: 'Material Icons'; - font-weight: normal; - font-style: normal; - font-size: 24px; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -moz-font-feature-settings: 'liga'; - -moz-osx-font-smoothing: grayscale; -} diff --git a/static/icons.woff2 b/static/icons.woff2 deleted file mode 100644 index f2b4bcc..0000000 Binary files a/static/icons.woff2 and /dev/null differ diff --git a/static/script.js b/static/script.js deleted file mode 100644 index 87d01bf..0000000 --- a/static/script.js +++ /dev/null @@ -1,275 +0,0 @@ -document.addEventListener('DOMContentLoaded', function () { - cheangpage(); - // References to all the element we will need. - var video = document.querySelector('#camera-stream'), - image = document.querySelector('#snap'), - start_camera = document.querySelector('#start-camera'), - controls = document.querySelector('.controls'), - take_photo_btn = document.querySelector('#take-photo'), - delete_photo_btn = document.querySelector('#delete-photo'), - error_message = document.querySelector('#error-message'); - - - // The getUserMedia interface is used for handling camera input. - // Some browsers need a prefix so here we're covering all the options - navigator.getMedia = ( navigator.getUserMedia || - navigator.webkitGetUserMedia || - navigator.mozGetUserMedia || - navigator.msGetUserMedia); - - - if(!navigator.getMedia){ - displayErrorMessage("Your browser doesn't have support for the navigator.getUserMedia interface."); - } - else{ - - // Request the camera. - navigator.getMedia( - { - video: true - }, - // Success Callback - function(stream){ - - // Create an object URL for the video stream and - // set it as src of our HTLM video element. - //video.src = window.URL.createObjectURL(stream); - video.srcObject = stream - - // Play the video element to start the stream. - video.play(); - video.onplay = function() { - showVideo(); - }; - - }, - // Error Callback - function(err){ - displayErrorMessage("There was an error with accessing the camera stream: " + err.name, err); - } - ); - - } - - - - // Mobile browsers cannot play video without user input, - // so here we're using a button to start it manually. - start_camera.addEventListener("click", function(e){ - - e.preventDefault(); - - // Start video playback manually. - video.play(); - showVideo(); - - }); - - - take_photo_btn.addEventListener("click", function(e){ - - e.preventDefault(); - - var snap = takeSnapshot(); - - // Show image. - image.setAttribute('src', snap); - image.classList.add("visible"); - - // Enable delete and save buttons - delete_photo_btn.classList.remove("disabled"); - - // Pause video playback of stream. - video.pause(); - search(snap); - - }); - - - delete_photo_btn.addEventListener("click", function(e){ - - e.preventDefault(); - - // Hide image. - image.setAttribute('src', ""); - image.classList.remove("visible"); - - // Disable delete and save buttons - delete_photo_btn.classList.add("disabled"); - - // Resume playback of stream. - video.play(); - - }); - - - - function showVideo(){ - // Display the video stream and the controls. - - hideUI(); - video.classList.add("visible"); - controls.classList.add("visible"); - } - - - function takeSnapshot(){ - // Here we're using a trick that involves a hidden canvas element. - - var hidden_canvas = document.querySelector('canvas'), - context = hidden_canvas.getContext('2d'); - - var width = video.videoWidth, - height = video.videoHeight; - - if (width && height) { - - // Setup a canvas with the same dimensions as the video. - hidden_canvas.width = width; - hidden_canvas.height = height; - - // Make a copy of the current frame in the video on the canvas. - context.drawImage(video, 0, 0, width, height); - - // Turn the canvas image into a dataURL that can be used as a src for our photo. - return hidden_canvas.toDataURL('image/png'); - } - } - - - function displayErrorMessage(error_msg, error){ - error = error || ""; - if(error){ - console.error(error); - } - - error_message.innerText = error_msg; - - hideUI(); - error_message.classList.add("visible"); - } - - - function hideUI(){ - // Helper function for clearing the app UI. - - controls.classList.remove("visible"); - start_camera.classList.remove("visible"); - video.classList.remove("visible"); - snap.classList.remove("visible"); - error_message.classList.remove("visible"); - } - -}); -function cheangpage(){ - var add_page = document.getElementById('add_page'); - var search_page = document.getElementById('search_page'); - var add_icon = document.getElementsByClassName('add_icon')[0]; - - if (add_icon.innerHTML == "add") - { - add_icon.innerHTML = "search"; - add_page.style.display = ""; - search_page.style.display = "none"; - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/add', true); - var formData = new FormData(); - xhr.onload = function(e) { }; - xhr.send(formData); - }else{ - add_icon.innerHTML = "add"; - add_page.style.display = "none"; - search_page.style.display = ""; - - } -}; -function setinit(typ, pid){ - if (pid != "0"){ - var xhr = new XMLHttpRequest(); - xhr.open('GET', '/' + typ + '/' + pid, true); - xhr.onload = function(e) { - document.getElementById("plast" + pid).innerHTML = "آخرین حضور: " + xhr.response; - }; - xhr.send(); -}}; -function cer_per (name, nave, last, pid) { - var per = document.createElement("DIV"); - var pname = document.createElement("P"); - var pnave = document.createElement("P"); - var plast = document.createElement("P"); - var lex = document.createElement("DIV"); - var ini = document.createElement("A"); - var out = document.createElement("A"); - - pname.innerHTML = "نام: " + name; - pnave.innerHTML = "کد ملی: " + nave; - plast.innerHTML = "آخرین حضور: " + last; - plast.id = "plast" + pid; - - lex.className = "flex"; - lex.dir = "ltr"; - - var aa = 'ثبت خروج'; - aa = aa + 'ثبت ورود'; - - lex.innerHTML = aa; - per.appendChild(pname); - per.appendChild(pnave); - per.appendChild(plast); - - lex.appendChild(out); - lex.appendChild(ini); - - per.appendChild(lex); - Persons.appendChild(per); -}; -function search (data) { - Persons.innerHTML = "

مشخصات فرد

"; - // define data and connections - var blob = new Blob([data]); - var url = URL.createObjectURL(blob); - var xhr = new XMLHttpRequest(); - xhr.open('POST', '/search', true); - - // define new form - var formData = new FormData(); - formData.append('image', data); - - // action after uploading happens - xhr.onload = function(e) { - var text = xhr.response; - if (text == "0" || text == ""){return;} - var users = JSON.parse(text); - var img = users.pop(); - snap.src = img; - for (index in users){ - var user = users[index]; - if (user == "0") {cer_per("یافت نشد.", "", "00:00", "0");} - else {cer_per(user[1], user[2], user[3], user[0]);} - } - }; - - // do the uploading - xhr.send(formData); - -}; -function savenew(){ - var fname = document.getElementById("fname"); - var fnav = document.getElementById("fnav"); - var add_result = document.getElementById("add_result"); - - var xhr = new XMLHttpRequest(); - var formData = new FormData(); - formData.append('name', fname.value); - formData.append('nav', fnav.value); - - - xhr.onload = function(e) { - var text = xhr.response; - if (text == "1") {add_result.innerHTML = "با موفقیت ثبت شد.";add_result.style.color = "green";} - else if (text == "0") {add_result.innerHTML = "مشکلی پیش آمد دوبار امتحان کنید.";add_result.style.color = "red";} - else {add_result.innerHTML = text;add_result.style.color = "red";} - }; - xhr.open('POST', '/savenew', true); - xhr.send(formData); -}; diff --git a/static/styles.css b/static/styles.css deleted file mode 100644 index b760a99..0000000 --- a/static/styles.css +++ /dev/null @@ -1,341 +0,0 @@ -@font-face { - font-family: 'IRANSansWeb'; - src: url('/static/IRANSansWeb/IRANSansWeb.eot'); - src: url('/static/IRANSansWeb/IRANSansWeb.eot?#iefix') format('embedded-opentype'), - url('/static/IRANSansWeb/IRANSansWeb.svg#IRANSansWeb') format('svg'), - url('/static/IRANSansWeb/IRANSansWeb.ttf') format('truetype'), - url('/static/IRANSansWeb/IRANSansWeb.woff') format('woff'), - url('/static/IRANSansWeb/IRANSansWeb.woff2') format('woff2'); - font-weight: normal; - font-style: normal; -} - -*{ - box-sizing: border-box; - margin: 0; - padding: 0; -} - -html{ - background-color: #fff; - font-family: 'IRANSansWeb'; - color: #333; -} - -h3, h4{ - font-size: 30px; - color: #2c3e50; - margin: 2% 0; - text-align: center; -} - -input{ - margin-top: 10px; - font-family: IRANSansWeb; - font-size: 20px; -} - -.fleft{ - float:left; -} -.fright{ - float:right; -} -.tright{ - text-align:right; -} -.file{ - margin: 20px; - min-width: 200px; - max-width: 200px; -} -.file form{ - margin-top: 20px; -} -.container{ - min-width: 45%; - max-width: 45%; - margin: 2%; - padding: 20px; - background-color: #efefef; -} - -.one_container{ - min-width: 90%; - max-width: 90%; - margin: 5%; - padding: 20px; - background-color: #efefef; -} - -@media only screen and (max-width: 1010px) { - .container { - min-width: 90%; - max-width: 90%; - margin: 2% auto; - } - .fone{ - float:initial; - } - h3, h4{ - font-size: 20px; - } -} - - -.app{ - width: 100%; - position: relative; -} - -.app #start-camera{ - display: none; - border-radius: 3px; - max-width: 400px; - color: #fff; - background-color: #448AFF; - text-decoration: none; - padding: 15px; - opacity: 0.8; - margin: 50px auto; - text-align: center; -} - -.app video#camera-stream{ - display: none; - width: 100%; -} - -.app img#snap{ - position: absolute; - top: 0; - left: 0; - width: 100%; - z-index: 10; - display: none; -} - -.app #error-message{ - width: 100%; - background-color: #ccc; - color: #9b9b9b; - font-size: 28px; - padding: 200px 100px; - text-align: center; - display: none; -} - -.app .controls{ - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - z-index: 20; - - display: flex; - align-items: flex-end; - justify-content: space-between; - padding: 30px; - display: none; -} - -.app .controls a{ - border-radius: 50%; - color: #fff; - background-color: #111; - text-decoration: none; - padding: 15px; - line-height: 0; - opacity: 0.7; - outline: none; - -webkit-tap-highlight-color: transparent; -} - -.app .controls a:hover{ - opacity: 1; -} - -.app .controls a.disabled{ - background-color: #555; - opacity: 0.5; - cursor: default; - pointer-events: none; -} - -.app .controls a.disabled:hover{ - opacity: 0.5; -} - -.app .controls a i{ - font-size: 18px; -} - -.app .controls #take-photo i{ - font-size: 32px; -} - -.app canvas{ - display: none; -} - - - -.app video#camera-stream.visible, -.app img#snap.visible, -.app #error-message.visible -{ - display: block; -} - -.app .controls.visible{ - display: flex; -} - - - -@media(max-width: 1000px){ - .container{ - margin: 40px; - } - - .app #start-camera.visible{ - display: block; - } - - .app .controls a i{ - font-size: 16px; - } - - .app .controls #take-photo i{ - font-size: 24px; - } -} - - -@media(max-width: 600px){ - .container{ - margin: 10px; - } - - .app #error-message{ - padding: 80px 50px; - font-size: 18px; - } - - .app .controls a i{ - font-size: 12px; - } - - .app .controls #take-photo i{ - font-size: 18px; - } -} - -////////////////////////////////// - -.buttons { - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - text-align: center; - width: 100%; - height: 100%; - margin: 0 auto; -/* padding: 2em 0em; */ -} - -.flex { - display: flex; - margin-top: 20px; - text-align: center; -} - - - -.btn { - user-select: none; - cursor: pointer; - font-size: 14px; - font-weight: 400; - line-height: 45px; - max-width: 160px; - position: relative; - text-decoration: none; - width: 100%; - - margin: 5px; -} -.btn:hover { - text-decoration: none; -} - -/*btn_background*/ -.effect01 { - color: #FFF; - border: 4px solid #000; - box-shadow:0px 0px 0px 1px #000 inset; - background-color: #000; - overflow: hidden; - position: relative; - transition: all 0.3s ease-in-out; -} -.effect01:hover { - border: 4px solid #666; - background-color: #FFF; - box-shadow:0px 0px 0px 4px #EEE inset; -} - -/*btn_text*/ -.effect01 span { - transition: all 0.2s ease-out; - z-index: 2; -} -.effect01:hover span{ - letter-spacing: 0.13em; - color: #333; -} - -/*highlight*/ -.effect01:after { - background: #FFF; - border: 0px solid #000; - content: ""; - height: 155px; - left: -75px; - opacity: .8; - position: absolute; - top: -50px; - -webkit-transform: rotate(35deg); - transform: rotate(35deg); - width: 50px; - transition: all 1s cubic-bezier(0.075, 0.82, 0.165, 1);/*easeOutCirc*/ - z-index: 1; -} -.effect01:hover:after { - background: #FFF; - border: 20px solid #000; - opacity: 0; - left: 120%; - -webkit-transform: rotate(40deg); - transform: rotate(40deg); -} - -.add_icon_Circle{ - cursor: pointer; - border-radius: 50%; - right: 20px; - bottom: 20px; - position: fixed; - user-select: none; - box-shadow: 0px 1px 18px 0px rgba(0,0,0,0.4), - 0px 3px 5px -1px rgba(0,0,0,0.2); - background-color: #fff; - width: 50px; - height: 50px; -} -.add_icon{ - font-size: 40px; - margin: 5px; -} \ No newline at end of file diff --git a/static/tadel/buttons.dataTables.min.css b/static/tadel/buttons.dataTables.min.css deleted file mode 100644 index a8758ee..0000000 --- a/static/tadel/buttons.dataTables.min.css +++ /dev/null @@ -1 +0,0 @@ -div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border:2px solid #111;box-shadow:3px 3px 8px rgba(0,0,0,0.3);border-radius:3px;text-align:center;z-index:21}div.dt-button-info h2{padding:0.5em;margin:0;font-weight:normal;border-bottom:1px solid #ddd;background-color:#f3f3f3}div.dt-button-info>div{padding:1em}button.dt-button,div.dt-button,a.dt-button{position:relative;display:inline-block;box-sizing:border-box;margin-right:0.333em;padding:0.5em 1em;border:1px solid #999;border-radius:2px;cursor:pointer;font-size:0.88em;color:black;white-space:nowrap;overflow:hidden;background-color:#e9e9e9;background-image:-webkit-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:-moz-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:-ms-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:-o-linear-gradient(top, #fff 0%, #e9e9e9 100%);background-image:linear-gradient(to bottom, #fff 0%, #e9e9e9 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='white', EndColorStr='#e9e9e9');-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;text-decoration:none;outline:none}button.dt-button.disabled,div.dt-button.disabled,a.dt-button.disabled{color:#999;border:1px solid #d0d0d0;cursor:default;background-color:#f9f9f9;background-image:-webkit-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:-moz-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:-ms-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:-o-linear-gradient(top, #fff 0%, #f9f9f9 100%);background-image:linear-gradient(to bottom, #fff 0%, #f9f9f9 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#ffffff', EndColorStr='#f9f9f9')}button.dt-button:active:not(.disabled),button.dt-button.active:not(.disabled),div.dt-button:active:not(.disabled),div.dt-button.active:not(.disabled),a.dt-button:active:not(.disabled),a.dt-button.active:not(.disabled){background-color:#e2e2e2;background-image:-webkit-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:-moz-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:-ms-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:-o-linear-gradient(top, #f3f3f3 0%, #e2e2e2 100%);background-image:linear-gradient(to bottom, #f3f3f3 0%, #e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f3f3f3', EndColorStr='#e2e2e2');box-shadow:inset 1px 1px 3px #999999}button.dt-button:active:not(.disabled):hover:not(.disabled),button.dt-button.active:not(.disabled):hover:not(.disabled),div.dt-button:active:not(.disabled):hover:not(.disabled),div.dt-button.active:not(.disabled):hover:not(.disabled),a.dt-button:active:not(.disabled):hover:not(.disabled),a.dt-button.active:not(.disabled):hover:not(.disabled){box-shadow:inset 1px 1px 3px #999999;background-color:#cccccc;background-image:-webkit-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:-moz-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:-ms-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:-o-linear-gradient(top, #eaeaea 0%, #ccc 100%);background-image:linear-gradient(to bottom, #eaeaea 0%, #ccc 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#eaeaea', EndColorStr='#cccccc')}button.dt-button:hover,div.dt-button:hover,a.dt-button:hover{text-decoration:none}button.dt-button:hover:not(.disabled),div.dt-button:hover:not(.disabled),a.dt-button:hover:not(.disabled){border:1px solid #666;background-color:#e0e0e0;background-image:-webkit-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:-moz-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:-ms-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:-o-linear-gradient(top, #f9f9f9 0%, #e0e0e0 100%);background-image:linear-gradient(to bottom, #f9f9f9 0%, #e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f9f9f9', EndColorStr='#e0e0e0')}button.dt-button:focus:not(.disabled),div.dt-button:focus:not(.disabled),a.dt-button:focus:not(.disabled){border:1px solid #426c9e;text-shadow:0 1px 0 #c4def1;outline:none;background-color:#79ace9;background-image:-webkit-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:-moz-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:-ms-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:-o-linear-gradient(top, #bddef4 0%, #79ace9 100%);background-image:linear-gradient(to bottom, #bddef4 0%, #79ace9 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#bddef4', EndColorStr='#79ace9')}.dt-button embed{outline:none}div.dt-buttons{position:relative;float:left}div.dt-buttons.buttons-right{float:right}div.dt-button-collection{position:absolute;top:0;left:0;width:150px;margin-top:3px;padding:8px 8px 4px 8px;border:1px solid #ccc;border:1px solid rgba(0,0,0,0.4);background-color:white;overflow:hidden;z-index:2002;border-radius:5px;box-shadow:3px 3px 5px rgba(0,0,0,0.3);z-index:2002;-webkit-column-gap:8px;-moz-column-gap:8px;-ms-column-gap:8px;-o-column-gap:8px;column-gap:8px}div.dt-button-collection button.dt-button,div.dt-button-collection div.dt-button,div.dt-button-collection a.dt-button{position:relative;left:0;right:0;display:block;float:none;margin-bottom:4px;margin-right:0}div.dt-button-collection button.dt-button:active:not(.disabled),div.dt-button-collection button.dt-button.active:not(.disabled),div.dt-button-collection div.dt-button:active:not(.disabled),div.dt-button-collection div.dt-button.active:not(.disabled),div.dt-button-collection a.dt-button:active:not(.disabled),div.dt-button-collection a.dt-button.active:not(.disabled){background-color:#dadada;background-image:-webkit-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:-moz-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:-ms-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:-o-linear-gradient(top, #f0f0f0 0%, #dadada 100%);background-image:linear-gradient(to bottom, #f0f0f0 0%, #dadada 100%);filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#f0f0f0', EndColorStr='#dadada');box-shadow:inset 1px 1px 3px #666}div.dt-button-collection.fixed{position:fixed;top:50%;left:50%;margin-left:-75px;border-radius:0}div.dt-button-collection.fixed.two-column{margin-left:-150px}div.dt-button-collection.fixed.three-column{margin-left:-225px}div.dt-button-collection.fixed.four-column{margin-left:-300px}div.dt-button-collection>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dt-button-collection.two-column{width:300px;padding-bottom:1px;-webkit-column-count:2;-moz-column-count:2;-ms-column-count:2;-o-column-count:2;column-count:2}div.dt-button-collection.three-column{width:450px;padding-bottom:1px;-webkit-column-count:3;-moz-column-count:3;-ms-column-count:3;-o-column-count:3;column-count:3}div.dt-button-collection.four-column{width:600px;padding-bottom:1px;-webkit-column-count:4;-moz-column-count:4;-ms-column-count:4;-o-column-count:4;column-count:4}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.7);background:-ms-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:-moz-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:-o-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:-webkit-gradient(radial, center center, 0, center center, 497, color-stop(0, rgba(0,0,0,0.3)), color-stop(1, rgba(0,0,0,0.7)));background:-webkit-radial-gradient(center, ellipse farthest-corner, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);background:radial-gradient(ellipse farthest-corner at center, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.7) 100%);z-index:2001}@media screen and (max-width: 640px){div.dt-buttons{float:none !important;text-align:center}} diff --git a/static/tadel/buttons.html5.min.js b/static/tadel/buttons.html5.min.js deleted file mode 100644 index 3fc1341..0000000 --- a/static/tadel/buttons.html5.min.js +++ /dev/null @@ -1,26 +0,0 @@ -(function(g){"function"===typeof define&&define.amd?define(["jquery","datatables.net","datatables.net-buttons"],function(j){return g(j,window,document)}):"object"===typeof exports?module.exports=function(j,i,q,r){j||(j=window);if(!i||!i.fn.dataTable)i=require("datatables.net")(j,i).$;i.fn.dataTable.Buttons||require("datatables.net-buttons")(j,i);return g(i,j,j.document,q,r)}:g(jQuery,window,document)})(function(g,j,i,q,r,m){function E(a,b){v===m&&(v=-1===y.serializeToString(g.parseXML(F["xl/worksheets/sheet1.xml"])).indexOf("xmlns:r")); -g.each(b,function(b,c){if(g.isPlainObject(c)){var e=a.folder(b);E(e,c)}else{if(v){var e=c.childNodes[0],f,h,n=[];for(f=e.attributes.length-1;0<=f;f--){h=e.attributes[f].nodeName;var k=e.attributes[f].nodeValue;-1!==h.indexOf(":")&&(n.push({name:h,value:k}),e.removeAttribute(h))}f=0;for(h=n.length;f'+ -e),e=e.replace(/_dt_b_namespace_token_/g,":"));e=e.replace(//g,"");a.file(b,e)}})}function o(a,b,d){var c=a.createElement(b);d&&(d.attr&&g(c).attr(d.attr),d.children&&g.each(d.children,function(a,b){c.appendChild(b)}),d.text&&c.appendChild(a.createTextNode(d.text)));return c}function N(a,b){var d=a.header[b].length,c;a.footer&&a.footer[b].length>d&&(d=a.footer[b].length);for(var e=0,f=a.body.length;ed&& -(d=c),400&&(b=b+f);b=b+(e?e+(""+a[c]).replace(g,h+e)+e:a[c])}return b},j=b.header?k(c.header)+d:"",i=b.footer&&c.footer?d+k(c.footer):"",u=[],D=0,l=c.body.length;D', -"xl/_rels/workbook.xml.rels":'',"[Content_Types].xml":'', -"xl/workbook.xml":'', -"xl/worksheets/sheet1.xml":'',"xl/styles.xml":''}; -s.ext.buttons.copyHtml5={className:"buttons-copy buttons-html5",text:function(a){return a.i18n("buttons.copy","Copy")},action:function(a,b,d,c){var a=L(b,c),e=a.str,d=g("
").css({height:1,width:1,overflow:"hidden",position:"fixed",top:0,left:0});c.customize&&(e=c.customize(e,c));c=g(""; - support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; - - // #11217 - WebKit loses check when the name is after the checked attribute - fragment.appendChild( div ); - - // Support: Windows Web Apps (WWA) - // `name` and `type` must use .setAttribute for WWA (#14901) - input = document.createElement( "input" ); - input.setAttribute( "type", "radio" ); - input.setAttribute( "checked", "checked" ); - input.setAttribute( "name", "t" ); - - div.appendChild( input ); - - // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 - // old WebKit doesn't clone checked state correctly in fragments - support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; - - // Support: IE<9 - // Cloned elements keep attachEvent handlers, we use addEventListener on IE9+ - support.noCloneEvent = !!div.addEventListener; - - // Support: IE<9 - // Since attributes and properties are the same in IE, - // cleanData must set properties to undefined rather than use removeAttribute - div[ jQuery.expando ] = 1; - support.attributes = !div.getAttribute( jQuery.expando ); -} )(); - - -// We have to close these tags to support XHTML (#13200) -var wrapMap = { - option: [ 1, "" ], - legend: [ 1, "
", "
" ], - area: [ 1, "", "" ], - - // Support: IE8 - param: [ 1, "", "" ], - thead: [ 1, "", "
" ], - tr: [ 2, "", "
" ], - col: [ 2, "", "
" ], - td: [ 3, "", "
" ], - - // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, - // unless wrapped in a div with non-breaking characters in front of it. - _default: support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
", "
" ] -}; - -// Support: IE8-IE9 -wrapMap.optgroup = wrapMap.option; - -wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; -wrapMap.th = wrapMap.td; - - -function getAll( context, tag ) { - var elems, elem, - i = 0, - found = typeof context.getElementsByTagName !== "undefined" ? - context.getElementsByTagName( tag || "*" ) : - typeof context.querySelectorAll !== "undefined" ? - context.querySelectorAll( tag || "*" ) : - undefined; - - if ( !found ) { - for ( found = [], elems = context.childNodes || context; - ( elem = elems[ i ] ) != null; - i++ - ) { - if ( !tag || jQuery.nodeName( elem, tag ) ) { - found.push( elem ); - } else { - jQuery.merge( found, getAll( elem, tag ) ); - } - } - } - - return tag === undefined || tag && jQuery.nodeName( context, tag ) ? - jQuery.merge( [ context ], found ) : - found; -} - - -// Mark scripts as having already been evaluated -function setGlobalEval( elems, refElements ) { - var elem, - i = 0; - for ( ; ( elem = elems[ i ] ) != null; i++ ) { - jQuery._data( - elem, - "globalEval", - !refElements || jQuery._data( refElements[ i ], "globalEval" ) - ); - } -} - - -var rhtml = /<|&#?\w+;/, - rtbody = / from table fragments - if ( !support.tbody ) { - - // String was a , *may* have spurious - elem = tag === "table" && !rtbody.test( elem ) ? - tmp.firstChild : - - // String was a bare or - wrap[ 1 ] === "
" && !rtbody.test( elem ) ? - tmp : - 0; - - j = elem && elem.childNodes.length; - while ( j-- ) { - if ( jQuery.nodeName( ( tbody = elem.childNodes[ j ] ), "tbody" ) && - !tbody.childNodes.length ) { - - elem.removeChild( tbody ); - } - } - } - - jQuery.merge( nodes, tmp.childNodes ); - - // Fix #12392 for WebKit and IE > 9 - tmp.textContent = ""; - - // Fix #12392 for oldIE - while ( tmp.firstChild ) { - tmp.removeChild( tmp.firstChild ); - } - - // Remember the top-level container for proper cleanup - tmp = safe.lastChild; - } - } - } - - // Fix #11356: Clear elements from fragment - if ( tmp ) { - safe.removeChild( tmp ); - } - - // Reset defaultChecked for any radios and checkboxes - // about to be appended to the DOM in IE 6/7 (#8060) - if ( !support.appendChecked ) { - jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); - } - - i = 0; - while ( ( elem = nodes[ i++ ] ) ) { - - // Skip elements already in the context collection (trac-4087) - if ( selection && jQuery.inArray( elem, selection ) > -1 ) { - if ( ignored ) { - ignored.push( elem ); - } - - continue; - } - - contains = jQuery.contains( elem.ownerDocument, elem ); - - // Append to fragment - tmp = getAll( safe.appendChild( elem ), "script" ); - - // Preserve script evaluation history - if ( contains ) { - setGlobalEval( tmp ); - } - - // Capture executables - if ( scripts ) { - j = 0; - while ( ( elem = tmp[ j++ ] ) ) { - if ( rscriptType.test( elem.type || "" ) ) { - scripts.push( elem ); - } - } - } - } - - tmp = null; - - return safe; -} - - -( function() { - var i, eventName, - div = document.createElement( "div" ); - - // Support: IE<9 (lack submit/change bubble), Firefox (lack focus(in | out) events) - for ( i in { submit: true, change: true, focusin: true } ) { - eventName = "on" + i; - - if ( !( support[ i ] = eventName in window ) ) { - - // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) - div.setAttribute( eventName, "t" ); - support[ i ] = div.attributes[ eventName ].expando === false; - } - } - - // Null elements to avoid leaks in IE. - div = null; -} )(); - - -var rformElems = /^(?:input|select|textarea)$/i, - rkeyEvent = /^key/, - rmouseEvent = /^(?:mouse|pointer|contextmenu|drag|drop)|click/, - rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, - rtypenamespace = /^([^.]*)(?:\.(.+)|)/; - -function returnTrue() { - return true; -} - -function returnFalse() { - return false; -} - -// Support: IE9 -// See #13393 for more info -function safeActiveElement() { - try { - return document.activeElement; - } catch ( err ) { } -} - -function on( elem, types, selector, data, fn, one ) { - var origFn, type; - - // Types can be a map of types/handlers - if ( typeof types === "object" ) { - - // ( types-Object, selector, data ) - if ( typeof selector !== "string" ) { - - // ( types-Object, data ) - data = data || selector; - selector = undefined; - } - for ( type in types ) { - on( elem, type, selector, data, types[ type ], one ); - } - return elem; - } - - if ( data == null && fn == null ) { - - // ( types, fn ) - fn = selector; - data = selector = undefined; - } else if ( fn == null ) { - if ( typeof selector === "string" ) { - - // ( types, selector, fn ) - fn = data; - data = undefined; - } else { - - // ( types, data, fn ) - fn = data; - data = selector; - selector = undefined; - } - } - if ( fn === false ) { - fn = returnFalse; - } else if ( !fn ) { - return elem; - } - - if ( one === 1 ) { - origFn = fn; - fn = function( event ) { - - // Can use an empty set, since event contains the info - jQuery().off( event ); - return origFn.apply( this, arguments ); - }; - - // Use same guid so caller can remove using origFn - fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); - } - return elem.each( function() { - jQuery.event.add( this, types, fn, data, selector ); - } ); -} - -/* - * Helper functions for managing events -- not part of the public interface. - * Props to Dean Edwards' addEvent library for many of the ideas. - */ -jQuery.event = { - - global: {}, - - add: function( elem, types, handler, data, selector ) { - var tmp, events, t, handleObjIn, - special, eventHandle, handleObj, - handlers, type, namespaces, origType, - elemData = jQuery._data( elem ); - - // Don't attach events to noData or text/comment nodes (but allow plain objects) - if ( !elemData ) { - return; - } - - // Caller can pass in an object of custom data in lieu of the handler - if ( handler.handler ) { - handleObjIn = handler; - handler = handleObjIn.handler; - selector = handleObjIn.selector; - } - - // Make sure that the handler has a unique ID, used to find/remove it later - if ( !handler.guid ) { - handler.guid = jQuery.guid++; - } - - // Init the element's event structure and main handler, if this is the first - if ( !( events = elemData.events ) ) { - events = elemData.events = {}; - } - if ( !( eventHandle = elemData.handle ) ) { - eventHandle = elemData.handle = function( e ) { - - // Discard the second event of a jQuery.event.trigger() and - // when an event is called after a page has unloaded - return typeof jQuery !== "undefined" && - ( !e || jQuery.event.triggered !== e.type ) ? - jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : - undefined; - }; - - // Add elem as a property of the handle fn to prevent a memory leak - // with IE non-native events - eventHandle.elem = elem; - } - - // Handle multiple events separated by a space - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // There *must* be a type, no attaching namespace-only handlers - if ( !type ) { - continue; - } - - // If event changes its type, use the special event handlers for the changed type - special = jQuery.event.special[ type ] || {}; - - // If selector defined, determine special event api type, otherwise given type - type = ( selector ? special.delegateType : special.bindType ) || type; - - // Update special based on newly reset type - special = jQuery.event.special[ type ] || {}; - - // handleObj is passed to all event handlers - handleObj = jQuery.extend( { - type: type, - origType: origType, - data: data, - handler: handler, - guid: handler.guid, - selector: selector, - needsContext: selector && jQuery.expr.match.needsContext.test( selector ), - namespace: namespaces.join( "." ) - }, handleObjIn ); - - // Init the event handler queue if we're the first - if ( !( handlers = events[ type ] ) ) { - handlers = events[ type ] = []; - handlers.delegateCount = 0; - - // Only use addEventListener/attachEvent if the special events handler returns false - if ( !special.setup || - special.setup.call( elem, data, namespaces, eventHandle ) === false ) { - - // Bind the global event handler to the element - if ( elem.addEventListener ) { - elem.addEventListener( type, eventHandle, false ); - - } else if ( elem.attachEvent ) { - elem.attachEvent( "on" + type, eventHandle ); - } - } - } - - if ( special.add ) { - special.add.call( elem, handleObj ); - - if ( !handleObj.handler.guid ) { - handleObj.handler.guid = handler.guid; - } - } - - // Add to the element's handler list, delegates in front - if ( selector ) { - handlers.splice( handlers.delegateCount++, 0, handleObj ); - } else { - handlers.push( handleObj ); - } - - // Keep track of which events have ever been used, for event optimization - jQuery.event.global[ type ] = true; - } - - // Nullify elem to prevent memory leaks in IE - elem = null; - }, - - // Detach an event or set of events from an element - remove: function( elem, types, handler, selector, mappedTypes ) { - var j, handleObj, tmp, - origCount, t, events, - special, handlers, type, - namespaces, origType, - elemData = jQuery.hasData( elem ) && jQuery._data( elem ); - - if ( !elemData || !( events = elemData.events ) ) { - return; - } - - // Once for each type.namespace in types; type may be omitted - types = ( types || "" ).match( rnotwhite ) || [ "" ]; - t = types.length; - while ( t-- ) { - tmp = rtypenamespace.exec( types[ t ] ) || []; - type = origType = tmp[ 1 ]; - namespaces = ( tmp[ 2 ] || "" ).split( "." ).sort(); - - // Unbind all events (on this namespace, if provided) for the element - if ( !type ) { - for ( type in events ) { - jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); - } - continue; - } - - special = jQuery.event.special[ type ] || {}; - type = ( selector ? special.delegateType : special.bindType ) || type; - handlers = events[ type ] || []; - tmp = tmp[ 2 ] && - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ); - - // Remove matching events - origCount = j = handlers.length; - while ( j-- ) { - handleObj = handlers[ j ]; - - if ( ( mappedTypes || origType === handleObj.origType ) && - ( !handler || handler.guid === handleObj.guid ) && - ( !tmp || tmp.test( handleObj.namespace ) ) && - ( !selector || selector === handleObj.selector || - selector === "**" && handleObj.selector ) ) { - handlers.splice( j, 1 ); - - if ( handleObj.selector ) { - handlers.delegateCount--; - } - if ( special.remove ) { - special.remove.call( elem, handleObj ); - } - } - } - - // Remove generic event handler if we removed something and no more handlers exist - // (avoids potential for endless recursion during removal of special event handlers) - if ( origCount && !handlers.length ) { - if ( !special.teardown || - special.teardown.call( elem, namespaces, elemData.handle ) === false ) { - - jQuery.removeEvent( elem, type, elemData.handle ); - } - - delete events[ type ]; - } - } - - // Remove the expando if it's no longer used - if ( jQuery.isEmptyObject( events ) ) { - delete elemData.handle; - - // removeData also checks for emptiness and clears the expando if empty - // so use it instead of delete - jQuery._removeData( elem, "events" ); - } - }, - - trigger: function( event, data, elem, onlyHandlers ) { - var handle, ontype, cur, - bubbleType, special, tmp, i, - eventPath = [ elem || document ], - type = hasOwn.call( event, "type" ) ? event.type : event, - namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split( "." ) : []; - - cur = tmp = elem = elem || document; - - // Don't do events on text and comment nodes - if ( elem.nodeType === 3 || elem.nodeType === 8 ) { - return; - } - - // focus/blur morphs to focusin/out; ensure we're not firing them right now - if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { - return; - } - - if ( type.indexOf( "." ) > -1 ) { - - // Namespaced trigger; create a regexp to match event type in handle() - namespaces = type.split( "." ); - type = namespaces.shift(); - namespaces.sort(); - } - ontype = type.indexOf( ":" ) < 0 && "on" + type; - - // Caller can pass in a jQuery.Event object, Object, or just an event type string - event = event[ jQuery.expando ] ? - event : - new jQuery.Event( type, typeof event === "object" && event ); - - // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) - event.isTrigger = onlyHandlers ? 2 : 3; - event.namespace = namespaces.join( "." ); - event.rnamespace = event.namespace ? - new RegExp( "(^|\\.)" + namespaces.join( "\\.(?:.*\\.|)" ) + "(\\.|$)" ) : - null; - - // Clean up the event in case it is being reused - event.result = undefined; - if ( !event.target ) { - event.target = elem; - } - - // Clone any incoming data and prepend the event, creating the handler arg list - data = data == null ? - [ event ] : - jQuery.makeArray( data, [ event ] ); - - // Allow special events to draw outside the lines - special = jQuery.event.special[ type ] || {}; - if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { - return; - } - - // Determine event propagation path in advance, per W3C events spec (#9951) - // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) - if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { - - bubbleType = special.delegateType || type; - if ( !rfocusMorph.test( bubbleType + type ) ) { - cur = cur.parentNode; - } - for ( ; cur; cur = cur.parentNode ) { - eventPath.push( cur ); - tmp = cur; - } - - // Only add window if we got to document (e.g., not plain obj or detached DOM) - if ( tmp === ( elem.ownerDocument || document ) ) { - eventPath.push( tmp.defaultView || tmp.parentWindow || window ); - } - } - - // Fire handlers on the event path - i = 0; - while ( ( cur = eventPath[ i++ ] ) && !event.isPropagationStopped() ) { - - event.type = i > 1 ? - bubbleType : - special.bindType || type; - - // jQuery handler - handle = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && - jQuery._data( cur, "handle" ); - - if ( handle ) { - handle.apply( cur, data ); - } - - // Native handler - handle = ontype && cur[ ontype ]; - if ( handle && handle.apply && acceptData( cur ) ) { - event.result = handle.apply( cur, data ); - if ( event.result === false ) { - event.preventDefault(); - } - } - } - event.type = type; - - // If nobody prevented the default action, do it now - if ( !onlyHandlers && !event.isDefaultPrevented() ) { - - if ( - ( !special._default || - special._default.apply( eventPath.pop(), data ) === false - ) && acceptData( elem ) - ) { - - // Call a native DOM method on the target with the same name name as the event. - // Can't use an .isFunction() check here because IE6/7 fails that test. - // Don't do default actions on window, that's where global variables be (#6170) - if ( ontype && elem[ type ] && !jQuery.isWindow( elem ) ) { - - // Don't re-trigger an onFOO event when we call its FOO() method - tmp = elem[ ontype ]; - - if ( tmp ) { - elem[ ontype ] = null; - } - - // Prevent re-triggering of the same event, since we already bubbled it above - jQuery.event.triggered = type; - try { - elem[ type ](); - } catch ( e ) { - - // IE<9 dies on focus/blur to hidden element (#1486,#12518) - // only reproducible on winXP IE8 native, not IE9 in IE8 mode - } - jQuery.event.triggered = undefined; - - if ( tmp ) { - elem[ ontype ] = tmp; - } - } - } - } - - return event.result; - }, - - dispatch: function( event ) { - - // Make a writable jQuery.Event from the native event object - event = jQuery.event.fix( event ); - - var i, j, ret, matched, handleObj, - handlerQueue = [], - args = slice.call( arguments ), - handlers = ( jQuery._data( this, "events" ) || {} )[ event.type ] || [], - special = jQuery.event.special[ event.type ] || {}; - - // Use the fix-ed jQuery.Event rather than the (read-only) native event - args[ 0 ] = event; - event.delegateTarget = this; - - // Call the preDispatch hook for the mapped type, and let it bail if desired - if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { - return; - } - - // Determine handlers - handlerQueue = jQuery.event.handlers.call( this, event, handlers ); - - // Run delegates first; they may want to stop propagation beneath us - i = 0; - while ( ( matched = handlerQueue[ i++ ] ) && !event.isPropagationStopped() ) { - event.currentTarget = matched.elem; - - j = 0; - while ( ( handleObj = matched.handlers[ j++ ] ) && - !event.isImmediatePropagationStopped() ) { - - // Triggered event must either 1) have no namespace, or 2) have namespace(s) - // a subset or equal to those in the bound event (both can have no namespace). - if ( !event.rnamespace || event.rnamespace.test( handleObj.namespace ) ) { - - event.handleObj = handleObj; - event.data = handleObj.data; - - ret = ( ( jQuery.event.special[ handleObj.origType ] || {} ).handle || - handleObj.handler ).apply( matched.elem, args ); - - if ( ret !== undefined ) { - if ( ( event.result = ret ) === false ) { - event.preventDefault(); - event.stopPropagation(); - } - } - } - } - } - - // Call the postDispatch hook for the mapped type - if ( special.postDispatch ) { - special.postDispatch.call( this, event ); - } - - return event.result; - }, - - handlers: function( event, handlers ) { - var i, matches, sel, handleObj, - handlerQueue = [], - delegateCount = handlers.delegateCount, - cur = event.target; - - // Support (at least): Chrome, IE9 - // Find delegate handlers - // Black-hole SVG instance trees (#13180) - // - // Support: Firefox<=42+ - // Avoid non-left-click in FF but don't block IE radio events (#3861, gh-2343) - if ( delegateCount && cur.nodeType && - ( event.type !== "click" || isNaN( event.button ) || event.button < 1 ) ) { - - /* jshint eqeqeq: false */ - for ( ; cur != this; cur = cur.parentNode || this ) { - /* jshint eqeqeq: true */ - - // Don't check non-elements (#13208) - // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) - if ( cur.nodeType === 1 && ( cur.disabled !== true || event.type !== "click" ) ) { - matches = []; - for ( i = 0; i < delegateCount; i++ ) { - handleObj = handlers[ i ]; - - // Don't conflict with Object.prototype properties (#13203) - sel = handleObj.selector + " "; - - if ( matches[ sel ] === undefined ) { - matches[ sel ] = handleObj.needsContext ? - jQuery( sel, this ).index( cur ) > -1 : - jQuery.find( sel, this, null, [ cur ] ).length; - } - if ( matches[ sel ] ) { - matches.push( handleObj ); - } - } - if ( matches.length ) { - handlerQueue.push( { elem: cur, handlers: matches } ); - } - } - } - } - - // Add the remaining (directly-bound) handlers - if ( delegateCount < handlers.length ) { - handlerQueue.push( { elem: this, handlers: handlers.slice( delegateCount ) } ); - } - - return handlerQueue; - }, - - fix: function( event ) { - if ( event[ jQuery.expando ] ) { - return event; - } - - // Create a writable copy of the event object and normalize some properties - var i, prop, copy, - type = event.type, - originalEvent = event, - fixHook = this.fixHooks[ type ]; - - if ( !fixHook ) { - this.fixHooks[ type ] = fixHook = - rmouseEvent.test( type ) ? this.mouseHooks : - rkeyEvent.test( type ) ? this.keyHooks : - {}; - } - copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; - - event = new jQuery.Event( originalEvent ); - - i = copy.length; - while ( i-- ) { - prop = copy[ i ]; - event[ prop ] = originalEvent[ prop ]; - } - - // Support: IE<9 - // Fix target property (#1925) - if ( !event.target ) { - event.target = originalEvent.srcElement || document; - } - - // Support: Safari 6-8+ - // Target should not be a text node (#504, #13143) - if ( event.target.nodeType === 3 ) { - event.target = event.target.parentNode; - } - - // Support: IE<9 - // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) - event.metaKey = !!event.metaKey; - - return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; - }, - - // Includes some event props shared by KeyEvent and MouseEvent - props: ( "altKey bubbles cancelable ctrlKey currentTarget detail eventPhase " + - "metaKey relatedTarget shiftKey target timeStamp view which" ).split( " " ), - - fixHooks: {}, - - keyHooks: { - props: "char charCode key keyCode".split( " " ), - filter: function( event, original ) { - - // Add which for key events - if ( event.which == null ) { - event.which = original.charCode != null ? original.charCode : original.keyCode; - } - - return event; - } - }, - - mouseHooks: { - props: ( "button buttons clientX clientY fromElement offsetX offsetY " + - "pageX pageY screenX screenY toElement" ).split( " " ), - filter: function( event, original ) { - var body, eventDoc, doc, - button = original.button, - fromElement = original.fromElement; - - // Calculate pageX/Y if missing and clientX/Y available - if ( event.pageX == null && original.clientX != null ) { - eventDoc = event.target.ownerDocument || document; - doc = eventDoc.documentElement; - body = eventDoc.body; - - event.pageX = original.clientX + - ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); - event.pageY = original.clientY + - ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - - ( doc && doc.clientTop || body && body.clientTop || 0 ); - } - - // Add relatedTarget, if necessary - if ( !event.relatedTarget && fromElement ) { - event.relatedTarget = fromElement === event.target ? - original.toElement : - fromElement; - } - - // Add which for click: 1 === left; 2 === middle; 3 === right - // Note: button is not normalized, so don't use it - if ( !event.which && button !== undefined ) { - event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); - } - - return event; - } - }, - - special: { - load: { - - // Prevent triggered image.load events from bubbling to window.load - noBubble: true - }, - focus: { - - // Fire native event if possible so blur/focus sequence is correct - trigger: function() { - if ( this !== safeActiveElement() && this.focus ) { - try { - this.focus(); - return false; - } catch ( e ) { - - // Support: IE<9 - // If we error on focus to hidden element (#1486, #12518), - // let .trigger() run the handlers - } - } - }, - delegateType: "focusin" - }, - blur: { - trigger: function() { - if ( this === safeActiveElement() && this.blur ) { - this.blur(); - return false; - } - }, - delegateType: "focusout" - }, - click: { - - // For checkbox, fire native event so checked state will be right - trigger: function() { - if ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { - this.click(); - return false; - } - }, - - // For cross-browser consistency, don't fire native .click() on links - _default: function( event ) { - return jQuery.nodeName( event.target, "a" ); - } - }, - - beforeunload: { - postDispatch: function( event ) { - - // Support: Firefox 20+ - // Firefox doesn't alert if the returnValue field is not set. - if ( event.result !== undefined && event.originalEvent ) { - event.originalEvent.returnValue = event.result; - } - } - } - }, - - // Piggyback on a donor event to simulate a different one - simulate: function( type, elem, event ) { - var e = jQuery.extend( - new jQuery.Event(), - event, - { - type: type, - isSimulated: true - - // Previously, `originalEvent: {}` was set here, so stopPropagation call - // would not be triggered on donor event, since in our own - // jQuery.event.stopPropagation function we had a check for existence of - // originalEvent.stopPropagation method, so, consequently it would be a noop. - // - // Guard for simulated events was moved to jQuery.event.stopPropagation function - // since `originalEvent` should point to the original event for the - // constancy with other events and for more focused logic - } - ); - - jQuery.event.trigger( e, null, elem ); - - if ( e.isDefaultPrevented() ) { - event.preventDefault(); - } - } -}; - -jQuery.removeEvent = document.removeEventListener ? - function( elem, type, handle ) { - - // This "if" is needed for plain objects - if ( elem.removeEventListener ) { - elem.removeEventListener( type, handle ); - } - } : - function( elem, type, handle ) { - var name = "on" + type; - - if ( elem.detachEvent ) { - - // #8545, #7054, preventing memory leaks for custom events in IE6-8 - // detachEvent needed property on element, by name of that event, - // to properly expose it to GC - if ( typeof elem[ name ] === "undefined" ) { - elem[ name ] = null; - } - - elem.detachEvent( name, handle ); - } - }; - -jQuery.Event = function( src, props ) { - - // Allow instantiation without the 'new' keyword - if ( !( this instanceof jQuery.Event ) ) { - return new jQuery.Event( src, props ); - } - - // Event object - if ( src && src.type ) { - this.originalEvent = src; - this.type = src.type; - - // Events bubbling up the document may have been marked as prevented - // by a handler lower down the tree; reflect the correct value. - this.isDefaultPrevented = src.defaultPrevented || - src.defaultPrevented === undefined && - - // Support: IE < 9, Android < 4.0 - src.returnValue === false ? - returnTrue : - returnFalse; - - // Event type - } else { - this.type = src; - } - - // Put explicitly provided properties onto the event object - if ( props ) { - jQuery.extend( this, props ); - } - - // Create a timestamp if incoming event doesn't have one - this.timeStamp = src && src.timeStamp || jQuery.now(); - - // Mark it as fixed - this[ jQuery.expando ] = true; -}; - -// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding -// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html -jQuery.Event.prototype = { - constructor: jQuery.Event, - isDefaultPrevented: returnFalse, - isPropagationStopped: returnFalse, - isImmediatePropagationStopped: returnFalse, - - preventDefault: function() { - var e = this.originalEvent; - - this.isDefaultPrevented = returnTrue; - if ( !e ) { - return; - } - - // If preventDefault exists, run it on the original event - if ( e.preventDefault ) { - e.preventDefault(); - - // Support: IE - // Otherwise set the returnValue property of the original event to false - } else { - e.returnValue = false; - } - }, - stopPropagation: function() { - var e = this.originalEvent; - - this.isPropagationStopped = returnTrue; - - if ( !e || this.isSimulated ) { - return; - } - - // If stopPropagation exists, run it on the original event - if ( e.stopPropagation ) { - e.stopPropagation(); - } - - // Support: IE - // Set the cancelBubble property of the original event to true - e.cancelBubble = true; - }, - stopImmediatePropagation: function() { - var e = this.originalEvent; - - this.isImmediatePropagationStopped = returnTrue; - - if ( e && e.stopImmediatePropagation ) { - e.stopImmediatePropagation(); - } - - this.stopPropagation(); - } -}; - -// Create mouseenter/leave events using mouseover/out and event-time checks -// so that event delegation works in jQuery. -// Do the same for pointerenter/pointerleave and pointerover/pointerout -// -// Support: Safari 7 only -// Safari sends mouseenter too often; see: -// https://code.google.com/p/chromium/issues/detail?id=470258 -// for the description of the bug (it existed in older Chrome versions as well). -jQuery.each( { - mouseenter: "mouseover", - mouseleave: "mouseout", - pointerenter: "pointerover", - pointerleave: "pointerout" -}, function( orig, fix ) { - jQuery.event.special[ orig ] = { - delegateType: fix, - bindType: fix, - - handle: function( event ) { - var ret, - target = this, - related = event.relatedTarget, - handleObj = event.handleObj; - - // For mouseenter/leave call the handler if related is outside the target. - // NB: No relatedTarget if the mouse left/entered the browser window - if ( !related || ( related !== target && !jQuery.contains( target, related ) ) ) { - event.type = handleObj.origType; - ret = handleObj.handler.apply( this, arguments ); - event.type = fix; - } - return ret; - } - }; -} ); - -// IE submit delegation -if ( !support.submit ) { - - jQuery.event.special.submit = { - setup: function() { - - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Lazy-add a submit handler when a descendant form may potentially be submitted - jQuery.event.add( this, "click._submit keypress._submit", function( e ) { - - // Node name check avoids a VML-related crash in IE (#9807) - var elem = e.target, - form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? - - // Support: IE <=8 - // We use jQuery.prop instead of elem.form - // to allow fixing the IE8 delegated submit issue (gh-2332) - // by 3rd party polyfills/workarounds. - jQuery.prop( elem, "form" ) : - undefined; - - if ( form && !jQuery._data( form, "submit" ) ) { - jQuery.event.add( form, "submit._submit", function( event ) { - event._submitBubble = true; - } ); - jQuery._data( form, "submit", true ); - } - } ); - - // return undefined since we don't need an event listener - }, - - postDispatch: function( event ) { - - // If form was submitted by the user, bubble the event up the tree - if ( event._submitBubble ) { - delete event._submitBubble; - if ( this.parentNode && !event.isTrigger ) { - jQuery.event.simulate( "submit", this.parentNode, event ); - } - } - }, - - teardown: function() { - - // Only need this for delegated form submit events - if ( jQuery.nodeName( this, "form" ) ) { - return false; - } - - // Remove delegated handlers; cleanData eventually reaps submit handlers attached above - jQuery.event.remove( this, "._submit" ); - } - }; -} - -// IE change delegation and checkbox/radio fix -if ( !support.change ) { - - jQuery.event.special.change = { - - setup: function() { - - if ( rformElems.test( this.nodeName ) ) { - - // IE doesn't fire change on a check/radio until blur; trigger it on click - // after a propertychange. Eat the blur-change in special.change.handle. - // This still fires onchange a second time for check/radio after blur. - if ( this.type === "checkbox" || this.type === "radio" ) { - jQuery.event.add( this, "propertychange._change", function( event ) { - if ( event.originalEvent.propertyName === "checked" ) { - this._justChanged = true; - } - } ); - jQuery.event.add( this, "click._change", function( event ) { - if ( this._justChanged && !event.isTrigger ) { - this._justChanged = false; - } - - // Allow triggered, simulated change events (#11500) - jQuery.event.simulate( "change", this, event ); - } ); - } - return false; - } - - // Delegated event; lazy-add a change handler on descendant inputs - jQuery.event.add( this, "beforeactivate._change", function( e ) { - var elem = e.target; - - if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "change" ) ) { - jQuery.event.add( elem, "change._change", function( event ) { - if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { - jQuery.event.simulate( "change", this.parentNode, event ); - } - } ); - jQuery._data( elem, "change", true ); - } - } ); - }, - - handle: function( event ) { - var elem = event.target; - - // Swallow native change events from checkbox/radio, we already triggered them above - if ( this !== elem || event.isSimulated || event.isTrigger || - ( elem.type !== "radio" && elem.type !== "checkbox" ) ) { - - return event.handleObj.handler.apply( this, arguments ); - } - }, - - teardown: function() { - jQuery.event.remove( this, "._change" ); - - return !rformElems.test( this.nodeName ); - } - }; -} - -// Support: Firefox -// Firefox doesn't have focus(in | out) events -// Related ticket - https://bugzilla.mozilla.org/show_bug.cgi?id=687787 -// -// Support: Chrome, Safari -// focus(in | out) events fire after focus & blur events, -// which is spec violation - http://www.w3.org/TR/DOM-Level-3-Events/#events-focusevent-event-order -// Related ticket - https://code.google.com/p/chromium/issues/detail?id=449857 -if ( !support.focusin ) { - jQuery.each( { focus: "focusin", blur: "focusout" }, function( orig, fix ) { - - // Attach a single capturing handler on the document while someone wants focusin/focusout - var handler = function( event ) { - jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ) ); - }; - - jQuery.event.special[ fix ] = { - setup: function() { - var doc = this.ownerDocument || this, - attaches = jQuery._data( doc, fix ); - - if ( !attaches ) { - doc.addEventListener( orig, handler, true ); - } - jQuery._data( doc, fix, ( attaches || 0 ) + 1 ); - }, - teardown: function() { - var doc = this.ownerDocument || this, - attaches = jQuery._data( doc, fix ) - 1; - - if ( !attaches ) { - doc.removeEventListener( orig, handler, true ); - jQuery._removeData( doc, fix ); - } else { - jQuery._data( doc, fix, attaches ); - } - } - }; - } ); -} - -jQuery.fn.extend( { - - on: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn ); - }, - one: function( types, selector, data, fn ) { - return on( this, types, selector, data, fn, 1 ); - }, - off: function( types, selector, fn ) { - var handleObj, type; - if ( types && types.preventDefault && types.handleObj ) { - - // ( event ) dispatched jQuery.Event - handleObj = types.handleObj; - jQuery( types.delegateTarget ).off( - handleObj.namespace ? - handleObj.origType + "." + handleObj.namespace : - handleObj.origType, - handleObj.selector, - handleObj.handler - ); - return this; - } - if ( typeof types === "object" ) { - - // ( types-object [, selector] ) - for ( type in types ) { - this.off( type, selector, types[ type ] ); - } - return this; - } - if ( selector === false || typeof selector === "function" ) { - - // ( types [, fn] ) - fn = selector; - selector = undefined; - } - if ( fn === false ) { - fn = returnFalse; - } - return this.each( function() { - jQuery.event.remove( this, types, fn, selector ); - } ); - }, - - trigger: function( type, data ) { - return this.each( function() { - jQuery.event.trigger( type, data, this ); - } ); - }, - triggerHandler: function( type, data ) { - var elem = this[ 0 ]; - if ( elem ) { - return jQuery.event.trigger( type, data, elem, true ); - } - } -} ); - - -var rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, - rnoshimcache = new RegExp( "<(?:" + nodeNames + ")[\\s/>]", "i" ), - rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:-]+)[^>]*)\/>/gi, - - // Support: IE 10-11, Edge 10240+ - // In IE/Edge using regex groups here causes severe slowdowns. - // See https://connect.microsoft.com/IE/feedback/details/1736512/ - rnoInnerhtml = /\s*$/g, - safeFragment = createSafeFragment( document ), - fragmentDiv = safeFragment.appendChild( document.createElement( "div" ) ); - -// Support: IE<8 -// Manipulating tables requires a tbody -function manipulationTarget( elem, content ) { - return jQuery.nodeName( elem, "table" ) && - jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? - - elem.getElementsByTagName( "tbody" )[ 0 ] || - elem.appendChild( elem.ownerDocument.createElement( "tbody" ) ) : - elem; -} - -// Replace/restore the type attribute of script elements for safe DOM manipulation -function disableScript( elem ) { - elem.type = ( jQuery.find.attr( elem, "type" ) !== null ) + "/" + elem.type; - return elem; -} -function restoreScript( elem ) { - var match = rscriptTypeMasked.exec( elem.type ); - if ( match ) { - elem.type = match[ 1 ]; - } else { - elem.removeAttribute( "type" ); - } - return elem; -} - -function cloneCopyEvent( src, dest ) { - if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { - return; - } - - var type, i, l, - oldData = jQuery._data( src ), - curData = jQuery._data( dest, oldData ), - events = oldData.events; - - if ( events ) { - delete curData.handle; - curData.events = {}; - - for ( type in events ) { - for ( i = 0, l = events[ type ].length; i < l; i++ ) { - jQuery.event.add( dest, type, events[ type ][ i ] ); - } - } - } - - // make the cloned public data object a copy from the original - if ( curData.data ) { - curData.data = jQuery.extend( {}, curData.data ); - } -} - -function fixCloneNodeIssues( src, dest ) { - var nodeName, e, data; - - // We do not need to do anything for non-Elements - if ( dest.nodeType !== 1 ) { - return; - } - - nodeName = dest.nodeName.toLowerCase(); - - // IE6-8 copies events bound via attachEvent when using cloneNode. - if ( !support.noCloneEvent && dest[ jQuery.expando ] ) { - data = jQuery._data( dest ); - - for ( e in data.events ) { - jQuery.removeEvent( dest, e, data.handle ); - } - - // Event data gets referenced instead of copied if the expando gets copied too - dest.removeAttribute( jQuery.expando ); - } - - // IE blanks contents when cloning scripts, and tries to evaluate newly-set text - if ( nodeName === "script" && dest.text !== src.text ) { - disableScript( dest ).text = src.text; - restoreScript( dest ); - - // IE6-10 improperly clones children of object elements using classid. - // IE10 throws NoModificationAllowedError if parent is null, #12132. - } else if ( nodeName === "object" ) { - if ( dest.parentNode ) { - dest.outerHTML = src.outerHTML; - } - - // This path appears unavoidable for IE9. When cloning an object - // element in IE9, the outerHTML strategy above is not sufficient. - // If the src has innerHTML and the destination does not, - // copy the src.innerHTML into the dest.innerHTML. #10324 - if ( support.html5Clone && ( src.innerHTML && !jQuery.trim( dest.innerHTML ) ) ) { - dest.innerHTML = src.innerHTML; - } - - } else if ( nodeName === "input" && rcheckableType.test( src.type ) ) { - - // IE6-8 fails to persist the checked state of a cloned checkbox - // or radio button. Worse, IE6-7 fail to give the cloned element - // a checked appearance if the defaultChecked value isn't also set - - dest.defaultChecked = dest.checked = src.checked; - - // IE6-7 get confused and end up setting the value of a cloned - // checkbox/radio button to an empty string instead of "on" - if ( dest.value !== src.value ) { - dest.value = src.value; - } - - // IE6-8 fails to return the selected option to the default selected - // state when cloning options - } else if ( nodeName === "option" ) { - dest.defaultSelected = dest.selected = src.defaultSelected; - - // IE6-8 fails to set the defaultValue to the correct value when - // cloning other types of input fields - } else if ( nodeName === "input" || nodeName === "textarea" ) { - dest.defaultValue = src.defaultValue; - } -} - -function domManip( collection, args, callback, ignored ) { - - // Flatten any nested arrays - args = concat.apply( [], args ); - - var first, node, hasScripts, - scripts, doc, fragment, - i = 0, - l = collection.length, - iNoClone = l - 1, - value = args[ 0 ], - isFunction = jQuery.isFunction( value ); - - // We can't cloneNode fragments that contain checked, in WebKit - if ( isFunction || - ( l > 1 && typeof value === "string" && - !support.checkClone && rchecked.test( value ) ) ) { - return collection.each( function( index ) { - var self = collection.eq( index ); - if ( isFunction ) { - args[ 0 ] = value.call( this, index, self.html() ); - } - domManip( self, args, callback, ignored ); - } ); - } - - if ( l ) { - fragment = buildFragment( args, collection[ 0 ].ownerDocument, false, collection, ignored ); - first = fragment.firstChild; - - if ( fragment.childNodes.length === 1 ) { - fragment = first; - } - - // Require either new content or an interest in ignored elements to invoke the callback - if ( first || ignored ) { - scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); - hasScripts = scripts.length; - - // Use the original fragment for the last item - // instead of the first because it can end up - // being emptied incorrectly in certain situations (#8070). - for ( ; i < l; i++ ) { - node = fragment; - - if ( i !== iNoClone ) { - node = jQuery.clone( node, true, true ); - - // Keep references to cloned scripts for later restoration - if ( hasScripts ) { - - // Support: Android<4.1, PhantomJS<2 - // push.apply(_, arraylike) throws on ancient WebKit - jQuery.merge( scripts, getAll( node, "script" ) ); - } - } - - callback.call( collection[ i ], node, i ); - } - - if ( hasScripts ) { - doc = scripts[ scripts.length - 1 ].ownerDocument; - - // Reenable scripts - jQuery.map( scripts, restoreScript ); - - // Evaluate executable scripts on first document insertion - for ( i = 0; i < hasScripts; i++ ) { - node = scripts[ i ]; - if ( rscriptType.test( node.type || "" ) && - !jQuery._data( node, "globalEval" ) && - jQuery.contains( doc, node ) ) { - - if ( node.src ) { - - // Optional AJAX dependency, but won't run scripts if not present - if ( jQuery._evalUrl ) { - jQuery._evalUrl( node.src ); - } - } else { - jQuery.globalEval( - ( node.text || node.textContent || node.innerHTML || "" ) - .replace( rcleanScript, "" ) - ); - } - } - } - } - - // Fix #11809: Avoid leaking memory - fragment = first = null; - } - } - - return collection; -} - -function remove( elem, selector, keepData ) { - var node, - elems = selector ? jQuery.filter( selector, elem ) : elem, - i = 0; - - for ( ; ( node = elems[ i ] ) != null; i++ ) { - - if ( !keepData && node.nodeType === 1 ) { - jQuery.cleanData( getAll( node ) ); - } - - if ( node.parentNode ) { - if ( keepData && jQuery.contains( node.ownerDocument, node ) ) { - setGlobalEval( getAll( node, "script" ) ); - } - node.parentNode.removeChild( node ); - } - } - - return elem; -} - -jQuery.extend( { - htmlPrefilter: function( html ) { - return html.replace( rxhtmlTag, "<$1>" ); - }, - - clone: function( elem, dataAndEvents, deepDataAndEvents ) { - var destElements, node, clone, i, srcElements, - inPage = jQuery.contains( elem.ownerDocument, elem ); - - if ( support.html5Clone || jQuery.isXMLDoc( elem ) || - !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { - - clone = elem.cloneNode( true ); - - // IE<=8 does not properly clone detached, unknown element nodes - } else { - fragmentDiv.innerHTML = elem.outerHTML; - fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); - } - - if ( ( !support.noCloneEvent || !support.noCloneChecked ) && - ( elem.nodeType === 1 || elem.nodeType === 11 ) && !jQuery.isXMLDoc( elem ) ) { - - // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 - destElements = getAll( clone ); - srcElements = getAll( elem ); - - // Fix all IE cloning issues - for ( i = 0; ( node = srcElements[ i ] ) != null; ++i ) { - - // Ensure that the destination node is not null; Fixes #9587 - if ( destElements[ i ] ) { - fixCloneNodeIssues( node, destElements[ i ] ); - } - } - } - - // Copy the events from the original to the clone - if ( dataAndEvents ) { - if ( deepDataAndEvents ) { - srcElements = srcElements || getAll( elem ); - destElements = destElements || getAll( clone ); - - for ( i = 0; ( node = srcElements[ i ] ) != null; i++ ) { - cloneCopyEvent( node, destElements[ i ] ); - } - } else { - cloneCopyEvent( elem, clone ); - } - } - - // Preserve script evaluation history - destElements = getAll( clone, "script" ); - if ( destElements.length > 0 ) { - setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); - } - - destElements = srcElements = node = null; - - // Return the cloned set - return clone; - }, - - cleanData: function( elems, /* internal */ forceAcceptData ) { - var elem, type, id, data, - i = 0, - internalKey = jQuery.expando, - cache = jQuery.cache, - attributes = support.attributes, - special = jQuery.event.special; - - for ( ; ( elem = elems[ i ] ) != null; i++ ) { - if ( forceAcceptData || acceptData( elem ) ) { - - id = elem[ internalKey ]; - data = id && cache[ id ]; - - if ( data ) { - if ( data.events ) { - for ( type in data.events ) { - if ( special[ type ] ) { - jQuery.event.remove( elem, type ); - - // This is a shortcut to avoid jQuery.event.remove's overhead - } else { - jQuery.removeEvent( elem, type, data.handle ); - } - } - } - - // Remove cache only if it was not already removed by jQuery.event.remove - if ( cache[ id ] ) { - - delete cache[ id ]; - - // Support: IE<9 - // IE does not allow us to delete expando properties from nodes - // IE creates expando attributes along with the property - // IE does not have a removeAttribute function on Document nodes - if ( !attributes && typeof elem.removeAttribute !== "undefined" ) { - elem.removeAttribute( internalKey ); - - // Webkit & Blink performance suffers when deleting properties - // from DOM nodes, so set to undefined instead - // https://code.google.com/p/chromium/issues/detail?id=378607 - } else { - elem[ internalKey ] = undefined; - } - - deletedIds.push( id ); - } - } - } - } - } -} ); - -jQuery.fn.extend( { - - // Keep domManip exposed until 3.0 (gh-2225) - domManip: domManip, - - detach: function( selector ) { - return remove( this, selector, true ); - }, - - remove: function( selector ) { - return remove( this, selector ); - }, - - text: function( value ) { - return access( this, function( value ) { - return value === undefined ? - jQuery.text( this ) : - this.empty().append( - ( this[ 0 ] && this[ 0 ].ownerDocument || document ).createTextNode( value ) - ); - }, null, value, arguments.length ); - }, - - append: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.appendChild( elem ); - } - } ); - }, - - prepend: function() { - return domManip( this, arguments, function( elem ) { - if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { - var target = manipulationTarget( this, elem ); - target.insertBefore( elem, target.firstChild ); - } - } ); - }, - - before: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this ); - } - } ); - }, - - after: function() { - return domManip( this, arguments, function( elem ) { - if ( this.parentNode ) { - this.parentNode.insertBefore( elem, this.nextSibling ); - } - } ); - }, - - empty: function() { - var elem, - i = 0; - - for ( ; ( elem = this[ i ] ) != null; i++ ) { - - // Remove element nodes and prevent memory leaks - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - } - - // Remove any remaining nodes - while ( elem.firstChild ) { - elem.removeChild( elem.firstChild ); - } - - // If this is a select, ensure that it displays empty (#12336) - // Support: IE<9 - if ( elem.options && jQuery.nodeName( elem, "select" ) ) { - elem.options.length = 0; - } - } - - return this; - }, - - clone: function( dataAndEvents, deepDataAndEvents ) { - dataAndEvents = dataAndEvents == null ? false : dataAndEvents; - deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; - - return this.map( function() { - return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); - } ); - }, - - html: function( value ) { - return access( this, function( value ) { - var elem = this[ 0 ] || {}, - i = 0, - l = this.length; - - if ( value === undefined ) { - return elem.nodeType === 1 ? - elem.innerHTML.replace( rinlinejQuery, "" ) : - undefined; - } - - // See if we can take a shortcut and just use innerHTML - if ( typeof value === "string" && !rnoInnerhtml.test( value ) && - ( support.htmlSerialize || !rnoshimcache.test( value ) ) && - ( support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && - !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { - - value = jQuery.htmlPrefilter( value ); - - try { - for ( ; i < l; i++ ) { - - // Remove element nodes and prevent memory leaks - elem = this[ i ] || {}; - if ( elem.nodeType === 1 ) { - jQuery.cleanData( getAll( elem, false ) ); - elem.innerHTML = value; - } - } - - elem = 0; - - // If using innerHTML throws an exception, use the fallback method - } catch ( e ) {} - } - - if ( elem ) { - this.empty().append( value ); - } - }, null, value, arguments.length ); - }, - - replaceWith: function() { - var ignored = []; - - // Make the changes, replacing each non-ignored context element with the new content - return domManip( this, arguments, function( elem ) { - var parent = this.parentNode; - - if ( jQuery.inArray( this, ignored ) < 0 ) { - jQuery.cleanData( getAll( this ) ); - if ( parent ) { - parent.replaceChild( elem, this ); - } - } - - // Force callback invocation - }, ignored ); - } -} ); - -jQuery.each( { - appendTo: "append", - prependTo: "prepend", - insertBefore: "before", - insertAfter: "after", - replaceAll: "replaceWith" -}, function( name, original ) { - jQuery.fn[ name ] = function( selector ) { - var elems, - i = 0, - ret = [], - insert = jQuery( selector ), - last = insert.length - 1; - - for ( ; i <= last; i++ ) { - elems = i === last ? this : this.clone( true ); - jQuery( insert[ i ] )[ original ]( elems ); - - // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() - push.apply( ret, elems.get() ); - } - - return this.pushStack( ret ); - }; -} ); - - -var iframe, - elemdisplay = { - - // Support: Firefox - // We have to pre-define these values for FF (#10227) - HTML: "block", - BODY: "block" - }; - -/** - * Retrieve the actual display of a element - * @param {String} name nodeName of the element - * @param {Object} doc Document object - */ - -// Called only from within defaultDisplay -function actualDisplay( name, doc ) { - var elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), - - display = jQuery.css( elem[ 0 ], "display" ); - - // We don't have any data stored on the element, - // so use "detach" method as fast way to get rid of the element - elem.detach(); - - return display; -} - -/** - * Try to determine the default display value of an element - * @param {String} nodeName - */ -function defaultDisplay( nodeName ) { - var doc = document, - display = elemdisplay[ nodeName ]; - - if ( !display ) { - display = actualDisplay( nodeName, doc ); - - // If the simple way fails, read from inside an iframe - if ( display === "none" || !display ) { - - // Use the already-created iframe if possible - iframe = ( iframe || jQuery( "