diff --git a/api/v1/serializers/config.py b/api/v1/serializers/config.py index f6ae89dd..718bea00 100644 --- a/api/v1/serializers/config.py +++ b/api/v1/serializers/config.py @@ -1,40 +1,27 @@ -from constance import config -from constance.admin import get_values, ConstanceForm from rest_framework import serializers -from rest_framework.fields import SkipField -from collections import OrderedDict +from apps.config.models import Config -def _set_constance_value(key, value): - form = ConstanceForm(initial=get_values()) - field = form.fields[key] - clean_value = field.clean(field.to_python(value)) - setattr(config, key, clean_value) - -class ConfigSerializer(serializers.Serializer): +class ConfigSerializer(serializers.ModelSerializer): SERVICE_COMMISSION = serializers.IntegerField(required=False) SERVICE_DISCOUNT_MIN_AMOUNT = serializers.IntegerField(required=False) SERVICE_DISCOUNT = serializers.IntegerField(required=False) INSTAGRAM_CLIENT_ACCESS_TOKEN = serializers.CharField(required=False) INSTAGRAM_CLIENT_SECRET = serializers.CharField(required=False) INSTAGRAM_PROFILE_URL = serializers.CharField(required=False) - # SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False) - - def to_representation(self, instance): - ret = OrderedDict() - fields = self._readable_fields - for field in fields: - attribute = instance.get(field.field_name) - ret[field.field_name] = field.to_representation(attribute) - return ret - - def to_internal_value(self, data): - ret = OrderedDict(get_values()) - for k, v in data.items(): - ret[k] = v - return ret + SCHOOL_LOGO_IMAGE = serializers.ImageField(required=False, allow_null=True) + MAIN_PAGE_TOP_IMAGE = serializers.ImageField(required=False, allow_null=True) - def update(self, instance, validated_data): - for k, v in validated_data.items(): - _set_constance_value(k, v) + class Meta: + model = Config + fields = ( + 'SERVICE_COMMISSION', + 'SERVICE_DISCOUNT_MIN_AMOUNT', + 'SERVICE_DISCOUNT', + 'INSTAGRAM_CLIENT_ACCESS_TOKEN', + 'INSTAGRAM_CLIENT_SECRET', + 'INSTAGRAM_PROFILE_URL', + 'SCHOOL_LOGO_IMAGE', + 'MAIN_PAGE_TOP_IMAGE', + ) diff --git a/api/v1/views.py b/api/v1/views.py index 909195d4..c47ce413 100644 --- a/api/v1/views.py +++ b/api/v1/views.py @@ -1,10 +1,6 @@ -from constance.admin import get_values - from django.contrib.auth import get_user_model -from rest_framework import status -from rest_framework import views, viewsets -from rest_framework import generics +from rest_framework import status, views, viewsets, generics from rest_framework.decorators import detail_route, list_route from rest_framework.response import Response @@ -43,6 +39,7 @@ from apps.course.models import ( Material, Lesson, Like, ) +from apps.config.models import Config from apps.content.models import ( Image, Text, ImageText, Video, Gallery, GalleryImage, ImageObject, @@ -327,18 +324,12 @@ class SchoolScheduleViewSet(ExtendedModelViewSet): class ConfigViewSet(generics.RetrieveUpdateAPIView): + queryset = Config.objects.all() serializer_class = ConfigSerializer permission_classes = (IsAdmin,) - def retrieve(self, request, *args, **kwargs): - serializer = ConfigSerializer(get_values()) - return Response(serializer.data) - - def patch(self, request, *args, **kwargs): - serializer = ConfigSerializer(data=request.data) - if serializer.is_valid(): - serializer.update(get_values(), serializer.validated_data) - return Response(serializer.data) + def get_object(self): + return Config.load() class CommentViewSet(ExtendedModelViewSet): diff --git a/apps/auth/templates/auth/password_reset.html b/apps/auth/templates/auth/password_reset.html index 0467d253..c611ee51 100644 --- a/apps/auth/templates/auth/password_reset.html +++ b/apps/auth/templates/auth/password_reset.html @@ -3,8 +3,8 @@ {% block content %}

Для восстановления пароля нажмите кнопку ниже.

- Нажмите для восстановления + Нажмите для восстановления

Или скопируйте ссылку ниже, и вставьте её в адресную строку браузера.

-

{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}

+

{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %}

{% endblock content %} diff --git a/apps/auth/templates/auth/password_reset.txt b/apps/auth/templates/auth/password_reset.txt index dfd0f38b..403aa7bd 100644 --- a/apps/auth/templates/auth/password_reset.txt +++ b/apps/auth/templates/auth/password_reset.txt @@ -1,2 +1,2 @@ Восстановление пароля для {{ email }}. Перейдите по ссылке ниже: -{{ protocol}}://{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %} \ No newline at end of file +{{ domain }}{% url 'lilcity:password_reset_confirm' uidb64=uid token=token %} diff --git a/apps/auth/views.py b/apps/auth/views.py index 3d4ba19d..8eb52b18 100644 --- a/apps/auth/views.py +++ b/apps/auth/views.py @@ -48,8 +48,9 @@ class LearnerRegistrationView(FormView): # fixme: change email text # fixme: async send email + refferer = self.request.META.get('HTTP_REFERER') token = verification_email_token.make_token(user) - url = self.request.scheme + '://' + self.request.get_host() + str(reverse_lazy('lilcity:verification-email', args=[token])) + url = refferer + str(reverse_lazy('lilcity:verification-email', args=[token])) send_email('Verification Email', email, "notification/email/verification_email.html", url=url) return JsonResponse({"success": True}, status=201) @@ -106,7 +107,9 @@ class PasswordResetView(views.PasswordContextMixin, BaseFormView): token_generator = views.default_token_generator def form_valid(self, form): + refferer = self.request.META.get('HTTP_REFERER') opts = { + 'domain_override': refferer, 'use_https': self.request.is_secure(), 'token_generator': self.token_generator, 'from_email': self.from_email, diff --git a/apps/config/__init__.py b/apps/config/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/config/admin.py b/apps/config/admin.py new file mode 100644 index 00000000..7b75773f --- /dev/null +++ b/apps/config/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin + +from .models import Config + +admin.site.register(Config) diff --git a/apps/config/apps.py b/apps/config/apps.py new file mode 100644 index 00000000..8d9481b1 --- /dev/null +++ b/apps/config/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ConfigConfig(AppConfig): + name = 'config' diff --git a/apps/config/migrations/0001_initial.py b/apps/config/migrations/0001_initial.py new file mode 100644 index 00000000..0aae28c7 --- /dev/null +++ b/apps/config/migrations/0001_initial.py @@ -0,0 +1,27 @@ +# Generated by Django 2.0.3 on 2018-03-26 10:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Config', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('INSTAGRAM_CLIENT_ACCESS_TOKEN', models.CharField(default='7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6', max_length=51)), + ('INSTAGRAM_CLIENT_SECRET', models.CharField(default='2334a921425140ccb180d145dcd35b25', max_length=32)), + ('INSTAGRAM_PROFILE_URL', models.CharField(default='#', max_length=126)), + ('SERVICE_COMMISSION', models.IntegerField(default=10)), + ('SERVICE_DISCOUNT_MIN_AMOUNT', models.IntegerField(default=3500)), + ('SERVICE_DISCOUNT', models.ImageField(default=1000, upload_to='')), + ('SCHOOL_LOGO_IMAGE', models.ImageField(null=True, upload_to='')), + ], + ), + ] diff --git a/apps/config/migrations/0002_auto_20180326_1026.py b/apps/config/migrations/0002_auto_20180326_1026.py new file mode 100644 index 00000000..b91b4812 --- /dev/null +++ b/apps/config/migrations/0002_auto_20180326_1026.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-03-26 10:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='SCHOOL_LOGO_IMAGE', + field=models.FileField(null=True, upload_to=''), + ), + ] diff --git a/apps/config/migrations/0003_auto_20180326_1027.py b/apps/config/migrations/0003_auto_20180326_1027.py new file mode 100644 index 00000000..19d5c208 --- /dev/null +++ b/apps/config/migrations/0003_auto_20180326_1027.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.3 on 2018-03-26 10:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0002_auto_20180326_1026'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='SCHOOL_LOGO_IMAGE', + field=models.ImageField(null=True, upload_to=''), + ), + migrations.AlterField( + model_name='config', + name='SERVICE_DISCOUNT', + field=models.IntegerField(default=1000), + ), + ] diff --git a/apps/config/migrations/0004_config_main_page_top_image.py b/apps/config/migrations/0004_config_main_page_top_image.py new file mode 100644 index 00000000..5810b8c6 --- /dev/null +++ b/apps/config/migrations/0004_config_main_page_top_image.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.3 on 2018-03-26 11:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0003_auto_20180326_1027'), + ] + + operations = [ + migrations.AddField( + model_name='config', + name='MAIN_PAGE_TOP_IMAGE', + field=models.ImageField(null=True, upload_to=''), + ), + ] diff --git a/apps/config/migrations/0005_auto_20180326_1314.py b/apps/config/migrations/0005_auto_20180326_1314.py new file mode 100644 index 00000000..af993523 --- /dev/null +++ b/apps/config/migrations/0005_auto_20180326_1314.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0.3 on 2018-03-26 13:14 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('config', '0004_config_main_page_top_image'), + ] + + operations = [ + migrations.AlterField( + model_name='config', + name='MAIN_PAGE_TOP_IMAGE', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + migrations.AlterField( + model_name='config', + name='SCHOOL_LOGO_IMAGE', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + ] diff --git a/apps/config/migrations/__init__.py b/apps/config/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/config/models.py b/apps/config/models.py new file mode 100644 index 00000000..240fe7d5 --- /dev/null +++ b/apps/config/models.py @@ -0,0 +1,39 @@ +from django.db import models + + +class Config(models.Model): + INSTAGRAM_CLIENT_ACCESS_TOKEN = models.CharField( + max_length=51, default='7145314808.f6fa114.6b737a5355534e0eb5cf7c40cb4998f6' + ) + INSTAGRAM_CLIENT_SECRET = models.CharField(max_length=32, default='2334a921425140ccb180d145dcd35b25') + INSTAGRAM_PROFILE_URL = models.CharField(max_length=126, default='#') + SERVICE_COMMISSION = models.IntegerField(default=10) + SERVICE_DISCOUNT_MIN_AMOUNT = models.IntegerField(default=3500) + SERVICE_DISCOUNT = models.IntegerField(default=1000) + SCHOOL_LOGO_IMAGE = models.ImageField(null=True, blank=True) + MAIN_PAGE_TOP_IMAGE = models.ImageField(null=True, blank=True) + + def save(self, *args, **kwargs): + self.pk = 1 + super().save(*args, **kwargs) + + def delete(self, *args, **kwargs): + pass + + @classmethod + def load(cls): + try: + obj, created = cls.objects.get_or_create(pk=1) + except: + # This magic for migrate + obj = { + 'INSTAGRAM_CLIENT_ACCESS_TOKEN': '', + 'INSTAGRAM_CLIENT_SECRET': '', + 'INSTAGRAM_PROFILE_URL': '', + 'SERVICE_COMMISSION': '', + 'SERVICE_DISCOUNT_MIN_AMOUNT': '', + 'SERVICE_DISCOUNT': '', + 'SCHOOL_LOGO_IMAGE': '', + 'MAIN_PAGE_TOP_IMAGE': '', + } + return obj diff --git a/apps/config/tests.py b/apps/config/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/apps/config/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/config/views.py b/apps/config/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/apps/config/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/content/admin.py b/apps/content/admin.py index 555ba173..39d1fb93 100644 --- a/apps/content/admin.py +++ b/apps/content/admin.py @@ -63,7 +63,6 @@ class ContentAdmin(PolymorphicParentModelAdmin): Text, ImageText, Video, - # GalleryAdmin, ) diff --git a/apps/content/tasks.py b/apps/content/tasks.py index 9f58dcaa..a3210c2b 100644 --- a/apps/content/tasks.py +++ b/apps/content/tasks.py @@ -3,22 +3,24 @@ import json import requests import shutil -from constance import config from instagram.client import InstagramAPI from project.celery import app from time import sleep from django.conf import settings +from apps.config.models import Config + @app.task def retrieve_photos(): + config = Config.load() api = InstagramAPI( access_token=config.INSTAGRAM_CLIENT_ACCESS_TOKEN, client_secret=config.INSTAGRAM_CLIENT_SECRET, ) recent_media, next_ = api.user_recent_media(user_id='self', count=20) - path = os.path.join(settings.BASE_DIR, config.INSTAGRAM_RESULTS_PATH) + path = os.path.join(settings.BASE_DIR, settings.INSTAGRAM_RESULTS_PATH) for idx, media in enumerate(recent_media): try: fname = os.path.join(path, f'{idx}.jpg') diff --git a/apps/course/templates/course/course.html b/apps/course/templates/course/course.html index 4426225e..0a3c2ce4 100644 --- a/apps/course/templates/course/course.html +++ b/apps/course/templates/course/course.html @@ -14,7 +14,7 @@ {% block ogdescription %}{{ course.short_description }}{% endblock ogdescription %} {% block content %} -
+
@@ -30,6 +30,7 @@ class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" {% if user.is_authenticated %} {% if not pending %} + data-course-buy href="{% url 'course-checkout' course.id %}" {% endif %} {% else %} @@ -394,6 +395,7 @@ class="go__btn btn{% if pending %} btn_gray{% endif %} btn_md" {% if user.is_authenticated %} {% if not pending %} + data-course-buy href="{% url 'course-checkout' course.id %}" {% endif %} {% else %} diff --git a/apps/payment/models.py b/apps/payment/models.py index d3b42c8c..8f1f36d8 100644 --- a/apps/payment/models.py +++ b/apps/payment/models.py @@ -1,4 +1,3 @@ -from constance import config from paymentwall import Pingback from polymorphic.models import PolymorphicModel @@ -9,9 +8,12 @@ from django.core.validators import RegexValidator from django.utils.timezone import now from apps.course.models import Course +from apps.config.models import Config from apps.school.models import SchoolSchedule from apps.notification.utils import send_email + +config = Config.load() User = get_user_model() CREDIT_CARD_RE = r'^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\\d{3})\d{11})$' diff --git a/apps/user/models.py b/apps/user/models.py index 058df6b6..8d29759f 100644 --- a/apps/user/models.py +++ b/apps/user/models.py @@ -110,7 +110,12 @@ def send_user_info_to_mixpanel(sender, instance=None, created=False, **kwargs): @receiver(post_save, sender=User) def auto_create_subscription(sender, instance=None, created=False, **kwargs): - if not hasattr(instance, 'email_subscription'): + try: + es = EmailSubscription.objects.get(email=instance.email) + if not es.user: + es.user = instance + es.save() + except EmailSubscription.DoesNotExist: instance.email_subscription = EmailSubscription.objects.create( user=instance, email=instance.email, diff --git a/docker-compose.yml b/docker-compose.yml index b3c84e2f..d848728c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: restart: always volumes: - .:/lilcity - command: bash -c "python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload" + command: bash -c "python manage.py collectstatic --no-input && python manage.py migrate && python manage.py loaddata /lilcity/apps/*/fixtures/*.json && gunicorn --workers=4 project.wsgi --bind=0.0.0.0:8000 --worker-class=gthread --reload" environment: - DJANGO_SETTINGS_MODULE=project.settings - DATABASE_SERVICE_HOST=db diff --git a/project/context_processors.py b/project/context_processors.py new file mode 100644 index 00000000..cf8dc47a --- /dev/null +++ b/project/context_processors.py @@ -0,0 +1,5 @@ +from apps.config.models import Config + + +def config(request): + return {"config": Config.load()} diff --git a/project/fields.py b/project/fields.py new file mode 100644 index 00000000..a2e4709f --- /dev/null +++ b/project/fields.py @@ -0,0 +1,14 @@ +from django.forms import ImageField as BaseImageField + + +class ImageField(BaseImageField): + + def to_internal_value(self, data): + # if data is None image field was not uploaded + if data: + file_object = super(ImageField, self).to_internal_value(data) + django_field = self._DjangoImageField() + django_field.error_messages = self.error_messages + django_field.to_python(file_object) + return file_object + return data diff --git a/project/settings.py b/project/settings.py index 9785e9ca..2934c1de 100644 --- a/project/settings.py +++ b/project/settings.py @@ -17,7 +17,6 @@ from celery.schedules import crontab from collections import OrderedDict from datetime import timedelta - # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -53,8 +52,6 @@ INSTALLED_APPS = [ 'rest_framework.authtoken', 'drf_yasg', 'corsheaders', - 'constance', - 'constance.backends.database', 'sorl.thumbnail', 'raven.contrib.django.raven_compat', ] + [ @@ -64,10 +61,9 @@ INSTALLED_APPS = [ 'apps.payment', 'apps.course', 'apps.content', + 'apps.config', 'apps.school', ] -if DEBUG: - INSTALLED_APPS += ['silk'] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', @@ -80,8 +76,6 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'apps.auth.middleware.TokenAuthLoginMiddleware', ] -if DEBUG: - MIDDLEWARE += ['silk.middleware.SilkyMiddleware'] ROOT_URLCONF = 'project.urls' @@ -91,32 +85,26 @@ TEMPLATES = [ 'DIRS': [ 'project', ], - 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ - 'constance.context_processors.config', + 'project.context_processors.config', 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], + 'loaders': [ + ('django.template.loaders.cached.Loader', [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ]), + ], }, }, ] WSGI_APPLICATION = 'project.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/2.0/ref/settings/#databases - -# DATABASES = { -# 'default': { -# 'ENGINE': 'django.db.backends.sqlite3', -# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), -# } -# } - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', @@ -208,6 +196,7 @@ REST_FRAMEWORK = { ), 'DEFAULT_RENDERER_CLASSES': ( 'rest_framework.renderers.JSONRenderer', + # 'rest_framework.renderers.BrowsableAPIRenderer', ), 'DEFAULT_FILTER_BACKENDS': ( 'django_filters.rest_framework.DjangoFilterBackend', @@ -227,51 +216,11 @@ CELERY_TASK_SERIALIZER = 'json' CELERY_BEAT_SCHEDULE = { 'retrieve_photos_from_instagram': { 'task': 'apps.content.tasks.retrieve_photos', - 'schedule': timedelta(minutes=2) if DEBUG else crontab(minute=0, hour=0), + 'schedule': timedelta(minutes=5) if DEBUG else crontab(minute=0, hour=0), 'args': (), }, } -# Dynamic settings - -CONSTANCE_BACKEND = 'constance.backends.database.DatabaseBackend' -CONSTANCE_ADDITIONAL_FIELDS = { - 'image_field': ['django.forms.ImageField', {}] -} -CONSTANCE_CONFIG = OrderedDict(( - ('INSTAGRAM_CLIENT_ACCESS_TOKEN', ('7145314808.f6fa114.ce354a5d876041fc9d3db04b0045587d', '')), - ('INSTAGRAM_CLIENT_SECRET', ('2334a921425140ccb180d145dcd35b25', '')), - ('INSTAGRAM_PROFILE_URL', ('#', 'URL профиля Instagram.')), - ('INSTAGRAM_RESULTS_TAG', ('#lil_акварель', 'Тэг результатов работ.')), - ('INSTAGRAM_RESULTS_PATH', ('media/instagram/results/', 'Путь до результатов работ.')), - ('SERVICE_COMMISSION', (10, 'Комиссия сервиса в процентах.')), - ('SERVICE_DISCOUNT_MIN_AMOUNT', (3500, 'Минимальная сумма платежа для школы, после которой вычитывается скидка SERVICE_DISCOUNT.')), - ('SERVICE_DISCOUNT', (1000, 'Комиссия сервиса при покупке всех дней.')), - # ('SCHOOL_LOGO_IMAGE', ('default.png', 'Изображение в диалоге покупки школы', 'image_field')), -)) - -CONSTANCE_CONFIG_FIELDSETS = OrderedDict({ - 'Service': ( - 'SERVICE_COMMISSION', - 'SERVICE_DISCOUNT_MIN_AMOUNT', - 'SERVICE_DISCOUNT', - # 'SCHOOL_LOGO_IMAGE', - ), - 'Instagram': ( - 'INSTAGRAM_CLIENT_ACCESS_TOKEN', - 'INSTAGRAM_CLIENT_SECRET', - 'INSTAGRAM_PROFILE_URL', - 'INSTAGRAM_RESULTS_TAG', - 'INSTAGRAM_RESULTS_PATH', - ), -}) - - -try: - from .local_settings import * -except ImportError: - pass - try: from paymentwall import * except ImportError: @@ -302,3 +251,10 @@ RAVEN_CONFIG = { # release based on the git info. 'release': raven.fetch_git_sha(BASE_DIR), } + +INSTAGRAM_RESULTS_PATH = 'media/instagram/results/' + +try: + from .local_settings import * +except ImportError: + pass diff --git a/project/templates/lilcity/index.html b/project/templates/lilcity/index.html index 9a6ee2b2..dbdbcfe2 100644 --- a/project/templates/lilcity/index.html +++ b/project/templates/lilcity/index.html @@ -47,12 +47,23 @@ viewportmeta.content = 'width=device-width, maximum-scale=1.6, initial-scale=1.0'; } } - + {% block mixpanel %} + + + + {% endblock mixpanel %} diff --git a/project/templates/lilcity/main.html b/project/templates/lilcity/main.html index 61fef12c..6fc3a9ea 100644 --- a/project/templates/lilcity/main.html +++ b/project/templates/lilcity/main.html @@ -2,7 +2,14 @@ {% block title %}School LIL.CITY{% endblock title %} {% block content %} -
+
Первая онлайн-школа креативного мышления для детей! 5+
/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + return escaped; + }; + + _.extend = function(obj) { + _.each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // from a comment on http://dbj.org/dbj/?p=286 + // fails on only one very rare and deliberate custom object: + // var bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }}; + _.isFunction = function(f) { + try { + return /^\s*\bfunction\b/.test(f); + } catch (x) { + return false; + } + }; + + _.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); + }; + + _.toArray = function(iterable) { + if (!iterable) { + return []; + } + if (iterable.toArray) { + return iterable.toArray(); + } + if (_.isArray(iterable)) { + return slice.call(iterable); + } + if (_.isArguments(iterable)) { + return slice.call(iterable); + } + return _.values(iterable); + }; + + _.keys = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value, key) { + results[results.length] = key; + }); + return results; + }; + + _.values = function(obj) { + var results = []; + if (obj === null) { + return results; + } + _.each(obj, function(value) { + results[results.length] = value; + }); + return results; + }; + + _.identity = function(value) { + return value; + }; + + _.include = function(obj, target) { + var found = false; + if (obj === null) { + return found; + } + if (nativeIndexOf && obj.indexOf === nativeIndexOf) { + return obj.indexOf(target) != -1; + } + _.each(obj, function(value) { + if (found || (found = (value === target))) { + return breaker; + } + }); + return found; + }; + + _.includes = function(str, needle) { + return str.indexOf(needle) !== -1; + }; + + // Underscore Addons + _.inherit = function(subclass, superclass) { + subclass.prototype = new superclass(); + subclass.prototype.constructor = subclass; + subclass.superclass = superclass.prototype; + return subclass; + }; + + _.isObject = function(obj) { + return (obj === Object(obj) && !_.isArray(obj)); + }; + + _.isEmptyObject = function(obj) { + if (_.isObject(obj)) { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + return false; + } + } + return true; + } + return false; + }; + + _.isUndefined = function(obj) { + return obj === void 0; + }; + + _.isString = function(obj) { + return toString.call(obj) == '[object String]'; + }; + + _.isDate = function(obj) { + return toString.call(obj) == '[object Date]'; + }; + + _.isNumber = function(obj) { + return toString.call(obj) == '[object Number]'; + }; + + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + _.encodeDates = function(obj) { + _.each(obj, function(v, k) { + if (_.isDate(v)) { + obj[k] = _.formatDate(v); + } else if (_.isObject(v)) { + obj[k] = _.encodeDates(v); // recurse + } + }); + return obj; + }; + + _.timestamp = function() { + Date.now = Date.now || function() { + return +new Date; + }; + return Date.now(); + }; + + _.formatDate = function(d) { + // YYYY-MM-DDTHH:MM:SS in UTC + function pad(n) { + return n < 10 ? '0' + n : n; + } + return d.getUTCFullYear() + '-' + + pad(d.getUTCMonth() + 1) + '-' + + pad(d.getUTCDate()) + 'T' + + pad(d.getUTCHours()) + ':' + + pad(d.getUTCMinutes()) + ':' + + pad(d.getUTCSeconds()); + }; + + _.safewrap = function(f) { + return function() { + try { + return f.apply(this, arguments); + } catch (e) { + console$1.critical('Implementation error. Please turn on debug and contact support@mixpanel.com.'); + if (Config.DEBUG){ + console$1.critical(e); + } + } + }; + }; + + _.safewrap_class = function(klass, functions) { + for (var i = 0; i < functions.length; i++) { + klass.prototype[functions[i]] = _.safewrap(klass.prototype[functions[i]]); + } + }; + + _.safewrap_instance_methods = function(obj) { + for (var func in obj) { + if (typeof(obj[func]) === 'function') { + obj[func] = _.safewrap(obj[func]); + } + } + }; + + _.strip_empty_properties = function(p) { + var ret = {}; + _.each(p, function(v, k) { + if (_.isString(v) && v.length > 0) { + ret[k] = v; + } + }); + return ret; + }; + + /* + * this function returns a copy of object after truncating it. If + * passed an Array or Object it will iterate through obj and + * truncate all the values recursively. + */ + _.truncate = function(obj, length) { + var ret; + + if (typeof(obj) === 'string') { + ret = obj.slice(0, length); + } else if (_.isArray(obj)) { + ret = []; + _.each(obj, function(val) { + ret.push(_.truncate(val, length)); + }); + } else if (_.isObject(obj)) { + ret = {}; + _.each(obj, function(val, key) { + ret[key] = _.truncate(val, length); + }); + } else { + ret = obj; + } + + return ret; + }; + + _.JSONEncode = (function() { + return function(mixed_val) { + var value = mixed_val; + var quote = function(string) { + var escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; // eslint-disable-line no-control-regex + var meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"': '\\"', + '\\': '\\\\' + }; + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function(a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + }; + + var str = function(key, holder) { + var gap = ''; + var indent = ' '; + var i = 0; // The loop counter. + var k = ''; // The member key. + var v = ''; // The member value. + var length = 0; + var mind = gap; + var partial = []; + var value = holder[key]; + + // If the value has a toJSON method, call it to obtain a replacement value. + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + + // What happens next depends on the value's type. + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + // JSON numbers must be finite. Encode non-finite numbers as null. + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + // If the value is a boolean or null, convert it to a string. Note: + // typeof null does not produce 'null'. The case is included here in + // the remote chance that this gets fixed someday. + + return String(value); + + case 'object': + // If the type is 'object', we might be dealing with an object or an array or + // null. + // Due to a specification blunder in ECMAScript, typeof null is 'object', + // so watch out for that case. + if (!value) { + return 'null'; + } + + // Make an array to hold the partial results of stringifying this object value. + gap += indent; + partial = []; + + // Is the value an array? + if (toString.apply(value) === '[object Array]') { + // The value is an array. Stringify every element. Use null as a placeholder + // for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + + // Join all of the elements together, separated with commas, and wrap them in + // brackets. + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + + // Iterate through all of the keys in the object. + for (k in value) { + if (hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + + // Join all of the member texts together, separated with commas, + // and wrap them in braces. + v = partial.length === 0 ? '{}' : + gap ? '{' + partial.join(',') + '' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + }; + + // Make a fake root object containing our value under the key of ''. + // Return the result of stringifying the value. + return str('', { + '': value + }); + }; + })(); + + /** + * From https://github.com/douglascrockford/JSON-js/blob/master/json_parse.js + * Slightly modified to throw a real Error rather than a POJO + */ + _.JSONDecode = (function() { + var at, // The index of the current character + ch, // The current character + escapee = { + '"': '"', + '\\': '\\', + '/': '/', + 'b': '\b', + 'f': '\f', + 'n': '\n', + 'r': '\r', + 't': '\t' + }, + text, + error = function(m) { + var e = new SyntaxError(m); + e.at = at; + e.text = text; + throw e; + }, + next = function(c) { + // If a c parameter is provided, verify that it matches the current character. + if (c && c !== ch) { + error('Expected \'' + c + '\' instead of \'' + ch + '\''); + } + // Get the next character. When there are no more characters, + // return the empty string. + ch = text.charAt(at); + at += 1; + return ch; + }, + number = function() { + // Parse a number value. + var number, + string = ''; + + if (ch === '-') { + string = '-'; + next('-'); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + if (ch === '.') { + string += '.'; + while (next() && ch >= '0' && ch <= '9') { + string += ch; + } + } + if (ch === 'e' || ch === 'E') { + string += ch; + next(); + if (ch === '-' || ch === '+') { + string += ch; + next(); + } + while (ch >= '0' && ch <= '9') { + string += ch; + next(); + } + } + number = +string; + if (!isFinite(number)) { + error('Bad number'); + } else { + return number; + } + }, + + string = function() { + // Parse a string value. + var hex, + i, + string = '', + uffff; + // When parsing for string values, we must look for " and \ characters. + if (ch === '"') { + while (next()) { + if (ch === '"') { + next(); + return string; + } + if (ch === '\\') { + next(); + if (ch === 'u') { + uffff = 0; + for (i = 0; i < 4; i += 1) { + hex = parseInt(next(), 16); + if (!isFinite(hex)) { + break; + } + uffff = uffff * 16 + hex; + } + string += String.fromCharCode(uffff); + } else if (typeof escapee[ch] === 'string') { + string += escapee[ch]; + } else { + break; + } + } else { + string += ch; + } + } + } + error('Bad string'); + }, + white = function() { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + }, + word = function() { + // true, false, or null. + switch (ch) { + case 't': + next('t'); + next('r'); + next('u'); + next('e'); + return true; + case 'f': + next('f'); + next('a'); + next('l'); + next('s'); + next('e'); + return false; + case 'n': + next('n'); + next('u'); + next('l'); + next('l'); + return null; + } + error('Unexpected "' + ch + '"'); + }, + value, // Placeholder for the value function. + array = function() { + // Parse an array value. + var array = []; + + if (ch === '[') { + next('['); + white(); + if (ch === ']') { + next(']'); + return array; // empty array + } + while (ch) { + array.push(value()); + white(); + if (ch === ']') { + next(']'); + return array; + } + next(','); + white(); + } + } + error('Bad array'); + }, + object = function() { + // Parse an object value. + var key, + object = {}; + + if (ch === '{') { + next('{'); + white(); + if (ch === '}') { + next('}'); + return object; // empty object + } + while (ch) { + key = string(); + white(); + next(':'); + if (Object.hasOwnProperty.call(object, key)) { + error('Duplicate key "' + key + '"'); + } + object[key] = value(); + white(); + if (ch === '}') { + next('}'); + return object; + } + next(','); + white(); + } + } + error('Bad object'); + }; + + value = function() { + // Parse a JSON value. It could be an object, an array, a string, + // a number, or a word. + white(); + switch (ch) { + case '{': + return object(); + case '[': + return array(); + case '"': + return string(); + case '-': + return number(); + default: + return ch >= '0' && ch <= '9' ? number() : word(); + } + }; + + // Return the json_parse function. It will have access to all of the + // above functions and variables. + return function(source) { + var result; + + text = source; + at = 0; + ch = ' '; + result = value(); + white(); + if (ch) { + error('Syntax error'); + } + + return result; + }; + })(); + + _.base64Encode = function(data) { + var b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, + ac = 0, + enc = '', + tmp_arr = []; + + if (!data) { + return data; + } + + data = _.utf8Encode(data); + + do { // pack three octets into four hexets + o1 = data.charCodeAt(i++); + o2 = data.charCodeAt(i++); + o3 = data.charCodeAt(i++); + + bits = o1 << 16 | o2 << 8 | o3; + + h1 = bits >> 18 & 0x3f; + h2 = bits >> 12 & 0x3f; + h3 = bits >> 6 & 0x3f; + h4 = bits & 0x3f; + + // use hexets to index into b64, and append result to encoded string + tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4); + } while (i < data.length); + + enc = tmp_arr.join(''); + + switch (data.length % 3) { + case 1: + enc = enc.slice(0, -2) + '=='; + break; + case 2: + enc = enc.slice(0, -1) + '='; + break; + } + + return enc; + }; + + _.utf8Encode = function(string) { + string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n'); + + var utftext = '', + start, + end; + var stringl = 0, + n; + + start = end = 0; + stringl = string.length; + + for (n = 0; n < stringl; n++) { + var c1 = string.charCodeAt(n); + var enc = null; + + if (c1 < 128) { + end++; + } else if ((c1 > 127) && (c1 < 2048)) { + enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128); + } else { + enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128); + } + if (enc !== null) { + if (end > start) { + utftext += string.substring(start, end); + } + utftext += enc; + start = end = n + 1; + } + } + + if (end > start) { + utftext += string.substring(start, string.length); + } + + return utftext; + }; + + _.UUID = (function() { + + // Time/ticks information + // 1*new Date() is a cross browser version of Date.now() + var T = function() { + var d = 1 * new Date(), + i = 0; + + // this while loop figures how many browser ticks go by + // before 1*new Date() returns a new number, ie the amount + // of ticks that go by per millisecond + while (d == 1 * new Date()) { + i++; + } + + return d.toString(16) + i.toString(16); + }; + + // Math.Random entropy + var R = function() { + return Math.random().toString(16).replace('.', ''); + }; + + // User agent entropy + // This function takes the user agent string, and then xors + // together each sequence of 8 bytes. This produces a final + // sequence of 8 bytes which it returns as hex. + var UA = function() { + var ua = userAgent, + i, ch, buffer = [], + ret = 0; + + function xor(result, byte_array) { + var j, tmp = 0; + for (j = 0; j < byte_array.length; j++) { + tmp |= (buffer[j] << j * 8); + } + return result ^ tmp; + } + + for (i = 0; i < ua.length; i++) { + ch = ua.charCodeAt(i); + buffer.unshift(ch & 0xFF); + if (buffer.length >= 4) { + ret = xor(ret, buffer); + buffer = []; + } + } + + if (buffer.length > 0) { + ret = xor(ret, buffer); + } + + return ret.toString(16); + }; + + return function() { + var se = (screen.height * screen.width).toString(16); + return (T() + '-' + R() + '-' + UA() + '-' + se + '-' + T()); + }; + })(); + + // _.isBlockedUA() + // This is to block various web spiders from executing our JS and + // sending false tracking data + _.isBlockedUA = function(ua) { + if (/(google web preview|baiduspider|yandexbot|bingbot|googlebot|yahoo! slurp)/i.test(ua)) { + return true; + } + return false; + }; + + /** + * @param {Object=} formdata + * @param {string=} arg_separator + */ + _.HTTPBuildQuery = function(formdata, arg_separator) { + var use_val, use_key, tmp_arr = []; + + if (_.isUndefined(arg_separator)) { + arg_separator = '&'; + } + + _.each(formdata, function(val, key) { + use_val = encodeURIComponent(val.toString()); + use_key = encodeURIComponent(key); + tmp_arr[tmp_arr.length] = use_key + '=' + use_val; + }); + + return tmp_arr.join(arg_separator); + }; + + _.getQueryParam = function(url, param) { + // Expects a raw URL + + param = param.replace(/[\[]/, '\\\[').replace(/[\]]/, '\\\]'); + var regexS = '[\\?&]' + param + '=([^&#]*)', + regex = new RegExp(regexS), + results = regex.exec(url); + if (results === null || (results && typeof(results[1]) !== 'string' && results[1].length)) { + return ''; + } else { + return decodeURIComponent(results[1]).replace(/\+/g, ' '); + } + }; + + _.getHashParam = function(hash, param) { + var matches = hash.match(new RegExp(param + '=([^&]*)')); + return matches ? matches[1] : null; + }; + + // _.cookie + // Methods partially borrowed from quirksmode.org/js/cookies.html + _.cookie = { + get: function(name) { + var nameEQ = name + '='; + var ca = document$1.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) == ' ') { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + return null; + }, + + parse: function(name) { + var cookie; + try { + cookie = _.JSONDecode(_.cookie.get(name)) || {}; + } catch (err) { + // noop + } + return cookie; + }, + + set_seconds: function(name, value, seconds, cross_subdomain, is_secure) { + var cdomain = '', + expires = '', + secure = ''; + + if (cross_subdomain) { + var matches = document$1.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i), + domain = matches ? matches[0] : ''; + + cdomain = ((domain) ? '; domain=.' + domain : ''); + } + + if (seconds) { + var date = new Date(); + date.setTime(date.getTime() + (seconds * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_secure) { + secure = '; secure'; + } + + document$1.cookie = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + }, + + set: function(name, value, days, cross_subdomain, is_secure) { + var cdomain = '', expires = '', secure = ''; + + if (cross_subdomain) { + var matches = document$1.location.hostname.match(/[a-z0-9][a-z0-9\-]+\.[a-z\.]{2,6}$/i), + domain = matches ? matches[0] : ''; + + cdomain = ((domain) ? '; domain=.' + domain : ''); + } + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } + + if (is_secure) { + secure = '; secure'; + } + + var new_cookie_val = name + '=' + encodeURIComponent(value) + expires + '; path=/' + cdomain + secure; + document$1.cookie = new_cookie_val; + return new_cookie_val; + }, + + remove: function(name, cross_subdomain) { + _.cookie.set(name, '', -1, cross_subdomain); + } + }; + + // _.localStorage + _.localStorage = { + error: function(msg) { + console$1.error('localStorage error: ' + msg); + }, + + get: function(name) { + try { + return window.localStorage.getItem(name); + } catch (err) { + _.localStorage.error(err); + } + return null; + }, + + parse: function(name) { + try { + return _.JSONDecode(_.localStorage.get(name)) || {}; + } catch (err) { + // noop + } + return null; + }, + + set: function(name, value) { + try { + window.localStorage.setItem(name, value); + } catch (err) { + _.localStorage.error(err); + } + }, + + remove: function(name) { + try { + window.localStorage.removeItem(name); + } catch (err) { + _.localStorage.error(err); + } + } + }; + + _.register_event = (function() { + // written by Dean Edwards, 2005 + // with input from Tino Zijdel - crisp@xs4all.nl + // with input from Carl Sverre - mail@carlsverre.com + // with input from Mixpanel + // http://dean.edwards.name/weblog/2005/10/add-event/ + // https://gist.github.com/1930440 + + /** + * @param {Object} element + * @param {string} type + * @param {function(...[*])} handler + * @param {boolean=} oldSchool + * @param {boolean=} useCapture + */ + var register_event = function(element, type, handler, oldSchool, useCapture) { + if (!element) { + console$1.error('No valid element provided to register_event'); + return; + } + + if (element.addEventListener && !oldSchool) { + element.addEventListener(type, handler, !!useCapture); + } else { + var ontype = 'on' + type; + var old_handler = element[ontype]; // can be undefined + element[ontype] = makeHandler(element, handler, old_handler); + } + }; + + function makeHandler(element, new_handler, old_handlers) { + var handler = function(event) { + event = event || fixEvent(window.event); + + // this basically happens in firefox whenever another script + // overwrites the onload callback and doesn't pass the event + // object to previously defined callbacks. All the browsers + // that don't define window.event implement addEventListener + // so the dom_loaded handler will still be fired as usual. + if (!event) { + return undefined; + } + + var ret = true; + var old_result, new_result; + + if (_.isFunction(old_handlers)) { + old_result = old_handlers(event); + } + new_result = new_handler.call(element, event); + + if ((false === old_result) || (false === new_result)) { + ret = false; + } + + return ret; + }; + + return handler; + } + + function fixEvent(event) { + if (event) { + event.preventDefault = fixEvent.preventDefault; + event.stopPropagation = fixEvent.stopPropagation; + } + return event; + } + fixEvent.preventDefault = function() { + this.returnValue = false; + }; + fixEvent.stopPropagation = function() { + this.cancelBubble = true; + }; + + return register_event; + })(); + + _.dom_query = (function() { + /* document.getElementsBySelector(selector) + - returns an array of element objects from the current document + matching the CSS selector. Selectors can contain element names, + class names and ids and can be nested. For example: + + elements = document.getElementsBySelector('div#main p a.external') + + Will return an array of all 'a' elements with 'external' in their + class attribute that are contained inside 'p' elements that are + contained inside the 'div' element which has id="main" + + New in version 0.4: Support for CSS2 and CSS3 attribute selectors: + See http://www.w3.org/TR/css3-selectors/#attribute-selectors + + Version 0.4 - Simon Willison, March 25th 2003 + -- Works in Phoenix 0.5, Mozilla 1.3, Opera 7, Internet Explorer 6, Internet Explorer 5 on Windows + -- Opera 7 fails + + Version 0.5 - Carl Sverre, Jan 7th 2013 + -- Now uses jQuery-esque `hasClass` for testing class name + equality. This fixes a bug related to '-' characters being + considered not part of a 'word' in regex. + */ + + function getAllChildren(e) { + // Returns all children of element. Workaround required for IE5/Windows. Ugh. + return e.all ? e.all : e.getElementsByTagName('*'); + } + + var bad_whitespace = /[\t\r\n]/g; + + function hasClass(elem, selector) { + var className = ' ' + selector + ' '; + return ((' ' + elem.className + ' ').replace(bad_whitespace, ' ').indexOf(className) >= 0); + } + + function getElementsBySelector(selector) { + // Attempt to fail gracefully in lesser browsers + if (!document$1.getElementsByTagName) { + return []; + } + // Split selector in to tokens + var tokens = selector.split(' '); + var token, bits, tagName, found, foundCount, i, j, k, elements, currentContextIndex; + var currentContext = [document$1]; + for (i = 0; i < tokens.length; i++) { + token = tokens[i].replace(/^\s+/, '').replace(/\s+$/, ''); + if (token.indexOf('#') > -1) { + // Token is an ID selector + bits = token.split('#'); + tagName = bits[0]; + var id = bits[1]; + var element = document$1.getElementById(id); + if (!element || (tagName && element.nodeName.toLowerCase() != tagName)) { + // element not found or tag with that ID not found, return false + return []; + } + // Set currentContext to contain just this element + currentContext = [element]; + continue; // Skip to next token + } + if (token.indexOf('.') > -1) { + // Token contains a class selector + bits = token.split('.'); + tagName = bits[0]; + var className = bits[1]; + if (!tagName) { + tagName = '*'; + } + // Get elements matching tag, filter them for class selector + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (found[j].className && + _.isString(found[j].className) && // some SVG elements have classNames which are not strings + hasClass(found[j], className) + ) { + currentContext[currentContextIndex++] = found[j]; + } + } + continue; // Skip to next token + } + // Code to deal with attribute selectors + var token_match = token.match(/^(\w*)\[(\w+)([=~\|\^\$\*]?)=?"?([^\]"]*)"?\]$/); + if (token_match) { + tagName = token_match[1]; + var attrName = token_match[2]; + var attrOperator = token_match[3]; + var attrValue = token_match[4]; + if (!tagName) { + tagName = '*'; + } + // Grab all of the tagName elements within current context + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + if (tagName == '*') { + elements = getAllChildren(currentContext[j]); + } else { + elements = currentContext[j].getElementsByTagName(tagName); + } + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = []; + currentContextIndex = 0; + var checkFunction; // This function will be used to filter the elements + switch (attrOperator) { + case '=': // Equality + checkFunction = function(e) { + return (e.getAttribute(attrName) == attrValue); + }; + break; + case '~': // Match one of space seperated words + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('\\b' + attrValue + '\\b'))); + }; + break; + case '|': // Match start with value followed by optional hyphen + checkFunction = function(e) { + return (e.getAttribute(attrName).match(new RegExp('^' + attrValue + '-?'))); + }; + break; + case '^': // Match starts with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) === 0); + }; + break; + case '$': // Match ends with value - fails with "Warning" in Opera 7 + checkFunction = function(e) { + return (e.getAttribute(attrName).lastIndexOf(attrValue) == e.getAttribute(attrName).length - attrValue.length); + }; + break; + case '*': // Match ends with value + checkFunction = function(e) { + return (e.getAttribute(attrName).indexOf(attrValue) > -1); + }; + break; + default: + // Just test for existence of attribute + checkFunction = function(e) { + return e.getAttribute(attrName); + }; + } + currentContext = []; + currentContextIndex = 0; + for (j = 0; j < found.length; j++) { + if (checkFunction(found[j])) { + currentContext[currentContextIndex++] = found[j]; + } + } + // alert('Attribute Selector: '+tagName+' '+attrName+' '+attrOperator+' '+attrValue); + continue; // Skip to next token + } + // If we get here, token is JUST an element (not a class or ID selector) + tagName = token; + found = []; + foundCount = 0; + for (j = 0; j < currentContext.length; j++) { + elements = currentContext[j].getElementsByTagName(tagName); + for (k = 0; k < elements.length; k++) { + found[foundCount++] = elements[k]; + } + } + currentContext = found; + } + return currentContext; + } + + return function(query) { + if (_.isElement(query)) { + return [query]; + } else if (_.isObject(query) && !_.isUndefined(query.length)) { + return query; + } else { + return getElementsBySelector.call(this, query); + } + }; + })(); + + _.info = { + campaignParams: function() { + var campaign_keywords = 'utm_source utm_medium utm_campaign utm_content utm_term'.split(' '), + kw = '', + params = {}; + _.each(campaign_keywords, function(kwkey) { + kw = _.getQueryParam(document$1.URL, kwkey); + if (kw.length) { + params[kwkey] = kw; + } + }); + + return params; + }, + + searchEngine: function(referrer) { + if (referrer.search('https?://(.*)google.([^/?]*)') === 0) { + return 'google'; + } else if (referrer.search('https?://(.*)bing.com') === 0) { + return 'bing'; + } else if (referrer.search('https?://(.*)yahoo.com') === 0) { + return 'yahoo'; + } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) { + return 'duckduckgo'; + } else { + return null; + } + }, + + searchInfo: function(referrer) { + var search = _.info.searchEngine(referrer), + param = (search != 'yahoo') ? 'q' : 'p', + ret = {}; + + if (search !== null) { + ret['$search_engine'] = search; + + var keyword = _.getQueryParam(referrer, param); + if (keyword.length) { + ret['mp_keyword'] = keyword; + } + } + + return ret; + }, + + /** + * This function detects which browser is running this script. + * The order of the checks are important since many user agents + * include key words used in later checks. + */ + browser: function(user_agent, vendor, opera) { + vendor = vendor || ''; // vendor is undefined for at least IE9 + if (opera || _.includes(user_agent, ' OPR/')) { + if (_.includes(user_agent, 'Mini')) { + return 'Opera Mini'; + } + return 'Opera'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (_.includes(user_agent, 'IEMobile') || _.includes(user_agent, 'WPDesktop')) { + return 'Internet Explorer Mobile'; + } else if (_.includes(user_agent, 'Edge')) { + return 'Microsoft Edge'; + } else if (_.includes(user_agent, 'FBIOS')) { + return 'Facebook Mobile'; + } else if (_.includes(user_agent, 'Chrome')) { + return 'Chrome'; + } else if (_.includes(user_agent, 'CriOS')) { + return 'Chrome iOS'; + } else if (_.includes(user_agent, 'UCWEB') || _.includes(user_agent, 'UCBrowser')) { + return 'UC Browser'; + } else if (_.includes(user_agent, 'FxiOS')) { + return 'Firefox iOS'; + } else if (_.includes(vendor, 'Apple')) { + if (_.includes(user_agent, 'Mobile')) { + return 'Mobile Safari'; + } + return 'Safari'; + } else if (_.includes(user_agent, 'Android')) { + return 'Android Mobile'; + } else if (_.includes(user_agent, 'Konqueror')) { + return 'Konqueror'; + } else if (_.includes(user_agent, 'Firefox')) { + return 'Firefox'; + } else if (_.includes(user_agent, 'MSIE') || _.includes(user_agent, 'Trident/')) { + return 'Internet Explorer'; + } else if (_.includes(user_agent, 'Gecko')) { + return 'Mozilla'; + } else { + return ''; + } + }, + + /** + * This function detects which browser version is running this script, + * parsing major and minor version (e.g., 42.1). User agent strings from: + * http://www.useragentstring.com/pages/useragentstring.php + */ + browserVersion: function(userAgent, vendor, opera) { + var browser = _.info.browser(userAgent, vendor, opera); + var versionRegexs = { + 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/, + 'Microsoft Edge': /Edge\/(\d+(\.\d+)?)/, + 'Chrome': /Chrome\/(\d+(\.\d+)?)/, + 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/, + 'UC Browser' : /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/, + 'Safari': /Version\/(\d+(\.\d+)?)/, + 'Mobile Safari': /Version\/(\d+(\.\d+)?)/, + 'Opera': /(Opera|OPR)\/(\d+(\.\d+)?)/, + 'Firefox': /Firefox\/(\d+(\.\d+)?)/, + 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/, + 'Konqueror': /Konqueror:(\d+(\.\d+)?)/, + 'BlackBerry': /BlackBerry (\d+(\.\d+)?)/, + 'Android Mobile': /android\s(\d+(\.\d+)?)/, + 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/, + 'Mozilla': /rv:(\d+(\.\d+)?)/ + }; + var regex = versionRegexs[browser]; + if (regex === undefined) { + return null; + } + var matches = userAgent.match(regex); + if (!matches) { + return null; + } + return parseFloat(matches[matches.length - 2]); + }, + + os: function() { + var a = userAgent; + if (/Windows/i.test(a)) { + if (/Phone/.test(a) || /WPDesktop/.test(a)) { + return 'Windows Phone'; + } + return 'Windows'; + } else if (/(iPhone|iPad|iPod)/.test(a)) { + return 'iOS'; + } else if (/Android/.test(a)) { + return 'Android'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(a)) { + return 'BlackBerry'; + } else if (/Mac/i.test(a)) { + return 'Mac OS X'; + } else if (/Linux/.test(a)) { + return 'Linux'; + } else if (/CrOS/.test(a)) { + return 'Chrome OS'; + } else { + return ''; + } + }, + + device: function(user_agent) { + if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) { + return 'Windows Phone'; + } else if (/iPad/.test(user_agent)) { + return 'iPad'; + } else if (/iPod/.test(user_agent)) { + return 'iPod Touch'; + } else if (/iPhone/.test(user_agent)) { + return 'iPhone'; + } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) { + return 'BlackBerry'; + } else if (/Android/.test(user_agent)) { + return 'Android'; + } else { + return ''; + } + }, + + referringDomain: function(referrer) { + var split = referrer.split('/'); + if (split.length >= 3) { + return split[2]; + } + return ''; + }, + + properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera), + '$referrer': document$1.referrer, + '$referring_domain': _.info.referringDomain(document$1.referrer), + '$device': _.info.device(userAgent) + }), { + '$current_url': win.location.href, + '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera), + '$screen_height': screen.height, + '$screen_width': screen.width, + 'mp_lib': 'web', + '$lib_version': Config.LIB_VERSION + }); + }, + + people_properties: function() { + return _.extend(_.strip_empty_properties({ + '$os': _.info.os(), + '$browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera) + }), { + '$browser_version': _.info.browserVersion(userAgent, navigator$1.vendor, windowOpera) + }); + }, + + pageviewInfo: function(page) { + return _.strip_empty_properties({ + 'mp_page': page, + 'mp_referrer': document$1.referrer, + 'mp_browser': _.info.browser(userAgent, navigator$1.vendor, windowOpera), + 'mp_platform': _.info.os() + }); + } + }; + + // EXPORTS (for closure compiler) + _['toArray'] = _.toArray; + _['isObject'] = _.isObject; + _['JSONEncode'] = _.JSONEncode; + _['JSONDecode'] = _.JSONDecode; + _['isBlockedUA'] = _.isBlockedUA; + _['isEmptyObject'] = _.isEmptyObject; + _['info'] = _.info; + _['info']['device'] = _.info.device; + _['info']['browser'] = _.info.browser; + _['info']['properties'] = _.info.properties; + + /* + * Get the className of an element, accounting for edge cases where element.className is an object + * @param {Element} el - element to get the className of + * @returns {string} the element's class + */ + function getClassName(el) { + switch(typeof el.className) { + case 'string': + return el.className; + case 'object': // handle cases where className might be SVGAnimatedString or some other type + return el.className.baseVal || el.getAttribute('class') || ''; + default: // future proof + return ''; + } + } + + /* + * Get the direct text content of an element, protecting against sensitive data collection. + * Concats textContent of each of the element's text node children; this avoids potential + * collection of sensitive data that could happen if we used element.textContent and the + * element had sensitive child elements, since element.textContent includes child content. + * Scrubs values that look like they could be sensitive (i.e. cc or ssn number). + * @param {Element} el - element to get the text of + * @returns {string} the element's direct text content + */ + function getSafeText(el) { + var elText = ''; + + if (shouldTrackElement(el) && el.childNodes && el.childNodes.length) { + _.each(el.childNodes, function(child) { + if (isTextNode(child) && child.textContent) { + elText += _.trim(child.textContent) + // scrub potentially sensitive values + .split(/(\s+)/).filter(shouldTrackValue).join('') + // normalize whitespace + .replace(/[\r\n]/g, ' ').replace(/[ ]+/g, ' ') + // truncate + .substring(0, 255); + } + }); + } + + return _.trim(elText); + } + + /* + * Check whether an element has nodeType Node.ELEMENT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ + function isElementNode(el) { + return el && el.nodeType === 1; // Node.ELEMENT_NODE - use integer constant for browser portability + } + + /* + * Check whether an element is of a given tag type. + * Due to potential reference discrepancies (such as the webcomponents.js polyfill), + * we want to match tagNames instead of specific references because something like + * element === document.body won't always work because element might not be a native + * element. + * @param {Element} el - element to check + * @param {string} tag - tag name (e.g., "div") + * @returns {boolean} whether el is of the given tag type + */ + function isTag(el, tag) { + return el && el.tagName && el.tagName.toLowerCase() === tag.toLowerCase(); + } + + /* + * Check whether an element has nodeType Node.TEXT_NODE + * @param {Element} el - element to check + * @returns {boolean} whether el is of the correct nodeType + */ + function isTextNode(el) { + return el && el.nodeType === 3; // Node.TEXT_NODE - use integer constant for browser portability + } + + /* + * Check whether a DOM event should be "tracked" or if it may contain sentitive data + * using a variety of heuristics. + * @param {Element} el - element to check + * @param {Event} event - event to check + * @returns {boolean} whether the event should be tracked + */ + function shouldTrackDomEvent(el, event) { + if (!el || isTag(el, 'html') || !isElementNode(el)) { + return false; + } + var tag = el.tagName.toLowerCase(); + switch (tag) { + case 'html': + return false; + case 'form': + return event.type === 'submit'; + case 'input': + if (['button', 'submit'].indexOf(el.getAttribute('type')) === -1) { + return event.type === 'change'; + } else { + return event.type === 'click'; + } + case 'select': + case 'textarea': + return event.type === 'change'; + default: + return event.type === 'click'; + } + } + + /* + * Check whether a DOM element should be "tracked" or if it may contain sentitive data + * using a variety of heuristics. + * @param {Element} el - element to check + * @returns {boolean} whether the element should be tracked + */ + function shouldTrackElement(el) { + for (var curEl = el; curEl.parentNode && !isTag(curEl, 'body'); curEl = curEl.parentNode) { + var classes = getClassName(curEl).split(' '); + if (_.includes(classes, 'mp-sensitive') || _.includes(classes, 'mp-no-track')) { + return false; + } + } + + if (_.includes(getClassName(el).split(' '), 'mp-include')) { + return true; + } + + // don't send data from inputs or similar elements since there will always be + // a risk of clientside javascript placing sensitive data in attributes + if ( + isTag(el, 'input') || + isTag(el, 'select') || + isTag(el, 'textarea') || + el.getAttribute('contenteditable') === 'true' + ) { + return false; + } + + // don't include hidden or password fields + var type = el.type || ''; + if (typeof type === 'string') { // it's possible for el.type to be a DOM element if el is a form with a child input[name="type"] + switch(type.toLowerCase()) { + case 'hidden': + return false; + case 'password': + return false; + } + } + + // filter out data from fields that look like sensitive fields + var name = el.name || el.id || ''; + if (typeof name === 'string') { // it's possible for el.name or el.id to be a DOM element if el is a form with a child input[name="name"] + var sensitiveNameRegex = /^cc|cardnum|ccnum|creditcard|csc|cvc|cvv|exp|pass|pwd|routing|seccode|securitycode|securitynum|socialsec|socsec|ssn/i; + if (sensitiveNameRegex.test(name.replace(/[^a-zA-Z0-9]/g, ''))) { + return false; + } + } + + return true; + } + + /* + * Check whether a string value should be "tracked" or if it may contain sentitive data + * using a variety of heuristics. + * @param {string} value - string value to check + * @returns {boolean} whether the element should be tracked + */ + function shouldTrackValue(value) { + if (value === null || _.isUndefined(value)) { + return false; + } + + if (typeof value === 'string') { + value = _.trim(value); + + // check to see if input value looks like a credit card number + // see: https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9781449327453/ch04s20.html + var ccRegex = /^(?:(4[0-9]{12}(?:[0-9]{3})?)|(5[1-5][0-9]{14})|(6(?:011|5[0-9]{2})[0-9]{12})|(3[47][0-9]{13})|(3(?:0[0-5]|[68][0-9])[0-9]{11})|((?:2131|1800|35[0-9]{3})[0-9]{11}))$/; + if (ccRegex.test((value || '').replace(/[\- ]/g, ''))) { + return false; + } + + // check to see if input value looks like a social security number + var ssnRegex = /(^\d{3}-?\d{2}-?\d{4}$)/; + if (ssnRegex.test(value)) { + return false; + } + } + + return true; + } + + var autotrack = { + _initializedTokens: [], + + _previousElementSibling: function(el) { + if (el.previousElementSibling) { + return el.previousElementSibling; + } else { + do { + el = el.previousSibling; + } while (el && !isElementNode(el)); + return el; + } + }, + + _loadScript: function(scriptUrlToLoad, callback) { + var scriptTag = document.createElement('script'); + scriptTag.type = 'text/javascript'; + scriptTag.src = scriptUrlToLoad; + scriptTag.onload = callback; + + var scripts = document.getElementsByTagName('script'); + if (scripts.length > 0) { + scripts[0].parentNode.insertBefore(scriptTag, scripts[0]); + } else { + document.body.appendChild(scriptTag); + } + }, + + _getPropertiesFromElement: function(elem) { + var props = { + 'classes': getClassName(elem).split(' '), + 'tag_name': elem.tagName.toLowerCase() + }; + + if (shouldTrackElement(elem)) { + _.each(elem.attributes, function(attr) { + if (shouldTrackValue(attr.value)) { + props['attr__' + attr.name] = attr.value; + } + }); + } + + var nthChild = 1; + var nthOfType = 1; + var currentElem = elem; + while (currentElem = this._previousElementSibling(currentElem)) { // eslint-disable-line no-cond-assign + nthChild++; + if (currentElem.tagName === elem.tagName) { + nthOfType++; + } + } + props['nth_child'] = nthChild; + props['nth_of_type'] = nthOfType; + + return props; + }, + + _getDefaultProperties: function(eventType) { + return { + '$event_type': eventType, + '$ce_version': 1, + '$host': window.location.host, + '$pathname': window.location.pathname + }; + }, + + _extractCustomPropertyValue: function(customProperty) { + var propValues = []; + _.each(document.querySelectorAll(customProperty['css_selector']), function(matchedElem) { + var value; + + if (['input', 'select'].indexOf(matchedElem.tagName.toLowerCase()) > -1) { + value = matchedElem['value']; + } else if (matchedElem['textContent']) { + value = matchedElem['textContent']; + } + + if (shouldTrackValue(value)) { + propValues.push(value); + } + }); + return propValues.join(', '); + }, + + _getCustomProperties: function(targetElementList) { + var props = {}; + _.each(this._customProperties, function(customProperty) { + _.each(customProperty['event_selectors'], function(eventSelector) { + var eventElements = document.querySelectorAll(eventSelector); + _.each(eventElements, function(eventElement) { + if (_.includes(targetElementList, eventElement) && shouldTrackElement(eventElement)) { + props[customProperty['name']] = this._extractCustomPropertyValue(customProperty); + } + }, this); + }, this); + }, this); + return props; + }, + + _getEventTarget: function(e) { + // https://developer.mozilla.org/en-US/docs/Web/API/Event/target#Compatibility_notes + if (typeof e.target === 'undefined') { + return e.srcElement; + } else { + return e.target; + } + }, + + _trackEvent: function(e, instance) { + /*** Don't mess with this code without running IE8 tests on it ***/ + var target = this._getEventTarget(e); + if (isTextNode(target)) { // defeat Safari bug (see: http://www.quirksmode.org/js/events_properties.html) + target = target.parentNode; + } + + if (shouldTrackDomEvent(target, e)) { + var targetElementList = [target]; + var curEl = target; + while (curEl.parentNode && !isTag(curEl, 'body')) { + targetElementList.push(curEl.parentNode); + curEl = curEl.parentNode; + } + + var elementsJson = []; + var href, explicitNoTrack = false; + _.each(targetElementList, function(el) { + var shouldTrackEl = shouldTrackElement(el); + + // if the element or a parent element is an anchor tag + // include the href as a property + if (el.tagName.toLowerCase() === 'a') { + href = el.getAttribute('href'); + href = shouldTrackEl && shouldTrackValue(href) && href; + } + + // allow users to programatically prevent tracking of elements by adding class 'mp-no-track' + var classes = getClassName(el).split(' '); + if (_.includes(classes, 'mp-no-track')) { + explicitNoTrack = true; + } + + elementsJson.push(this._getPropertiesFromElement(el)); + }, this); + + if (explicitNoTrack) { + return false; + } + + // only populate text content from target element (not parents) + // to prevent text within a sensitive element from being collected + // as part of a parent's el.textContent + var elementText; + var safeElementText = getSafeText(target); + if (safeElementText && safeElementText.length) { + elementText = safeElementText; + } + + var props = _.extend( + this._getDefaultProperties(e.type), + { + '$elements': elementsJson, + '$el_attr__href': href, + '$el_text': elementText + }, + this._getCustomProperties(targetElementList) + ); + + instance.track('$web_event', props); + return true; + } + }, + + // only reason is to stub for unit tests + // since you can't override window.location props + _navigate: function(href) { + window.location.href = href; + }, + + _addDomEventHandlers: function(instance) { + var handler = _.bind(function(e) { + e = e || window.event; + this._trackEvent(e, instance); + }, this); + _.register_event(document, 'submit', handler, false, true); + _.register_event(document, 'change', handler, false, true); + _.register_event(document, 'click', handler, false, true); + }, + + _customProperties: {}, + init: function(instance) { + if (!(document && document.body)) { + console.log('document not ready yet, trying again in 500 milliseconds...'); + var that = this; + setTimeout(function() { that.init(instance); }, 500); + return; + } + + var token = instance.get_config('token'); + if (this._initializedTokens.indexOf(token) > -1) { + console.log('autotrack already initialized for token "' + token + '"'); + return; + } + this._initializedTokens.push(token); + + if (!this._maybeLoadEditor(instance)) { // don't autotrack actions when the editor is enabled + var parseDecideResponse = _.bind(function(response) { + if (response && response['config'] && response['config']['enable_collect_everything'] === true) { + + if (response['custom_properties']) { + this._customProperties = response['custom_properties']; + } + + instance.track('$web_event', _.extend({ + '$title': document.title + }, this._getDefaultProperties('pageview'))); + + this._addDomEventHandlers(instance); + + } else { + instance['__autotrack_enabled'] = false; + } + }, this); + + instance._send_request( + instance.get_config('api_host') + '/decide/', { + 'verbose': true, + 'version': '1', + 'lib': 'web', + 'token': token + }, + instance._prepare_callback(parseDecideResponse) + ); + } + }, + + _editorParamsFromHash: function(instance, hash) { + var editorParams; + try { + var state = _.getHashParam(hash, 'state'); + state = JSON.parse(decodeURIComponent(state)); + var expiresInSeconds = _.getHashParam(hash, 'expires_in'); + editorParams = { + 'accessToken': _.getHashParam(hash, 'access_token'), + 'accessTokenExpiresAt': (new Date()).getTime() + (Number(expiresInSeconds) * 1000), + 'bookmarkletMode': !!state['bookmarkletMode'], + 'projectId': state['projectId'], + 'projectOwnerId': state['projectOwnerId'], + 'projectToken': state['token'], + 'readOnly': state['readOnly'], + 'userFlags': state['userFlags'], + 'userId': state['userId'] + }; + window.sessionStorage.setItem('editorParams', JSON.stringify(editorParams)); + + if (state['desiredHash']) { + window.location.hash = state['desiredHash']; + } else if (window.history) { + history.replaceState('', document.title, window.location.pathname + window.location.search); // completely remove hash + } else { + window.location.hash = ''; // clear hash (but leaves # unfortunately) + } + } catch (e) { + console.error('Unable to parse data from hash', e); + } + return editorParams; + }, + + /** + * To load the visual editor, we need an access token and other state. That state comes from one of three places: + * 1. In the URL hash params if the customer is using an old snippet + * 2. From session storage under the key `_mpcehash` if the snippet already parsed the hash + * 3. From session storage under the key `editorParams` if the editor was initialized on a previous page + */ + _maybeLoadEditor: function(instance) { + try { + var parseFromUrl = false; + if (_.getHashParam(window.location.hash, 'state')) { + var state = _.getHashParam(window.location.hash, 'state'); + state = JSON.parse(decodeURIComponent(state)); + parseFromUrl = state['action'] === 'mpeditor'; + } + var parseFromStorage = !!window.sessionStorage.getItem('_mpcehash'); + var editorParams; + + if (parseFromUrl) { // happens if they are initializing the editor using an old snippet + editorParams = this._editorParamsFromHash(instance, window.location.hash); + } else if (parseFromStorage) { // happens if they are initialized the editor and using the new snippet + editorParams = this._editorParamsFromHash(instance, window.sessionStorage.getItem('_mpcehash')); + window.sessionStorage.removeItem('_mpcehash'); + } else { // get credentials from sessionStorage from a previous initialzation + editorParams = JSON.parse(window.sessionStorage.getItem('editorParams') || '{}'); + } + + if (editorParams['projectToken'] && instance.get_config('token') === editorParams['projectToken']) { + this._loadEditor(instance, editorParams); + return true; + } else { + return false; + } + } catch (e) { + return false; + } + }, + + _loadEditor: function(instance, editorParams) { + if (!window['_mpEditorLoaded']) { // only load the codeless event editor once, even if there are multiple instances of MixpanelLib + window['_mpEditorLoaded'] = true; + var editorUrl = instance.get_config('app_host') + + '/js-bundle/reports/collect-everything/editor.js?_ts=' + + (new Date()).getTime(); + this._loadScript(editorUrl, function() { + window['mp_load_editor'](editorParams); + }); + return true; + } + return false; + }, + + // this is a mechanism to ramp up CE with no server-side interaction. + // when CE is active, every page load results in a decide request. we + // need to gently ramp this up so we don't overload decide. this decides + // deterministically if CE is enabled for this project by modding the char + // value of the project token. + enabledForProject: function(token, numBuckets, numEnabledBuckets) { + numBuckets = !_.isUndefined(numBuckets) ? numBuckets : 10; + numEnabledBuckets = !_.isUndefined(numEnabledBuckets) ? numEnabledBuckets : 10; + var charCodeSum = 0; + for (var i = 0; i < token.length; i++) { + charCodeSum += token.charCodeAt(i); + } + return (charCodeSum % numBuckets) < numEnabledBuckets; + }, + + isBrowserSupported: function() { + return _.isFunction(document.querySelectorAll); + } + }; + + _.bind_instance_methods(autotrack); + _.safewrap_instance_methods(autotrack); + + /* + * Mixpanel JS Library + * + * Copyright 2012, Mixpanel, Inc. All Rights Reserved + * http://mixpanel.com/ + * + * Includes portions of Underscore.js + * http://documentcloud.github.com/underscore/ + * (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. + * Released under the MIT License. + */ + + // ==ClosureCompiler== + // @compilation_level ADVANCED_OPTIMIZATIONS + // @output_file_name mixpanel-2.8.min.js + // ==/ClosureCompiler== + + /* + SIMPLE STYLE GUIDE: + + this.x === public function + this._x === internal - only use within this file + this.__x === private - only use within the class + + Globals should be all caps + */ + + var init_type; // MODULE or SNIPPET loader + var mixpanel_master; // main mixpanel instance / object + var INIT_MODULE = 0; + var INIT_SNIPPET = 1; + + /* + * Constants + */ + /** @const */ var PRIMARY_INSTANCE_NAME = 'mixpanel'; + /** @const */ var SET_QUEUE_KEY = '__mps'; + /** @const */ var SET_ONCE_QUEUE_KEY = '__mpso'; + /** @const */ var UNSET_QUEUE_KEY = '__mpus'; + /** @const */ var ADD_QUEUE_KEY = '__mpa'; + /** @const */ var APPEND_QUEUE_KEY = '__mpap'; + /** @const */ var UNION_QUEUE_KEY = '__mpu'; + /** @const */ var SET_ACTION = '$set'; + /** @const */ var SET_ONCE_ACTION = '$set_once'; + /** @const */ var UNSET_ACTION = '$unset'; + /** @const */ var ADD_ACTION = '$add'; + /** @const */ var APPEND_ACTION = '$append'; + /** @const */ var UNION_ACTION = '$union'; + // This key is deprecated, but we want to check for it to see whether aliasing is allowed. + /** @const */ var PEOPLE_DISTINCT_ID_KEY = '$people_distinct_id'; + /** @const */ var ALIAS_ID_KEY = '__alias'; + /** @const */ var CAMPAIGN_IDS_KEY = '__cmpns'; + /** @const */ var EVENT_TIMERS_KEY = '__timers'; + /** @const */ var RESERVED_PROPERTIES = [ + SET_QUEUE_KEY, + SET_ONCE_QUEUE_KEY, + UNSET_QUEUE_KEY, + ADD_QUEUE_KEY, + APPEND_QUEUE_KEY, + UNION_QUEUE_KEY, + PEOPLE_DISTINCT_ID_KEY, + ALIAS_ID_KEY, + CAMPAIGN_IDS_KEY, + EVENT_TIMERS_KEY + ]; + + /* + * Dynamic... constants? Is that an oxymoron? + */ + // http://hacks.mozilla.org/2009/07/cross-site-xmlhttprequest-with-cors/ + // https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#withCredentials + var USE_XHR = (win.XMLHttpRequest && 'withCredentials' in new XMLHttpRequest()); + + // IE<10 does not support cross-origin XHR's but script tags + // with defer won't block window.onload; ENQUEUE_REQUESTS + // should only be true for Opera<12 + var ENQUEUE_REQUESTS = !USE_XHR && (userAgent.indexOf('MSIE') === -1) && (userAgent.indexOf('Mozilla') === -1); + + /* + * Module-level globals + */ + var DEFAULT_CONFIG = { + 'api_host': 'https://api.mixpanel.com', + 'app_host': 'https://mixpanel.com', + 'autotrack': true, + 'cdn': 'https://cdn.mxpnl.com', + 'cross_subdomain_cookie': true, + 'persistence': 'cookie', + 'persistence_name': '', + 'cookie_name': '', + 'loaded': function() {}, + 'store_google': true, + 'save_referrer': true, + 'test': false, + 'verbose': false, + 'img': false, + 'track_pageview': true, + 'debug': false, + 'track_links_timeout': 300, + 'cookie_expiration': 365, + 'upgrade': false, + 'disable_persistence': false, + 'disable_cookie': false, + 'secure_cookie': false, + 'ip': true, + 'property_blacklist': [] + }; + + var DOM_LOADED = false; + + /** + * DomTracker Object + * @constructor + */ + var DomTracker = function() {}; + + // interface + DomTracker.prototype.create_properties = function() {}; + DomTracker.prototype.event_handler = function() {}; + DomTracker.prototype.after_track_handler = function() {}; + + DomTracker.prototype.init = function(mixpanel_instance) { + this.mp = mixpanel_instance; + return this; + }; + + /** + * @param {Object|string} query + * @param {string} event_name + * @param {Object=} properties + * @param {function(...[*])=} user_callback + */ + DomTracker.prototype.track = function(query, event_name, properties, user_callback) { + var that = this; + var elements = _.dom_query(query); + + if (elements.length === 0) { + console$1.error('The DOM query (' + query + ') returned 0 elements'); + return; + } + + _.each(elements, function(element) { + _.register_event(element, this.override_event, function(e) { + var options = {}; + var props = that.create_properties(properties, this); + var timeout = that.mp.get_config('track_links_timeout'); + + that.event_handler(e, this, options); + + // in case the mixpanel servers don't get back to us in time + win.setTimeout(that.track_callback(user_callback, props, options, true), timeout); + + // fire the tracking event + that.mp.track(event_name, props, that.track_callback(user_callback, props, options)); + }); + }, this); + + return true; + }; + + /** + * @param {function(...[*])} user_callback + * @param {Object} props + * @param {boolean=} timeout_occured + */ + DomTracker.prototype.track_callback = function(user_callback, props, options, timeout_occured) { + timeout_occured = timeout_occured || false; + var that = this; + + return function() { + // options is referenced from both callbacks, so we can have + // a 'lock' of sorts to ensure only one fires + if (options.callback_fired) { return; } + options.callback_fired = true; + + if (user_callback && user_callback(timeout_occured, props) === false) { + // user can prevent the default functionality by + // returning false from their callback + return; + } + + that.after_track_handler(props, options, timeout_occured); + }; + }; + + DomTracker.prototype.create_properties = function(properties, element) { + var props; + + if (typeof(properties) === 'function') { + props = properties(element); + } else { + props = _.extend({}, properties); + } + + return props; + }; + + /** + * LinkTracker Object + * @constructor + * @extends DomTracker + */ + var LinkTracker = function() { + this.override_event = 'click'; + }; + _.inherit(LinkTracker, DomTracker); + + LinkTracker.prototype.create_properties = function(properties, element) { + var props = LinkTracker.superclass.create_properties.apply(this, arguments); + + if (element.href) { props['url'] = element.href; } + + return props; + }; + + LinkTracker.prototype.event_handler = function(evt, element, options) { + options.new_tab = ( + evt.which === 2 || + evt.metaKey || + evt.ctrlKey || + element.target === '_blank' + ); + options.href = element.href; + + if (!options.new_tab) { + evt.preventDefault(); + } + }; + + LinkTracker.prototype.after_track_handler = function(props, options) { + if (options.new_tab) { return; } + + setTimeout(function() { + win.location = options.href; + }, 0); + }; + + /** + * FormTracker Object + * @constructor + * @extends DomTracker + */ + var FormTracker = function() { + this.override_event = 'submit'; + }; + _.inherit(FormTracker, DomTracker); + + FormTracker.prototype.event_handler = function(evt, element, options) { + options.element = element; + evt.preventDefault(); + }; + + FormTracker.prototype.after_track_handler = function(props, options) { + setTimeout(function() { + options.element.submit(); + }, 0); + }; + + /** + * Mixpanel Persistence Object + * @constructor + */ + var MixpanelPersistence = function(config) { + this['props'] = {}; + this.campaign_params_saved = false; + + if (config['persistence_name']) { + this.name = 'mp_' + config['persistence_name']; + } else { + this.name = 'mp_' + config['token'] + '_mixpanel'; + } + + var storage_type = config['persistence']; + if (storage_type !== 'cookie' && storage_type !== 'localStorage') { + console$1.critical('Unknown persistence type ' + storage_type + '; falling back to cookie'); + storage_type = config['persistence'] = 'cookie'; + } + + var localStorage_supported = function() { + var supported = true; + try { + var key = '__mplssupport__', + val = 'xyz'; + _.localStorage.set(key, val); + if (_.localStorage.get(key) !== val) { + supported = false; + } + _.localStorage.remove(key); + } catch (err) { + supported = false; + } + if (!supported) { + console$1.error('localStorage unsupported; falling back to cookie store'); + } + return supported; + }; + if (storage_type === 'localStorage' && localStorage_supported()) { + this.storage = _.localStorage; + } else { + this.storage = _.cookie; + } + + this.load(); + this.update_config(config); + this.upgrade(config); + this.save(); + }; + + MixpanelPersistence.prototype.properties = function() { + var p = {}; + // Filter out reserved properties + _.each(this['props'], function(v, k) { + if (!_.include(RESERVED_PROPERTIES, k)) { + p[k] = v; + } + }); + return p; + }; + + MixpanelPersistence.prototype.load = function() { + if (this.disabled) { return; } + + var entry = this.storage.parse(this.name); + + if (entry) { + this['props'] = _.extend({}, entry); + } + }; + + MixpanelPersistence.prototype.upgrade = function(config) { + var upgrade_from_old_lib = config['upgrade'], + old_cookie_name, + old_cookie; + + if (upgrade_from_old_lib) { + old_cookie_name = 'mp_super_properties'; + // Case where they had a custom cookie name before. + if (typeof(upgrade_from_old_lib) === 'string') { + old_cookie_name = upgrade_from_old_lib; + } + + old_cookie = this.storage.parse(old_cookie_name); + + // remove the cookie + this.storage.remove(old_cookie_name); + this.storage.remove(old_cookie_name, true); + + if (old_cookie) { + this['props'] = _.extend( + this['props'], + old_cookie['all'], + old_cookie['events'] + ); + } + } + + if (!config['cookie_name'] && config['name'] !== 'mixpanel') { + // special case to handle people with cookies of the form + // mp_TOKEN_INSTANCENAME from the first release of this library + old_cookie_name = 'mp_' + config['token'] + '_' + config['name']; + old_cookie = this.storage.parse(old_cookie_name); + + if (old_cookie) { + this.storage.remove(old_cookie_name); + this.storage.remove(old_cookie_name, true); + + // Save the prop values that were in the cookie from before - + // this should only happen once as we delete the old one. + this.register_once(old_cookie); + } + } + + if (this.storage === _.localStorage) { + old_cookie = _.cookie.parse(this.name); + + _.cookie.remove(this.name); + _.cookie.remove(this.name, true); + + if (old_cookie) { + this.register_once(old_cookie); + } + } + }; + + MixpanelPersistence.prototype.save = function() { + if (this.disabled) { return; } + this._expire_notification_campaigns(); + this.storage.set( + this.name, + _.JSONEncode(this['props']), + this.expire_days, + this.cross_subdomain, + this.secure + ); + }; + + MixpanelPersistence.prototype.remove = function() { + // remove both domain and subdomain cookies + this.storage.remove(this.name, false); + this.storage.remove(this.name, true); + }; + + // removes the storage entry and deletes all loaded data + // forced name for tests + MixpanelPersistence.prototype.clear = function() { + this.remove(); + this['props'] = {}; + }; + + /** + * @param {Object} props + * @param {*=} default_value + * @param {number=} days + */ + MixpanelPersistence.prototype.register_once = function(props, default_value, days) { + if (_.isObject(props)) { + if (typeof(default_value) === 'undefined') { default_value = 'None'; } + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + _.each(props, function(val, prop) { + if (!this['props'].hasOwnProperty(prop) || this['props'][prop] === default_value) { + this['props'][prop] = val; + } + }, this); + + this.save(); + + return true; + } + return false; + }; + + /** + * @param {Object} props + * @param {number=} days + */ + MixpanelPersistence.prototype.register = function(props, days) { + if (_.isObject(props)) { + this.expire_days = (typeof(days) === 'undefined') ? this.default_expiry : days; + + _.extend(this['props'], props); + + this.save(); + + return true; + } + return false; + }; + + MixpanelPersistence.prototype.unregister = function(prop) { + if (prop in this['props']) { + delete this['props'][prop]; + this.save(); + } + }; + + MixpanelPersistence.prototype._expire_notification_campaigns = _.safewrap(function() { + var campaigns_shown = this['props'][CAMPAIGN_IDS_KEY], + EXPIRY_TIME = Config.DEBUG ? 60 * 1000 : 60 * 60 * 1000; // 1 minute (Config.DEBUG) / 1 hour (PDXN) + if (!campaigns_shown) { + return; + } + for (var campaign_id in campaigns_shown) { + if (1 * new Date() - campaigns_shown[campaign_id] > EXPIRY_TIME) { + delete campaigns_shown[campaign_id]; + } + } + if (_.isEmptyObject(campaigns_shown)) { + delete this['props'][CAMPAIGN_IDS_KEY]; + } + }); + + MixpanelPersistence.prototype.update_campaign_params = function() { + if (!this.campaign_params_saved) { + this.register_once(_.info.campaignParams()); + this.campaign_params_saved = true; + } + }; + + MixpanelPersistence.prototype.update_search_keyword = function(referrer) { + this.register(_.info.searchInfo(referrer)); + }; + + // EXPORTED METHOD, we test this directly. + MixpanelPersistence.prototype.update_referrer_info = function(referrer) { + // If referrer doesn't exist, we want to note the fact that it was type-in traffic. + this.register_once({ + '$initial_referrer': referrer || '$direct', + '$initial_referring_domain': _.info.referringDomain(referrer) || '$direct' + }, ''); + }; + + MixpanelPersistence.prototype.get_referrer_info = function() { + return _.strip_empty_properties({ + '$initial_referrer': this['props']['$initial_referrer'], + '$initial_referring_domain': this['props']['$initial_referring_domain'] + }); + }; + + // safely fills the passed in object with stored properties, + // does not override any properties defined in both + // returns the passed in object + MixpanelPersistence.prototype.safe_merge = function(props) { + _.each(this['props'], function(val, prop) { + if (!(prop in props)) { + props[prop] = val; + } + }); + + return props; + }; + + MixpanelPersistence.prototype.update_config = function(config) { + this.default_expiry = this.expire_days = config['cookie_expiration']; + this.set_disabled(config['disable_persistence']); + this.set_cross_subdomain(config['cross_subdomain_cookie']); + this.set_secure(config['secure_cookie']); + }; + + MixpanelPersistence.prototype.set_disabled = function(disabled) { + this.disabled = disabled; + if (this.disabled) { + this.remove(); + } + }; + + MixpanelPersistence.prototype.set_cross_subdomain = function(cross_subdomain) { + if (cross_subdomain !== this.cross_subdomain) { + this.cross_subdomain = cross_subdomain; + this.remove(); + this.save(); + } + }; + + MixpanelPersistence.prototype.get_cross_subdomain = function() { + return this.cross_subdomain; + }; + + MixpanelPersistence.prototype.set_secure = function(secure) { + if (secure !== this.secure) { + this.secure = secure ? true : false; + this.remove(); + this.save(); + } + }; + + MixpanelPersistence.prototype._add_to_people_queue = function(queue, data) { + var q_key = this._get_queue_key(queue), + q_data = data[queue], + set_q = this._get_or_create_queue(SET_ACTION), + set_once_q = this._get_or_create_queue(SET_ONCE_ACTION), + unset_q = this._get_or_create_queue(UNSET_ACTION), + add_q = this._get_or_create_queue(ADD_ACTION), + union_q = this._get_or_create_queue(UNION_ACTION), + append_q = this._get_or_create_queue(APPEND_ACTION, []); + + if (q_key === SET_QUEUE_KEY) { + // Update the set queue - we can override any existing values + _.extend(set_q, q_data); + // if there was a pending increment, override it + // with the set. + this._pop_from_people_queue(ADD_ACTION, q_data); + // if there was a pending union, override it + // with the set. + this._pop_from_people_queue(UNION_ACTION, q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === SET_ONCE_QUEUE_KEY) { + // only queue the data if there is not already a set_once call for it. + _.each(q_data, function(v, k) { + if (!(k in set_once_q)) { + set_once_q[k] = v; + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNSET_QUEUE_KEY) { + _.each(q_data, function(prop) { + + // undo previously-queued actions on this key + _.each([set_q, set_once_q, add_q, union_q], function(enqueued_obj) { + if (prop in enqueued_obj) { + delete enqueued_obj[prop]; + } + }); + _.each(append_q, function(append_obj) { + if (prop in append_obj) { + delete append_obj[prop]; + } + }); + + unset_q[prop] = true; + + }); + } else if (q_key === ADD_QUEUE_KEY) { + _.each(q_data, function(v, k) { + // If it exists in the set queue, increment + // the value + if (k in set_q) { + set_q[k] += v; + } else { + // If it doesn't exist, update the add + // queue + if (!(k in add_q)) { + add_q[k] = 0; + } + add_q[k] += v; + } + }, this); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === UNION_QUEUE_KEY) { + _.each(q_data, function(v, k) { + if (_.isArray(v)) { + if (!(k in union_q)) { + union_q[k] = []; + } + // We may send duplicates, the server will dedup them. + union_q[k] = union_q[k].concat(v); + } + }); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } else if (q_key === APPEND_QUEUE_KEY) { + append_q.push(q_data); + this._pop_from_people_queue(UNSET_ACTION, q_data); + } + + console$1.log('MIXPANEL PEOPLE REQUEST (QUEUED, PENDING IDENTIFY):'); + console$1.log(data); + + this.save(); + }; + + MixpanelPersistence.prototype._pop_from_people_queue = function(queue, data) { + var q = this._get_queue(queue); + if (!_.isUndefined(q)) { + _.each(data, function(v, k) { + delete q[k]; + }, this); + + this.save(); + } + }; + + MixpanelPersistence.prototype._get_queue_key = function(queue) { + if (queue === SET_ACTION) { + return SET_QUEUE_KEY; + } else if (queue === SET_ONCE_ACTION) { + return SET_ONCE_QUEUE_KEY; + } else if (queue === UNSET_ACTION) { + return UNSET_QUEUE_KEY; + } else if (queue === ADD_ACTION) { + return ADD_QUEUE_KEY; + } else if (queue === APPEND_ACTION) { + return APPEND_QUEUE_KEY; + } else if (queue === UNION_ACTION) { + return UNION_QUEUE_KEY; + } else { + console$1.error('Invalid queue:', queue); + } + }; + + MixpanelPersistence.prototype._get_queue = function(queue) { + return this['props'][this._get_queue_key(queue)]; + }; + MixpanelPersistence.prototype._get_or_create_queue = function(queue, default_val) { + var key = this._get_queue_key(queue); + default_val = _.isUndefined(default_val) ? {} : default_val; + + return this['props'][key] || (this['props'][key] = default_val); + }; + + MixpanelPersistence.prototype.set_event_timer = function(event_name, timestamp) { + var timers = this['props'][EVENT_TIMERS_KEY] || {}; + timers[event_name] = timestamp; + this['props'][EVENT_TIMERS_KEY] = timers; + this.save(); + }; + + MixpanelPersistence.prototype.remove_event_timer = function(event_name) { + var timers = this['props'][EVENT_TIMERS_KEY] || {}; + var timestamp = timers[event_name]; + if (!_.isUndefined(timestamp)) { + delete this['props'][EVENT_TIMERS_KEY][event_name]; + this.save(); + } + return timestamp; + }; + + /** + * Mixpanel Library Object + * @constructor + */ + var MixpanelLib = function() {}; + + /** + * Mixpanel People Object + * @constructor + */ + var MixpanelPeople = function() {}; + + var MPNotif; + + /** + * create_mplib(token:string, config:object, name:string) + * + * This function is used by the init method of MixpanelLib objects + * as well as the main initializer at the end of the JSLib (that + * initializes document.mixpanel as well as any additional instances + * declared before this file has loaded). + */ + var create_mplib = function(token, config, name) { + var instance, + target = (name === PRIMARY_INSTANCE_NAME) ? mixpanel_master : mixpanel_master[name]; + + if (target && init_type === INIT_MODULE) { + instance = target; + } else { + if (target && !_.isArray(target)) { + console$1.error('You have already initialized ' + name); + return; + } + instance = new MixpanelLib(); + } + + instance._init(token, config, name); + + instance['people'] = new MixpanelPeople(); + instance['people']._init(instance); + + // if any instance on the page has debug = true, we set the + // global debug to be true + Config.DEBUG = Config.DEBUG || instance.get_config('debug'); + + instance['__autotrack_enabled'] = instance.get_config('autotrack'); + if (instance.get_config('autotrack')) { + var num_buckets = 100; + var num_enabled_buckets = 100; + if (!autotrack.enabledForProject(instance.get_config('token'), num_buckets, num_enabled_buckets)) { + instance['__autotrack_enabled'] = false; + console$1.log('Not in active bucket: disabling Automatic Event Collection.'); + } else if (!autotrack.isBrowserSupported()) { + instance['__autotrack_enabled'] = false; + console$1.log('Disabling Automatic Event Collection because this browser is not supported'); + } else { + autotrack.init(instance); + } + + try { + add_dom_event_counting_handlers(instance); + } catch (e) { + console$1.error(e); + } + } + + // if target is not defined, we called init after the lib already + // loaded, so there won't be an array of things to execute + if (!_.isUndefined(target) && _.isArray(target)) { + // Crunch through the people queue first - we queue this data up & + // flush on identify, so it's better to do all these operations first + instance._execute_array.call(instance['people'], target['people']); + instance._execute_array(target); + } + + return instance; + }; + + // Initialization methods + + /** + * This function initializes a new instance of the Mixpanel tracking object. + * All new instances are added to the main mixpanel object as sub properties (such as + * mixpanel.library_name) and also returned by this function. To define a + * second instance on the page, you would call: + * + * mixpanel.init('new token', { your: 'config' }, 'library_name'); + * + * and use it like so: + * + * mixpanel.library_name.track(...); + * + * @param {String} token Your Mixpanel API token + * @param {Object} [config] A dictionary of config options to override. See a list of default config options. + * @param {String} [name] The name for the new mixpanel instance that you want created + */ + MixpanelLib.prototype.init = function (token, config, name) { + if (_.isUndefined(name)) { + console$1.error('You must name your new library: init(token, config, name)'); + return; + } + if (name === PRIMARY_INSTANCE_NAME) { + console$1.error('You must initialize the main mixpanel object right after you include the Mixpanel js snippet'); + return; + } + + var instance = create_mplib(token, config, name); + mixpanel_master[name] = instance; + instance._loaded(); + + return instance; + }; + + // mixpanel._init(token:string, config:object, name:string) + // + // This function sets up the current instance of the mixpanel + // library. The difference between this method and the init(...) + // method is this one initializes the actual instance, whereas the + // init(...) method sets up a new library and calls _init on it. + // + MixpanelLib.prototype._init = function(token, config, name) { + this['__loaded'] = true; + this['config'] = {}; + + this.set_config(_.extend({}, DEFAULT_CONFIG, config, { + 'name': name, + 'token': token, + 'callback_fn': ((name === PRIMARY_INSTANCE_NAME) ? name : PRIMARY_INSTANCE_NAME + '.' + name) + '._jsc' + })); + + this['_jsc'] = function() {}; + + this.__dom_loaded_queue = []; + this.__request_queue = []; + this.__disabled_events = []; + this._flags = { + 'disable_all_events': false, + 'identify_called': false + }; + + this['persistence'] = this['cookie'] = new MixpanelPersistence(this['config']); + this.register_once({'distinct_id': _.UUID()}, ''); + }; + + // Private methods + + MixpanelLib.prototype._loaded = function() { + this.get_config('loaded')(this); + + // this happens after so a user can call identify/name_tag in + // the loaded callback + if (this.get_config('track_pageview')) { + this.track_pageview(); + } + }; + + MixpanelLib.prototype._dom_loaded = function() { + _.each(this.__dom_loaded_queue, function(item) { + this._track_dom.apply(this, item); + }, this); + _.each(this.__request_queue, function(item) { + this._send_request.apply(this, item); + }, this); + delete this.__dom_loaded_queue; + delete this.__request_queue; + }; + + MixpanelLib.prototype._track_dom = function(DomClass, args) { + if (this.get_config('img')) { + console$1.error('You can\'t use DOM tracking functions with img = true.'); + return false; + } + + if (!DOM_LOADED) { + this.__dom_loaded_queue.push([DomClass, args]); + return false; + } + + var dt = new DomClass().init(this); + return dt.track.apply(dt, args); + }; + + /** + * _prepare_callback() should be called by callers of _send_request for use + * as the callback argument. + * + * If there is no callback, this returns null. + * If we are going to make XHR/XDR requests, this returns a function. + * If we are going to use script tags, this returns a string to use as the + * callback GET param. + */ + MixpanelLib.prototype._prepare_callback = function(callback, data) { + if (_.isUndefined(callback)) { + return null; + } + + if (USE_XHR) { + var callback_function = function(response) { + callback(response, data); + }; + return callback_function; + } else { + // if the user gives us a callback, we store as a random + // property on this instances jsc function and update our + // callback string to reflect that. + var jsc = this['_jsc']; + var randomized_cb = '' + Math.floor(Math.random() * 100000000); + var callback_string = this.get_config('callback_fn') + '[' + randomized_cb + ']'; + jsc[randomized_cb] = function(response) { + delete jsc[randomized_cb]; + callback(response, data); + }; + return callback_string; + } + }; + + MixpanelLib.prototype._send_request = function(url, data, callback) { + if (ENQUEUE_REQUESTS) { + this.__request_queue.push(arguments); + return; + } + + // needed to correctly format responses + var verbose_mode = this.get_config('verbose'); + if (data['verbose']) { verbose_mode = true; } + + if (this.get_config('test')) { data['test'] = 1; } + if (verbose_mode) { data['verbose'] = 1; } + if (this.get_config('img')) { data['img'] = 1; } + if (!USE_XHR) { + if (callback) { + data['callback'] = callback; + } else if (verbose_mode || this.get_config('test')) { + // Verbose output (from verbose mode, or an error in test mode) is a json blob, + // which by itself is not valid javascript. Without a callback, this verbose output will + // cause an error when returned via jsonp, so we force a no-op callback param. + // See the ECMA script spec: http://www.ecma-international.org/ecma-262/5.1/#sec-12.4 + data['callback'] = '(function(){})'; + } + } + + data['ip'] = this.get_config('ip')?1:0; + data['_'] = new Date().getTime().toString(); + url += '?' + _.HTTPBuildQuery(data); + + if ('img' in data) { + var img = document$1.createElement('img'); + img.src = url; + document$1.body.appendChild(img); + } else if (USE_XHR) { + try { + var req = new XMLHttpRequest(); + req.open('GET', url, true); + // send the mp_optout cookie + // withCredentials cannot be modified until after calling .open on Android and Mobile Safari + req.withCredentials = true; + req.onreadystatechange = function () { + if (req.readyState === 4) { // XMLHttpRequest.DONE == 4, except in safari 4 + if (req.status === 200) { + if (callback) { + if (verbose_mode) { + var response; + try { + response = _.JSONDecode(req.responseText); + } catch (e) { + console$1.error(e); + return; + } + callback(response); + } else { + callback(Number(req.responseText)); + } + } + } else { + var error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText; + console$1.error(error); + if (callback) { + if (verbose_mode) { + callback({status: 0, error: error}); + } else { + callback(0); + } + } + } + } + }; + req.send(null); + } catch (e) { + console$1.error(e); + } + } else { + var script = document$1.createElement('script'); + script.type = 'text/javascript'; + script.async = true; + script.defer = true; + script.src = url; + var s = document$1.getElementsByTagName('script')[0]; + s.parentNode.insertBefore(script, s); + } + }; + + /** + * _execute_array() deals with processing any mixpanel function + * calls that were called before the Mixpanel library were loaded + * (and are thus stored in an array so they can be called later) + * + * Note: we fire off all the mixpanel function calls && user defined + * functions BEFORE we fire off mixpanel tracking calls. This is so + * identify/register/set_config calls can properly modify early + * tracking calls. + * + * @param {Array} array + */ + MixpanelLib.prototype._execute_array = function(array) { + var fn_name, alias_calls = [], other_calls = [], tracking_calls = []; + _.each(array, function(item) { + if (item) { + fn_name = item[0]; + if (typeof(item) === 'function') { + item.call(this); + } else if (_.isArray(item) && fn_name === 'alias') { + alias_calls.push(item); + } else if (_.isArray(item) && fn_name.indexOf('track') !== -1 && typeof(this[fn_name]) === 'function') { + tracking_calls.push(item); + } else { + other_calls.push(item); + } + } + }, this); + + var execute = function(calls, context) { + _.each(calls, function(item) { + this[item[0]].apply(this, item.slice(1)); + }, context); + }; + + execute(alias_calls, this); + execute(other_calls, this); + execute(tracking_calls, this); + }; + + /** + * push() keeps the standard async-array-push + * behavior around after the lib is loaded. + * This is only useful for external integrations that + * do not wish to rely on our convenience methods + * (created in the snippet). + * + * ### Usage: + * mixpanel.push(['register', { a: 'b' }]); + * + * @param {Array} item A [function_name, args...] array to be executed + */ + MixpanelLib.prototype.push = function(item) { + this._execute_array([item]); + }; + + /** + * Disable events on the Mixpanel object. If passed no arguments, + * this function disables tracking of any event. If passed an + * array of event names, those events will be disabled, but other + * events will continue to be tracked. + * + * Note: this function does not stop other mixpanel functions from + * firing, such as register() or people.set(). + * + * @param {Array} [events] An array of event names to disable + */ + MixpanelLib.prototype.disable = function(events) { + if (typeof(events) === 'undefined') { + this._flags.disable_all_events = true; + } else { + this.__disabled_events = this.__disabled_events.concat(events); + } + }; + + /** + * Track an event. This is the most important and + * frequently used Mixpanel function. + * + * ### Usage: + * + * // track an event named 'Registered' + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * To track link clicks or form submissions, see track_links() or track_forms(). + * + * @param {String} event_name The name of the event. This can be anything the user does - 'Button Click', 'Sign Up', 'Item Purchased', etc. + * @param {Object} [properties] A set of properties to include with the event you're sending. These describe the user who did the event or details about the event itself. + * @param {Function} [callback] If provided, the callback function will be called after tracking the event. + */ + MixpanelLib.prototype.track = function(event_name, properties, callback) { + if (typeof(callback) !== 'function') { + callback = function() {}; + } + + if (_.isUndefined(event_name)) { + console$1.error('No event name provided to mixpanel.track'); + return; + } + + if (this._event_is_disabled(event_name)) { + callback(0); + return; + } + + // set defaults + properties = properties || {}; + properties['token'] = this.get_config('token'); + + // set $duration if time_event was previously called for this event + var start_timestamp = this['persistence'].remove_event_timer(event_name); + if (!_.isUndefined(start_timestamp)) { + var duration_in_ms = new Date().getTime() - start_timestamp; + properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3)); + } + + // update persistence + this['persistence'].update_search_keyword(document$1.referrer); + + if (this.get_config('store_google')) { this['persistence'].update_campaign_params(); } + if (this.get_config('save_referrer')) { this['persistence'].update_referrer_info(document$1.referrer); } + + // note: extend writes to the first object, so lets make sure we + // don't write to the persistence properties object and info + // properties object by passing in a new object + + // update properties with pageview info and super-properties + properties = _.extend( + {}, + _.info.properties(), + this['persistence'].properties(), + properties + ); + + try { + if (this.get_config('autotrack') && event_name !== 'mp_page_view' && event_name !== '$create_alias') { + // The point of $__c is to count how many clicks occur per tracked event. Since we're + // tracking an event in this function, we need to reset the $__c value. + properties = _.extend({}, properties, this.mp_counts); + this.mp_counts = {'$__c': 0}; + _.cookie.set('mp_' + this.get_config('name') + '__c', 0, 1, true); + } + } catch (e) { + console$1.error(e); + } + + var property_blacklist = this.get_config('property_blacklist'); + if (_.isArray(property_blacklist)) { + _.each(property_blacklist, function(blacklisted_prop) { + delete properties[blacklisted_prop]; + }); + } else { + console$1.error('Invalid value for property_blacklist config: ' + property_blacklist); + } + + var data = { + 'event': event_name, + 'properties': properties + }; + + var truncated_data = _.truncate(data, 255); + var json_data = _.JSONEncode(truncated_data); + var encoded_data = _.base64Encode(json_data); + + console$1.log('MIXPANEL REQUEST:'); + console$1.log(truncated_data); + + this._send_request( + this.get_config('api_host') + '/track/', + { 'data': encoded_data }, + this._prepare_callback(callback, truncated_data) + ); + + return truncated_data; + }; + + /** + * Track a page view event, which is currently ignored by the server. + * This function is called by default on page load unless the + * track_pageview configuration variable is false. + * + * @param {String} [page] The url of the page to record. If you don't include this, it defaults to the current url. + * @api private + */ + MixpanelLib.prototype.track_pageview = function(page) { + if (_.isUndefined(page)) { + page = document$1.location.href; + } + this.track('mp_page_view', _.info.pageviewInfo(page)); + }; + + /** + * Track clicks on a set of document elements. Selector must be a + * valid query. Elements must exist on the page at the time track_links is called. + * + * ### Usage: + * + * // track click for link id #nav + * mixpanel.track_links('#nav', 'Clicked Nav Link'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the Mixpanel + * servers to respond. If they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] A properties object or function that returns a dictionary of properties when passed a DOMElement + */ + MixpanelLib.prototype.track_links = function() { + return this._track_dom.call(this, LinkTracker, arguments); + }; + + /** + * Track form submissions. Selector must be a valid query. + * + * ### Usage: + * + * // track submission for form id 'register' + * mixpanel.track_forms('#register', 'Created Account'); + * + * ### Notes: + * + * This function will wait up to 300 ms for the mixpanel + * servers to respond, if they have not responded by that time + * it will head to the link without ensuring that your event + * has been tracked. To configure this timeout please see the + * set_config() documentation below. + * + * If you pass a function in as the properties argument, the + * function will receive the DOMElement that triggered the + * event as an argument. You are expected to return an object + * from the function; any properties defined on this object + * will be sent to mixpanel as event properties. + * + * @type {Function} + * @param {Object|String} query A valid DOM query, element or jQuery-esque list + * @param {String} event_name The name of the event to track + * @param {Object|Function} [properties] This can be a set of properties, or a function that returns a set of properties after being passed a DOMElement + */ + MixpanelLib.prototype.track_forms = function() { + return this._track_dom.call(this, FormTracker, arguments); + }; + + /** + * Time an event by including the time between this call and a + * later 'track' call for the same event in the properties sent + * with the event. + * + * ### Usage: + * + * // time an event named 'Registered' + * mixpanel.time_event('Registered'); + * mixpanel.track('Registered', {'Gender': 'Male', 'Age': 21}); + * + * When called for a particular event name, the next track call for that event + * name will include the elapsed time between the 'time_event' and 'track' + * calls. This value is stored as seconds in the '$duration' property. + * + * @param {String} event_name The name of the event. + */ + MixpanelLib.prototype.time_event = function(event_name) { + if (_.isUndefined(event_name)) { + console$1.error('No event name provided to mixpanel.time_event'); + return; + } + + if (this._event_is_disabled(event_name)) { + return; + } + + this['persistence'].set_event_timer(event_name, new Date().getTime()); + }; + + /** + * Register a set of super properties, which are included with all + * events. This will overwrite previous super property values. + * + * ### Usage: + * + * // register 'Gender' as a super property + * mixpanel.register({'Gender': 'Female'}); + * + * // register several super properties when a user signs up + * mixpanel.register({ + * 'Email': 'jdoe@example.com', + * 'Account Type': 'Free' + * }); + * + * @param {Object} properties An associative array of properties to store about the user + * @param {Number} [days] How many days since the user's last visit to store the super properties + */ + MixpanelLib.prototype.register = function(props, days) { + this['persistence'].register(props, days); + }; + + /** + * Register a set of super properties only once. This will not + * overwrite previous super property values, unlike register(). + * + * ### Usage: + * + * // register a super property for the first time only + * mixpanel.register_once({ + * 'First Login Date': new Date().toISOString() + * }); + * + * ### Notes: + * + * If default_value is specified, current super properties + * with that value will be overwritten. + * + * @param {Object} properties An associative array of properties to store about the user + * @param {*} [default_value] Value to override if already set in super properties (ex: 'False') Default: 'None' + * @param {Number} [days] How many days since the users last visit to store the super properties + */ + MixpanelLib.prototype.register_once = function(props, default_value, days) { + this['persistence'].register_once(props, default_value, days); + }; + + /** + * Delete a super property stored with the current user. + * + * @param {String} property The name of the super property to remove + */ + MixpanelLib.prototype.unregister = function(property) { + this['persistence'].unregister(property); + }; + + MixpanelLib.prototype._register_single = function(prop, value) { + var props = {}; + props[prop] = value; + this.register(props); + }; + + /** + * Identify a user with a unique ID instead of a Mixpanel + * randomly generated distinct_id. If the method is never called, + * then unique visitors will be identified by a UUID generated + * the first time they visit the site. + * + * ### Notes: + * + * You can call this function to overwrite a previously set + * unique ID for the current user. Mixpanel cannot translate + * between IDs at this time, so when you change a user's ID + * they will appear to be a new user. + * + * When used alone, mixpanel.identify will change the user's + * distinct_id to the unique ID provided. When used in tandem + * with mixpanel.alias, it will allow you to identify based on + * unique ID and map that back to the original, anonymous + * distinct_id given to the user upon her first arrival to your + * site (thus connecting anonymous pre-signup activity to + * post-signup activity). Though the two work together, do not + * call identify() at the same time as alias(). Calling the two + * at the same time can cause a race condition, so it is best + * practice to call identify on the original, anonymous ID + * right after you've aliased it. + * Learn more about how mixpanel.identify and mixpanel.alias can be used. + * + * @param {String} [unique_id] A string that uniquely identifies a user. If not provided, the distinct_id currently in the persistent store (cookie or localStorage) will be used. + */ + MixpanelLib.prototype.identify = function( + unique_id, _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback + ) { + // Optional Parameters + // _set_callback:function A callback to be run if and when the People set queue is flushed + // _add_callback:function A callback to be run if and when the People add queue is flushed + // _append_callback:function A callback to be run if and when the People append queue is flushed + // _set_once_callback:function A callback to be run if and when the People set_once queue is flushed + // _union_callback:function A callback to be run if and when the People union queue is flushed + // _unset_callback:function A callback to be run if and when the People unset queue is flushed + + // identify only changes the distinct id if it doesn't match either the existing or the alias; + // if it's new, blow away the alias as well. + if (unique_id !== this.get_distinct_id() && unique_id !== this.get_property(ALIAS_ID_KEY)) { + this.unregister(ALIAS_ID_KEY); + this._register_single('distinct_id', unique_id); + } + this._check_and_handle_notifications(this.get_distinct_id()); + this._flags.identify_called = true; + // Flush any queued up people requests + this['people']._flush(_set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback); + }; + + /** + * Clears super properties and generates a new random distinct_id for this instance. + * Useful for clearing data when a user logs out. + */ + MixpanelLib.prototype.reset = function() { + this['persistence'].clear(); + this._flags.identify_called = false; + this.register_once({'distinct_id': _.UUID()}, ''); + }; + + /** + * Returns the current distinct id of the user. This is either the id automatically + * generated by the library or the id that has been passed by a call to identify(). + * + * ### Notes: + * + * get_distinct_id() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // set distinct_id after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * distinct_id = mixpanel.get_distinct_id(); + * } + * }); + */ + MixpanelLib.prototype.get_distinct_id = function() { + return this.get_property('distinct_id'); + }; + + /** + * Create an alias, which Mixpanel will use to link two distinct_ids going forward (not retroactively). + * Multiple aliases can map to the same original ID, but not vice-versa. Aliases can also be chained - the + * following is a valid scenario: + * + * mixpanel.alias('new_id', 'existing_id'); + * ... + * mixpanel.alias('newer_id', 'new_id'); + * + * If the original ID is not passed in, we will use the current distinct_id - probably the auto-generated GUID. + * + * ### Notes: + * + * The best practice is to call alias() when a unique ID is first created for a user + * (e.g., when a user first registers for an account and provides an email address). + * alias() should never be called more than once for a given user, except to + * chain a newer ID to a previously new ID, as described above. + * + * @param {String} alias A unique identifier that you want to use for this user in the future. + * @param {String} [original] The current identifier being used for this user. + */ + MixpanelLib.prototype.alias = function(alias, original) { + // If the $people_distinct_id key exists in persistence, there has been a previous + // mixpanel.people.identify() call made for this user. It is VERY BAD to make an alias with + // this ID, as it will duplicate users. + if (alias === this.get_property(PEOPLE_DISTINCT_ID_KEY)) { + console$1.critical('Attempting to create alias for existing People user - aborting.'); + return -2; + } + + var _this = this; + if (_.isUndefined(original)) { + original = this.get_distinct_id(); + } + if (alias !== original) { + this._register_single(ALIAS_ID_KEY, alias); + return this.track('$create_alias', { 'alias': alias, 'distinct_id': original }, function() { + // Flush the people queue + _this.identify(alias); + }); + } else { + console$1.error('alias matches current distinct_id - skipping api call.'); + this.identify(alias); + return -1; + } + }; + + /** + * Provide a string to recognize the user by. The string passed to + * this method will appear in the Mixpanel Streams product rather + * than an automatically generated name. Name tags do not have to + * be unique. + * + * This value will only be included in Streams data. + * + * @param {String} name_tag A human readable name for the user + * @api private + */ + MixpanelLib.prototype.name_tag = function(name_tag) { + this._register_single('mp_name_tag', name_tag); + }; + + /** + * Update the configuration of a mixpanel library instance. + * + * The default config is: + * + * { + * // super properties cookie expiration (in days) + * cookie_expiration: 365 + * + * // super properties span subdomains + * cross_subdomain_cookie: true + * + * // debug mode + * debug: false + * + * // if this is true, the mixpanel cookie or localStorage entry + * // will be deleted, and no user persistence will take place + * disable_persistence: false + * + * // if this is true, Mixpanel will automatically determine + * // City, Region and Country data using the IP address of + * //the client + * ip: true + * + * // type of persistent store for super properties (cookie/ + * // localStorage) if set to 'localStorage', any existing + * // mixpanel cookie value with the same persistence_name + * // will be transferred to localStorage and deleted + * persistence: 'cookie' + * + * // name for super properties persistent store + * persistence_name: '' + * + * // names of properties/superproperties which should never + * // be sent with track() calls + * property_blacklist: [] + * + * // if this is true, mixpanel cookies will be marked as + * // secure, meaning they will only be transmitted over https + * secure_cookie: false + * + * // the amount of time track_links will + * // wait for Mixpanel's servers to respond + * track_links_timeout: 300 + * + * // should we track a page view on page load + * track_pageview: true + * + * // if you set upgrade to be true, the library will check for + * // a cookie from our old js library and import super + * // properties from it, then the old cookie is deleted + * // The upgrade config option only works in the initialization, + * // so make sure you set it when you create the library. + * upgrade: false + * } + * + * + * @param {Object} config A dictionary of new configuration values to update + */ + MixpanelLib.prototype.set_config = function(config) { + if (_.isObject(config)) { + _.extend(this['config'], config); + + if (!this.get_config('persistence_name')) { + this['config']['persistence_name'] = this['config']['cookie_name']; + } + if (!this.get_config('disable_persistence')) { + this['config']['disable_persistence'] = this['config']['disable_cookie']; + } + + if (this['persistence']) { + this['persistence'].update_config(this['config']); + } + Config.DEBUG = Config.DEBUG || this.get_config('debug'); + } + }; + + /** + * returns the current config object for the library. + */ + MixpanelLib.prototype.get_config = function(prop_name) { + return this['config'][prop_name]; + }; + + /** + * Returns the value of the super property named property_name. If no such + * property is set, get_property() will return the undefined value. + * + * ### Notes: + * + * get_property() can only be called after the Mixpanel library has finished loading. + * init() has a loaded function available to handle this automatically. For example: + * + * // grab value for 'user_id' after the mixpanel library has loaded + * mixpanel.init('YOUR PROJECT TOKEN', { + * loaded: function(mixpanel) { + * user_id = mixpanel.get_property('user_id'); + * } + * }); + * + * @param {String} property_name The name of the super property you want to retrieve + */ + MixpanelLib.prototype.get_property = function(property_name) { + return this['persistence']['props'][property_name]; + }; + + MixpanelLib.prototype.toString = function() { + var name = this.get_config('name'); + if (name !== PRIMARY_INSTANCE_NAME) { + name = PRIMARY_INSTANCE_NAME + '.' + name; + } + return name; + }; + + MixpanelLib.prototype._event_is_disabled = function(event_name) { + return _.isBlockedUA(userAgent) || + this._flags.disable_all_events || + _.include(this.__disabled_events, event_name); + }; + + MixpanelLib.prototype._check_and_handle_notifications = function(distinct_id) { + if (!distinct_id || this._flags.identify_called || this.get_config('disable_notifications')) { + return; + } + + console$1.log('MIXPANEL NOTIFICATION CHECK'); + + var data = { + 'verbose': true, + 'version': '2', + 'lib': 'web', + 'token': this.get_config('token'), + 'distinct_id': distinct_id + }; + var self = this; + this._send_request( + this.get_config('api_host') + '/decide/', + data, + this._prepare_callback(function(r) { + if (r['notifications'] && r['notifications'].length > 0) { + self._show_notification.call(self, r['notifications'][0]); + } + }) + ); + }; + + MixpanelLib.prototype._show_notification = function(notification_data) { + var notification = new MPNotif(notification_data, this); + notification.show(); + }; + + MixpanelPeople.prototype._init = function(mixpanel_instance) { + this._mixpanel = mixpanel_instance; + }; + + /* + * Set properties on a user record. + * + * ### Usage: + * + * mixpanel.people.set('gender', 'm'); + * + * // or set multiple properties at once + * mixpanel.people.set({ + * 'Company': 'Acme', + * 'Plan': 'Premium', + * 'Upgrade date': new Date() + * }); + * // properties can be strings, integers, dates, or lists + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.set = function(prop, to, callback) { + var data = {}; + var $set = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set[k] = v; + } + }, this); + callback = to; + } else { + $set[prop] = to; + } + + // make sure that the referrer info has been updated and saved + if (this._get_config('save_referrer')) { + this._mixpanel['persistence'].update_referrer_info(document$1.referrer); + } + + // update $set object with default people properties + $set = _.extend( + {}, + _.info.people_properties(), + this._mixpanel['persistence'].get_referrer_info(), + $set + ); + + data[SET_ACTION] = $set; + + return this._send_request(data, callback); + }; + + /* + * Set properties on a user record, only if they do not yet exist. + * This will not overwrite previous people property values, unlike + * people.set(). + * + * ### Usage: + * + * mixpanel.people.set_once('First Login Date', new Date()); + * + * // or set multiple properties at once + * mixpanel.people.set_once({ + * 'First Login Date': new Date(), + * 'Starting Plan': 'Premium' + * }); + * + * // properties can be strings, integers or dates + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [to] A value to set on the given property name + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.set_once = function(prop, to, callback) { + var data = {}; + var $set_once = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + $set_once[k] = v; + } + }, this); + callback = to; + } else { + $set_once[prop] = to; + } + data[SET_ONCE_ACTION] = $set_once; + return this._send_request(data, callback); + }; + + /* + * Unset properties on a user record (permanently removes the properties and their values from a profile). + * + * ### Usage: + * + * mixpanel.people.unset('gender'); + * + * // or unset multiple properties at once + * mixpanel.people.unset(['gender', 'Company']); + * + * @param {Array|String} prop If a string, this is the name of the property. If an array, this is a list of property names. + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.unset = function(prop, callback) { + var data = {}; + var $unset = []; + if (!_.isArray(prop)) { + prop = [prop]; + } + + _.each(prop, function(k) { + if (!this._is_reserved_property(k)) { + $unset.push(k); + } + }, this); + + data[UNSET_ACTION] = $unset; + + return this._send_request(data, callback); + }; + + /* + * Increment/decrement numeric people analytics properties. + * + * ### Usage: + * + * mixpanel.people.increment('page_views', 1); + * + * // or, for convenience, if you're just incrementing a counter by + * // 1, you can simply do + * mixpanel.people.increment('page_views'); + * + * // to decrement a counter, pass a negative number + * mixpanel.people.increment('credits_left', -1); + * + * // like mixpanel.people.set(), you can increment multiple + * // properties at once: + * mixpanel.people.increment({ + * counter1: 1, + * counter2: 6 + * }); + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and numeric values. + * @param {Number} [by] An amount to increment the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.increment = function(prop, by, callback) { + var data = {}; + var $add = {}; + if (_.isObject(prop)) { + _.each(prop, function(v, k) { + if (!this._is_reserved_property(k)) { + if (isNaN(parseFloat(v))) { + console$1.error('Invalid increment value passed to mixpanel.people.increment - must be a number'); + return; + } else { + $add[k] = v; + } + } + }, this); + callback = by; + } else { + // convenience: mixpanel.people.increment('property'); will + // increment 'property' by 1 + if (_.isUndefined(by)) { + by = 1; + } + $add[prop] = by; + } + data[ADD_ACTION] = $add; + + return this._send_request(data, callback); + }; + + /* + * Append a value to a list-valued people analytics property. + * + * ### Usage: + * + * // append a value to a list, creating it if needed + * mixpanel.people.append('pages_visited', 'homepage'); + * + * // like mixpanel.people.set(), you can append multiple + * // properties at once: + * mixpanel.people.append({ + * list1: 'bob', + * list2: 123 + * }); + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [value] An item to append to the list + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.append = function(list_name, value, callback) { + var data = {}; + var $append = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $append[k] = v; + } + }, this); + callback = value; + } else { + $append[list_name] = value; + } + data[APPEND_ACTION] = $append; + + return this._send_request(data, callback); + }; + + /* + * Merge a given list with a list-valued people analytics property, + * excluding duplicate values. + * + * ### Usage: + * + * // merge a value to a list, creating it if needed + * mixpanel.people.union('pages_visited', 'homepage'); + * + * // like mixpanel.people.set(), you can append multiple + * // properties at once: + * mixpanel.people.union({ + * list1: 'bob', + * list2: 123 + * }); + * + * // like mixpanel.people.append(), you can append multiple + * // values to the same list: + * mixpanel.people.union({ + * list1: ['bob', 'billy'] + * }); + * + * @param {Object|String} prop If a string, this is the name of the property. If an object, this is an associative array of names and values. + * @param {*} [value] Value / values to merge with the given property + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.union = function(list_name, values, callback) { + var data = {}; + var $union = {}; + if (_.isObject(list_name)) { + _.each(list_name, function(v, k) { + if (!this._is_reserved_property(k)) { + $union[k] = _.isArray(v) ? v : [v]; + } + }, this); + callback = values; + } else { + $union[list_name] = _.isArray(values) ? values : [values]; + } + data[UNION_ACTION] = $union; + + return this._send_request(data, callback); + }; + + /* + * Record that you have charged the current user a certain amount + * of money. Charges recorded with track_charge() will appear in the + * Mixpanel revenue report. + * + * ### Usage: + * + * // charge a user $50 + * mixpanel.people.track_charge(50); + * + * // charge a user $30.50 on the 2nd of january + * mixpanel.people.track_charge(30.50, { + * '$time': new Date('jan 1 2012') + * }); + * + * @param {Number} amount The amount of money charged to the current user + * @param {Object} [properties] An associative array of properties associated with the charge + * @param {Function} [callback] If provided, the callback will be called when the server responds + */ + MixpanelPeople.prototype.track_charge = function(amount, properties, callback) { + if (!_.isNumber(amount)) { + amount = parseFloat(amount); + if (isNaN(amount)) { + console$1.error('Invalid value passed to mixpanel.people.track_charge - must be a number'); + return; + } + } + + return this.append('$transactions', _.extend({ + '$amount': amount + }, properties), callback); + }; + + /* + * Permanently clear all revenue report transactions from the + * current user's people analytics profile. + * + * ### Usage: + * + * mixpanel.people.clear_charges(); + * + * @param {Function} [callback] If provided, the callback will be called after the tracking event + */ + MixpanelPeople.prototype.clear_charges = function(callback) { + return this.set('$transactions', [], callback); + }; + + /* + * Permanently deletes the current people analytics profile from + * Mixpanel (using the current distinct_id). + * + * ### Usage: + * + * // remove the all data you have stored about the current user + * mixpanel.people.delete_user(); + * + */ + MixpanelPeople.prototype.delete_user = function() { + if (!this._identify_called()) { + console$1.error('mixpanel.people.delete_user() requires you to call identify() first'); + return; + } + var data = {'$delete': this._mixpanel.get_distinct_id()}; + return this._send_request(data); + }; + + MixpanelPeople.prototype.toString = function() { + return this._mixpanel.toString() + '.people'; + }; + + MixpanelPeople.prototype._send_request = function(data, callback) { + data['$token'] = this._get_config('token'); + data['$distinct_id'] = this._mixpanel.get_distinct_id(); + + var date_encoded_data = _.encodeDates(data); + var truncated_data = _.truncate(date_encoded_data, 255); + var json_data = _.JSONEncode(date_encoded_data); + var encoded_data = _.base64Encode(json_data); + + if (!this._identify_called()) { + this._enqueue(data); + if (!_.isUndefined(callback)) { + if (this._get_config('verbose')) { + callback({status: -1, error: null}); + } else { + callback(-1); + } + } + return truncated_data; + } + + console$1.log('MIXPANEL PEOPLE REQUEST:'); + console$1.log(truncated_data); + + this._mixpanel._send_request( + this._get_config('api_host') + '/engage/', + {'data': encoded_data}, + this._mixpanel._prepare_callback(callback, truncated_data) + ); + + return truncated_data; + }; + + MixpanelPeople.prototype._get_config = function(conf_var) { + return this._mixpanel.get_config(conf_var); + }; + + MixpanelPeople.prototype._identify_called = function() { + return this._mixpanel._flags.identify_called === true; + }; + + // Queue up engage operations if identify hasn't been called yet. + MixpanelPeople.prototype._enqueue = function(data) { + if (SET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ACTION, data); + } else if (SET_ONCE_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(SET_ONCE_ACTION, data); + } else if (UNSET_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNSET_ACTION, data); + } else if (ADD_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(ADD_ACTION, data); + } else if (APPEND_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, data); + } else if (UNION_ACTION in data) { + this._mixpanel['persistence']._add_to_people_queue(UNION_ACTION, data); + } else { + console$1.error('Invalid call to _enqueue():', data); + } + }; + + MixpanelPeople.prototype._flush_one_queue = function(action, action_method, callback, queue_to_params_fn) { + var _this = this; + var queued_data = _.extend({}, this._mixpanel['persistence']._get_queue(action)); + var action_params = queued_data; + + if (!_.isUndefined(queued_data) && _.isObject(queued_data) && !_.isEmptyObject(queued_data)) { + _this._mixpanel['persistence']._pop_from_people_queue(action, queued_data); + if (queue_to_params_fn) { + action_params = queue_to_params_fn(queued_data); + } + action_method.call(_this, action_params, function(response, data) { + // on bad response, we want to add it back to the queue + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(action, queued_data); + } + if (!_.isUndefined(callback)) { + callback(response, data); + } + }); + } + }; + + // Flush queued engage operations - order does not matter, + // and there are network level race conditions anyway + MixpanelPeople.prototype._flush = function( + _set_callback, _add_callback, _append_callback, _set_once_callback, _union_callback, _unset_callback + ) { + var _this = this; + var $append_queue = this._mixpanel['persistence']._get_queue(APPEND_ACTION); + + this._flush_one_queue(SET_ACTION, this.set, _set_callback); + this._flush_one_queue(SET_ONCE_ACTION, this.set_once, _set_once_callback); + this._flush_one_queue(UNSET_ACTION, this.unset, _unset_callback, function(queue) { return _.keys(queue); }); + this._flush_one_queue(ADD_ACTION, this.increment, _add_callback); + this._flush_one_queue(UNION_ACTION, this.union, _union_callback); + + // we have to fire off each $append individually since there is + // no concat method server side + if (!_.isUndefined($append_queue) && _.isArray($append_queue) && $append_queue.length) { + var $append_item; + var callback = function(response, data) { + if (response === 0) { + _this._mixpanel['persistence']._add_to_people_queue(APPEND_ACTION, $append_item); + } + if (!_.isUndefined(_append_callback)) { + _append_callback(response, data); + } + }; + for (var i = $append_queue.length - 1; i >= 0; i--) { + $append_item = $append_queue.pop(); + _this.append($append_item, callback); + } + // Save the shortened append queue + _this._mixpanel['persistence'].save(); + } + }; + + MixpanelPeople.prototype._is_reserved_property = function(prop) { + return prop === '$distinct_id' || prop === '$token'; + }; + + + // Internal class for notification display + MixpanelLib._Notification = function(notif_data, mixpanel_instance) { + _.bind_instance_methods(this); + + this.mixpanel = mixpanel_instance; + this.persistence = this.mixpanel['persistence']; + + this.campaign_id = _.escapeHTML(notif_data['id']); + this.message_id = _.escapeHTML(notif_data['message_id']); + + this.body = (_.escapeHTML(notif_data['body']) || '').replace(/\n/g, '
'); + this.cta = _.escapeHTML(notif_data['cta']) || 'Close'; + this.notif_type = _.escapeHTML(notif_data['type']) || 'takeover'; + this.style = _.escapeHTML(notif_data['style']) || 'light'; + this.title = _.escapeHTML(notif_data['title']) || ''; + this.video_width = MPNotif.VIDEO_WIDTH; + this.video_height = MPNotif.VIDEO_HEIGHT; + + // These fields are url-sanitized in the backend already. + this.dest_url = notif_data['cta_url'] || null; + this.image_url = notif_data['image_url'] || null; + this.thumb_image_url = notif_data['thumb_image_url'] || null; + this.video_url = notif_data['video_url'] || null; + + this.clickthrough = true; + if (!this.dest_url) { + this.dest_url = '#dismiss'; + this.clickthrough = false; + } + + this.mini = this.notif_type === 'mini'; + if (!this.mini) { + this.notif_type = 'takeover'; + } + this.notif_width = !this.mini ? MPNotif.NOTIF_WIDTH : MPNotif.NOTIF_WIDTH_MINI; + + this._set_client_config(); + this.imgs_to_preload = this._init_image_html(); + this._init_video(); + }; + + MPNotif = MixpanelLib._Notification; + + MPNotif.ANIM_TIME = 200; + MPNotif.MARKUP_PREFIX = 'mixpanel-notification'; + MPNotif.BG_OPACITY = 0.6; + MPNotif.NOTIF_TOP = 25; + MPNotif.NOTIF_START_TOP = 200; + MPNotif.NOTIF_WIDTH = 388; + MPNotif.NOTIF_WIDTH_MINI = 420; + MPNotif.NOTIF_HEIGHT_MINI = 85; + MPNotif.THUMB_BORDER_SIZE = 5; + MPNotif.THUMB_IMG_SIZE = 60; + MPNotif.THUMB_OFFSET = Math.round(MPNotif.THUMB_IMG_SIZE / 2); + MPNotif.VIDEO_WIDTH = 595; + MPNotif.VIDEO_HEIGHT = 334; + + MPNotif.prototype.show = function() { + var self = this; + this._set_client_config(); + + // don't display until HTML body exists + if (!this.body_el) { + setTimeout(function() { self.show(); }, 300); + return; + } + + this._init_styles(); + this._init_notification_el(); + + // wait for any images to load before showing notification + this._preload_images(this._attach_and_animate); + }; + + MPNotif.prototype.dismiss = _.safewrap(function() { + if (!this.marked_as_shown) { + // unexpected condition: user interacted with notif even though we didn't consider it + // visible (see _mark_as_shown()); send tracking signals to mark delivery + this._mark_delivery({'invisible': true}); + } + + var exiting_el = this.showing_video ? this._get_el('video') : this._get_notification_display_el(); + if (this.use_transitions) { + this._remove_class('bg', 'visible'); + this._add_class(exiting_el, 'exiting'); + setTimeout(this._remove_notification_el, MPNotif.ANIM_TIME); + } else { + var notif_attr, notif_start, notif_goal; + if (this.mini) { + notif_attr = 'right'; + notif_start = 20; + notif_goal = -100; + } else { + notif_attr = 'top'; + notif_start = MPNotif.NOTIF_TOP; + notif_goal = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; + } + this._animate_els([ + { + el: this._get_el('bg'), + attr: 'opacity', + start: MPNotif.BG_OPACITY, + goal: 0.0 + }, + { + el: exiting_el, + attr: 'opacity', + start: 1.0, + goal: 0.0 + }, + { + el: exiting_el, + attr: notif_attr, + start: notif_start, + goal: notif_goal + } + ], MPNotif.ANIM_TIME, this._remove_notification_el); + } + }); + + MPNotif.prototype._add_class = _.safewrap(function(el, class_name) { + class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; + if (typeof el === 'string') { + el = this._get_el(el); + } + if (!el.className) { + el.className = class_name; + } else if (!~(' ' + el.className + ' ').indexOf(' ' + class_name + ' ')) { + el.className += ' ' + class_name; + } + }); + MPNotif.prototype._remove_class = _.safewrap(function(el, class_name) { + class_name = MPNotif.MARKUP_PREFIX + '-' + class_name; + if (typeof el === 'string') { + el = this._get_el(el); + } + if (el.className) { + el.className = (' ' + el.className + ' ') + .replace(' ' + class_name + ' ', '') + .replace(/^[\s\xA0]+/, '') + .replace(/[\s\xA0]+$/, ''); + } + }); + + MPNotif.prototype._animate_els = _.safewrap(function(anims, mss, done_cb, start_time) { + var self = this, + in_progress = false, + ai, anim, + cur_time = 1 * new Date(), time_diff; + + start_time = start_time || cur_time; + time_diff = cur_time - start_time; + + for (ai = 0; ai < anims.length; ai++) { + anim = anims[ai]; + if (typeof anim.val === 'undefined') { + anim.val = anim.start; + } + if (anim.val !== anim.goal) { + in_progress = true; + var anim_diff = anim.goal - anim.start, + anim_dir = anim.goal >= anim.start ? 1 : -1; + anim.val = anim.start + anim_diff * time_diff / mss; + if (anim.attr !== 'opacity') { + anim.val = Math.round(anim.val); + } + if ((anim_dir > 0 && anim.val >= anim.goal) || (anim_dir < 0 && anim.val <= anim.goal)) { + anim.val = anim.goal; + } + } + } + if (!in_progress) { + if (done_cb) { + done_cb(); + } + return; + } + + for (ai = 0; ai < anims.length; ai++) { + anim = anims[ai]; + if (anim.el) { + var suffix = anim.attr === 'opacity' ? '' : 'px'; + anim.el.style[anim.attr] = String(anim.val) + suffix; + } + } + setTimeout(function() { self._animate_els(anims, mss, done_cb, start_time); }, 10); + }); + + MPNotif.prototype._attach_and_animate = _.safewrap(function() { + var self = this; + + // no possibility to double-display + if (this.shown || this._get_shown_campaigns()[this.campaign_id]) { + return; + } + this.shown = true; + + this.body_el.appendChild(this.notification_el); + setTimeout(function() { + var notif_el = self._get_notification_display_el(); + if (self.use_transitions) { + if (!self.mini) { + self._add_class('bg', 'visible'); + } + self._add_class(notif_el, 'visible'); + self._mark_as_shown(); + } else { + var notif_attr, notif_start, notif_goal; + if (self.mini) { + notif_attr = 'right'; + notif_start = -100; + notif_goal = 20; + } else { + notif_attr = 'top'; + notif_start = MPNotif.NOTIF_START_TOP + MPNotif.NOTIF_TOP; + notif_goal = MPNotif.NOTIF_TOP; + } + self._animate_els([ + { + el: self._get_el('bg'), + attr: 'opacity', + start: 0.0, + goal: MPNotif.BG_OPACITY + }, + { + el: notif_el, + attr: 'opacity', + start: 0.0, + goal: 1.0 + }, + { + el: notif_el, + attr: notif_attr, + start: notif_start, + goal: notif_goal + } + ], MPNotif.ANIM_TIME, self._mark_as_shown); + } + }, 100); + _.register_event(self._get_el('cancel'), 'click', function(e) { + e.preventDefault(); + self.dismiss(); + }); + var click_el = self._get_el('button') || + self._get_el('mini-content'); + _.register_event(click_el, 'click', function(e) { + e.preventDefault(); + if (self.show_video) { + self._track_event('$campaign_open', {'$resource_type': 'video'}); + self._switch_to_video(); + } else { + self.dismiss(); + if (self.clickthrough) { + self._track_event('$campaign_open', {'$resource_type': 'link'}, function() { + win.location.href = self.dest_url; + }); + } + } + }); + }); + + MPNotif.prototype._get_el = function(id) { + return document$1.getElementById(MPNotif.MARKUP_PREFIX + '-' + id); + }; + + MPNotif.prototype._get_notification_display_el = function() { + return this._get_el(this.notif_type); + }; + + MPNotif.prototype._get_shown_campaigns = function() { + return this.persistence['props'][CAMPAIGN_IDS_KEY] || (this.persistence['props'][CAMPAIGN_IDS_KEY] = {}); + }; + + MPNotif.prototype._browser_lte = function(browser, version) { + return this.browser_versions[browser] && this.browser_versions[browser] <= version; + }; + + MPNotif.prototype._init_image_html = function() { + var imgs_to_preload = []; + + if (!this.mini) { + if (this.image_url) { + imgs_to_preload.push(this.image_url); + this.img_html = ''; + } else { + this.img_html = ''; + } + if (this.thumb_image_url) { + imgs_to_preload.push(this.thumb_image_url); + this.thumb_img_html = + '
' + + '' + + '
'; + } else { + this.thumb_img_html = ''; + } + } else { + this.thumb_image_url = this.thumb_image_url || '//cdn.mxpnl.com/site_media/images/icons/notifications/mini-news-dark.png'; + imgs_to_preload.push(this.thumb_image_url); + } + + return imgs_to_preload; + }; + + MPNotif.prototype._init_notification_el = function() { + var notification_html = ''; + var video_src = ''; + var video_html = ''; + var cancel_html = '
' + + '
' + + '
'; + + this.notification_el = document$1.createElement('div'); + this.notification_el.id = MPNotif.MARKUP_PREFIX + '-wrapper'; + if (!this.mini) { + // TAKEOVER notification + var close_html = (this.clickthrough || this.show_video) ? '' : '
', + play_html = this.show_video ? '
' : ''; + if (this._browser_lte('ie', 7)) { + close_html = ''; + play_html = ''; + } + notification_html = + '
' + + this.thumb_img_html + + '
' + + cancel_html + + '
' + + this.img_html + + '
' + this.title + '
' + + '
' + this.body + '
' + + '
' + + 'POWERED BY MIXPANEL' + + '
' + + '
' + + '
' + + close_html + + '' + this.cta + '' + + play_html + + '
' + + '
' + + '
'; + } else { + // MINI notification + notification_html = + '
' + + '
' + + cancel_html + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + this.body + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + } + if (this.youtube_video) { + video_src = '//www.youtube.com/embed/' + this.youtube_video + + '?wmode=transparent&showinfo=0&modestbranding=0&rel=0&autoplay=1&loop=0&vq=hd1080'; + if (this.yt_custom) { + video_src += '&enablejsapi=1&html5=1&controls=0'; + video_html = + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
'; + } + } else if (this.vimeo_video) { + video_src = '//player.vimeo.com/video/' + this.vimeo_video + '?autoplay=1&title=0&byline=0&portrait=0'; + } + if (this.show_video) { + this.video_iframe = + ''; + video_html = + '
' + + '
' + + '
' + + video_html + + '
' + + '
'; + } + var main_html = video_html + notification_html; + if (this.flip_animate) { + main_html = + (this.mini ? notification_html : '') + + '
' + + (this.mini ? video_html : main_html) + + '
'; + } + + this.notification_el.innerHTML = + ('
' + + '
' + + '
' + + '
' + + main_html + + '
' + + '
' + + '
') + .replace(/class=\"/g, 'class="' + MPNotif.MARKUP_PREFIX + '-') + .replace(/id=\"/g, 'id="' + MPNotif.MARKUP_PREFIX + '-'); + }; + + MPNotif.prototype._init_styles = function() { + if (this.style === 'dark') { + this.style_vals = { + bg: '#1d1f25', + bg_actions: '#282b32', + bg_hover: '#3a4147', + bg_light: '#4a5157', + border_gray: '#32353c', + cancel_opacity: '0.4', + mini_hover: '#2a3137', + text_title: '#fff', + text_main: '#9498a3', + text_tagline: '#464851', + text_hover: '#ddd' + }; + } else { + this.style_vals = { + bg: '#fff', + bg_actions: '#e7eaee', + bg_hover: '#eceff3', + bg_light: '#f5f5f5', + border_gray: '#e4ecf2', + cancel_opacity: '1.0', + mini_hover: '#fafafa', + text_title: '#5c6578', + text_main: '#8b949b', + text_tagline: '#ced9e6', + text_hover: '#7c8598' + }; + } + var shadow = '0px 0px 35px 0px rgba(45, 49, 56, 0.7)', + video_shadow = shadow, + mini_shadow = shadow, + thumb_total_size = MPNotif.THUMB_IMG_SIZE + MPNotif.THUMB_BORDER_SIZE * 2, + anim_seconds = (MPNotif.ANIM_TIME / 1000) + 's'; + if (this.mini) { + shadow = 'none'; + } + + // don't display on small viewports + var notif_media_queries = {}, + min_width = MPNotif.NOTIF_WIDTH_MINI + 20; + notif_media_queries['@media only screen and (max-width: ' + (min_width - 1) + 'px)'] = { + '#overlay': { + 'display': 'none' + } + }; + var notif_styles = { + '.flipped': { + 'transform': 'rotateY(180deg)' + }, + '#overlay': { + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'overflow': 'auto', + 'text-align': 'center', + 'z-index': '10000', + 'font-family': '"Helvetica", "Arial", sans-serif', + '-webkit-font-smoothing': 'antialiased', + '-moz-osx-font-smoothing': 'grayscale' + }, + '#overlay.mini': { + 'height': '0', + 'overflow': 'visible' + }, + '#overlay a': { + 'width': 'initial', + 'padding': '0', + 'text-decoration': 'none', + 'text-transform': 'none', + 'color': 'inherit' + }, + '#bgwrapper': { + 'position': 'relative', + 'width': '100%', + 'height': '100%' + }, + '#bg': { + 'position': 'fixed', + 'top': '0', + 'left': '0', + 'width': '100%', + 'height': '100%', + 'min-width': this.doc_width * 4 + 'px', + 'min-height': this.doc_height * 4 + 'px', + 'background-color': 'black', + 'opacity': '0.0', + '-ms-filter': 'progid:DXImageTransform.Microsoft.Alpha(Opacity=60)', // IE8 + 'filter': 'alpha(opacity=60)', // IE5-7 + 'transition': 'opacity ' + anim_seconds + }, + '#bg.visible': { + 'opacity': MPNotif.BG_OPACITY + }, + '.mini #bg': { + 'width': '0', + 'height': '0', + 'min-width': '0' + }, + '#flipcontainer': { + 'perspective': '1000px', + 'position': 'absolute', + 'width': '100%' + }, + '#flipper': { + 'position': 'relative', + 'transform-style': 'preserve-3d', + 'transition': '0.3s' + }, + '#takeover': { + 'position': 'absolute', + 'left': '50%', + 'width': MPNotif.NOTIF_WIDTH + 'px', + 'margin-left': Math.round(-MPNotif.NOTIF_WIDTH / 2) + 'px', + 'backface-visibility': 'hidden', + 'transform': 'rotateY(0deg)', + 'opacity': '0.0', + 'top': MPNotif.NOTIF_START_TOP + 'px', + 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds + }, + '#takeover.visible': { + 'opacity': '1.0', + 'top': MPNotif.NOTIF_TOP + 'px' + }, + '#takeover.exiting': { + 'opacity': '0.0', + 'top': MPNotif.NOTIF_START_TOP + 'px' + }, + '#thumbspacer': { + 'height': MPNotif.THUMB_OFFSET + 'px' + }, + '#thumbborder-wrapper': { + 'position': 'absolute', + 'top': (-MPNotif.THUMB_BORDER_SIZE) + 'px', + 'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET - MPNotif.THUMB_BORDER_SIZE) + 'px', + 'width': thumb_total_size + 'px', + 'height': (thumb_total_size / 2) + 'px', + 'overflow': 'hidden' + }, + '#thumbborder': { + 'position': 'absolute', + 'width': thumb_total_size + 'px', + 'height': thumb_total_size + 'px', + 'border-radius': thumb_total_size + 'px', + 'background-color': this.style_vals.bg_actions, + 'opacity': '0.5' + }, + '#thumbnail': { + 'position': 'absolute', + 'top': '0px', + 'left': (MPNotif.NOTIF_WIDTH / 2 - MPNotif.THUMB_OFFSET) + 'px', + 'width': MPNotif.THUMB_IMG_SIZE + 'px', + 'height': MPNotif.THUMB_IMG_SIZE + 'px', + 'overflow': 'hidden', + 'z-index': '100', + 'border-radius': MPNotif.THUMB_IMG_SIZE + 'px' + }, + '#mini': { + 'position': 'absolute', + 'right': '20px', + 'top': MPNotif.NOTIF_TOP + 'px', + 'width': this.notif_width + 'px', + 'height': MPNotif.NOTIF_HEIGHT_MINI * 2 + 'px', + 'margin-top': 20 - MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'backface-visibility': 'hidden', + 'opacity': '0.0', + 'transform': 'rotateX(90deg)', + 'transition': 'opacity 0.3s, transform 0.3s, right 0.3s' + }, + '#mini.visible': { + 'opacity': '1.0', + 'transform': 'rotateX(0deg)' + }, + '#mini.exiting': { + 'opacity': '0.0', + 'right': '-150px' + }, + '#mainbox': { + 'border-radius': '4px', + 'box-shadow': shadow, + 'text-align': 'center', + 'background-color': this.style_vals.bg, + 'font-size': '14px', + 'color': this.style_vals.text_main + }, + '#mini #mainbox': { + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'border-radius': '3px', + 'transition': 'background-color ' + anim_seconds + }, + '#mini-border': { + 'height': (MPNotif.NOTIF_HEIGHT_MINI + 6) + 'px', + 'width': (MPNotif.NOTIF_WIDTH_MINI + 6) + 'px', + 'position': 'absolute', + 'top': '-3px', + 'left': '-3px', + 'margin-top': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'border-radius': '6px', + 'opacity': '0.25', + 'background-color': '#fff', + 'z-index': '-1', + 'box-shadow': mini_shadow + }, + '#mini-icon': { + 'position': 'relative', + 'display': 'inline-block', + 'width': '75px', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'border-radius': '3px 0 0 3px', + 'background-color': this.style_vals.bg_actions, + 'background': 'linear-gradient(135deg, ' + this.style_vals.bg_light + ' 0%, ' + this.style_vals.bg_actions + ' 100%)', + 'transition': 'background-color ' + anim_seconds + }, + '#mini:hover #mini-icon': { + 'background-color': this.style_vals.mini_hover + }, + '#mini:hover #mainbox': { + 'background-color': this.style_vals.mini_hover + }, + '#mini-icon-img': { + 'position': 'absolute', + 'background-image': 'url(' + this.thumb_image_url + ')', + 'width': '48px', + 'height': '48px', + 'top': '20px', + 'left': '12px' + }, + '#content': { + 'padding': '30px 20px 0px 20px' + }, + '#mini-content': { + 'text-align': 'left', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'cursor': 'pointer' + }, + '#img': { + 'width': '328px', + 'margin-top': '30px', + 'border-radius': '5px' + }, + '#title': { + 'max-height': '600px', + 'overflow': 'hidden', + 'word-wrap': 'break-word', + 'padding': '25px 0px 20px 0px', + 'font-size': '19px', + 'font-weight': 'bold', + 'color': this.style_vals.text_title + }, + '#body': { + 'max-height': '600px', + 'margin-bottom': '25px', + 'overflow': 'hidden', + 'word-wrap': 'break-word', + 'line-height': '21px', + 'font-size': '15px', + 'font-weight': 'normal', + 'text-align': 'left' + }, + '#mini #body': { + 'display': 'inline-block', + 'max-width': '250px', + 'margin': '0 0 0 30px', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px', + 'font-size': '16px', + 'letter-spacing': '0.8px', + 'color': this.style_vals.text_title + }, + '#mini #body-text': { + 'display': 'table', + 'height': MPNotif.NOTIF_HEIGHT_MINI + 'px' + }, + '#mini #body-text div': { + 'display': 'table-cell', + 'vertical-align': 'middle' + }, + '#tagline': { + 'margin-bottom': '15px', + 'font-size': '10px', + 'font-weight': '600', + 'letter-spacing': '0.8px', + 'color': '#ccd7e0', + 'text-align': 'left' + }, + '#tagline a': { + 'color': this.style_vals.text_tagline, + 'transition': 'color ' + anim_seconds + }, + '#tagline a:hover': { + 'color': this.style_vals.text_hover + }, + '#cancel': { + 'position': 'absolute', + 'right': '0', + 'width': '8px', + 'height': '8px', + 'padding': '10px', + 'border-radius': '20px', + 'margin': '12px 12px 0 0', + 'box-sizing': 'content-box', + 'cursor': 'pointer', + 'transition': 'background-color ' + anim_seconds + }, + '#mini #cancel': { + 'margin': '7px 7px 0 0' + }, + '#cancel-icon': { + 'width': '8px', + 'height': '8px', + 'overflow': 'hidden', + 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/cancel-x.png)', + 'opacity': this.style_vals.cancel_opacity + }, + '#cancel:hover': { + 'background-color': this.style_vals.bg_hover + }, + '#button': { + 'display': 'block', + 'height': '60px', + 'line-height': '60px', + 'text-align': 'center', + 'background-color': this.style_vals.bg_actions, + 'border-radius': '0 0 4px 4px', + 'overflow': 'hidden', + 'cursor': 'pointer', + 'transition': 'background-color ' + anim_seconds + }, + '#button-close': { + 'display': 'inline-block', + 'width': '9px', + 'height': '60px', + 'margin-right': '8px', + 'vertical-align': 'top', + 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/close-x-' + this.style + '.png)', + 'background-repeat': 'no-repeat', + 'background-position': '0px 25px' + }, + '#button-play': { + 'display': 'inline-block', + 'width': '30px', + 'height': '60px', + 'margin-left': '15px', + 'background-image': 'url(//cdn.mxpnl.com/site_media/images/icons/notifications/play-' + this.style + '-small.png)', + 'background-repeat': 'no-repeat', + 'background-position': '0px 15px' + }, + 'a#button-link': { + 'display': 'inline-block', + 'vertical-align': 'top', + 'text-align': 'center', + 'font-size': '17px', + 'font-weight': 'bold', + 'overflow': 'hidden', + 'word-wrap': 'break-word', + 'color': this.style_vals.text_title, + 'transition': 'color ' + anim_seconds + }, + '#button:hover': { + 'background-color': this.style_vals.bg_hover, + 'color': this.style_vals.text_hover + }, + '#button:hover a': { + 'color': this.style_vals.text_hover + }, + + '#video-noflip': { + 'position': 'relative', + 'top': (-this.video_height * 2) + 'px' + }, + '#video-flip': { + 'backface-visibility': 'hidden', + 'transform': 'rotateY(180deg)' + }, + '#video': { + 'position': 'absolute', + 'width': (this.video_width - 1) + 'px', + 'height': this.video_height + 'px', + 'top': MPNotif.NOTIF_TOP + 'px', + 'margin-top': '100px', + 'left': '50%', + 'margin-left': Math.round(-this.video_width / 2) + 'px', + 'overflow': 'hidden', + 'border-radius': '5px', + 'box-shadow': video_shadow, + 'transform': 'translateZ(1px)', // webkit rendering bug http://stackoverflow.com/questions/18167981/clickable-link-area-unexpectedly-smaller-after-css-transform + 'transition': 'opacity ' + anim_seconds + ', top ' + anim_seconds + }, + '#video.exiting': { + 'opacity': '0.0', + 'top': this.video_height + 'px' + }, + '#video-holder': { + 'position': 'absolute', + 'width': (this.video_width - 1) + 'px', + 'height': this.video_height + 'px', + 'overflow': 'hidden', + 'border-radius': '5px' + }, + '#video-frame': { + 'margin-left': '-1px', + 'width': this.video_width + 'px' + }, + '#video-controls': { + 'opacity': '0', + 'transition': 'opacity 0.5s' + }, + '#video:hover #video-controls': { + 'opacity': '1.0' + }, + '#video .video-progress-el': { + 'position': 'absolute', + 'bottom': '0', + 'height': '25px', + 'border-radius': '0 0 0 5px' + }, + '#video-progress': { + 'width': '90%' + }, + '#video-progress-total': { + 'width': '100%', + 'background-color': this.style_vals.bg, + 'opacity': '0.7' + }, + '#video-elapsed': { + 'width': '0', + 'background-color': '#6cb6f5', + 'opacity': '0.9' + }, + '#video #video-time': { + 'width': '10%', + 'right': '0', + 'font-size': '11px', + 'line-height': '25px', + 'color': this.style_vals.text_main, + 'background-color': '#666', + 'border-radius': '0 0 5px 0' + } + }; + + // IE hacks + if (this._browser_lte('ie', 8)) { + _.extend(notif_styles, { + '* html #overlay': { + 'position': 'absolute' + }, + '* html #bg': { + 'position': 'absolute' + }, + 'html, body': { + 'height': '100%' + } + }); + } + if (this._browser_lte('ie', 7)) { + _.extend(notif_styles, { + '#mini #body': { + 'display': 'inline', + 'zoom': '1', + 'border': '1px solid ' + this.style_vals.bg_hover + }, + '#mini #body-text': { + 'padding': '20px' + }, + '#mini #mini-icon': { + 'display': 'none' + } + }); + } + + // add vendor-prefixed style rules + var VENDOR_STYLES = ['backface-visibility', 'border-radius', 'box-shadow', 'opacity', + 'perspective', 'transform', 'transform-style', 'transition'], + VENDOR_PREFIXES = ['khtml', 'moz', 'ms', 'o', 'webkit']; + for (var selector in notif_styles) { + for (var si = 0; si < VENDOR_STYLES.length; si++) { + var prop = VENDOR_STYLES[si]; + if (prop in notif_styles[selector]) { + var val = notif_styles[selector][prop]; + for (var pi = 0; pi < VENDOR_PREFIXES.length; pi++) { + notif_styles[selector]['-' + VENDOR_PREFIXES[pi] + '-' + prop] = val; + } + } + } + } + + var inject_styles = function(styles, media_queries) { + var create_style_text = function(style_defs) { + var st = ''; + for (var selector in style_defs) { + var mp_selector = selector + .replace(/#/g, '#' + MPNotif.MARKUP_PREFIX + '-') + .replace(/\./g, '.' + MPNotif.MARKUP_PREFIX + '-'); + st += '\n' + mp_selector + ' {'; + var props = style_defs[selector]; + for (var k in props) { + st += k + ':' + props[k] + ';'; + } + st += '}'; + } + return st; + }; + var create_media_query_text = function(mq_defs) { + var mqt = ''; + for (var mq in mq_defs) { + mqt += '\n' + mq + ' {' + create_style_text(mq_defs[mq]) + '\n}'; + } + return mqt; + }; + + var style_text = create_style_text(styles) + create_media_query_text(media_queries), + head_el = document$1.head || document$1.getElementsByTagName('head')[0] || document$1.documentElement, + style_el = document$1.createElement('style'); + head_el.appendChild(style_el); + style_el.setAttribute('type', 'text/css'); + if (style_el.styleSheet) { // IE + style_el.styleSheet.cssText = style_text; + } else { + style_el.textContent = style_text; + } + }; + inject_styles(notif_styles, notif_media_queries); + }; + + MPNotif.prototype._init_video = _.safewrap(function() { + if (!this.video_url) { + return; + } + var self = this; + + // Youtube iframe API compatibility + self.yt_custom = 'postMessage' in win; + + self.dest_url = self.video_url; + var youtube_match = self.video_url.match( + // http://stackoverflow.com/questions/2936467/parse-youtube-video-id-using-preg-match + /(?:youtube(?:-nocookie)?\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i + ), + vimeo_match = self.video_url.match( + /vimeo\.com\/.*?(\d+)/i + ); + if (youtube_match) { + self.show_video = true; + self.youtube_video = youtube_match[1]; + + if (self.yt_custom) { + win['onYouTubeIframeAPIReady'] = function() { + if (self._get_el('video-frame')) { + self._yt_video_ready(); + } + }; + + // load Youtube iframe API; see https://developers.google.com/youtube/iframe_api_reference + var tag = document$1.createElement('script'); + tag.src = '//www.youtube.com/iframe_api'; + var firstScriptTag = document$1.getElementsByTagName('script')[0]; + firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); + } + } else if (vimeo_match) { + self.show_video = true; + self.vimeo_video = vimeo_match[1]; + } + + // IE <= 7, FF <= 3: fall through to video link rather than embedded player + if (self._browser_lte('ie', 7) || self._browser_lte('firefox', 3)) { + self.show_video = false; + self.clickthrough = true; + } + }); + + MPNotif.prototype._mark_as_shown = _.safewrap(function() { + // click on background to dismiss + var self = this; + _.register_event(self._get_el('bg'), 'click', function() { + self.dismiss(); + }); + + var get_style = function(el, style_name) { + var styles = {}; + if (document$1.defaultView && document$1.defaultView.getComputedStyle) { + styles = document$1.defaultView.getComputedStyle(el, null); // FF3 requires both args + } else if (el.currentStyle) { // IE + styles = el.currentStyle; + } + return styles[style_name]; + }; + + if (this.campaign_id) { + var notif_el = this._get_el('overlay'); + if (notif_el && get_style(notif_el, 'visibility') !== 'hidden' && get_style(notif_el, 'display') !== 'none') { + this._mark_delivery(); + } + } + }); + + MPNotif.prototype._mark_delivery = _.safewrap(function(extra_props) { + if (!this.marked_as_shown) { + this.marked_as_shown = true; + + if (this.campaign_id) { + // mark notification shown (local cache) + this._get_shown_campaigns()[this.campaign_id] = 1 * new Date(); + this.persistence.save(); + } + + // track delivery + this._track_event('$campaign_delivery', extra_props); + + // mark notification shown (mixpanel property) + this.mixpanel['people']['append']({ + '$campaigns': this.campaign_id, + '$notifications': { + 'campaign_id': this.campaign_id, + 'message_id': this.message_id, + 'type': 'web', + 'time': new Date() + } + }); + } + }); + + MPNotif.prototype._preload_images = function(all_loaded_cb) { + var self = this; + if (this.imgs_to_preload.length === 0) { + all_loaded_cb(); + return; + } + + var preloaded_imgs = 0; + var img_objs = []; + var onload = function() { + preloaded_imgs++; + if (preloaded_imgs === self.imgs_to_preload.length && all_loaded_cb) { + all_loaded_cb(); + all_loaded_cb = null; + } + }; + for (var i = 0; i < this.imgs_to_preload.length; i++) { + var img = new Image(); + img.onload = onload; + img.src = this.imgs_to_preload[i]; + if (img.complete) { + onload(); + } + img_objs.push(img); + } + + // IE6/7 doesn't fire onload reliably + if (this._browser_lte('ie', 7)) { + setTimeout(function() { + var imgs_loaded = true; + for (i = 0; i < img_objs.length; i++) { + if (!img_objs[i].complete) { + imgs_loaded = false; + } + } + if (imgs_loaded && all_loaded_cb) { + all_loaded_cb(); + all_loaded_cb = null; + } + }, 500); + } + }; + + MPNotif.prototype._remove_notification_el = _.safewrap(function() { + win.clearInterval(this._video_progress_checker); + this.notification_el.style.visibility = 'hidden'; + this.body_el.removeChild(this.notification_el); + }); + + MPNotif.prototype._set_client_config = function() { + var get_browser_version = function(browser_ex) { + var match = navigator.userAgent.match(browser_ex); + return match && match[1]; + }; + this.browser_versions = {}; + this.browser_versions['chrome'] = get_browser_version(/Chrome\/(\d+)/); + this.browser_versions['firefox'] = get_browser_version(/Firefox\/(\d+)/); + this.browser_versions['ie'] = get_browser_version(/MSIE (\d+).+/); + if (!this.browser_versions['ie'] && !(win.ActiveXObject) && 'ActiveXObject' in win) { + this.browser_versions['ie'] = 11; + } + + this.body_el = document$1.body || document$1.getElementsByTagName('body')[0]; + if (this.body_el) { + this.doc_width = Math.max( + this.body_el.scrollWidth, document$1.documentElement.scrollWidth, + this.body_el.offsetWidth, document$1.documentElement.offsetWidth, + this.body_el.clientWidth, document$1.documentElement.clientWidth + ); + this.doc_height = Math.max( + this.body_el.scrollHeight, document$1.documentElement.scrollHeight, + this.body_el.offsetHeight, document$1.documentElement.offsetHeight, + this.body_el.clientHeight, document$1.documentElement.clientHeight + ); + } + + // detect CSS compatibility + var ie_ver = this.browser_versions['ie']; + var sample_styles = document$1.createElement('div').style, + is_css_compatible = function(rule) { + if (rule in sample_styles) { + return true; + } + if (!ie_ver) { + rule = rule[0].toUpperCase() + rule.slice(1); + var props = ['O' + rule, 'Webkit' + rule, 'Moz' + rule]; + for (var i = 0; i < props.length; i++) { + if (props[i] in sample_styles) { + return true; + } + } + } + return false; + }; + this.use_transitions = this.body_el && + is_css_compatible('transition') && + is_css_compatible('transform'); + this.flip_animate = (this.browser_versions['chrome'] >= 33 || this.browser_versions['firefox'] >= 15) && + this.body_el && + is_css_compatible('backfaceVisibility') && + is_css_compatible('perspective') && + is_css_compatible('transform'); + }; + + MPNotif.prototype._switch_to_video = _.safewrap(function() { + var self = this, + anims = [ + { + el: self._get_notification_display_el(), + attr: 'opacity', + start: 1.0, + goal: 0.0 + }, + { + el: self._get_notification_display_el(), + attr: 'top', + start: MPNotif.NOTIF_TOP, + goal: -500 + }, + { + el: self._get_el('video-noflip'), + attr: 'opacity', + start: 0.0, + goal: 1.0 + }, + { + el: self._get_el('video-noflip'), + attr: 'top', + start: -self.video_height * 2, + goal: 0 + } + ]; + + if (self.mini) { + var bg = self._get_el('bg'), + overlay = self._get_el('overlay'); + bg.style.width = '100%'; + bg.style.height = '100%'; + overlay.style.width = '100%'; + + self._add_class(self._get_notification_display_el(), 'exiting'); + self._add_class(bg, 'visible'); + + anims.push({ + el: self._get_el('bg'), + attr: 'opacity', + start: 0.0, + goal: MPNotif.BG_OPACITY + }); + } + + var video_el = self._get_el('video-holder'); + video_el.innerHTML = self.video_iframe; + + var video_ready = function() { + if (win['YT'] && win['YT']['loaded']) { + self._yt_video_ready(); + } + self.showing_video = true; + self._get_notification_display_el().style.visibility = 'hidden'; + }; + if (self.flip_animate) { + self._add_class('flipper', 'flipped'); + setTimeout(video_ready, MPNotif.ANIM_TIME); + } else { + self._animate_els(anims, MPNotif.ANIM_TIME, video_ready); + } + }); + + MPNotif.prototype._track_event = function(event_name, properties, cb) { + if (this.campaign_id) { + properties = properties || {}; + properties = _.extend(properties, { + 'campaign_id': this.campaign_id, + 'message_id': this.message_id, + 'message_type': 'web_inapp', + 'message_subtype': this.notif_type + }); + this.mixpanel['track'](event_name, properties, cb); + } else if (cb) { + cb.call(); + } + }; + + MPNotif.prototype._yt_video_ready = _.safewrap(function() { + var self = this; + if (self.video_inited) { + return; + } + self.video_inited = true; + + var progress_bar = self._get_el('video-elapsed'), + progress_time = self._get_el('video-time'), + progress_el = self._get_el('video-progress'); + + new win['YT']['Player'](MPNotif.MARKUP_PREFIX + '-video-frame', { + 'events': { + 'onReady': function(event) { + var ytplayer = event['target'], + video_duration = ytplayer['getDuration'](), + pad = function(i) { + return ('00' + i).slice(-2); + }, + update_video_time = function(current_time) { + var secs = Math.round(video_duration - current_time), + mins = Math.floor(secs / 60), + hours = Math.floor(mins / 60); + secs -= mins * 60; + mins -= hours * 60; + progress_time.innerHTML = '-' + (hours ? hours + ':' : '') + pad(mins) + ':' + pad(secs); + }; + update_video_time(0); + self._video_progress_checker = win.setInterval(function() { + var current_time = ytplayer['getCurrentTime'](); + progress_bar.style.width = (current_time / video_duration * 100) + '%'; + update_video_time(current_time); + }, 250); + _.register_event(progress_el, 'click', function(e) { + var clickx = Math.max(0, e.pageX - progress_el.getBoundingClientRect().left); + ytplayer['seekTo'](video_duration * clickx / progress_el.clientWidth, true); + }); + } + } + }); + }); + + // EXPORTS (for closure compiler) + + // MixpanelLib Exports + MixpanelLib.prototype['init'] = MixpanelLib.prototype.init; + MixpanelLib.prototype['reset'] = MixpanelLib.prototype.reset; + MixpanelLib.prototype['disable'] = MixpanelLib.prototype.disable; + MixpanelLib.prototype['time_event'] = MixpanelLib.prototype.time_event; + MixpanelLib.prototype['track'] = MixpanelLib.prototype.track; + MixpanelLib.prototype['track_links'] = MixpanelLib.prototype.track_links; + MixpanelLib.prototype['track_forms'] = MixpanelLib.prototype.track_forms; + MixpanelLib.prototype['track_pageview'] = MixpanelLib.prototype.track_pageview; + MixpanelLib.prototype['register'] = MixpanelLib.prototype.register; + MixpanelLib.prototype['register_once'] = MixpanelLib.prototype.register_once; + MixpanelLib.prototype['unregister'] = MixpanelLib.prototype.unregister; + MixpanelLib.prototype['identify'] = MixpanelLib.prototype.identify; + MixpanelLib.prototype['alias'] = MixpanelLib.prototype.alias; + MixpanelLib.prototype['name_tag'] = MixpanelLib.prototype.name_tag; + MixpanelLib.prototype['set_config'] = MixpanelLib.prototype.set_config; + MixpanelLib.prototype['get_config'] = MixpanelLib.prototype.get_config; + MixpanelLib.prototype['get_property'] = MixpanelLib.prototype.get_property; + MixpanelLib.prototype['get_distinct_id'] = MixpanelLib.prototype.get_distinct_id; + MixpanelLib.prototype['toString'] = MixpanelLib.prototype.toString; + MixpanelLib.prototype['_check_and_handle_notifications'] = MixpanelLib.prototype._check_and_handle_notifications; + MixpanelLib.prototype['_show_notification'] = MixpanelLib.prototype._show_notification; + + // MixpanelPersistence Exports + MixpanelPersistence.prototype['properties'] = MixpanelPersistence.prototype.properties; + MixpanelPersistence.prototype['update_search_keyword'] = MixpanelPersistence.prototype.update_search_keyword; + MixpanelPersistence.prototype['update_referrer_info'] = MixpanelPersistence.prototype.update_referrer_info; + MixpanelPersistence.prototype['get_cross_subdomain'] = MixpanelPersistence.prototype.get_cross_subdomain; + MixpanelPersistence.prototype['clear'] = MixpanelPersistence.prototype.clear; + + // MixpanelPeople Exports + MixpanelPeople.prototype['set'] = MixpanelPeople.prototype.set; + MixpanelPeople.prototype['set_once'] = MixpanelPeople.prototype.set_once; + MixpanelPeople.prototype['unset'] = MixpanelPeople.prototype.unset; + MixpanelPeople.prototype['increment'] = MixpanelPeople.prototype.increment; + MixpanelPeople.prototype['append'] = MixpanelPeople.prototype.append; + MixpanelPeople.prototype['union'] = MixpanelPeople.prototype.union; + MixpanelPeople.prototype['track_charge'] = MixpanelPeople.prototype.track_charge; + MixpanelPeople.prototype['clear_charges'] = MixpanelPeople.prototype.clear_charges; + MixpanelPeople.prototype['delete_user'] = MixpanelPeople.prototype.delete_user; + MixpanelPeople.prototype['toString'] = MixpanelPeople.prototype.toString; + + _.safewrap_class(MixpanelLib, ['identify', '_check_and_handle_notifications', '_show_notification']); + + var instances = {}; + var extend_mp = function() { + // add all the sub mixpanel instances + _.each(instances, function(instance, name) { + if (name !== PRIMARY_INSTANCE_NAME) { mixpanel_master[name] = instance; } + }); + + // add private functions as _ + mixpanel_master['_'] = _; + }; + + var override_mp_init_func = function() { + // we override the snippets init function to handle the case where a + // user initializes the mixpanel library after the script loads & runs + mixpanel_master['init'] = function(token, config, name) { + if (name) { + // initialize a sub library + if (!mixpanel_master[name]) { + mixpanel_master[name] = instances[name] = create_mplib(token, config, name); + mixpanel_master[name]._loaded(); + } + return mixpanel_master[name]; + } else { + var instance = mixpanel_master; + + if (instances[PRIMARY_INSTANCE_NAME]) { + // main mixpanel lib already initialized + instance = instances[PRIMARY_INSTANCE_NAME]; + } else if (token) { + // intialize the main mixpanel lib + instance = create_mplib(token, config, PRIMARY_INSTANCE_NAME); + instance._loaded(); + instances[PRIMARY_INSTANCE_NAME] = instance; + } + + mixpanel_master = instance; + if (init_type === INIT_SNIPPET) { + win[PRIMARY_INSTANCE_NAME] = mixpanel_master; + } + extend_mp(); + } + }; + }; + + var add_dom_loaded_handler = function() { + // Cross browser DOM Loaded support + function dom_loaded_handler() { + // function flag since we only want to execute this once + if (dom_loaded_handler.done) { return; } + dom_loaded_handler.done = true; + + DOM_LOADED = true; + ENQUEUE_REQUESTS = false; + + _.each(instances, function(inst) { + inst._dom_loaded(); + }); + } + + function do_scroll_check() { + try { + document$1.documentElement.doScroll('left'); + } catch(e) { + setTimeout(do_scroll_check, 1); + return; + } + + dom_loaded_handler(); + } + + if (document$1.addEventListener) { + if (document$1.readyState === 'complete') { + // safari 4 can fire the DOMContentLoaded event before loading all + // external JS (including this file). you will see some copypasta + // on the internet that checks for 'complete' and 'loaded', but + // 'loaded' is an IE thing + dom_loaded_handler(); + } else { + document$1.addEventListener('DOMContentLoaded', dom_loaded_handler, false); + } + } else if (document$1.attachEvent) { + // IE + document$1.attachEvent('onreadystatechange', dom_loaded_handler); + + // check to make sure we arn't in a frame + var toplevel = false; + try { + toplevel = win.frameElement === null; + } catch(e) { + // noop + } + + if (document$1.documentElement.doScroll && toplevel) { + do_scroll_check(); + } + } + + // fallback handler, always will work + _.register_event(win, 'load', dom_loaded_handler, true); + }; + + var add_dom_event_counting_handlers = function(instance) { + var name = instance.get_config('name'); + + instance.mp_counts = instance.mp_counts || {}; + instance.mp_counts['$__c'] = parseInt(_.cookie.get('mp_' + name + '__c')) || 0; + + var increment_count = function() { + instance.mp_counts['$__c'] = (instance.mp_counts['$__c'] || 0) + 1; + _.cookie.set('mp_' + name + '__c', instance.mp_counts['$__c'], 1, true); + }; + + var evtCallback = function() { + try { + instance.mp_counts = instance.mp_counts || {}; + increment_count(); + } catch (e) { + console$1.error(e); + } + }; + _.register_event(document$1, 'submit', evtCallback); + _.register_event(document$1, 'change', evtCallback); + var mousedownTarget = null; + _.register_event(document$1, 'mousedown', function(e) { + mousedownTarget = e.target; + }); + _.register_event(document$1, 'mouseup', function(e) { + if (e.target === mousedownTarget) { + evtCallback(e); + } + }); + }; + + function init_from_snippet() { + init_type = INIT_SNIPPET; + mixpanel_master = win[PRIMARY_INSTANCE_NAME]; + + // Initialization + if (_.isUndefined(mixpanel_master)) { + // mixpanel wasn't initialized properly, report error and quit + console$1.critical('"mixpanel" object not initialized. Ensure you are using the latest version of the Mixpanel JS Library along with the snippet we provide.'); + return; + } + if (mixpanel_master['__loaded'] || (mixpanel_master['config'] && mixpanel_master['persistence'])) { + // lib has already been loaded at least once; we don't want to override the global object this time so bomb early + console$1.error('Mixpanel library has already been downloaded at least once.'); + return; + } + var snippet_version = mixpanel_master['__SV'] || 0; + if (snippet_version < 1.1) { + // mixpanel wasn't initialized properly, report error and quit + console$1.critical('Version mismatch; please ensure you\'re using the latest version of the Mixpanel code snippet.'); + return; + } + + // Load instances of the Mixpanel Library + _.each(mixpanel_master['_i'], function(item) { + if (item && _.isArray(item)) { + instances[item[item.length-1]] = create_mplib.apply(this, item); + } + }); + + override_mp_init_func(); + mixpanel_master['init'](); + + // Fire loaded events after updating the window's mixpanel object + _.each(instances, function(instance) { + instance._loaded(); + }); + + add_dom_loaded_handler(); + } + + init_from_snippet(); + +}()); diff --git a/web/webpack.config.js b/web/webpack.config.js index bb23a500..7b73ecc4 100644 --- a/web/webpack.config.js +++ b/web/webpack.config.js @@ -6,7 +6,8 @@ const ExtractTextPlugin = require("extract-text-webpack-plugin"); module.exports = { entry: { app: "./src/js/app.js", - courseRedactor: "./src/js/course-redactor.js" + courseRedactor: "./src/js/course-redactor.js", + mixpanel: "./src/js/third_party/mixpanel-2-latest.js" }, output: { path: path.join(__dirname, "build"), @@ -20,7 +21,7 @@ module.exports = { loaders: [ { test: /\.js$/, - exclude: /(node_modules|bower_components)/, + exclude: /(node_modules|bower_components|third_party)/, use: { loader: 'babel-loader', options: { @@ -28,6 +29,16 @@ module.exports = { } } }, + { + test: /third_party\/.*\.js$/, + exclude: /(node_modules|bower_components)/, + use: { + loader: 'file-loader', + options: { + name: "[name].[ext]" + } + } + }, { test: /\.css$/, use: ExtractTextPlugin.extract({ @@ -97,4 +108,4 @@ if (NODE_ENV === 'production') { } }) ); -} \ No newline at end of file +} diff --git a/web/yarn.lock b/web/yarn.lock index 0a35d84c..b8b74a0f 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -168,7 +168,7 @@ anymatch@^1.3.0: micromatch "^2.1.5" normalize-path "^2.0.0" -aproba@^1.0.3: +aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -260,7 +260,7 @@ arraybuffer.slice@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" -arrify@^1.0.0: +arrify@^1.0.0, arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" @@ -961,7 +961,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@^3.0.5, bluebird@^3.1.1: +bluebird@^3.0.5, bluebird@^3.1.1, bluebird@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -1139,6 +1139,10 @@ bs-recipes@1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/bs-recipes/-/bs-recipes-1.3.4.tgz#0d2d4d48a718c8c044769fdc4f89592dc8b69585" +buffer-from@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.0.0.tgz#4cb8832d23612589b0406e9e2956c17f06fdf531" + buffer-xor@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/buffer-xor/-/buffer-xor-1.0.3.tgz#26e61ed1422fb70dd42e6e36729ed51d855fe8d9" @@ -1165,6 +1169,24 @@ builtin-status-codes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" +cacache@^10.0.4: + version "10.0.4" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-10.0.4.tgz#6452367999eff9d4188aefd9a14e9d7c6a263460" + dependencies: + bluebird "^3.5.1" + chownr "^1.0.1" + glob "^7.1.2" + graceful-fs "^4.1.11" + lru-cache "^4.1.1" + mississippi "^2.0.0" + mkdirp "^0.5.1" + move-concurrently "^1.0.1" + promise-inflight "^1.0.1" + rimraf "^2.6.2" + ssri "^5.2.4" + unique-filename "^1.1.0" + y18n "^4.0.0" + cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" @@ -1323,6 +1345,10 @@ chokidar@1.7.0, chokidar@^1.7.0: optionalDependencies: fsevents "^1.0.0" +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" @@ -1503,6 +1529,15 @@ concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" +concat-stream@^1.5.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + config-chain@~1.1.5: version "1.1.11" resolved "https://registry.yarnpkg.com/config-chain/-/config-chain-1.1.11.tgz#aba09747dfbe4c3e70e766a6e41586e1859fc6f2" @@ -1562,10 +1597,34 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +copy-concurrently@^1.0.0: + version "1.0.5" + resolved "https://registry.yarnpkg.com/copy-concurrently/-/copy-concurrently-1.0.5.tgz#92297398cae34937fcafd6ec8139c18051f0b5e0" + dependencies: + aproba "^1.1.1" + fs-write-stream-atomic "^1.0.8" + iferr "^0.1.5" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.0" + copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" +copy-webpack-plugin@^4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.5.1.tgz#fc4f68f4add837cc5e13d111b20715793225d29c" + dependencies: + cacache "^10.0.4" + find-cache-dir "^1.0.0" + globby "^7.1.1" + is-glob "^4.0.0" + loader-utils "^1.1.0" + minimatch "^3.0.4" + p-limit "^1.0.0" + serialize-javascript "^1.4.0" + core-js@^2.4.0, core-js@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" @@ -1770,6 +1829,10 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +cyclist@~0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" @@ -1946,6 +2009,13 @@ diffie-hellman@^5.0.0: miller-rabin "^4.0.0" randombytes "^2.0.0" +dir-glob@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" + dependencies: + arrify "^1.0.1" + path-type "^3.0.0" + doctypes@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9" @@ -1999,6 +2069,15 @@ duplexer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" +duplexify@^3.4.2, duplexify@^3.5.3: + version "3.5.4" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.4.tgz#4bb46c1796eabebeec4ca9a2e66b808cb7a3d8b4" + dependencies: + end-of-stream "^1.0.0" + inherits "^2.0.1" + readable-stream "^2.0.0" + stream-shift "^1.0.0" + duplexify@^3.5.0: version "3.5.3" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.5.3.tgz#8b5818800df92fd0125b27ab896491912858243e" @@ -2068,7 +2147,7 @@ encodeurl@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" -end-of-stream@^1.0.0: +end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" dependencies: @@ -2495,6 +2574,13 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flush-write-stream@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.4" + follow-redirects@^1.2.5: version "1.4.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.4.1.tgz#d8120f4518190f55aac65bb6fc7b85fcd666d6aa" @@ -2559,6 +2645,13 @@ fresh@^0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" +from2@^2.1.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/from2/-/from2-2.3.0.tgz#8bfb5502bde4a4d36cfdeea007fcca21d7e382af" + dependencies: + inherits "^2.0.1" + readable-stream "^2.0.0" + from@~0: version "0.1.7" resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" @@ -2577,6 +2670,15 @@ fs-extra@3.0.1: jsonfile "^3.0.0" universalify "^0.1.0" +fs-write-stream-atomic@^1.0.8: + version "1.0.10" + resolved "https://registry.yarnpkg.com/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz#b47df53493ef911df75731e70a9ded0189db40c9" + dependencies: + graceful-fs "^4.1.2" + iferr "^0.1.5" + imurmurhash "^0.1.4" + readable-stream "1 || 2" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -2711,7 +2813,7 @@ glob@^5.0.12: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.2, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -2763,6 +2865,17 @@ globby@^5.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" +globby@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" + dependencies: + array-union "^1.0.1" + dir-glob "^2.0.0" + glob "^7.1.2" + ignore "^3.3.5" + pify "^3.0.0" + slash "^1.0.0" + globule@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/globule/-/globule-1.2.0.tgz#1dc49c6822dd9e8a2fa00ba2a295006e8664bd09" @@ -2785,7 +2898,7 @@ glogg@^1.0.0: dependencies: sparkles "^1.0.0" -graceful-fs@4.X, graceful-fs@^4.1.2, graceful-fs@^4.1.6: +graceful-fs@4.X, graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: version "4.1.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" @@ -3294,6 +3407,14 @@ ieee754@^1.1.4: version "1.1.8" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" +iferr@^0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/iferr/-/iferr-0.1.5.tgz#c60eed69e6d8fdb6b3104a1fcbca1c192dc5b501" + +ignore@^3.3.5: + version "3.3.7" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.7.tgz#612289bfb3c220e186a58118618d5be8c1bab021" + ilyabirman-likely@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/ilyabirman-likely/-/ilyabirman-likely-2.3.0.tgz#4462becc5dedeb36b74bf4ba339a0ceab820785f" @@ -3302,6 +3423,10 @@ immutable@3.8.2, immutable@^3.7.6: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + in-publish@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" @@ -3474,7 +3599,7 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -is-extglob@^2.1.0: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3506,6 +3631,12 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-number-like@^1.0.3: version "1.0.8" resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3" @@ -4411,6 +4542,21 @@ minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" +mississippi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/mississippi/-/mississippi-2.0.0.tgz#3442a508fafc28500486feea99409676e4ee5a6f" + dependencies: + concat-stream "^1.5.0" + duplexify "^3.4.2" + end-of-stream "^1.1.0" + flush-write-stream "^1.0.0" + from2 "^2.1.0" + parallel-transform "^1.1.0" + pump "^2.0.1" + pumpify "^1.3.3" + stream-each "^1.1.0" + through2 "^2.0.0" + mixin-deep@^1.2.0: version "1.3.0" resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.0.tgz#47a8732ba97799457c8c1eca28f95132d7e8150a" @@ -4435,6 +4581,17 @@ moment@^2.20.1: version "2.20.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.20.1.tgz#d6eb1a46cbcc14a2b2f9434112c1ff8907f313fd" +move-concurrently@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" + dependencies: + aproba "^1.1.1" + copy-concurrently "^1.0.0" + fs-write-stream-atomic "^1.0.8" + mkdirp "^0.5.1" + rimraf "^2.5.4" + run-queue "^1.0.3" + ms@0.7.1: version "0.7.1" resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" @@ -4744,7 +4901,7 @@ on-finished@~2.3.0: dependencies: ee-first "1.1.1" -once@^1.3.0, once@^1.3.3, once@^1.4.0: +once@^1.3.0, once@^1.3.1, once@^1.3.3, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" dependencies: @@ -4822,7 +4979,7 @@ p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" -p-limit@^1.1.0: +p-limit@^1.0.0, p-limit@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" dependencies: @@ -4842,6 +4999,14 @@ pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" +parallel-transform@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.1.0.tgz#d410f065b05da23081fcd10f28854c29bda33b06" + dependencies: + cyclist "~0.2.2" + inherits "^2.0.3" + readable-stream "^2.1.5" + parse-asn1@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.0.tgz#37c4f9b7ed3ab65c74817b5f2480937fbf97c712" @@ -4953,6 +5118,12 @@ path-type@^2.0.0: dependencies: pify "^2.0.0" +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + dependencies: + pify "^3.0.0" + pause-stream@0.0.11: version "0.0.11" resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" @@ -5324,10 +5495,18 @@ process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + process@^0.11.10: version "0.11.10" resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + promise@^7.0.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" @@ -5449,6 +5628,21 @@ pug-walk@^1.1.5: pug-runtime "^2.0.3" pug-strip-comments "^1.0.2" +pump@^2.0.0, pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pumpify@^1.3.3: + version "1.4.0" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.4.0.tgz#80b7c5df7e24153d03f0e7ac8a05a5d068bd07fb" + dependencies: + duplexify "^3.5.3" + inherits "^2.0.3" + pump "^2.0.0" + punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -5551,6 +5745,18 @@ read-pkg@^2.0.0: normalize-package-data "^2.3.2" path-type "^2.0.0" +"readable-stream@1 || 2", readable-stream@^2.0.4, readable-stream@^2.2.2: + version "2.3.5" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.5.tgz#b4f85003a938cbb6ecbce2a124fb1012bd1a838d" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.0.3" + util-deprecate "~1.0.1" + "readable-stream@>=1.0.33-1 <1.1.0-0", readable-stream@~1.0.17: version "1.0.34" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" @@ -5811,7 +6017,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2: version "2.6.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" dependencies: @@ -5824,6 +6030,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1: hash-base "^2.0.0" inherits "^2.0.1" +run-queue@^1.0.0, run-queue@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/run-queue/-/run-queue-1.0.3.tgz#e848396f057d223f24386924618e25694161ec47" + dependencies: + aproba "^1.1.1" + run-sequence@^1.1.5: version "1.2.2" resolved "https://registry.yarnpkg.com/run-sequence/-/run-sequence-1.2.2.tgz#5095a0bebe98733b0140bd08dd80ec030ddacdeb" @@ -5916,6 +6128,10 @@ sequencify@~0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/sequencify/-/sequencify-0.0.7.tgz#90cff19d02e07027fd767f5ead3e7b95d1e7380c" +serialize-javascript@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-1.4.0.tgz#7c958514db6ac2443a8abc062dc9f7886a7f6005" + serve-index@1.8.0: version "1.8.0" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.8.0.tgz#7c5d96c13fb131101f93c1c5774f8516a1e78d3b" @@ -6226,6 +6442,12 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +ssri@^5.2.4: + version "5.3.0" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-5.3.0.tgz#ba3872c9c6d33a0704a7d71ff045e5ec48999d06" + dependencies: + safe-buffer "^5.1.1" + static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" @@ -6258,6 +6480,13 @@ stream-consume@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stream-consume/-/stream-consume-0.1.0.tgz#a41ead1a6d6081ceb79f65b061901b6d8f3d1d0f" +stream-each@^1.1.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.2.tgz#8e8c463f91da8991778765873fe4d960d8f616bd" + dependencies: + end-of-stream "^1.1.0" + stream-shift "^1.0.0" + stream-http@^2.7.2: version "2.8.0" resolved "https://registry.yarnpkg.com/stream-http/-/stream-http-2.8.0.tgz#fd86546dac9b1c91aff8fc5d287b98fafb41bc10" @@ -6563,6 +6792,10 @@ tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + ua-parser-js@0.7.12: version "0.7.12" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb" @@ -6623,6 +6856,18 @@ uniqs@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02" +unique-filename@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.0.tgz#d05f2fe4032560871f30e93cbe735eea201514f3" + dependencies: + unique-slug "^2.0.0" + +unique-slug@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.0.tgz#db6676e7c7cc0629878ff196097c78855ae9f4ab" + dependencies: + imurmurhash "^0.1.4" + unique-stream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unique-stream/-/unique-stream-1.0.0.tgz#d59a4a75427447d9aa6c91e70263f8d26a4b104b" @@ -6988,6 +7233,10 @@ y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"