Browse Source

Merge pull request #1104 from pixelfed/frontend-ui-refactor

Frontend ui refactor
daniel 6 years ago
parent
commit
82102376fe
40 changed files with 1439 additions and 3184 deletions
  1. 3 1
      app/Http/Controllers/CommentController.php
  2. 7 2
      app/Http/Controllers/FederationController.php
  3. 56 37
      app/Http/Controllers/SearchController.php
  4. 1 1
      config/pixelfed.php
  5. BIN
      public/css/app.css
  6. BIN
      public/css/appdark.css
  7. BIN
      public/css/landing.css
  8. BIN
      public/js/app.js
  9. BIN
      public/js/components.js
  10. BIN
      public/js/compose.js
  11. BIN
      public/js/landing.js
  12. BIN
      public/js/profile.js
  13. BIN
      public/js/search.js
  14. BIN
      public/js/status.js
  15. BIN
      public/js/timeline.js
  16. BIN
      public/mix-manifest.json
  17. 0 2
      resources/assets/js/bootstrap.js
  18. 6 1
      resources/assets/js/components.js
  19. 0 314
      resources/assets/js/components/LandingPage.vue
  20. 335 36
      resources/assets/js/components/PostComponent.vue
  21. 68 17
      resources/assets/js/components/Profile.vue
  22. 46 28
      resources/assets/js/components/SearchResults.vue
  23. 0 71
      resources/assets/js/components/searchform.js
  24. 0 10
      resources/assets/js/landing.js
  25. 0 952
      resources/assets/js/lib/bloodhound.js
  26. 0 1674
      resources/assets/js/lib/typeahead.js
  27. 4 0
      resources/assets/js/search.js
  28. 0 10
      resources/assets/sass/custom.scss
  29. 13 0
      resources/assets/sass/landing.scss
  30. 126 0
      resources/assets/sass/landing/carousel.scss
  31. 593 0
      resources/assets/sass/landing/devices.scss
  32. 4 4
      resources/views/admin/settings/system.blade.php
  33. 8 3
      resources/views/layouts/partial/nav.blade.php
  34. 3 2
      resources/views/profile/show.blade.php
  35. 5 0
      resources/views/pxtv/home.blade.php
  36. 3 1
      resources/views/search/results.blade.php
  37. 150 8
      resources/views/site/index.blade.php
  38. 2 0
      routes/web.php
  39. 0 7
      tests/Feature/InstalledTest.php
  40. 6 3
      webpack.mix.js

+ 3 - 1
app/Http/Controllers/CommentController.php

@@ -9,6 +9,7 @@ use Cache;
 use App\Comment;
 use App\Jobs\CommentPipeline\CommentPipeline;
 use App\Jobs\StatusPipeline\NewStatusPipeline;
+use App\Util\Lexer\Autolink;
 use App\Profile;
 use App\Status;
 use League\Fractal;
@@ -53,10 +54,11 @@ class CommentController extends Controller
 
         Cache::forget('transform:status:'.$status->url());
 
+        $autolink = Autolink::create()->autolink($comment);
         $reply = new Status();
         $reply->profile_id = $profile->id;
         $reply->caption = e($comment);
-        $reply->rendered = $comment;
+        $reply->rendered = $autolink;
         $reply->in_reply_to_id = $status->id;
         $reply->in_reply_to_profile_id = $status->profile_id;
         $reply->save();

+ 7 - 2
app/Http/Controllers/FederationController.php

@@ -35,8 +35,8 @@ class FederationController extends Controller
     {
         $this->authCheck();
         $this->validate($request, [
-        'acct' => 'required|string|min:3|max:255',
-      ]);
+            'acct' => 'required|string|min:3|max:255',
+        ]);
         $acct = $request->input('acct');
         $nickname = Nickname::normalizeProfileUrl($acct);
 
@@ -63,6 +63,11 @@ class FederationController extends Controller
 
         $follower = Auth::user()->profile;
         $url = $request->input('url');
+        $url = Helpers::validateUrl($url);
+
+        if(!$url) {
+            return;
+        }
 
         RemoteFollowPipeline::dispatch($follower, $url);
 

+ 56 - 37
app/Http/Controllers/SearchController.php

@@ -9,6 +9,11 @@ use App\Status;
 use Illuminate\Http\Request;
 use App\Util\ActivityPub\Helpers;
 use Illuminate\Support\Facades\Cache;
+use App\Transformer\Api\{
+    AccountTransformer,
+    HashtagTransformer,
+    StatusTransformer,
+};
 
 class SearchController extends Controller
 {
@@ -22,26 +27,33 @@ class SearchController extends Controller
         if(mb_strlen($tag) < 3) {
             return;
         }
+        $tag = e(urldecode($tag));
+
         $hash = hash('sha256', $tag);
         $tokens = Cache::remember('api:search:tag:'.$hash, now()->addMinutes(5), function () use ($tag) {
-            $tokens = collect([]);
-            if(Helpers::validateUrl($tag)) {
+            $tokens = [];
+            if(Helpers::validateUrl($tag) != false) {
                 $remote = Helpers::fetchFromUrl($tag);
                 if(isset($remote['type']) && in_array($remote['type'], ['Create', 'Person']) == true) {
                     $type = $remote['type'];
                     if($type == 'Person') {
                         $item = Helpers::profileFirstOrNew($tag);
-                        $tokens->push([[
+                        $tokens['profiles'] = [[
                             'count'  => 1,
                             'url'    => $item->url(),
                             'type'   => 'profile',
                             'value'  => $item->username,
                             'tokens' => [$item->username],
                             'name'   => $item->name,
-                        ]]);
+                            'entity' => [
+                                'id' => $item->id,
+                                'following' => $item->followedBy(Auth::user()->profile),
+                                'thumb' => $item->avatarUrl()
+                            ]
+                        ]];
                     } else if ($type == 'Create') {
                         $item = Helpers::statusFirstOrFetch($tag, false);
-                        $tokens->push([[
+                        $tokens['posts'] = [[
                             'count'  => 0,
                             'url'    => $item->url(),
                             'type'   => 'status',
@@ -49,10 +61,9 @@ class SearchController extends Controller
                             'tokens' => [$item->caption],
                             'name'   => $item->caption,
                             'thumb'  => $item->thumb(),
-                        ]]);
+                        ]];
                     }
                 }
-
             }
             $hashtags = Hashtag::select('id', 'name', 'slug')->where('slug', 'like', '%'.$tag.'%')->whereHas('posts')->limit(20)->get();
             if($hashtags->count() > 0) {
@@ -62,37 +73,46 @@ class SearchController extends Controller
                         'url'    => $item->url(),
                         'type'   => 'hashtag',
                         'value'  => $item->name,
-                        'tokens' => explode('-', $item->name),
+                        'tokens' => '',
                         'name'   => null,
                     ];
                 });
-                $tokens->push($tags);
-            }
-            $users = Profile::select('username', 'name', 'id')
-                ->whereNull('status')
-                ->whereNull('domain')
-                ->where('username', 'like', '%'.$tag.'%')
-                //->orWhere('remote_url', $tag)
-                ->limit(20)
-                ->get();
-
-            if($users->count() > 0) {
-                $profiles = $users->map(function ($item, $key) {
-                    return [
-                        'count'  => 0,
-                        'url'    => $item->url(),
-                        'type'   => 'profile',
-                        'value'  => $item->username,
-                        'tokens' => [$item->username],
-                        'name'   => $item->name,
-                        'id'     =>  $item->id
-                    ];
-                });
-                $tokens->push($profiles);
+                $tokens['hashtags'] = $tags;
             }
-
             return $tokens;
         });
+        $users = Profile::select('username', 'name', 'id')
+            ->whereNull('status')
+            ->where('id', '!=', Auth::user()->profile->id)
+            ->where('username', 'like', '%'.$tag.'%')
+            ->orWhere('remote_url', $tag)
+            ->limit(20)
+            ->get();
+
+        if($users->count() > 0) {
+            $profiles = $users->map(function ($item, $key) {
+                return [
+                    'count'  => 0,
+                    'url'    => $item->url(),
+                    'type'   => 'profile',
+                    'value'  => $item->username,
+                    'tokens' => [$item->username],
+                    'name'   => $item->name,
+                    'avatar' => $item->avatarUrl(),
+                    'id'     =>  $item->id,
+                    'entity' => [
+                        'id' => $item->id,
+                        'following' => $item->followedBy(Auth::user()->profile),
+                        'thumb' => $item->avatarUrl()
+                    ]
+                ];
+            });
+            if(isset($tokens['profiles'])) {
+                array_push($tokens['profiles'], $profiles);
+            } else {
+                $tokens['profiles'] = $profiles;
+            }
+        }
         $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
                     ->whereHas('media')
                     ->whereNull('in_reply_to_id')
@@ -100,7 +120,8 @@ class SearchController extends Controller
                     ->whereProfileId(Auth::user()->profile->id)
                     ->where('caption', 'like', '%'.$tag.'%')
                     ->orWhere('uri', $tag)
-                    ->orderBy('created_at', 'desc')
+                    ->latest()
+                    ->limit(10)
                     ->get();
 
         if($posts->count() > 0) {
@@ -115,11 +136,9 @@ class SearchController extends Controller
                     'thumb'  => $item->thumb(),
                 ];
             });
-            $tokens = $tokens->push($posts);
-        }
-        if($tokens->count() > 0) {
-            $tokens = $tokens[0];
+            $tokens['posts'] = $posts;
         }
+
         return response()->json($tokens);
     }
 

+ 1 - 1
config/pixelfed.php

@@ -23,7 +23,7 @@ return [
     | This value is the version of your PixelFed instance.
     |
     */
-    'version' => '0.8.4',
+    'version' => '0.8.5',
 
     /*
     |--------------------------------------------------------------------------

BIN
public/css/app.css


BIN
public/css/appdark.css


BIN
public/css/landing.css


BIN
public/js/app.js


BIN
public/js/components.js


BIN
public/js/compose.js


BIN
public/js/landing.js


BIN
public/js/profile.js


BIN
public/js/search.js


BIN
public/js/status.js


BIN
public/js/timeline.js


BIN
public/mix-manifest.json


+ 0 - 2
resources/assets/js/bootstrap.js

@@ -3,8 +3,6 @@ window.Popper = require('popper.js').default;
 window.pixelfed = window.pixelfed || {};
 window.$ = window.jQuery = require('jquery');
 require('bootstrap');
-window.typeahead = require('./lib/typeahead');
-window.Bloodhound = require('./lib/bloodhound');
 window.axios = require('axios');
 window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 require('readmore-js');

+ 6 - 1
resources/assets/js/components.js

@@ -38,7 +38,7 @@ import swal from 'sweetalert';
 
 // require('./components/localstorage');
 // require('./components/commentform');
-require('./components/searchform');
+//require('./components/searchform');
 // require('./components/bookmarkform');
 // require('./components/statusform');
 //require('./components/embed');
@@ -63,6 +63,11 @@ require('./components/searchform');
 // Initialize Notification Helper
 window.pixelfed.n = {};
 
+// Vue.component(
+//     'search-results',
+//     require('./components/SearchResults.vue').default
+// );
+
 // Vue.component(
 //     'photo-presenter',
 //     require('./components/presenter/PhotoPresenter.vue').default

File diff suppressed because it is too large
+ 0 - 314
resources/assets/js/components/LandingPage.vue


+ 335 - 36
resources/assets/js/components/PostComponent.vue

@@ -1,18 +1,3 @@
-<style scoped>
-.status-comments,
-.reactions,
-.col-md-4 {
-  background: #fff;
-}
-.postPresenterContainer {
-  background: #fff;
-}
-@media(min-width: 720px) {
-  .postPresenterContainer {
-    min-height: 600px;
-  }
-}
-</style>
 <template>
 <div class="postComponent d-none">
   <div class="container px-0">
@@ -40,7 +25,7 @@
                   <a class="dropdown-item font-weight-bold" v-on:click="blockProfile()">Block Profile</a>
                 </div>
                 <div v-if="ownerOrAdmin()">
-                  <!-- <a class="dropdown-item font-weight-bold" :href="editUrl()">Disable Comments</a> -->
+                  <a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
                   <a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
                   <a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost(status)">Delete</a>
                 </div>
@@ -92,19 +77,18 @@
             </a>
               <div class="float-right">
                 <div class="post-actions">
-                <div class="dropdown">
+                <div v-if="user != false" class="dropdown">
                   <button class="btn btn-link text-dark no-caret dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" title="Post options">
                   <span class="fas fa-ellipsis-v text-muted"></span>
                   </button>
                       <div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuButton">
-                        <span class="menu-user d-none">
+                        <span v-if="!owner()">
                           <a class="dropdown-item font-weight-bold" :href="reportUrl()">Report</a>
                           <a class="dropdown-item font-weight-bold" v-on:click="muteProfile">Mute Profile</a>
                           <a class="dropdown-item font-weight-bold" v-on:click="blockProfile">Block Profile</a>
                         </span>
-                        <span class="menu-author d-none">
-                          <!-- <a class="dropdown-item font-weight-bold" :href="editUrl()">Mute Comments</a>
-                          <a class="dropdown-item font-weight-bold" :href="editUrl()">Disable Comments</a> -->
+                        <span v-if="ownerOrAdmin()">
+                          <a class="dropdown-item font-weight-bold" href="#" v-on:click.prevent="toggleCommentVisibility">{{ showComments ? 'Disable' : 'Enable'}} Comments</a>
                           <a class="dropdown-item font-weight-bold" :href="editUrl()">Edit</a>
                           <a class="dropdown-item font-weight-bold text-danger" v-on:click="deletePost">Delete</a>
                         </span>
@@ -114,19 +98,49 @@
               </div>
           </div>
           <div class="d-flex flex-md-column flex-column-reverse h-100">
-            <div class="card-body status-comments">
+            <div class="card-body status-comments pb-5">
               <div class="status-comment">
                 <p class="mb-1 read-more" style="overflow: hidden;">
                   <span class="font-weight-bold pr-1">{{statusUsername}}</span>
                   <span class="comment-text" :id="status.id + '-status-readmore'" v-html="status.content"></span>
                 </p>
-                <post-comments :user="this.user" :post-id="statusId" :post-username="statusUsername"></post-comments>
+
+                <div v-if="showComments">
+                  <div class="postCommentsLoader text-center">
+                    <div class="spinner-border" role="status">
+                      <span class="sr-only">Loading...</span>
+                    </div>
+                  </div>
+                  <div class="postCommentsContainer d-none pt-3">
+                    <p class="mb-1 text-center load-more-link d-none"><a href="#" class="text-muted" v-on:click="loadMore">Load more comments</a></p>
+                    <div class="comments" data-min-id="0" data-max-id="0">
+                      <div v-for="(reply, index) in results" class="pb-3">
+                        <p class="d-flex justify-content-between align-items-top read-more" style="overflow-y: hidden;">
+                          <span>
+                            <a class="text-dark font-weight-bold mr-1" :href="reply.account.url" v-bind:title="reply.account.username">{{truncate(reply.account.username,15)}}</a>
+                            <span class="text-break" v-html="reply.content"></span>
+                          </span>
+                          <span class="pl-2" style="min-width:38px">
+                            <span v-on:click="likeReply(reply, $event)"><i v-bind:class="[reply.favourited ? 'fas fa-heart fa-sm text-danger':'far fa-heart fa-sm text-lighter']"></i></span>
+                              <post-menu :status="reply" :profile="user" :size="'sm'" :modal="'true'" class="d-inline-block pl-2" v-on:deletePost="deleteComment(reply.id, index)"></post-menu>
+                          </span>
+                        </p>
+                        <p class="">
+                          <span class="text-muted mr-3" style="width: 20px;" v-text="timeAgo(reply.created_at)"></span>
+                          <span v-if="reply.favourites_count" class="text-muted comment-reaction font-weight-bold mr-3">{{reply.favourites_count == 1 ? '1 like' : reply.favourites_count + ' likes'}}</span>
+                          <span class="text-muted comment-reaction font-weight-bold cursor-pointer" v-on:click="replyFocus(reply)">Reply</span>
+                        </p>
+                      </div>
+                    </div>
+                  </div>
+                </div>
+
               </div>
             </div>
             <div class="card-body flex-grow-0 py-1">
               <div class="reactions my-1">
                 <h3 v-bind:class="[reactions.liked ? 'fas fa-heart text-danger pr-3 m-0 cursor-pointer' : 'far fa-heart pr-3 m-0 like-btn cursor-pointer']" title="Like" v-on:click="likeStatus"></h3>
-                <h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="commentFocus"></h3>
+                <h3 class="far fa-comment pr-3 m-0 cursor-pointer" title="Comment" v-on:click="replyFocus(status)"></h3>
                 <h3 v-bind:class="[reactions.shared ? 'far fa-share-square pr-3 m-0 text-primary cursor-pointer' : 'far fa-share-square pr-3 m-0 share-btn cursor-pointer']" title="Share" v-on:click="shareStatus"></h3>
                 <h3 v-bind:class="[reactions.bookmarked ? 'fas fa-bookmark text-warning m-0 float-right cursor-pointer' : 'far fa-bookmark m-0 float-right cursor-pointer']" title="Bookmark" v-on:click="bookmarkStatus"></h3>
               </div>
@@ -145,15 +159,29 @@
               </div>
             </div>
           </div>
-          <div class="card-footer bg-white sticky-md-bottom">
-            <div class="comment-form-guest">
+          <div v-if="showComments && user.length !== 0" class="card-footer bg-white px-2 py-0">
+            <ul class="nav align-items-center emoji-reactions" style="overflow-x: scroll;flex-wrap: unset;">
+              <li class="nav-item" v-on:click="emojiReaction">😂</li>
+              <li class="nav-item" v-on:click="emojiReaction">💯</li>
+              <li class="nav-item" v-on:click="emojiReaction">❤️</li>
+              <li class="nav-item" v-on:click="emojiReaction">🙌</li>
+              <li class="nav-item" v-on:click="emojiReaction">👏</li>
+              <li class="nav-item" v-on:click="emojiReaction">😍</li>
+              <li class="nav-item" v-on:click="emojiReaction">😯</li>
+              <li class="nav-item" v-on:click="emojiReaction">😢</li>
+              <li class="nav-item" v-on:click="emojiReaction">😅</li>
+              <li class="nav-item" v-on:click="emojiReaction">😁</li>
+              <li class="nav-item" v-on:click="emojiReaction">🙂</li>
+              <li class="nav-item" v-on:click="emojiReaction">😎</li>
+            </ul>
+          </div>
+          <div v-if="showComments" class="card-footer bg-white sticky-md-bottom p-0">
+            <div v-if="user.length == 0" class="comment-form-guest p-3">
               <a href="/login">Login</a> to like or comment.
             </div>
-            <form class="comment-form d-none" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
-              <input type="hidden" name="_token" value="">
-              <input type="hidden" name="item" :value="statusId">
-              <input class="form-control" name="comment" placeholder="Add a comment…" autocomplete="off">
-              <input type="submit" value="Send" class="btn btn-primary comment-submit" />
+            <form v-else class="border-0 rounded-0 align-middle" method="post" action="/i/comment" :data-id="statusId" data-truncate="false">
+              <textarea class="form-control border-0 rounded-0" name="comment" placeholder="Add a comment…" autocomplete="off" autocorrect="off" style="height:56px;line-height: 18px;max-height:80px;resize: none; padding-right:4.2rem;" v-model="replyText"></textarea>
+              <input type="button" value="Post" class="d-inline-block btn btn-link font-weight-bold reply-btn text-decoration-none" v-on:click.prevent="postReply"/>
             </form>
           </div>
         </div>
@@ -243,6 +271,68 @@
 </div>
 </template>
 
+<style type="text/css" scoped>
+  .status-comments,
+  .reactions,
+  .col-md-4 {
+    background: #fff;
+  }
+  .postPresenterContainer {
+    background: #fff;
+  }
+  @media(min-width: 720px) {
+    .postPresenterContainer {
+      min-height: 600px;
+    }
+  }
+  ::-webkit-scrollbar {
+      width: 0px;
+      background: transparent;
+  }
+  .reply-btn {
+    position: absolute;
+    bottom: 12px;
+    right: 20px;
+    width: 60px;
+    text-align: center;
+    border-radius: 0 3px 3px 0;
+  }
+  .text-lighter {
+    color:#B8C2CC !important;
+  }
+  .text-break {
+    overflow-wrap: break-word;
+  }
+  .comments p {
+    margin-bottom: 0;
+  }
+  .comment-reaction {
+    font-size: 80%;
+  }
+  .show-reply-bar {
+    display: inline-block;
+    border-bottom: 1px solid #999;
+    height: 0;
+    margin-right: 16px;
+    vertical-align: middle;
+    width: 24px;
+  }
+  .comment-thread {
+    margin: 4px 0 0 40px;
+    width: calc(100% - 40px);
+  }
+  .emoji-reactions .nav-item {
+    font-size: 1.2rem;
+    padding: 7px;
+    cursor: pointer;
+  }
+  .emoji-reactions::-webkit-scrollbar {
+    width: 0px;
+    height: 0px;
+    background: transparent;
+  }
+</style>
+
 <script>
 
 pixelfed.postComponent = {};
@@ -262,7 +352,16 @@ export default {
             likesPage: 1,
             shares: [],
             sharesPage: 1,
-            lightboxMedia: false
+            lightboxMedia: false,
+            replyText: '',
+
+            results: [],
+            pagination: {},
+            min_id: 0,
+            max_id: 0,
+            reply_to_profile_id: 0,
+            thread: false,
+            showComments: false
           }
     },
 
@@ -279,6 +378,8 @@ export default {
     updated() {
       $('.carousel').carousel();
 
+      pixelfed.readmore();
+
       if(this.reactions) {
         if(this.reactions.bookmarked == true) {
           $('.postComponent .far.fa-bookmark').removeClass('far').addClass('fas text-warning');
@@ -345,6 +446,10 @@ export default {
                 this.showMuteBlock();
                 loader.hide();
                 pixelfed.readmore();
+                if(self.status.comments_disabled == false) {
+                  self.showComments = true;
+                  this.fetchComments();
+                }
                 $('.postComponent').removeClass('d-none');
                 $('.postPresenterLoader').addClass('d-none');
                 $('.postPresenterContainer').removeClass('d-none');
@@ -369,10 +474,6 @@ export default {
             });
       },
 
-      commentFocus() {
-        $('.comment-form input[name="comment"]').focus();
-      },
-
       likesModal() {
         if(this.status.favourites_count == 0 || $('body').hasClass('loggedIn') == false) {
           return;
@@ -422,6 +523,7 @@ export default {
 
       likeStatus(event) {
         if($('body').hasClass('loggedIn') == false) {
+          window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
           return;
         }
 
@@ -448,6 +550,7 @@ export default {
 
       shareStatus() {
         if($('body').hasClass('loggedIn') == false) {
+          window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
           return;
         }
 
@@ -474,6 +577,7 @@ export default {
 
       bookmarkStatus() {
         if($('body').hasClass('loggedIn') == false) {
+          window.location.href = '/login?next=' + encodeURIComponent(window.location.pathname);
           return;
         }
 
@@ -521,6 +625,9 @@ export default {
       },
 
       deletePost(status) {
+        if(!this.ownerOrAdmin()) {
+          return;
+        }
         var result = confirm('Are you sure you want to delete this post?');
         if (result) {
             if($('body').hasClass('loggedIn') == false) {
@@ -553,6 +660,198 @@ export default {
       lightbox(src) {
         this.lightboxMedia = src;
         this.$refs.lightboxModal.show();
+      },
+
+      postReply() {
+        let self = this;
+        if(this.replyText.length == 0 || 
+          this.replyText.trim() == '@'+this.status.account.acct) {
+          self.replyText = null;
+          $('textarea[name="comment"]').blur();
+          return;
+        }
+        let data = {
+          item: this.statusId,
+          comment: this.replyText
+        }
+        axios.post('/i/comment', data)
+        .then(function(res) {
+          let entity = res.data.entity;
+          self.results.push(entity);
+          self.replyText = '';
+          let elem = $('.status-comments')[0];
+          elem.scrollTop = elem.clientHeight;
+        });
+      },
+
+      deleteComment(id, i) {
+        axios.post('/i/delete', {
+          type: 'comment',
+          item: id
+        }).then(res => {
+          this.results.splice(i, 1);
+        }).catch(err => {
+          swal('Something went wrong!', 'Please try again later', 'error');
+        });
+      },
+      l(e) {
+        let len = e.length;
+        if(len < 10) { return e; } 
+        return e.substr(0, 10)+'...';
+      },
+      replyFocus(e) {
+          this.reply_to_profile_id = e.account.id;
+          this.replyText = '@' + e.account.username + ' ';
+          $('textarea[name="comment"]').focus();
+      },
+      fetchComments() {
+          let url = '/api/v2/comments/'+this.statusUsername+'/status/'+this.statusId;
+          axios.get(url)
+            .then(response => {
+                let self = this;
+                this.results = _.reverse(response.data.data);
+                this.pagination = response.data.meta.pagination;
+                if(this.results.length > 0) {
+                  $('.load-more-link').removeClass('d-none');
+                }
+                $('.postCommentsLoader').addClass('d-none');
+                $('.postCommentsContainer').removeClass('d-none');
+            }).catch(error => {
+              if(!error.response) {
+                $('.postCommentsLoader .lds-ring')
+                  .attr('style','width:100%')
+                  .addClass('pt-4 font-weight-bold text-muted')
+                  .text('An error occurred, cannot fetch comments. Please try again later.');
+              } else {
+                switch(error.response.status) {
+                  case 401:
+                    $('.postCommentsLoader .lds-ring')
+                      .attr('style','width:100%')
+                      .addClass('pt-4 font-weight-bold text-muted')
+                      .text('Please login to view.');
+                  break;
+
+                  default:
+                    $('.postCommentsLoader .lds-ring')
+                      .attr('style','width:100%')
+                      .addClass('pt-4 font-weight-bold text-muted')
+                      .text('An error occurred, cannot fetch comments. Please try again later.');
+                  break;
+                }
+              }
+            });
+      },
+      loadMore(e) {
+          e.preventDefault();
+          if(this.pagination.total_pages == 1 || this.pagination.current_page == this.pagination.total_pages) {
+            $('.load-more-link').addClass('d-none');
+            return;
+          }
+          $('.postCommentsLoader').removeClass('d-none');
+          let next = this.pagination.links.next;
+          axios.get(next)
+            .then(response => {
+                let self = this;
+                let res =  response.data.data;
+                $('.postCommentsLoader').addClass('d-none');
+                for(let i=0; i < res.length; i++) {
+                  this.results.unshift(res[i]);
+                }
+                this.pagination = response.data.meta.pagination;
+            });
+      },
+      likeReply(status, $event) {
+        if($('body').hasClass('loggedIn') == false) {
+          return;
+        }
+        
+        axios.post('/i/like', {
+          item: status.id
+        }).then(res => {
+          status.favourites_count = res.data.count;
+          if(status.favourited == true) {
+            status.favourited = false;
+          } else {
+            status.favourited = true;
+          }
+        }).catch(err => {
+          swal('Error', 'Something went wrong, please try again later.', 'error');
+        });
+      },
+      truncate(str,lim) {
+        return _.truncate(str,{
+          length: lim
+        });
+      },
+      timeAgo(ts) {
+        let date = Date.parse(ts);
+        let seconds = Math.floor((new Date() - date) / 1000);
+        let interval = Math.floor(seconds / 31536000);
+        if (interval >= 1) {
+          return interval + "y";
+        }
+        interval = Math.floor(seconds / 604800);
+        if (interval >= 1) {
+          return interval + "w";
+        }
+        interval = Math.floor(seconds / 86400);
+        if (interval >= 1) {
+          return interval + "d";
+        }
+        interval = Math.floor(seconds / 3600);
+        if (interval >= 1) {
+          return interval + "h";
+        }
+        interval = Math.floor(seconds / 60);
+        if (interval >= 1) {
+          return interval + "m";
+        }
+        return Math.floor(seconds) + "s";
+      },
+
+      emojiReaction() {
+        let em = event.target.innerText;
+        if(this.replyText.length == 0) {
+          this.reply_to_profile_id = this.status.account.id;
+          this.replyText = '@' + this.status.account.username + ' ' + em;
+          $('textarea[name="comment"]').focus();
+        } else {
+          this.reply_to_profile_id = this.status.account.id;
+          this.replyText += em;
+          $('textarea[name="comment"]').focus();
+        }
+      }, 
+
+      toggleCommentVisibility() {
+        if(this.ownerOrAdmin() == false) {
+          return;
+        }
+
+        let state = this.status.comments_disabled;
+        let self = this;
+
+        if(state == true) {
+          // re-enable comments
+          axios.post('/i/visibility', {
+            item: self.status.id,
+            disableComments: false
+          }).then(function(res) {
+              window.location.href = self.status.url;
+          }).catch(function(err) {
+            return;
+          });
+        } else {
+          // disable comments
+          axios.post('/i/visibility', {
+            item: self.status.id,
+            disableComments: true
+          }).then(function(res) {
+            self.status.comments_disabled = false;
+            self.showComments = false;
+          }).catch(function(err) {
+            return;
+          });
+        }
       }
 
     },

+ 68 - 17
resources/assets/js/components/Profile.vue

@@ -8,33 +8,56 @@
 			<div class="container">
 				<div class="row">
 					<div class="col-12 col-md-4 d-flex">
-						<div class="profile-avatar mx-auto">
-							<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
+						<div class="profile-avatar mx-md-auto">
+							<div class="d-block d-md-none">
+								<div class="row">
+									<div class="col-5">
+										<img class="rounded-circle box-shadow mr-5" :src="profile.avatar" width="77px" height="77px">
+									</div>
+									<div class="col-7 pl-2">
+										<p class="font-weight-ultralight h3 mb-0">{{profile.username}}</p>
+										<p v-if="profile.id == user.id && user.hasOwnProperty('id')">
+											<a class="btn btn-outline-dark py-0 px-4 mt-3" href="/settings/home">Edit Profile</a>
+										</p>
+										<div v-if="profile.id != user.id && user.hasOwnProperty('id')">
+											<p class="mt-3 mb-0" v-if="relationship.following == true">
+												<button type="button"  class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow">Unfollow</button>
+											</p>
+											<p class="mt-3 mb-0" v-if="!relationship.following">
+												<button type="button" class="btn btn-outline-dark font-weight-bold px-4 py-0" v-on:click="followProfile()" data-toggle="tooltip" title="Follow">Follow</button>
+											</p>
+										</div>
+									</div>
+								</div>
+							</div>
+							<div class="d-none d-md-block">
+								<img class="rounded-circle box-shadow" :src="profile.avatar" width="172px" height="172px">
+							</div>
 						</div>
 					</div>
 					<div class="col-12 col-md-8 d-flex align-items-center">
 						<div class="profile-details">
-							<div class="username-bar pb-2 d-flex align-items-center">
-								<span class="font-weight-ultralight h1">{{profile.username}}</span>
+							<div class="d-none d-md-flex username-bar pb-2 align-items-center">
+								<span class="font-weight-ultralight h3">{{profile.username}}</span>
 								<span class="pl-4" v-if="profile.is_admin">
 									<span class="btn btn-outline-secondary font-weight-bold py-0">ADMIN</span>
 								</span>
 								<span class="pl-4">
-									<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted"></a>
+									<a :href="'/users/'+profile.username+'.atom'" class="fas fa-rss fa-lg text-muted text-decoration-none"></a>
 								</span>	
 								<span class="pl-4" v-if="owner">
-									<a class="fas fa-cog fa-lg text-muted" href="/settings/home"></a>
+									<a class="fas fa-cog fa-lg text-muted text-decoration-none" href="/settings/home"></a>
 								</span>
 								<span v-if="profile.id != user.id && user.hasOwnProperty('id')">
 									<span class="pl-4" v-if="relationship.following == true">
-										<button type="button"  class="btn btn-outline-secondary font-weight-bold px-4 py-0" v-on:click="followProfile()">Unfollow</button>
+										<button type="button"  class="btn btn-outline-secondary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Unfollow"><i class="fas fa-user-minus"></i></button>
 									</span>
 									<span class="pl-4" v-if="!relationship.following">
-										<button type="button" class="btn btn-primary font-weight-bold px-4 py-0" v-on:click="followProfile()">Follow</button>
+										<button type="button" class="btn btn-primary font-weight-bold btn-sm" v-on:click="followProfile()" data-toggle="tooltip" title="Follow"><i class="fas fa-user-plus"></i></button>
 									</span>
 								</span>
 							</div>
-							<div class="profile-stats pb-3 d-inline-flex lead">
+							<div class="d-none d-md-inline-flex profile-stats pb-3 lead">
 								<div class="font-weight-light pr-5">
 									<a class="text-dark" :href="profile.url">
 										<span class="font-weight-bold">{{profile.statuses_count}}</span>
@@ -54,7 +77,7 @@
 									</a>
 								</div>
 							</div>
-							<p class="lead mb-0 d-flex align-items-center">
+							<p class="lead mb-0 d-flex align-items-center pt-3">
 								<span class="font-weight-bold pr-3">{{profile.display_name}}</span>
 							</p>
 							<div v-if="profile.note" class="mb-0 lead" v-html="profile.note"></div>
@@ -64,22 +87,50 @@
 				</div>
 			</div>
 		</div>
-		<div>
+		<div class="d-block d-md-none bg-white my-0 py-2 border-bottom">
+			<ul class="nav d-flex justify-content-center">
+				<li class="nav-item">
+					<div class="font-weight-light">
+						<span class="text-dark text-center">
+							<p class="font-weight-bold mb-0">{{profile.statuses_count}}</p>
+							<p class="text-muted mb-0">Posts</p>
+						</span>
+					</div>
+				</li>
+				<li class="nav-item px-5">
+					<div v-if="profileSettings.followers.count" class="font-weight-light">
+						<a class="text-dark cursor-pointer text-center" v-on:click="followersModal()">
+							<p class="font-weight-bold mb-0">{{profile.followers_count}}</p>
+							<p class="text-muted mb-0">Followers</p>
+						</a>
+					</div>
+				</li>
+				<li class="nav-item">
+					<div v-if="profileSettings.following.count" class="font-weight-light">
+						<a class="text-dark cursor-pointer text-center" v-on:click="followingModal()">
+							<p class="font-weight-bold mb-0">{{profile.following_count}}</p>
+							<p class="text-muted mb-0">Following</p>
+						</a>
+					</div>
+				</li>
+			</ul>
+		</div>
+		<div class="bg-white">
 			<ul class="nav nav-topbar d-flex justify-content-center border-0">
 				<!-- 			<li class="nav-item">
 								<a class="nav-link active font-weight-bold text-uppercase" :href="profile.url">Posts</a>
 							</li>
 				 -->
 				<li class="nav-item">
-					<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th"></i></a>
+					<a :class="this.mode == 'grid' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('grid')"><i class="fas fa-th fa-lg"></i></a>
 				</li>
 
 				<!-- <li class="nav-item">
 					<a :class="this.mode == 'masonry' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('masonry')"><i class="fas fa-th-large"></i></a>
 				</li> -->
 
-				<li class="nav-item">
-					<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase active' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list"></i></a>
+				<li class="nav-item px-3">
+					<a :class="this.mode == 'list' ? 'nav-link font-weight-bold text-uppercase text-primary' : 'nav-link font-weight-bold text-uppercase'" href="#" v-on:click.prevent="switchMode('list')"><i class="fas fa-th-list fa-lg"></i></a>
 				</li>
 
 				<li class="nav-item" v-if="owner">
@@ -89,7 +140,7 @@
 		</div>
 
 		<div class="container">
-			<div class="profile-timeline mt-2 mt-md-4">
+			<div class="profile-timeline mt-md-4">
 				<div class="row" v-if="mode == 'grid'">
 					<div class="col-4 p-0 p-sm-2 p-md-3" v-for="(s, index) in timeline">
 						<a class="card info-overlay card-md-border-0" :href="s.url">
@@ -116,8 +167,8 @@
 					</div>
 				</div>
 				<div class="row" v-if="mode == 'list'">
-					<div class="col-md-8 col-lg-8 offset-md-2 pt-2 px-0 my-3 timeline">
-						<div class="card mb-4 status-card card-md-rounded-0" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
+					<div class="col-md-8 col-lg-8 offset-md-2 px-0 mb-3 timeline">
+						<div class="card status-card card-md-rounded-0" :data-status-id="status.id" v-for="(status, index) in timeline" :key="status.id">
 
 							<div class="card-header d-inline-flex align-items-center bg-white">
 								<img v-bind:src="status.account.avatar" width="32px" height="32px" style="border-radius: 32px;">

+ 46 - 28
resources/assets/js/components/SearchResults.vue

@@ -11,7 +11,7 @@
 
 	<div v-if="!loading && !networkError" class="mt-5 row">
 
-		<div class="col-12 col-md-3">
+		<div class="col-12 col-md-3 mb-4">
 			<div>
 				<p class="font-weight-bold">Filters</p>
 				<div class="custom-control custom-checkbox">
@@ -29,14 +29,14 @@
 			</div>
 		</div>
 		<div class="col-12 col-md-9">
-			<p class="h3 font-weight-lighter">Showing results for <i>{{query}}</i></p>
+			<p class="h5 font-weight-bold">Showing results for <i>{{query}}</i></p>
 			<hr>
 
-			<div v-if="filters.hashtags && results.hashtags.length" class="row mb-4">
+			<div v-if="filters.hashtags && results.hashtags" class="row mb-4">
 				<p class="col-12 font-weight-bold text-muted">Hashtags</p>
-				<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-4" style="text-decoration: none;" :href="hashtag.url">
+				<a v-for="(hashtag, index) in results.hashtags" class="col-12 col-md-3 mb-3" style="text-decoration: none;" :href="hashtag.url">
 					<div class="card card-body text-center">
-						<p class="lead mb-0 text-truncate text-dark">
+						<p class="lead mb-0 text-truncate text-dark" data-toggle="tooltip" :title="hashtag.value">
 							#{{hashtag.value}}
 						</p>
 						<p class="lead mb-0 small font-weight-bold text-dark">
@@ -46,28 +46,42 @@
 				</a>
 			</div>
 
-			<div v-if="filters.profiles && results.profiles.length" class="row mb-4">
+			<div v-if="filters.profiles && results.profiles" class="row mb-4">
 				<p class="col-12 font-weight-bold text-muted">Profiles</p>
-				<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4" style="text-decoration: none;" :href="profile.url">
-					<div class="card card-body text-center border-left-primary">
-						<p class="lead mb-0 text-truncate text-dark">
+				<a v-for="(profile, index) in results.profiles" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="profile.url">
+					<div class="card card-body text-center">
+						<p class="text-center">
+							<img :src="profile.entity.thumb" width="32px" height="32px" class="rounded-circle box-shadow">
+						</p>
+						<p class="font-weight-bold text-truncate text-dark">
 							{{profile.value}}
 						</p>
+						<!-- <p class="mb-0 text-center">
+							 <button :class="[profile.entity.following ? 'btn btn-secondary btn-sm py-1 font-weight-bold' : 'btn btn-primary btn-sm py-1 font-weight-bold']" v-on:click="followProfile(profile.entity.id)">
+							 	{{profile.entity.following ? 'Unfollow' : 'Follow'}}
+							 </button>
+						</p> -->
 					</div>
 				</a>
 			</div>
 
-			<div v-if="filters.statuses && results.statuses.length" class="row mb-4">
+			<div v-if="filters.statuses && results.statuses" class="row mb-4">
 				<p class="col-12 font-weight-bold text-muted">Statuses</p>
-				<a v-for="(status, index) in results.statuses" class="col-12 col-md-4" style="text-decoration: none;" :href="status.url">
-					<div class="card card-body text-center border-left-primary">
-						<p class="lead mb-0 text-truncate text-dark">
-							{{status.value}}
-						</p>
+				<a v-for="(status, index) in results.statuses" class="col-12 col-md-4 mb-3" style="text-decoration: none;" :href="status.url">
+					<div class="card">
+						<img class="card-img-top img-fluid" :src="status.thumb" style="height:180px;">
+						<div class="card-body text-center ">
+							<p class="mb-0 small text-truncate font-weight-bold text-muted" v-html="status.value">
+							</p>
+						</div>
 					</div>
 				</a>
 			</div>
 
+			<div v-if="!results.hashtags && !results.profiles && !results.statuses">
+				<p class="text-center lead">No results found!</p>
+			</div>
+
 		</div>
 
 	</div>
@@ -81,7 +95,7 @@
 
 <script type="text/javascript">
 export default {
-	props: ['query'],
+	props: ['query', 'profileId'],
 
 	data() {
 		return {
@@ -103,31 +117,35 @@ export default {
 		this.fetchSearchResults();
 	},
 	mounted() {
-		$('.search-form input').val(this.query);
+		$('.search-bar input').val(this.query);
 	},
 	updated() {
 
 	},
 	methods: {
 		fetchSearchResults() {
-			axios.get('/api/search/' + this.query)
+			axios.get('/api/search/' + encodeURI(this.query))
 				.then(res => {
 					let results = res.data;
-					this.results.hashtags = results.filter(i => {
-						return i.type == 'hashtag';
-					});
-					this.results.profiles = results.filter(i => {
-						return i.type == 'profile';
-					});
-					this.results.statuses = results.filter(i => {
-						return i.type == 'status';
-					});
+					this.results.hashtags = results.hashtags;
+					this.results.profiles = results.profiles;
+					this.results.statuses = results.posts;
 					this.loading = false;
 				}).catch(err => {
 					this.loading = false;
-					this.networkError = true;
+					// this.networkError = true;
 				})
 		},
+
+		followProfile(id) {
+			// todo: finish AP Accept handling to enable remote follows
+			return;
+			// axios.post('/i/follow', {
+			// 	item: id
+			// }).then(res => {
+			// 	window.location.href = window.location.href;
+			// });
+		},
 	}
 
 }

+ 0 - 71
resources/assets/js/components/searchform.js

@@ -1,71 +0,0 @@
-$(document).ready(function() {
-
-  let queryEngine = new Bloodhound({
-    datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
-    queryTokenizer: Bloodhound.tokenizers.whitespace,
-    remote: {
-      url: process.env.MIX_API_SEARCH + '/%QUERY%',
-      wildcard: '%QUERY%'
-    }
-  });
-
-  $('.search-form .search-form-input').typeahead(null, {
-    name: 'search',
-    display: 'value',
-    source: queryEngine,
-    limit: 40,
-    templates: {
-      empty: [
-        '<div class="alert alert-info mb-0 font-weight-bold">',
-          'No Results Found',
-        '</div>'
-      ].join('\n'),
-      suggestion: function(data) {
-        let type = data.type;
-        let res = false;
-        switch(type) {
-          case 'hashtag':
-            res = '<a href="'+data.url+'?src=search">' +
-            '<div class="media d-flex align-items-center">' +
-            '<div class="mr-3 h4 text-muted"><span class="fas fa-hashtag"></span></div>' +
-            '<div class="media-body text-truncate">' +
-            '<p class="mt-0 mb-0 font-weight-bold">'+data.value+'</p>' +
-            '<p class="text-muted mb-0">'+data.count+' posts</p>' +
-            '</div>' +
-            '</div>' +
-            '</a>';
-          break;
-          case 'profile':
-            res = '<a href="'+data.url+'?src=search">' +
-            '<div class="media d-flex align-items-center">' +
-            '<div class="mr-3 h4 text-muted"><span class="far fa-user"></span></div>' +
-            '<div class="media-body text-truncate">' +
-            '<p class="mt-0 mb-0 font-weight-bold">'+data.name+'</p>' +
-            '<p class="text-muted mb-0">'+data.value+'</p>' +
-            '</div>' +
-            '</div>' +
-            '</a>';
-          break;
-          case 'status':
-            res = '<a href="'+data.url+'?src=search">' +
-            '<div class="media d-flex align-items-center">' +
-            '<div class="mr-3 h4 text-muted"><img src="'+data.thumb+'" width="32px"></div>' +
-            '<div class="media-body text-truncate">' +
-            '<p class="mt-0 mb-0 font-weight-bold">'+data.name+'</p>' +
-            '<p class="text-muted mb-0 small">'+data.value+'</p>' +
-            '</div>' +
-            '</div>' +
-            '</a>';
-          break;
-          default:
-            res = false;
-          break;
-        }
-        if(res !== false) {
-          return res;
-        }
-      }
-    }
-  });
-
-});

+ 0 - 10
resources/assets/js/landing.js

@@ -1,10 +0,0 @@
-window.Vue = require('vue');
-
-Vue.component(
-    'landing-page',
-    require('./components/LandingPage.vue').default
-);
-
-new Vue({
-	el: '#content'
-});

+ 0 - 952
resources/assets/js/lib/bloodhound.js

@@ -1,952 +0,0 @@
-/*!
- * typeahead.js 1.2.0
- * https://github.com/twitter/typeahead.js
- * Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
- */
-
-(function(root, factory) {
-    if (typeof define === "function" && define.amd) {
-        define([ "jquery" ], function(a0) {
-            return root["Bloodhound"] = factory(a0);
-        });
-    } else if (typeof exports === "object") {
-        module.exports = factory(require("jquery"));
-    } else {
-        root["Bloodhound"] = factory(root["jQuery"]);
-    }
-})(this, function($) {
-    var _ = function() {
-        "use strict";
-        return {
-            isMsie: function() {
-                return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
-            },
-            isBlankString: function(str) {
-                return !str || /^\s*$/.test(str);
-            },
-            escapeRegExChars: function(str) {
-                return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-            },
-            isString: function(obj) {
-                return typeof obj === "string";
-            },
-            isNumber: function(obj) {
-                return typeof obj === "number";
-            },
-            isArray: $.isArray,
-            isFunction: $.isFunction,
-            isObject: $.isPlainObject,
-            isUndefined: function(obj) {
-                return typeof obj === "undefined";
-            },
-            isElement: function(obj) {
-                return !!(obj && obj.nodeType === 1);
-            },
-            isJQuery: function(obj) {
-                return obj instanceof $;
-            },
-            toStr: function toStr(s) {
-                return _.isUndefined(s) || s === null ? "" : s + "";
-            },
-            bind: $.proxy,
-            each: function(collection, cb) {
-                $.each(collection, reverseArgs);
-                function reverseArgs(index, value) {
-                    return cb(value, index);
-                }
-            },
-            map: $.map,
-            filter: $.grep,
-            every: function(obj, test) {
-                var result = true;
-                if (!obj) {
-                    return result;
-                }
-                $.each(obj, function(key, val) {
-                    if (!(result = test.call(null, val, key, obj))) {
-                        return false;
-                    }
-                });
-                return !!result;
-            },
-            some: function(obj, test) {
-                var result = false;
-                if (!obj) {
-                    return result;
-                }
-                $.each(obj, function(key, val) {
-                    if (result = test.call(null, val, key, obj)) {
-                        return false;
-                    }
-                });
-                return !!result;
-            },
-            mixin: $.extend,
-            identity: function(x) {
-                return x;
-            },
-            clone: function(obj) {
-                return $.extend(true, {}, obj);
-            },
-            getIdGenerator: function() {
-                var counter = 0;
-                return function() {
-                    return counter++;
-                };
-            },
-            templatify: function templatify(obj) {
-                return $.isFunction(obj) ? obj : template;
-                function template() {
-                    return String(obj);
-                }
-            },
-            defer: function(fn) {
-                setTimeout(fn, 0);
-            },
-            debounce: function(func, wait, immediate) {
-                var timeout, result;
-                return function() {
-                    var context = this, args = arguments, later, callNow;
-                    later = function() {
-                        timeout = null;
-                        if (!immediate) {
-                            result = func.apply(context, args);
-                        }
-                    };
-                    callNow = immediate && !timeout;
-                    clearTimeout(timeout);
-                    timeout = setTimeout(later, wait);
-                    if (callNow) {
-                        result = func.apply(context, args);
-                    }
-                    return result;
-                };
-            },
-            throttle: function(func, wait) {
-                var context, args, timeout, result, previous, later;
-                previous = 0;
-                later = function() {
-                    previous = new Date();
-                    timeout = null;
-                    result = func.apply(context, args);
-                };
-                return function() {
-                    var now = new Date(), remaining = wait - (now - previous);
-                    context = this;
-                    args = arguments;
-                    if (remaining <= 0) {
-                        clearTimeout(timeout);
-                        timeout = null;
-                        previous = now;
-                        result = func.apply(context, args);
-                    } else if (!timeout) {
-                        timeout = setTimeout(later, remaining);
-                    }
-                    return result;
-                };
-            },
-            stringify: function(val) {
-                return _.isString(val) ? val : JSON.stringify(val);
-            },
-            guid: function() {
-                function _p8(s) {
-                    var p = (Math.random().toString(16) + "000000000").substr(2, 8);
-                    return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
-                }
-                return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
-            },
-            noop: function() {}
-        };
-    }();
-    var VERSION = "1.2.0";
-    var tokenizers = function() {
-        "use strict";
-        return {
-            nonword: nonword,
-            whitespace: whitespace,
-            ngram: ngram,
-            obj: {
-                nonword: getObjTokenizer(nonword),
-                whitespace: getObjTokenizer(whitespace),
-                ngram: getObjTokenizer(ngram)
-            }
-        };
-        function whitespace(str) {
-            str = _.toStr(str);
-            return str ? str.split(/\s+/) : [];
-        }
-        function nonword(str) {
-            str = _.toStr(str);
-            return str ? str.split(/\W+/) : [];
-        }
-        function ngram(str) {
-            str = _.toStr(str);
-            var tokens = [], word = "";
-            _.each(str.split(""), function(char) {
-                if (char.match(/\s+/)) {
-                    word = "";
-                } else {
-                    tokens.push(word + char);
-                    word += char;
-                }
-            });
-            return tokens;
-        }
-        function getObjTokenizer(tokenizer) {
-            return function setKey(keys) {
-                keys = _.isArray(keys) ? keys : [].slice.call(arguments, 0);
-                return function tokenize(o) {
-                    var tokens = [];
-                    _.each(keys, function(k) {
-                        tokens = tokens.concat(tokenizer(_.toStr(o[k])));
-                    });
-                    return tokens;
-                };
-            };
-        }
-    }();
-    var LruCache = function() {
-        "use strict";
-        function LruCache(maxSize) {
-            this.maxSize = _.isNumber(maxSize) ? maxSize : 100;
-            this.reset();
-            if (this.maxSize <= 0) {
-                this.set = this.get = $.noop;
-            }
-        }
-        _.mixin(LruCache.prototype, {
-            set: function set(key, val) {
-                var tailItem = this.list.tail, node;
-                if (this.size >= this.maxSize) {
-                    this.list.remove(tailItem);
-                    delete this.hash[tailItem.key];
-                    this.size--;
-                }
-                if (node = this.hash[key]) {
-                    node.val = val;
-                    this.list.moveToFront(node);
-                } else {
-                    node = new Node(key, val);
-                    this.list.add(node);
-                    this.hash[key] = node;
-                    this.size++;
-                }
-            },
-            get: function get(key) {
-                var node = this.hash[key];
-                if (node) {
-                    this.list.moveToFront(node);
-                    return node.val;
-                }
-            },
-            reset: function reset() {
-                this.size = 0;
-                this.hash = {};
-                this.list = new List();
-            }
-        });
-        function List() {
-            this.head = this.tail = null;
-        }
-        _.mixin(List.prototype, {
-            add: function add(node) {
-                if (this.head) {
-                    node.next = this.head;
-                    this.head.prev = node;
-                }
-                this.head = node;
-                this.tail = this.tail || node;
-            },
-            remove: function remove(node) {
-                node.prev ? node.prev.next = node.next : this.head = node.next;
-                node.next ? node.next.prev = node.prev : this.tail = node.prev;
-            },
-            moveToFront: function(node) {
-                this.remove(node);
-                this.add(node);
-            }
-        });
-        function Node(key, val) {
-            this.key = key;
-            this.val = val;
-            this.prev = this.next = null;
-        }
-        return LruCache;
-    }();
-    var PersistentStorage = function() {
-        "use strict";
-        var LOCAL_STORAGE;
-        try {
-            LOCAL_STORAGE = window.localStorage;
-            LOCAL_STORAGE.setItem("~~~", "!");
-            LOCAL_STORAGE.removeItem("~~~");
-        } catch (err) {
-            LOCAL_STORAGE = null;
-        }
-        function PersistentStorage(namespace, override) {
-            this.prefix = [ "__", namespace, "__" ].join("");
-            this.ttlKey = "__ttl__";
-            this.keyMatcher = new RegExp("^" + _.escapeRegExChars(this.prefix));
-            this.ls = override || LOCAL_STORAGE;
-            !this.ls && this._noop();
-        }
-        _.mixin(PersistentStorage.prototype, {
-            _prefix: function(key) {
-                return this.prefix + key;
-            },
-            _ttlKey: function(key) {
-                return this._prefix(key) + this.ttlKey;
-            },
-            _noop: function() {
-                this.get = this.set = this.remove = this.clear = this.isExpired = _.noop;
-            },
-            _safeSet: function(key, val) {
-                try {
-                    this.ls.setItem(key, val);
-                } catch (err) {
-                    if (err.name === "QuotaExceededError") {
-                        this.clear();
-                        this._noop();
-                    }
-                }
-            },
-            get: function(key) {
-                if (this.isExpired(key)) {
-                    this.remove(key);
-                }
-                return decode(this.ls.getItem(this._prefix(key)));
-            },
-            set: function(key, val, ttl) {
-                if (_.isNumber(ttl)) {
-                    this._safeSet(this._ttlKey(key), encode(now() + ttl));
-                } else {
-                    this.ls.removeItem(this._ttlKey(key));
-                }
-                return this._safeSet(this._prefix(key), encode(val));
-            },
-            remove: function(key) {
-                this.ls.removeItem(this._ttlKey(key));
-                this.ls.removeItem(this._prefix(key));
-                return this;
-            },
-            clear: function() {
-                var i, keys = gatherMatchingKeys(this.keyMatcher);
-                for (i = keys.length; i--; ) {
-                    this.remove(keys[i]);
-                }
-                return this;
-            },
-            isExpired: function(key) {
-                var ttl = decode(this.ls.getItem(this._ttlKey(key)));
-                return _.isNumber(ttl) && now() > ttl ? true : false;
-            }
-        });
-        return PersistentStorage;
-        function now() {
-            return new Date().getTime();
-        }
-        function encode(val) {
-            return JSON.stringify(_.isUndefined(val) ? null : val);
-        }
-        function decode(val) {
-            return $.parseJSON(val);
-        }
-        function gatherMatchingKeys(keyMatcher) {
-            var i, key, keys = [], len = LOCAL_STORAGE.length;
-            for (i = 0; i < len; i++) {
-                if ((key = LOCAL_STORAGE.key(i)).match(keyMatcher)) {
-                    keys.push(key.replace(keyMatcher, ""));
-                }
-            }
-            return keys;
-        }
-    }();
-    var Transport = function() {
-        "use strict";
-        var pendingRequestsCount = 0, pendingRequests = {}, sharedCache = new LruCache(10);
-        function Transport(o) {
-            o = o || {};
-            this.maxPendingRequests = o.maxPendingRequests || 6;
-            this.cancelled = false;
-            this.lastReq = null;
-            this._send = o.transport;
-            this._get = o.limiter ? o.limiter(this._get) : this._get;
-            this._cache = o.cache === false ? new LruCache(0) : sharedCache;
-        }
-        Transport.setMaxPendingRequests = function setMaxPendingRequests(num) {
-            this.maxPendingRequests = num;
-        };
-        Transport.resetCache = function resetCache() {
-            sharedCache.reset();
-        };
-        _.mixin(Transport.prototype, {
-            _fingerprint: function fingerprint(o) {
-                o = o || {};
-                return o.url + o.type + $.param(o.data || {});
-            },
-            _get: function(o, cb) {
-                var that = this, fingerprint, jqXhr;
-                fingerprint = this._fingerprint(o);
-                if (this.cancelled || fingerprint !== this.lastReq) {
-                    return;
-                }
-                if (jqXhr = pendingRequests[fingerprint]) {
-                    jqXhr.done(done).fail(fail);
-                } else if (pendingRequestsCount < this.maxPendingRequests) {
-                    pendingRequestsCount++;
-                    pendingRequests[fingerprint] = this._send(o).done(done).fail(fail).always(always);
-                } else {
-                    this.onDeckRequestArgs = [].slice.call(arguments, 0);
-                }
-                function done(resp) {
-                    cb(null, resp);
-                    that._cache.set(fingerprint, resp);
-                }
-                function fail() {
-                    cb(true);
-                }
-                function always() {
-                    pendingRequestsCount--;
-                    delete pendingRequests[fingerprint];
-                    if (that.onDeckRequestArgs) {
-                        that._get.apply(that, that.onDeckRequestArgs);
-                        that.onDeckRequestArgs = null;
-                    }
-                }
-            },
-            get: function(o, cb) {
-                var resp, fingerprint;
-                cb = cb || $.noop;
-                o = _.isString(o) ? {
-                    url: o
-                } : o || {};
-                fingerprint = this._fingerprint(o);
-                this.cancelled = false;
-                this.lastReq = fingerprint;
-                if (resp = this._cache.get(fingerprint)) {
-                    cb(null, resp);
-                } else {
-                    this._get(o, cb);
-                }
-            },
-            cancel: function() {
-                this.cancelled = true;
-            }
-        });
-        return Transport;
-    }();
-    var SearchIndex = window.SearchIndex = function() {
-        "use strict";
-        var CHILDREN = "c", IDS = "i";
-        function SearchIndex(o) {
-            o = o || {};
-            if (!o.datumTokenizer || !o.queryTokenizer) {
-                $.error("datumTokenizer and queryTokenizer are both required");
-            }
-            this.identify = o.identify || _.stringify;
-            this.datumTokenizer = o.datumTokenizer;
-            this.queryTokenizer = o.queryTokenizer;
-            this.matchAnyQueryToken = o.matchAnyQueryToken;
-            this.reset();
-        }
-        _.mixin(SearchIndex.prototype, {
-            bootstrap: function bootstrap(o) {
-                this.datums = o.datums;
-                this.trie = o.trie;
-            },
-            add: function(data) {
-                var that = this;
-                data = _.isArray(data) ? data : [ data ];
-                _.each(data, function(datum) {
-                    var id, tokens;
-                    that.datums[id = that.identify(datum)] = datum;
-                    tokens = normalizeTokens(that.datumTokenizer(datum));
-                    _.each(tokens, function(token) {
-                        var node, chars, ch;
-                        node = that.trie;
-                        chars = token.split("");
-                        while (ch = chars.shift()) {
-                            node = node[CHILDREN][ch] || (node[CHILDREN][ch] = newNode());
-                            node[IDS].push(id);
-                        }
-                    });
-                });
-            },
-            get: function get(ids) {
-                var that = this;
-                return _.map(ids, function(id) {
-                    return that.datums[id];
-                });
-            },
-            search: function search(query) {
-                var that = this, tokens, matches;
-                tokens = normalizeTokens(this.queryTokenizer(query));
-                _.each(tokens, function(token) {
-                    var node, chars, ch, ids;
-                    if (matches && matches.length === 0 && !that.matchAnyQueryToken) {
-                        return false;
-                    }
-                    node = that.trie;
-                    chars = token.split("");
-                    while (node && (ch = chars.shift())) {
-                        node = node[CHILDREN][ch];
-                    }
-                    if (node && chars.length === 0) {
-                        ids = node[IDS].slice(0);
-                        matches = matches ? getIntersection(matches, ids) : ids;
-                    } else {
-                        if (!that.matchAnyQueryToken) {
-                            matches = [];
-                            return false;
-                        }
-                    }
-                });
-                return matches ? _.map(unique(matches), function(id) {
-                    return that.datums[id];
-                }) : [];
-            },
-            all: function all() {
-                var values = [];
-                for (var key in this.datums) {
-                    values.push(this.datums[key]);
-                }
-                return values;
-            },
-            reset: function reset() {
-                this.datums = {};
-                this.trie = newNode();
-            },
-            serialize: function serialize() {
-                return {
-                    datums: this.datums,
-                    trie: this.trie
-                };
-            }
-        });
-        return SearchIndex;
-        function normalizeTokens(tokens) {
-            tokens = _.filter(tokens, function(token) {
-                return !!token;
-            });
-            tokens = _.map(tokens, function(token) {
-                return token.toLowerCase();
-            });
-            return tokens;
-        }
-        function newNode() {
-            var node = {};
-            node[IDS] = [];
-            node[CHILDREN] = {};
-            return node;
-        }
-        function unique(array) {
-            var seen = {}, uniques = [];
-            for (var i = 0, len = array.length; i < len; i++) {
-                if (!seen[array[i]]) {
-                    seen[array[i]] = true;
-                    uniques.push(array[i]);
-                }
-            }
-            return uniques;
-        }
-        function getIntersection(arrayA, arrayB) {
-            var ai = 0, bi = 0, intersection = [];
-            arrayA = arrayA.sort();
-            arrayB = arrayB.sort();
-            var lenArrayA = arrayA.length, lenArrayB = arrayB.length;
-            while (ai < lenArrayA && bi < lenArrayB) {
-                if (arrayA[ai] < arrayB[bi]) {
-                    ai++;
-                } else if (arrayA[ai] > arrayB[bi]) {
-                    bi++;
-                } else {
-                    intersection.push(arrayA[ai]);
-                    ai++;
-                    bi++;
-                }
-            }
-            return intersection;
-        }
-    }();
-    var Prefetch = function() {
-        "use strict";
-        var keys;
-        keys = {
-            data: "data",
-            protocol: "protocol",
-            thumbprint: "thumbprint"
-        };
-        function Prefetch(o) {
-            this.url = o.url;
-            this.ttl = o.ttl;
-            this.cache = o.cache;
-            this.prepare = o.prepare;
-            this.transform = o.transform;
-            this.transport = o.transport;
-            this.thumbprint = o.thumbprint;
-            this.storage = new PersistentStorage(o.cacheKey);
-        }
-        _.mixin(Prefetch.prototype, {
-            _settings: function settings() {
-                return {
-                    url: this.url,
-                    type: "GET",
-                    dataType: "json"
-                };
-            },
-            store: function store(data) {
-                if (!this.cache) {
-                    return;
-                }
-                this.storage.set(keys.data, data, this.ttl);
-                this.storage.set(keys.protocol, location.protocol, this.ttl);
-                this.storage.set(keys.thumbprint, this.thumbprint, this.ttl);
-            },
-            fromCache: function fromCache() {
-                var stored = {}, isExpired;
-                if (!this.cache) {
-                    return null;
-                }
-                stored.data = this.storage.get(keys.data);
-                stored.protocol = this.storage.get(keys.protocol);
-                stored.thumbprint = this.storage.get(keys.thumbprint);
-                isExpired = stored.thumbprint !== this.thumbprint || stored.protocol !== location.protocol;
-                return stored.data && !isExpired ? stored.data : null;
-            },
-            fromNetwork: function(cb) {
-                var that = this, settings;
-                if (!cb) {
-                    return;
-                }
-                settings = this.prepare(this._settings());
-                this.transport(settings).fail(onError).done(onResponse);
-                function onError() {
-                    cb(true);
-                }
-                function onResponse(resp) {
-                    cb(null, that.transform(resp));
-                }
-            },
-            clear: function clear() {
-                this.storage.clear();
-                return this;
-            }
-        });
-        return Prefetch;
-    }();
-    var Remote = function() {
-        "use strict";
-        function Remote(o) {
-            this.url = o.url;
-            this.prepare = o.prepare;
-            this.transform = o.transform;
-            this.indexResponse = o.indexResponse;
-            this.transport = new Transport({
-                cache: o.cache,
-                limiter: o.limiter,
-                transport: o.transport,
-                maxPendingRequests: o.maxPendingRequests
-            });
-        }
-        _.mixin(Remote.prototype, {
-            _settings: function settings() {
-                return {
-                    url: this.url,
-                    type: "GET",
-                    dataType: "json"
-                };
-            },
-            get: function get(query, cb) {
-                var that = this, settings;
-                if (!cb) {
-                    return;
-                }
-                query = query || "";
-                settings = this.prepare(query, this._settings());
-                return this.transport.get(settings, onResponse);
-                function onResponse(err, resp) {
-                    err ? cb([]) : cb(that.transform(resp));
-                }
-            },
-            cancelLastRequest: function cancelLastRequest() {
-                this.transport.cancel();
-            }
-        });
-        return Remote;
-    }();
-    var oParser = function() {
-        "use strict";
-        return function parse(o) {
-            var defaults, sorter;
-            defaults = {
-                initialize: true,
-                identify: _.stringify,
-                datumTokenizer: null,
-                queryTokenizer: null,
-                matchAnyQueryToken: false,
-                sufficient: 5,
-                indexRemote: false,
-                sorter: null,
-                local: [],
-                prefetch: null,
-                remote: null
-            };
-            o = _.mixin(defaults, o || {});
-            !o.datumTokenizer && $.error("datumTokenizer is required");
-            !o.queryTokenizer && $.error("queryTokenizer is required");
-            sorter = o.sorter;
-            o.sorter = sorter ? function(x) {
-                return x.sort(sorter);
-            } : _.identity;
-            o.local = _.isFunction(o.local) ? o.local() : o.local;
-            o.prefetch = parsePrefetch(o.prefetch);
-            o.remote = parseRemote(o.remote);
-            return o;
-        };
-        function parsePrefetch(o) {
-            var defaults;
-            if (!o) {
-                return null;
-            }
-            defaults = {
-                url: null,
-                ttl: 24 * 60 * 60 * 1e3,
-                cache: true,
-                cacheKey: null,
-                thumbprint: "",
-                prepare: _.identity,
-                transform: _.identity,
-                transport: null
-            };
-            o = _.isString(o) ? {
-                url: o
-            } : o;
-            o = _.mixin(defaults, o);
-            !o.url && $.error("prefetch requires url to be set");
-            o.transform = o.filter || o.transform;
-            o.cacheKey = o.cacheKey || o.url;
-            o.thumbprint = VERSION + o.thumbprint;
-            o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
-            return o;
-        }
-        function parseRemote(o) {
-            var defaults;
-            if (!o) {
-                return;
-            }
-            defaults = {
-                url: null,
-                cache: true,
-                prepare: null,
-                replace: null,
-                wildcard: null,
-                limiter: null,
-                rateLimitBy: "debounce",
-                rateLimitWait: 300,
-                transform: _.identity,
-                transport: null
-            };
-            o = _.isString(o) ? {
-                url: o
-            } : o;
-            o = _.mixin(defaults, o);
-            !o.url && $.error("remote requires url to be set");
-            o.transform = o.filter || o.transform;
-            o.prepare = toRemotePrepare(o);
-            o.limiter = toLimiter(o);
-            o.transport = o.transport ? callbackToDeferred(o.transport) : $.ajax;
-            delete o.replace;
-            delete o.wildcard;
-            delete o.rateLimitBy;
-            delete o.rateLimitWait;
-            return o;
-        }
-        function toRemotePrepare(o) {
-            var prepare, replace, wildcard;
-            prepare = o.prepare;
-            replace = o.replace;
-            wildcard = o.wildcard;
-            if (prepare) {
-                return prepare;
-            }
-            if (replace) {
-                prepare = prepareByReplace;
-            } else if (o.wildcard) {
-                prepare = prepareByWildcard;
-            } else {
-                prepare = identityPrepare;
-            }
-            return prepare;
-            function prepareByReplace(query, settings) {
-                settings.url = replace(settings.url, query);
-                return settings;
-            }
-            function prepareByWildcard(query, settings) {
-                settings.url = settings.url.replace(wildcard, encodeURIComponent(query));
-                return settings;
-            }
-            function identityPrepare(query, settings) {
-                return settings;
-            }
-        }
-        function toLimiter(o) {
-            var limiter, method, wait;
-            limiter = o.limiter;
-            method = o.rateLimitBy;
-            wait = o.rateLimitWait;
-            if (!limiter) {
-                limiter = /^throttle$/i.test(method) ? throttle(wait) : debounce(wait);
-            }
-            return limiter;
-            function debounce(wait) {
-                return function debounce(fn) {
-                    return _.debounce(fn, wait);
-                };
-            }
-            function throttle(wait) {
-                return function throttle(fn) {
-                    return _.throttle(fn, wait);
-                };
-            }
-        }
-        function callbackToDeferred(fn) {
-            return function wrapper(o) {
-                var deferred = $.Deferred();
-                fn(o, onSuccess, onError);
-                return deferred;
-                function onSuccess(resp) {
-                    _.defer(function() {
-                        deferred.resolve(resp);
-                    });
-                }
-                function onError(err) {
-                    _.defer(function() {
-                        deferred.reject(err);
-                    });
-                }
-            };
-        }
-    }();
-    var Bloodhound = function() {
-        "use strict";
-        var old;
-        old = window && window.Bloodhound;
-        function Bloodhound(o) {
-            o = oParser(o);
-            this.sorter = o.sorter;
-            this.identify = o.identify;
-            this.sufficient = o.sufficient;
-            this.indexRemote = o.indexRemote;
-            this.local = o.local;
-            this.remote = o.remote ? new Remote(o.remote) : null;
-            this.prefetch = o.prefetch ? new Prefetch(o.prefetch) : null;
-            this.index = new SearchIndex({
-                identify: this.identify,
-                datumTokenizer: o.datumTokenizer,
-                queryTokenizer: o.queryTokenizer
-            });
-            o.initialize !== false && this.initialize();
-        }
-        Bloodhound.noConflict = function noConflict() {
-            window && (window.Bloodhound = old);
-            return Bloodhound;
-        };
-        Bloodhound.tokenizers = tokenizers;
-        _.mixin(Bloodhound.prototype, {
-            __ttAdapter: function ttAdapter() {
-                var that = this;
-                return this.remote ? withAsync : withoutAsync;
-                function withAsync(query, sync, async) {
-                    return that.search(query, sync, async);
-                }
-                function withoutAsync(query, sync) {
-                    return that.search(query, sync);
-                }
-            },
-            _loadPrefetch: function loadPrefetch() {
-                var that = this, deferred, serialized;
-                deferred = $.Deferred();
-                if (!this.prefetch) {
-                    deferred.resolve();
-                } else if (serialized = this.prefetch.fromCache()) {
-                    this.index.bootstrap(serialized);
-                    deferred.resolve();
-                } else {
-                    this.prefetch.fromNetwork(done);
-                }
-                return deferred.promise();
-                function done(err, data) {
-                    if (err) {
-                        return deferred.reject();
-                    }
-                    that.add(data);
-                    that.prefetch.store(that.index.serialize());
-                    deferred.resolve();
-                }
-            },
-            _initialize: function initialize() {
-                var that = this, deferred;
-                this.clear();
-                (this.initPromise = this._loadPrefetch()).done(addLocalToIndex);
-                return this.initPromise;
-                function addLocalToIndex() {
-                    that.add(that.local);
-                }
-            },
-            initialize: function initialize(force) {
-                return !this.initPromise || force ? this._initialize() : this.initPromise;
-            },
-            add: function add(data) {
-                this.index.add(data);
-                return this;
-            },
-            get: function get(ids) {
-                ids = _.isArray(ids) ? ids : [].slice.call(arguments);
-                return this.index.get(ids);
-            },
-            search: function search(query, sync, async) {
-                var that = this, local;
-                sync = sync || _.noop;
-                async = async || _.noop;
-                local = this.sorter(this.index.search(query));
-                sync(this.remote ? local.slice() : local);
-                if (this.remote && local.length < this.sufficient) {
-                    this.remote.get(query, processRemote);
-                } else if (this.remote) {
-                    this.remote.cancelLastRequest();
-                }
-                return this;
-                function processRemote(remote) {
-                    var nonDuplicates = [];
-                    _.each(remote, function(r) {
-                        !_.some(local, function(l) {
-                            return that.identify(r) === that.identify(l);
-                        }) && nonDuplicates.push(r);
-                    });
-                    that.indexRemote && that.add(nonDuplicates);
-                    async(nonDuplicates);
-                }
-            },
-            all: function all() {
-                return this.index.all();
-            },
-            clear: function clear() {
-                this.index.reset();
-                return this;
-            },
-            clearPrefetchCache: function clearPrefetchCache() {
-                this.prefetch && this.prefetch.clear();
-                return this;
-            },
-            clearRemoteCache: function clearRemoteCache() {
-                Transport.resetCache();
-                return this;
-            },
-            ttAdapter: function ttAdapter() {
-                return this.__ttAdapter();
-            }
-        });
-        return Bloodhound;
-    }();
-    return Bloodhound;
-});

+ 0 - 1674
resources/assets/js/lib/typeahead.js

@@ -1,1674 +0,0 @@
-/*!
- * typeahead.js 1.2.0
- * https://github.com/twitter/typeahead.js
- * Copyright 2013-2017 Twitter, Inc. and other contributors; Licensed MIT
- */
-
-(function(root, factory) {
-    if (typeof define === "function" && define.amd) {
-        define([ "jquery" ], function(a0) {
-            return factory(a0);
-        });
-    } else if (typeof exports === "object") {
-        module.exports = factory(require("jquery"));
-    } else {
-        factory(root["jQuery"]);
-    }
-})(this, function($) {
-    var _ = function() {
-        "use strict";
-        return {
-            isMsie: function() {
-                return /(msie|trident)/i.test(navigator.userAgent) ? navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2] : false;
-            },
-            isBlankString: function(str) {
-                return !str || /^\s*$/.test(str);
-            },
-            escapeRegExChars: function(str) {
-                return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
-            },
-            isString: function(obj) {
-                return typeof obj === "string";
-            },
-            isNumber: function(obj) {
-                return typeof obj === "number";
-            },
-            isArray: $.isArray,
-            isFunction: $.isFunction,
-            isObject: $.isPlainObject,
-            isUndefined: function(obj) {
-                return typeof obj === "undefined";
-            },
-            isElement: function(obj) {
-                return !!(obj && obj.nodeType === 1);
-            },
-            isJQuery: function(obj) {
-                return obj instanceof $;
-            },
-            toStr: function toStr(s) {
-                return _.isUndefined(s) || s === null ? "" : s + "";
-            },
-            bind: $.proxy,
-            each: function(collection, cb) {
-                $.each(collection, reverseArgs);
-                function reverseArgs(index, value) {
-                    return cb(value, index);
-                }
-            },
-            map: $.map,
-            filter: $.grep,
-            every: function(obj, test) {
-                var result = true;
-                if (!obj) {
-                    return result;
-                }
-                $.each(obj, function(key, val) {
-                    if (!(result = test.call(null, val, key, obj))) {
-                        return false;
-                    }
-                });
-                return !!result;
-            },
-            some: function(obj, test) {
-                var result = false;
-                if (!obj) {
-                    return result;
-                }
-                $.each(obj, function(key, val) {
-                    if (result = test.call(null, val, key, obj)) {
-                        return false;
-                    }
-                });
-                return !!result;
-            },
-            mixin: $.extend,
-            identity: function(x) {
-                return x;
-            },
-            clone: function(obj) {
-                return $.extend(true, {}, obj);
-            },
-            getIdGenerator: function() {
-                var counter = 0;
-                return function() {
-                    return counter++;
-                };
-            },
-            templatify: function templatify(obj) {
-                return $.isFunction(obj) ? obj : template;
-                function template() {
-                    return String(obj);
-                }
-            },
-            defer: function(fn) {
-                setTimeout(fn, 0);
-            },
-            debounce: function(func, wait, immediate) {
-                var timeout, result;
-                return function() {
-                    var context = this, args = arguments, later, callNow;
-                    later = function() {
-                        timeout = null;
-                        if (!immediate) {
-                            result = func.apply(context, args);
-                        }
-                    };
-                    callNow = immediate && !timeout;
-                    clearTimeout(timeout);
-                    timeout = setTimeout(later, wait);
-                    if (callNow) {
-                        result = func.apply(context, args);
-                    }
-                    return result;
-                };
-            },
-            throttle: function(func, wait) {
-                var context, args, timeout, result, previous, later;
-                previous = 0;
-                later = function() {
-                    previous = new Date();
-                    timeout = null;
-                    result = func.apply(context, args);
-                };
-                return function() {
-                    var now = new Date(), remaining = wait - (now - previous);
-                    context = this;
-                    args = arguments;
-                    if (remaining <= 0) {
-                        clearTimeout(timeout);
-                        timeout = null;
-                        previous = now;
-                        result = func.apply(context, args);
-                    } else if (!timeout) {
-                        timeout = setTimeout(later, remaining);
-                    }
-                    return result;
-                };
-            },
-            stringify: function(val) {
-                return _.isString(val) ? val : JSON.stringify(val);
-            },
-            guid: function() {
-                function _p8(s) {
-                    var p = (Math.random().toString(16) + "000000000").substr(2, 8);
-                    return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
-                }
-                return "tt-" + _p8() + _p8(true) + _p8(true) + _p8();
-            },
-            noop: function() {}
-        };
-    }();
-    var WWW = function() {
-        "use strict";
-        var defaultClassNames = {
-            wrapper: "twitter-typeahead",
-            input: "tt-input",
-            hint: "tt-hint",
-            menu: "tt-menu",
-            dataset: "tt-dataset",
-            suggestion: "tt-suggestion",
-            selectable: "tt-selectable",
-            empty: "tt-empty",
-            open: "tt-open",
-            cursor: "tt-cursor",
-            highlight: "tt-highlight"
-        };
-        return build;
-        function build(o) {
-            var www, classes;
-            classes = _.mixin({}, defaultClassNames, o);
-            www = {
-                css: buildCss(),
-                classes: classes,
-                html: buildHtml(classes),
-                selectors: buildSelectors(classes)
-            };
-            return {
-                css: www.css,
-                html: www.html,
-                classes: www.classes,
-                selectors: www.selectors,
-                mixin: function(o) {
-                    _.mixin(o, www);
-                }
-            };
-        }
-        function buildHtml(c) {
-            return {
-                wrapper: '<span class="' + c.wrapper + '"></span>',
-                menu: '<div role="listbox" class="' + c.menu + '"></div>'
-            };
-        }
-        function buildSelectors(classes) {
-            var selectors = {};
-            _.each(classes, function(v, k) {
-                selectors[k] = "." + v;
-            });
-            return selectors;
-        }
-        function buildCss() {
-            var css = {
-                wrapper: {
-                    position: "relative",
-                    display: "inline-block"
-                },
-                hint: {
-                    position: "absolute",
-                    top: "0",
-                    left: "0",
-                    borderColor: "transparent",
-                    boxShadow: "none",
-                    opacity: "1"
-                },
-                input: {
-                    position: "relative",
-                    verticalAlign: "top",
-                    backgroundColor: "transparent"
-                },
-                inputWithNoHint: {
-                    position: "relative",
-                    verticalAlign: "top"
-                },
-                menu: {
-                    position: "absolute",
-                    top: "100%",
-                    left: "0",
-                    zIndex: "100",
-                    display: "none"
-                },
-                ltr: {
-                    left: "0",
-                    right: "auto"
-                },
-                rtl: {
-                    left: "auto",
-                    right: " 0"
-                }
-            };
-            if (_.isMsie()) {
-                _.mixin(css.input, {
-                    backgroundImage: "url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"
-                });
-            }
-            return css;
-        }
-    }();
-    var EventBus = function() {
-        "use strict";
-        var namespace, deprecationMap;
-        namespace = "typeahead:";
-        deprecationMap = {
-            render: "rendered",
-            cursorchange: "cursorchanged",
-            select: "selected",
-            autocomplete: "autocompleted"
-        };
-        function EventBus(o) {
-            if (!o || !o.el) {
-                $.error("EventBus initialized without el");
-            }
-            this.$el = $(o.el);
-        }
-        _.mixin(EventBus.prototype, {
-            _trigger: function(type, args) {
-                var $e = $.Event(namespace + type);
-                this.$el.trigger.call(this.$el, $e, args || []);
-                return $e;
-            },
-            before: function(type) {
-                var args, $e;
-                args = [].slice.call(arguments, 1);
-                $e = this._trigger("before" + type, args);
-                return $e.isDefaultPrevented();
-            },
-            trigger: function(type) {
-                var deprecatedType;
-                this._trigger(type, [].slice.call(arguments, 1));
-                if (deprecatedType = deprecationMap[type]) {
-                    this._trigger(deprecatedType, [].slice.call(arguments, 1));
-                }
-            }
-        });
-        return EventBus;
-    }();
-    var EventEmitter = function() {
-        "use strict";
-        var splitter = /\s+/, nextTick = getNextTick();
-        return {
-            onSync: onSync,
-            onAsync: onAsync,
-            off: off,
-            trigger: trigger
-        };
-        function on(method, types, cb, context) {
-            var type;
-            if (!cb) {
-                return this;
-            }
-            types = types.split(splitter);
-            cb = context ? bindContext(cb, context) : cb;
-            this._callbacks = this._callbacks || {};
-            while (type = types.shift()) {
-                this._callbacks[type] = this._callbacks[type] || {
-                    sync: [],
-                    async: []
-                };
-                this._callbacks[type][method].push(cb);
-            }
-            return this;
-        }
-        function onAsync(types, cb, context) {
-            return on.call(this, "async", types, cb, context);
-        }
-        function onSync(types, cb, context) {
-            return on.call(this, "sync", types, cb, context);
-        }
-        function off(types) {
-            var type;
-            if (!this._callbacks) {
-                return this;
-            }
-            types = types.split(splitter);
-            while (type = types.shift()) {
-                delete this._callbacks[type];
-            }
-            return this;
-        }
-        function trigger(types) {
-            var type, callbacks, args, syncFlush, asyncFlush;
-            if (!this._callbacks) {
-                return this;
-            }
-            types = types.split(splitter);
-            args = [].slice.call(arguments, 1);
-            while ((type = types.shift()) && (callbacks = this._callbacks[type])) {
-                syncFlush = getFlush(callbacks.sync, this, [ type ].concat(args));
-                asyncFlush = getFlush(callbacks.async, this, [ type ].concat(args));
-                syncFlush() && nextTick(asyncFlush);
-            }
-            return this;
-        }
-        function getFlush(callbacks, context, args) {
-            return flush;
-            function flush() {
-                var cancelled;
-                for (var i = 0, len = callbacks.length; !cancelled && i < len; i += 1) {
-                    cancelled = callbacks[i].apply(context, args) === false;
-                }
-                return !cancelled;
-            }
-        }
-        function getNextTick() {
-            var nextTickFn;
-            if (window.setImmediate) {
-                nextTickFn = function nextTickSetImmediate(fn) {
-                    setImmediate(function() {
-                        fn();
-                    });
-                };
-            } else {
-                nextTickFn = function nextTickSetTimeout(fn) {
-                    setTimeout(function() {
-                        fn();
-                    }, 0);
-                };
-            }
-            return nextTickFn;
-        }
-        function bindContext(fn, context) {
-            return fn.bind ? fn.bind(context) : function() {
-                fn.apply(context, [].slice.call(arguments, 0));
-            };
-        }
-    }();
-    var highlight = function(doc) {
-        "use strict";
-        var defaults = {
-            node: null,
-            pattern: null,
-            tagName: "strong",
-            className: null,
-            wordsOnly: false,
-            caseSensitive: false,
-            diacriticInsensitive: false
-        };
-        var accented = {
-            A: "[AaªÀ-Åà-åĀ-ąǍǎȀ-ȃȦȧᴬᵃḀḁẚẠ-ảₐ℀℁℻⒜Ⓐⓐ㍱-㍴㎀-㎄㎈㎉㎩-㎯㏂㏊㏟㏿Aa]",
-            B: "[BbᴮᵇḂ-ḇℬ⒝Ⓑⓑ㍴㎅-㎇㏃㏈㏔㏝Bb]",
-            C: "[CcÇçĆ-čᶜ℀ℂ℃℅℆ℭⅭⅽ⒞Ⓒⓒ㍶㎈㎉㎝㎠㎤㏄-㏇Cc]",
-            D: "[DdĎďDŽ-džDZ-dzᴰᵈḊ-ḓⅅⅆⅮⅾ⒟Ⓓⓓ㋏㍲㍷-㍹㎗㎭-㎯㏅㏈Dd]",
-            E: "[EeÈ-Ëè-ëĒ-ěȄ-ȇȨȩᴱᵉḘ-ḛẸ-ẽₑ℡ℯℰⅇ⒠Ⓔⓔ㉐㋍㋎Ee]",
-            F: "[FfᶠḞḟ℉ℱ℻⒡Ⓕⓕ㎊-㎌㎙ff-fflFf]",
-            G: "[GgĜ-ģǦǧǴǵᴳᵍḠḡℊ⒢Ⓖⓖ㋌㋍㎇㎍-㎏㎓㎬㏆㏉㏒㏿Gg]",
-            H: "[HhĤĥȞȟʰᴴḢ-ḫẖℋ-ℎ⒣Ⓗⓗ㋌㍱㎐-㎔㏊㏋㏗Hh]",
-            I: "[IiÌ-Ïì-ïĨ-İIJijǏǐȈ-ȋᴵᵢḬḭỈ-ịⁱℐℑℹⅈⅠ-ⅣⅥ-ⅨⅪⅫⅰ-ⅳⅵ-ⅸⅺⅻ⒤Ⓘⓘ㍺㏌㏕fiffiIi]",
-            J: "[JjIJ-ĵLJ-njǰʲᴶⅉ⒥ⒿⓙⱼJj]",
-            K: "[KkĶķǨǩᴷᵏḰ-ḵK⒦Ⓚⓚ㎄㎅㎉㎏㎑㎘㎞㎢㎦㎪㎸㎾㏀㏆㏍-㏏Kk]",
-            L: "[LlĹ-ŀLJ-ljˡᴸḶḷḺ-ḽℒℓ℡Ⅼⅼ⒧Ⓛⓛ㋏㎈㎉㏐-㏓㏕㏖㏿flfflLl]",
-            M: "[MmᴹᵐḾ-ṃ℠™ℳⅯⅿ⒨Ⓜⓜ㍷-㍹㎃㎆㎎㎒㎖㎙-㎨㎫㎳㎷㎹㎽㎿㏁㏂㏎㏐㏔-㏖㏘㏙㏞㏟Mm]",
-            N: "[NnÑñŃ-ʼnNJ-njǸǹᴺṄ-ṋⁿℕ№⒩Ⓝⓝ㎁㎋㎚㎱㎵㎻㏌㏑Nn]",
-            O: "[OoºÒ-Öò-öŌ-őƠơǑǒǪǫȌ-ȏȮȯᴼᵒỌ-ỏₒ℅№ℴ⒪Ⓞⓞ㍵㏇㏒㏖Oo]",
-            P: "[PpᴾᵖṔ-ṗℙ⒫Ⓟⓟ㉐㍱㍶㎀㎊㎩-㎬㎰㎴㎺㏋㏗-㏚Pp]",
-            Q: "[Qqℚ⒬Ⓠⓠ㏃Qq]",
-            R: "[RrŔ-řȐ-ȓʳᴿᵣṘ-ṛṞṟ₨ℛ-ℝ⒭Ⓡⓡ㋍㍴㎭-㎯㏚㏛Rr]",
-            S: "[SsŚ-šſȘșˢṠ-ṣ₨℁℠⒮Ⓢⓢ㎧㎨㎮-㎳㏛㏜stSs]",
-            T: "[TtŢ-ťȚțᵀᵗṪ-ṱẗ℡™⒯Ⓣⓣ㉐㋏㎔㏏ſtstTt]",
-            U: "[UuÙ-Üù-üŨ-ųƯưǓǔȔ-ȗᵁᵘᵤṲ-ṷỤ-ủ℆⒰Ⓤⓤ㍳㍺Uu]",
-            V: "[VvᵛᵥṼ-ṿⅣ-Ⅷⅳ-ⅷ⒱Ⓥⓥⱽ㋎㍵㎴-㎹㏜㏞Vv]",
-            W: "[WwŴŵʷᵂẀ-ẉẘ⒲Ⓦⓦ㎺-㎿㏝Ww]",
-            X: "[XxˣẊ-ẍₓ℻Ⅸ-Ⅻⅸ-ⅻ⒳Ⓧⓧ㏓Xx]",
-            Y: "[YyÝýÿŶ-ŸȲȳʸẎẏẙỲ-ỹ⒴Ⓨⓨ㏉Yy]",
-            Z: "[ZzŹ-žDZ-dzᶻẐ-ẕℤℨ⒵Ⓩⓩ㎐-㎔Zz]"
-        };
-        return function hightlight(o) {
-            var regex;
-            o = _.mixin({}, defaults, o);
-            if (!o.node || !o.pattern) {
-                return;
-            }
-            o.pattern = _.isArray(o.pattern) ? o.pattern : [ o.pattern ];
-            regex = getRegex(o.pattern, o.caseSensitive, o.wordsOnly, o.diacriticInsensitive);
-            traverse(o.node, hightlightTextNode);
-            function hightlightTextNode(textNode) {
-                var match, patternNode, wrapperNode;
-                if (match = regex.exec(textNode.data)) {
-                    wrapperNode = doc.createElement(o.tagName);
-                    o.className && (wrapperNode.className = o.className);
-                    patternNode = textNode.splitText(match.index);
-                    patternNode.splitText(match[0].length);
-                    wrapperNode.appendChild(patternNode.cloneNode(true));
-                    textNode.parentNode.replaceChild(wrapperNode, patternNode);
-                }
-                return !!match;
-            }
-            function traverse(el, hightlightTextNode) {
-                var childNode, TEXT_NODE_TYPE = 3;
-                for (var i = 0; i < el.childNodes.length; i++) {
-                    childNode = el.childNodes[i];
-                    if (childNode.nodeType === TEXT_NODE_TYPE) {
-                        i += hightlightTextNode(childNode) ? 1 : 0;
-                    } else {
-                        traverse(childNode, hightlightTextNode);
-                    }
-                }
-            }
-        };
-        function accent_replacer(chr) {
-            return accented[chr.toUpperCase()] || chr;
-        }
-        function getRegex(patterns, caseSensitive, wordsOnly, diacriticInsensitive) {
-            var escapedPatterns = [], regexStr;
-            for (var i = 0, len = patterns.length; i < len; i++) {
-                var escapedWord = _.escapeRegExChars(patterns[i]);
-                if (diacriticInsensitive) {
-                    escapedWord = escapedWord.replace(/\S/g, accent_replacer);
-                }
-                escapedPatterns.push(escapedWord);
-            }
-            regexStr = wordsOnly ? "\\b(" + escapedPatterns.join("|") + ")\\b" : "(" + escapedPatterns.join("|") + ")";
-            return caseSensitive ? new RegExp(regexStr) : new RegExp(regexStr, "i");
-        }
-    }(window.document);
-    var Input = function() {
-        "use strict";
-        var specialKeyCodeMap;
-        specialKeyCodeMap = {
-            9: "tab",
-            27: "esc",
-            37: "left",
-            39: "right",
-            13: "enter",
-            38: "up",
-            40: "down"
-        };
-        function Input(o, www) {
-            o = o || {};
-            if (!o.input) {
-                $.error("input is missing");
-            }
-            www.mixin(this);
-            this.$hint = $(o.hint);
-            this.$input = $(o.input);
-            this.$input.attr({
-                "aria-activedescendant": "",
-                "aria-owns": this.$input.attr("id") + "_listbox",
-                role: "combobox",
-                "aria-readonly": "true",
-                "aria-autocomplete": "list"
-            });
-            $(www.menu).attr("id", this.$input.attr("id") + "_listbox");
-            this.query = this.$input.val();
-            this.queryWhenFocused = this.hasFocus() ? this.query : null;
-            this.$overflowHelper = buildOverflowHelper(this.$input);
-            this._checkLanguageDirection();
-            if (this.$hint.length === 0) {
-                this.setHint = this.getHint = this.clearHint = this.clearHintIfInvalid = _.noop;
-            }
-            this.onSync("cursorchange", this._updateDescendent);
-        }
-        Input.normalizeQuery = function(str) {
-            return _.toStr(str).replace(/^\s*/g, "").replace(/\s{2,}/g, " ");
-        };
-        _.mixin(Input.prototype, EventEmitter, {
-            _onBlur: function onBlur() {
-                this.resetInputValue();
-                this.trigger("blurred");
-            },
-            _onFocus: function onFocus() {
-                this.queryWhenFocused = this.query;
-                this.trigger("focused");
-            },
-            _onKeydown: function onKeydown($e) {
-                var keyName = specialKeyCodeMap[$e.which || $e.keyCode];
-                this._managePreventDefault(keyName, $e);
-                if (keyName && this._shouldTrigger(keyName, $e)) {
-                    this.trigger(keyName + "Keyed", $e);
-                }
-            },
-            _onInput: function onInput() {
-                this._setQuery(this.getInputValue());
-                this.clearHintIfInvalid();
-                this._checkLanguageDirection();
-            },
-            _managePreventDefault: function managePreventDefault(keyName, $e) {
-                var preventDefault;
-                switch (keyName) {
-                  case "up":
-                  case "down":
-                    preventDefault = !withModifier($e);
-                    break;
-
-                  default:
-                    preventDefault = false;
-                }
-                preventDefault && $e.preventDefault();
-            },
-            _shouldTrigger: function shouldTrigger(keyName, $e) {
-                var trigger;
-                switch (keyName) {
-                  case "tab":
-                    trigger = !withModifier($e);
-                    break;
-
-                  default:
-                    trigger = true;
-                }
-                return trigger;
-            },
-            _checkLanguageDirection: function checkLanguageDirection() {
-                var dir = (this.$input.css("direction") || "ltr").toLowerCase();
-                if (this.dir !== dir) {
-                    this.dir = dir;
-                    this.$hint.attr("dir", dir);
-                    this.trigger("langDirChanged", dir);
-                }
-            },
-            _setQuery: function setQuery(val, silent) {
-                var areEquivalent, hasDifferentWhitespace;
-                areEquivalent = areQueriesEquivalent(val, this.query);
-                hasDifferentWhitespace = areEquivalent ? this.query.length !== val.length : false;
-                this.query = val;
-                if (!silent && !areEquivalent) {
-                    this.trigger("queryChanged", this.query);
-                } else if (!silent && hasDifferentWhitespace) {
-                    this.trigger("whitespaceChanged", this.query);
-                }
-            },
-            _updateDescendent: function updateDescendent(event, id) {
-                this.$input.attr("aria-activedescendant", id);
-            },
-            bind: function() {
-                var that = this, onBlur, onFocus, onKeydown, onInput;
-                onBlur = _.bind(this._onBlur, this);
-                onFocus = _.bind(this._onFocus, this);
-                onKeydown = _.bind(this._onKeydown, this);
-                onInput = _.bind(this._onInput, this);
-                this.$input.on("blur.tt", onBlur).on("focus.tt", onFocus).on("keydown.tt", onKeydown);
-                if (!_.isMsie() || _.isMsie() > 9) {
-                    this.$input.on("input.tt", onInput);
-                } else {
-                    this.$input.on("keydown.tt keypress.tt cut.tt paste.tt", function($e) {
-                        if (specialKeyCodeMap[$e.which || $e.keyCode]) {
-                            return;
-                        }
-                        _.defer(_.bind(that._onInput, that, $e));
-                    });
-                }
-                return this;
-            },
-            focus: function focus() {
-                this.$input.focus();
-            },
-            blur: function blur() {
-                this.$input.blur();
-            },
-            getLangDir: function getLangDir() {
-                return this.dir;
-            },
-            getQuery: function getQuery() {
-                return this.query || "";
-            },
-            setQuery: function setQuery(val, silent) {
-                this.setInputValue(val);
-                this._setQuery(val, silent);
-            },
-            hasQueryChangedSinceLastFocus: function hasQueryChangedSinceLastFocus() {
-                return this.query !== this.queryWhenFocused;
-            },
-            getInputValue: function getInputValue() {
-                return this.$input.val();
-            },
-            setInputValue: function setInputValue(value) {
-                this.$input.val(value);
-                this.clearHintIfInvalid();
-                this._checkLanguageDirection();
-            },
-            resetInputValue: function resetInputValue() {
-                this.setInputValue(this.query);
-            },
-            getHint: function getHint() {
-                return this.$hint.val();
-            },
-            setHint: function setHint(value) {
-                this.$hint.val(value);
-            },
-            clearHint: function clearHint() {
-                this.setHint("");
-            },
-            clearHintIfInvalid: function clearHintIfInvalid() {
-                var val, hint, valIsPrefixOfHint, isValid;
-                val = this.getInputValue();
-                hint = this.getHint();
-                valIsPrefixOfHint = val !== hint && hint.indexOf(val) === 0;
-                isValid = val !== "" && valIsPrefixOfHint && !this.hasOverflow();
-                !isValid && this.clearHint();
-            },
-            hasFocus: function hasFocus() {
-                return this.$input.is(":focus");
-            },
-            hasOverflow: function hasOverflow() {
-                var constraint = this.$input.width() - 2;
-                this.$overflowHelper.text(this.getInputValue());
-                return this.$overflowHelper.width() >= constraint;
-            },
-            isCursorAtEnd: function() {
-                var valueLength, selectionStart, range;
-                valueLength = this.$input.val().length;
-                selectionStart = this.$input[0].selectionStart;
-                if (_.isNumber(selectionStart)) {
-                    return selectionStart === valueLength;
-                } else if (document.selection) {
-                    range = document.selection.createRange();
-                    range.moveStart("character", -valueLength);
-                    return valueLength === range.text.length;
-                }
-                return true;
-            },
-            destroy: function destroy() {
-                this.$hint.off(".tt");
-                this.$input.off(".tt");
-                this.$overflowHelper.remove();
-                this.$hint = this.$input = this.$overflowHelper = $("<div>");
-            }
-        });
-        return Input;
-        function buildOverflowHelper($input) {
-            return $('<pre aria-hidden="true"></pre>').css({
-                position: "absolute",
-                visibility: "hidden",
-                whiteSpace: "pre",
-                fontFamily: $input.css("font-family"),
-                fontSize: $input.css("font-size"),
-                fontStyle: $input.css("font-style"),
-                fontVariant: $input.css("font-variant"),
-                fontWeight: $input.css("font-weight"),
-                wordSpacing: $input.css("word-spacing"),
-                letterSpacing: $input.css("letter-spacing"),
-                textIndent: $input.css("text-indent"),
-                textRendering: $input.css("text-rendering"),
-                textTransform: $input.css("text-transform")
-            }).insertAfter($input);
-        }
-        function areQueriesEquivalent(a, b) {
-            return Input.normalizeQuery(a) === Input.normalizeQuery(b);
-        }
-        function withModifier($e) {
-            return $e.altKey || $e.ctrlKey || $e.metaKey || $e.shiftKey;
-        }
-    }();
-    var Dataset = function() {
-        "use strict";
-        var keys, nameGenerator;
-        keys = {
-            dataset: "tt-selectable-dataset",
-            val: "tt-selectable-display",
-            obj: "tt-selectable-object"
-        };
-        nameGenerator = _.getIdGenerator();
-        function Dataset(o, www) {
-            o = o || {};
-            o.templates = o.templates || {};
-            o.templates.notFound = o.templates.notFound || o.templates.empty;
-            if (!o.source) {
-                $.error("missing source");
-            }
-            if (!o.node) {
-                $.error("missing node");
-            }
-            if (o.name && !isValidName(o.name)) {
-                $.error("invalid dataset name: " + o.name);
-            }
-            www.mixin(this);
-            this.highlight = !!o.highlight;
-            this.name = _.toStr(o.name || nameGenerator());
-            this.limit = o.limit || 5;
-            this.displayFn = getDisplayFn(o.display || o.displayKey);
-            this.templates = getTemplates(o.templates, this.displayFn);
-            this.source = o.source.__ttAdapter ? o.source.__ttAdapter() : o.source;
-            this.async = _.isUndefined(o.async) ? this.source.length > 2 : !!o.async;
-            this._resetLastSuggestion();
-            this.$el = $(o.node).attr("role", "presentation").addClass(this.classes.dataset).addClass(this.classes.dataset + "-" + this.name);
-        }
-        Dataset.extractData = function extractData(el) {
-            var $el = $(el);
-            if ($el.data(keys.obj)) {
-                return {
-                    dataset: $el.data(keys.dataset) || "",
-                    val: $el.data(keys.val) || "",
-                    obj: $el.data(keys.obj) || null
-                };
-            }
-            return null;
-        };
-        _.mixin(Dataset.prototype, EventEmitter, {
-            _overwrite: function overwrite(query, suggestions) {
-                suggestions = suggestions || [];
-                if (suggestions.length) {
-                    this._renderSuggestions(query, suggestions);
-                } else if (this.async && this.templates.pending) {
-                    this._renderPending(query);
-                } else if (!this.async && this.templates.notFound) {
-                    this._renderNotFound(query);
-                } else {
-                    this._empty();
-                }
-                this.trigger("rendered", suggestions, false, this.name);
-            },
-            _append: function append(query, suggestions) {
-                suggestions = suggestions || [];
-                if (suggestions.length && this.$lastSuggestion.length) {
-                    this._appendSuggestions(query, suggestions);
-                } else if (suggestions.length) {
-                    this._renderSuggestions(query, suggestions);
-                } else if (!this.$lastSuggestion.length && this.templates.notFound) {
-                    this._renderNotFound(query);
-                }
-                this.trigger("rendered", suggestions, true, this.name);
-            },
-            _renderSuggestions: function renderSuggestions(query, suggestions) {
-                var $fragment;
-                $fragment = this._getSuggestionsFragment(query, suggestions);
-                this.$lastSuggestion = $fragment.children().last();
-                this.$el.html($fragment).prepend(this._getHeader(query, suggestions)).append(this._getFooter(query, suggestions));
-            },
-            _appendSuggestions: function appendSuggestions(query, suggestions) {
-                var $fragment, $lastSuggestion;
-                $fragment = this._getSuggestionsFragment(query, suggestions);
-                $lastSuggestion = $fragment.children().last();
-                this.$lastSuggestion.after($fragment);
-                this.$lastSuggestion = $lastSuggestion;
-            },
-            _renderPending: function renderPending(query) {
-                var template = this.templates.pending;
-                this._resetLastSuggestion();
-                template && this.$el.html(template({
-                    query: query,
-                    dataset: this.name
-                }));
-            },
-            _renderNotFound: function renderNotFound(query) {
-                var template = this.templates.notFound;
-                this._resetLastSuggestion();
-                template && this.$el.html(template({
-                    query: query,
-                    dataset: this.name
-                }));
-            },
-            _empty: function empty() {
-                this.$el.empty();
-                this._resetLastSuggestion();
-            },
-            _getSuggestionsFragment: function getSuggestionsFragment(query, suggestions) {
-                var that = this, fragment;
-                fragment = document.createDocumentFragment();
-                _.each(suggestions, function getSuggestionNode(suggestion) {
-                    var $el, context;
-                    context = that._injectQuery(query, suggestion);
-                    $el = $(that.templates.suggestion(context)).data(keys.dataset, that.name).data(keys.obj, suggestion).data(keys.val, that.displayFn(suggestion)).addClass(that.classes.suggestion + " " + that.classes.selectable);
-                    fragment.appendChild($el[0]);
-                });
-                this.highlight && highlight({
-                    className: this.classes.highlight,
-                    node: fragment,
-                    pattern: query
-                });
-                return $(fragment);
-            },
-            _getFooter: function getFooter(query, suggestions) {
-                return this.templates.footer ? this.templates.footer({
-                    query: query,
-                    suggestions: suggestions,
-                    dataset: this.name
-                }) : null;
-            },
-            _getHeader: function getHeader(query, suggestions) {
-                return this.templates.header ? this.templates.header({
-                    query: query,
-                    suggestions: suggestions,
-                    dataset: this.name
-                }) : null;
-            },
-            _resetLastSuggestion: function resetLastSuggestion() {
-                this.$lastSuggestion = $();
-            },
-            _injectQuery: function injectQuery(query, obj) {
-                return _.isObject(obj) ? _.mixin({
-                    _query: query
-                }, obj) : obj;
-            },
-            update: function update(query) {
-                var that = this, canceled = false, syncCalled = false, rendered = 0;
-                this.cancel();
-                this.cancel = function cancel() {
-                    canceled = true;
-                    that.cancel = $.noop;
-                    that.async && that.trigger("asyncCanceled", query, that.name);
-                };
-                this.source(query, sync, async);
-                !syncCalled && sync([]);
-                function sync(suggestions) {
-                    if (syncCalled) {
-                        return;
-                    }
-                    syncCalled = true;
-                    suggestions = (suggestions || []).slice(0, that.limit);
-                    rendered = suggestions.length;
-                    that._overwrite(query, suggestions);
-                    if (rendered < that.limit && that.async) {
-                        that.trigger("asyncRequested", query, that.name);
-                    }
-                }
-                function async(suggestions) {
-                    suggestions = suggestions || [];
-                    if (!canceled && rendered < that.limit) {
-                        that.cancel = $.noop;
-                        var idx = Math.abs(rendered - that.limit);
-                        rendered += idx;
-                        that._append(query, suggestions.slice(0, idx));
-                        that.async && that.trigger("asyncReceived", query, that.name);
-                    }
-                }
-            },
-            cancel: $.noop,
-            clear: function clear() {
-                this._empty();
-                this.cancel();
-                this.trigger("cleared");
-            },
-            isEmpty: function isEmpty() {
-                return this.$el.is(":empty");
-            },
-            destroy: function destroy() {
-                this.$el = $("<div>");
-            }
-        });
-        return Dataset;
-        function getDisplayFn(display) {
-            display = display || _.stringify;
-            return _.isFunction(display) ? display : displayFn;
-            function displayFn(obj) {
-                return obj[display];
-            }
-        }
-        function getTemplates(templates, displayFn) {
-            return {
-                notFound: templates.notFound && _.templatify(templates.notFound),
-                pending: templates.pending && _.templatify(templates.pending),
-                header: templates.header && _.templatify(templates.header),
-                footer: templates.footer && _.templatify(templates.footer),
-                suggestion: templates.suggestion || suggestionTemplate
-            };
-            function suggestionTemplate(context) {
-                return $('<div role="option">').attr("id", _.guid()).text(displayFn(context));
-            }
-        }
-        function isValidName(str) {
-            return /^[_a-zA-Z0-9-]+$/.test(str);
-        }
-    }();
-    var Menu = function() {
-        "use strict";
-        function Menu(o, www) {
-            var that = this;
-            o = o || {};
-            if (!o.node) {
-                $.error("node is required");
-            }
-            www.mixin(this);
-            this.$node = $(o.node);
-            this.query = null;
-            this.datasets = _.map(o.datasets, initializeDataset);
-            function initializeDataset(oDataset) {
-                var node = that.$node.find(oDataset.node).first();
-                oDataset.node = node.length ? node : $("<div>").appendTo(that.$node);
-                return new Dataset(oDataset, www);
-            }
-        }
-        _.mixin(Menu.prototype, EventEmitter, {
-            _onSelectableClick: function onSelectableClick($e) {
-                this.trigger("selectableClicked", $($e.currentTarget));
-            },
-            _onRendered: function onRendered(type, dataset, suggestions, async) {
-                this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
-                this.trigger("datasetRendered", dataset, suggestions, async);
-            },
-            _onCleared: function onCleared() {
-                this.$node.toggleClass(this.classes.empty, this._allDatasetsEmpty());
-                this.trigger("datasetCleared");
-            },
-            _propagate: function propagate() {
-                this.trigger.apply(this, arguments);
-            },
-            _allDatasetsEmpty: function allDatasetsEmpty() {
-                return _.every(this.datasets, _.bind(function isDatasetEmpty(dataset) {
-                    var isEmpty = dataset.isEmpty();
-                    this.$node.attr("aria-expanded", !isEmpty);
-                    return isEmpty;
-                }, this));
-            },
-            _getSelectables: function getSelectables() {
-                return this.$node.find(this.selectors.selectable);
-            },
-            _removeCursor: function _removeCursor() {
-                var $selectable = this.getActiveSelectable();
-                $selectable && $selectable.removeClass(this.classes.cursor);
-            },
-            _ensureVisible: function ensureVisible($el) {
-                var elTop, elBottom, nodeScrollTop, nodeHeight;
-                elTop = $el.position().top;
-                elBottom = elTop + $el.outerHeight(true);
-                nodeScrollTop = this.$node.scrollTop();
-                nodeHeight = this.$node.height() + parseInt(this.$node.css("paddingTop"), 10) + parseInt(this.$node.css("paddingBottom"), 10);
-                if (elTop < 0) {
-                    this.$node.scrollTop(nodeScrollTop + elTop);
-                } else if (nodeHeight < elBottom) {
-                    this.$node.scrollTop(nodeScrollTop + (elBottom - nodeHeight));
-                }
-            },
-            bind: function() {
-                var that = this, onSelectableClick;
-                onSelectableClick = _.bind(this._onSelectableClick, this);
-                this.$node.on("click.tt", this.selectors.selectable, onSelectableClick);
-                this.$node.on("mouseover", this.selectors.selectable, function() {
-                    that.setCursor($(this));
-                });
-                this.$node.on("mouseleave", function() {
-                    that._removeCursor();
-                });
-                _.each(this.datasets, function(dataset) {
-                    dataset.onSync("asyncRequested", that._propagate, that).onSync("asyncCanceled", that._propagate, that).onSync("asyncReceived", that._propagate, that).onSync("rendered", that._onRendered, that).onSync("cleared", that._onCleared, that);
-                });
-                return this;
-            },
-            isOpen: function isOpen() {
-                return this.$node.hasClass(this.classes.open);
-            },
-            open: function open() {
-                this.$node.scrollTop(0);
-                this.$node.addClass(this.classes.open);
-            },
-            close: function close() {
-                this.$node.attr("aria-expanded", false);
-                this.$node.removeClass(this.classes.open);
-                this._removeCursor();
-            },
-            setLanguageDirection: function setLanguageDirection(dir) {
-                this.$node.attr("dir", dir);
-            },
-            selectableRelativeToCursor: function selectableRelativeToCursor(delta) {
-                var $selectables, $oldCursor, oldIndex, newIndex;
-                $oldCursor = this.getActiveSelectable();
-                $selectables = this._getSelectables();
-                oldIndex = $oldCursor ? $selectables.index($oldCursor) : -1;
-                newIndex = oldIndex + delta;
-                newIndex = (newIndex + 1) % ($selectables.length + 1) - 1;
-                newIndex = newIndex < -1 ? $selectables.length - 1 : newIndex;
-                return newIndex === -1 ? null : $selectables.eq(newIndex);
-            },
-            setCursor: function setCursor($selectable) {
-                this._removeCursor();
-                if ($selectable = $selectable && $selectable.first()) {
-                    $selectable.addClass(this.classes.cursor);
-                    this._ensureVisible($selectable);
-                }
-            },
-            getSelectableData: function getSelectableData($el) {
-                return $el && $el.length ? Dataset.extractData($el) : null;
-            },
-            getActiveSelectable: function getActiveSelectable() {
-                var $selectable = this._getSelectables().filter(this.selectors.cursor).first();
-                return $selectable.length ? $selectable : null;
-            },
-            getTopSelectable: function getTopSelectable() {
-                var $selectable = this._getSelectables().first();
-                return $selectable.length ? $selectable : null;
-            },
-            update: function update(query) {
-                var isValidUpdate = query !== this.query;
-                if (isValidUpdate) {
-                    this.query = query;
-                    _.each(this.datasets, updateDataset);
-                }
-                return isValidUpdate;
-                function updateDataset(dataset) {
-                    dataset.update(query);
-                }
-            },
-            empty: function empty() {
-                _.each(this.datasets, clearDataset);
-                this.query = null;
-                this.$node.addClass(this.classes.empty);
-                function clearDataset(dataset) {
-                    dataset.clear();
-                }
-            },
-            destroy: function destroy() {
-                this.$node.off(".tt");
-                this.$node = $("<div>");
-                _.each(this.datasets, destroyDataset);
-                function destroyDataset(dataset) {
-                    dataset.destroy();
-                }
-            }
-        });
-        return Menu;
-    }();
-    var Status = function() {
-        "use strict";
-        function Status(options) {
-            this.$el = $("<span></span>", {
-                role: "status",
-                "aria-live": "polite"
-            }).css({
-                position: "absolute",
-                padding: "0",
-                border: "0",
-                height: "1px",
-                width: "1px",
-                "margin-bottom": "-1px",
-                "margin-right": "-1px",
-                overflow: "hidden",
-                clip: "rect(0 0 0 0)",
-                "white-space": "nowrap"
-            });
-            options.$input.after(this.$el);
-            _.each(options.menu.datasets, _.bind(function(dataset) {
-                if (dataset.onSync) {
-                    dataset.onSync("rendered", _.bind(this.update, this));
-                    dataset.onSync("cleared", _.bind(this.cleared, this));
-                }
-            }, this));
-        }
-        _.mixin(Status.prototype, {
-            update: function update(event, suggestions) {
-                var length = suggestions.length;
-                var words;
-                if (length === 1) {
-                    words = {
-                        result: "result",
-                        is: "is"
-                    };
-                } else {
-                    words = {
-                        result: "results",
-                        is: "are"
-                    };
-                }
-                this.$el.text(length + " " + words.result + " " + words.is + " available, use up and down arrow keys to navigate.");
-            },
-            cleared: function() {
-                this.$el.text("");
-            }
-        });
-        return Status;
-    }();
-    var DefaultMenu = function() {
-        "use strict";
-        var s = Menu.prototype;
-        function DefaultMenu() {
-            Menu.apply(this, [].slice.call(arguments, 0));
-        }
-        _.mixin(DefaultMenu.prototype, Menu.prototype, {
-            open: function open() {
-                !this._allDatasetsEmpty() && this._show();
-                return s.open.apply(this, [].slice.call(arguments, 0));
-            },
-            close: function close() {
-                this._hide();
-                return s.close.apply(this, [].slice.call(arguments, 0));
-            },
-            _onRendered: function onRendered() {
-                if (this._allDatasetsEmpty()) {
-                    this._hide();
-                } else {
-                    this.isOpen() && this._show();
-                }
-                return s._onRendered.apply(this, [].slice.call(arguments, 0));
-            },
-            _onCleared: function onCleared() {
-                if (this._allDatasetsEmpty()) {
-                    this._hide();
-                } else {
-                    this.isOpen() && this._show();
-                }
-                return s._onCleared.apply(this, [].slice.call(arguments, 0));
-            },
-            setLanguageDirection: function setLanguageDirection(dir) {
-                this.$node.css(dir === "ltr" ? this.css.ltr : this.css.rtl);
-                return s.setLanguageDirection.apply(this, [].slice.call(arguments, 0));
-            },
-            _hide: function hide() {
-                this.$node.hide();
-            },
-            _show: function show() {
-                this.$node.css("display", "block");
-            }
-        });
-        return DefaultMenu;
-    }();
-    var Typeahead = function() {
-        "use strict";
-        function Typeahead(o, www) {
-            var onFocused, onBlurred, onEnterKeyed, onTabKeyed, onEscKeyed, onUpKeyed, onDownKeyed, onLeftKeyed, onRightKeyed, onQueryChanged, onWhitespaceChanged;
-            o = o || {};
-            if (!o.input) {
-                $.error("missing input");
-            }
-            if (!o.menu) {
-                $.error("missing menu");
-            }
-            if (!o.eventBus) {
-                $.error("missing event bus");
-            }
-            www.mixin(this);
-            this.eventBus = o.eventBus;
-            this.minLength = _.isNumber(o.minLength) ? o.minLength : 1;
-            this.input = o.input;
-            this.menu = o.menu;
-            this.enabled = true;
-            this.autoselect = !!o.autoselect;
-            this.active = false;
-            this.input.hasFocus() && this.activate();
-            this.dir = this.input.getLangDir();
-            this._hacks();
-            this.menu.bind().onSync("selectableClicked", this._onSelectableClicked, this).onSync("asyncRequested", this._onAsyncRequested, this).onSync("asyncCanceled", this._onAsyncCanceled, this).onSync("asyncReceived", this._onAsyncReceived, this).onSync("datasetRendered", this._onDatasetRendered, this).onSync("datasetCleared", this._onDatasetCleared, this);
-            onFocused = c(this, "activate", "open", "_onFocused");
-            onBlurred = c(this, "deactivate", "_onBlurred");
-            onEnterKeyed = c(this, "isActive", "isOpen", "_onEnterKeyed");
-            onTabKeyed = c(this, "isActive", "isOpen", "_onTabKeyed");
-            onEscKeyed = c(this, "isActive", "_onEscKeyed");
-            onUpKeyed = c(this, "isActive", "open", "_onUpKeyed");
-            onDownKeyed = c(this, "isActive", "open", "_onDownKeyed");
-            onLeftKeyed = c(this, "isActive", "isOpen", "_onLeftKeyed");
-            onRightKeyed = c(this, "isActive", "isOpen", "_onRightKeyed");
-            onQueryChanged = c(this, "_openIfActive", "_onQueryChanged");
-            onWhitespaceChanged = c(this, "_openIfActive", "_onWhitespaceChanged");
-            this.input.bind().onSync("focused", onFocused, this).onSync("blurred", onBlurred, this).onSync("enterKeyed", onEnterKeyed, this).onSync("tabKeyed", onTabKeyed, this).onSync("escKeyed", onEscKeyed, this).onSync("upKeyed", onUpKeyed, this).onSync("downKeyed", onDownKeyed, this).onSync("leftKeyed", onLeftKeyed, this).onSync("rightKeyed", onRightKeyed, this).onSync("queryChanged", onQueryChanged, this).onSync("whitespaceChanged", onWhitespaceChanged, this).onSync("langDirChanged", this._onLangDirChanged, this);
-        }
-        _.mixin(Typeahead.prototype, {
-            _hacks: function hacks() {
-                var $input, $menu;
-                $input = this.input.$input || $("<div>");
-                $menu = this.menu.$node || $("<div>");
-                $input.on("blur.tt", function($e) {
-                    var active, isActive, hasActive;
-                    active = document.activeElement;
-                    isActive = $menu.is(active);
-                    hasActive = $menu.has(active).length > 0;
-                    if (_.isMsie() && (isActive || hasActive)) {
-                        $e.preventDefault();
-                        $e.stopImmediatePropagation();
-                        _.defer(function() {
-                            $input.focus();
-                        });
-                    }
-                });
-                $menu.on("mousedown.tt", function($e) {
-                    $e.preventDefault();
-                });
-            },
-            _onSelectableClicked: function onSelectableClicked(type, $el) {
-                this.select($el);
-            },
-            _onDatasetCleared: function onDatasetCleared() {
-                this._updateHint();
-            },
-            _onDatasetRendered: function onDatasetRendered(type, suggestions, async, dataset) {
-                this._updateHint();
-                if (this.autoselect) {
-                    var cursorClass = this.selectors.cursor.substr(1);
-                    this.menu.$node.find(this.selectors.suggestion).first().addClass(cursorClass);
-                }
-                this.eventBus.trigger("render", suggestions, async, dataset);
-            },
-            _onAsyncRequested: function onAsyncRequested(type, dataset, query) {
-                this.eventBus.trigger("asyncrequest", query, dataset);
-            },
-            _onAsyncCanceled: function onAsyncCanceled(type, dataset, query) {
-                this.eventBus.trigger("asynccancel", query, dataset);
-            },
-            _onAsyncReceived: function onAsyncReceived(type, dataset, query) {
-                this.eventBus.trigger("asyncreceive", query, dataset);
-            },
-            _onFocused: function onFocused() {
-                this._minLengthMet() && this.menu.update(this.input.getQuery());
-            },
-            _onBlurred: function onBlurred() {
-                if (this.input.hasQueryChangedSinceLastFocus()) {
-                    this.eventBus.trigger("change", this.input.getQuery());
-                }
-            },
-            _onEnterKeyed: function onEnterKeyed(type, $e) {
-                var $selectable;
-                if ($selectable = this.menu.getActiveSelectable()) {
-                    if (this.select($selectable)) {
-                        $e.preventDefault();
-                        $e.stopPropagation();
-                    }
-                } else if (this.autoselect) {
-                    if (this.select(this.menu.getTopSelectable())) {
-                        $e.preventDefault();
-                        $e.stopPropagation();
-                    }
-                }
-            },
-            _onTabKeyed: function onTabKeyed(type, $e) {
-                var $selectable;
-                if ($selectable = this.menu.getActiveSelectable()) {
-                    this.select($selectable) && $e.preventDefault();
-                } else if ($selectable = this.menu.getTopSelectable()) {
-                    this.autocomplete($selectable) && $e.preventDefault();
-                }
-            },
-            _onEscKeyed: function onEscKeyed() {
-                this.close();
-            },
-            _onUpKeyed: function onUpKeyed() {
-                this.moveCursor(-1);
-            },
-            _onDownKeyed: function onDownKeyed() {
-                this.moveCursor(+1);
-            },
-            _onLeftKeyed: function onLeftKeyed() {
-                if (this.dir === "rtl" && this.input.isCursorAtEnd()) {
-                    this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
-                }
-            },
-            _onRightKeyed: function onRightKeyed() {
-                if (this.dir === "ltr" && this.input.isCursorAtEnd()) {
-                    this.autocomplete(this.menu.getActiveSelectable() || this.menu.getTopSelectable());
-                }
-            },
-            _onQueryChanged: function onQueryChanged(e, query) {
-                this._minLengthMet(query) ? this.menu.update(query) : this.menu.empty();
-            },
-            _onWhitespaceChanged: function onWhitespaceChanged() {
-                this._updateHint();
-            },
-            _onLangDirChanged: function onLangDirChanged(e, dir) {
-                if (this.dir !== dir) {
-                    this.dir = dir;
-                    this.menu.setLanguageDirection(dir);
-                }
-            },
-            _openIfActive: function openIfActive() {
-                this.isActive() && this.open();
-            },
-            _minLengthMet: function minLengthMet(query) {
-                query = _.isString(query) ? query : this.input.getQuery() || "";
-                return query.length >= this.minLength;
-            },
-            _updateHint: function updateHint() {
-                var $selectable, data, val, query, escapedQuery, frontMatchRegEx, match;
-                $selectable = this.menu.getTopSelectable();
-                data = this.menu.getSelectableData($selectable);
-                val = this.input.getInputValue();
-                if (data && !_.isBlankString(val) && !this.input.hasOverflow()) {
-                    query = Input.normalizeQuery(val);
-                    escapedQuery = _.escapeRegExChars(query);
-                    frontMatchRegEx = new RegExp("^(?:" + escapedQuery + ")(.+$)", "i");
-                    match = frontMatchRegEx.exec(data.val);
-                    match && this.input.setHint(val + match[1]);
-                } else {
-                    this.input.clearHint();
-                }
-            },
-            isEnabled: function isEnabled() {
-                return this.enabled;
-            },
-            enable: function enable() {
-                this.enabled = true;
-            },
-            disable: function disable() {
-                this.enabled = false;
-            },
-            isActive: function isActive() {
-                return this.active;
-            },
-            activate: function activate() {
-                if (this.isActive()) {
-                    return true;
-                } else if (!this.isEnabled() || this.eventBus.before("active")) {
-                    return false;
-                } else {
-                    this.active = true;
-                    this.eventBus.trigger("active");
-                    return true;
-                }
-            },
-            deactivate: function deactivate() {
-                if (!this.isActive()) {
-                    return true;
-                } else if (this.eventBus.before("idle")) {
-                    return false;
-                } else {
-                    this.active = false;
-                    this.close();
-                    this.eventBus.trigger("idle");
-                    return true;
-                }
-            },
-            isOpen: function isOpen() {
-                return this.menu.isOpen();
-            },
-            open: function open() {
-                if (!this.isOpen() && !this.eventBus.before("open")) {
-                    this.menu.open();
-                    this._updateHint();
-                    this.eventBus.trigger("open");
-                }
-                return this.isOpen();
-            },
-            close: function close() {
-                if (this.isOpen() && !this.eventBus.before("close")) {
-                    this.menu.close();
-                    this.input.clearHint();
-                    this.input.resetInputValue();
-                    this.eventBus.trigger("close");
-                }
-                return !this.isOpen();
-            },
-            setVal: function setVal(val) {
-                this.input.setQuery(_.toStr(val));
-            },
-            getVal: function getVal() {
-                return this.input.getQuery();
-            },
-            select: function select($selectable) {
-                var data = this.menu.getSelectableData($selectable);
-                if (data && !this.eventBus.before("select", data.obj, data.dataset)) {
-                    this.input.setQuery(data.val, true);
-                    this.eventBus.trigger("select", data.obj, data.dataset);
-                    this.close();
-                    return true;
-                }
-                return false;
-            },
-            autocomplete: function autocomplete($selectable) {
-                var query, data, isValid;
-                query = this.input.getQuery();
-                data = this.menu.getSelectableData($selectable);
-                isValid = data && query !== data.val;
-                if (isValid && !this.eventBus.before("autocomplete", data.obj, data.dataset)) {
-                    this.input.setQuery(data.val);
-                    this.eventBus.trigger("autocomplete", data.obj, data.dataset);
-                    return true;
-                }
-                return false;
-            },
-            moveCursor: function moveCursor(delta) {
-                var query, $candidate, data, suggestion, datasetName, cancelMove, id;
-                query = this.input.getQuery();
-                $candidate = this.menu.selectableRelativeToCursor(delta);
-                data = this.menu.getSelectableData($candidate);
-                suggestion = data ? data.obj : null;
-                datasetName = data ? data.dataset : null;
-                id = $candidate ? $candidate.attr("id") : null;
-                this.input.trigger("cursorchange", id);
-                cancelMove = this._minLengthMet() && this.menu.update(query);
-                if (!cancelMove && !this.eventBus.before("cursorchange", suggestion, datasetName)) {
-                    this.menu.setCursor($candidate);
-                    if (data) {
-                        this.input.setInputValue(data.val);
-                    } else {
-                        this.input.resetInputValue();
-                        this._updateHint();
-                    }
-                    this.eventBus.trigger("cursorchange", suggestion, datasetName);
-                    return true;
-                }
-                return false;
-            },
-            destroy: function destroy() {
-                this.input.destroy();
-                this.menu.destroy();
-            }
-        });
-        return Typeahead;
-        function c(ctx) {
-            var methods = [].slice.call(arguments, 1);
-            return function() {
-                var args = [].slice.call(arguments);
-                _.each(methods, function(method) {
-                    return ctx[method].apply(ctx, args);
-                });
-            };
-        }
-    }();
-    (function() {
-        "use strict";
-        var old, keys, methods;
-        old = $.fn.typeahead;
-        keys = {
-            www: "tt-www",
-            attrs: "tt-attrs",
-            typeahead: "tt-typeahead"
-        };
-        methods = {
-            initialize: function initialize(o, datasets) {
-                var www;
-                datasets = _.isArray(datasets) ? datasets : [].slice.call(arguments, 1);
-                o = o || {};
-                www = WWW(o.classNames);
-                return this.each(attach);
-                function attach() {
-                    var $input, $wrapper, $hint, $menu, defaultHint, defaultMenu, eventBus, input, menu, status, typeahead, MenuConstructor;
-                    _.each(datasets, function(d) {
-                        d.highlight = !!o.highlight;
-                    });
-                    $input = $(this);
-                    $wrapper = $(www.html.wrapper);
-                    $hint = $elOrNull(o.hint);
-                    $menu = $elOrNull(o.menu);
-                    defaultHint = o.hint !== false && !$hint;
-                    defaultMenu = o.menu !== false && !$menu;
-                    defaultHint && ($hint = buildHintFromInput($input, www));
-                    defaultMenu && ($menu = $(www.html.menu).css(www.css.menu));
-                    $hint && $hint.val("");
-                    $input = prepInput($input, www);
-                    if (defaultHint || defaultMenu) {
-                        $wrapper.css(www.css.wrapper);
-                        $input.css(defaultHint ? www.css.input : www.css.inputWithNoHint);
-                        $input.wrap($wrapper).parent().prepend(defaultHint ? $hint : null).append(defaultMenu ? $menu : null);
-                    }
-                    MenuConstructor = defaultMenu ? DefaultMenu : Menu;
-                    eventBus = new EventBus({
-                        el: $input
-                    });
-                    input = new Input({
-                        hint: $hint,
-                        input: $input
-                    }, www);
-                    menu = new MenuConstructor({
-                        node: $menu,
-                        datasets: datasets
-                    }, www);
-                    status = new Status({
-                        $input: $input,
-                        menu: menu
-                    });
-                    typeahead = new Typeahead({
-                        input: input,
-                        menu: menu,
-                        eventBus: eventBus,
-                        minLength: o.minLength,
-                        autoselect: o.autoselect
-                    }, www);
-                    $input.data(keys.www, www);
-                    $input.data(keys.typeahead, typeahead);
-                }
-            },
-            isEnabled: function isEnabled() {
-                var enabled;
-                ttEach(this.first(), function(t) {
-                    enabled = t.isEnabled();
-                });
-                return enabled;
-            },
-            enable: function enable() {
-                ttEach(this, function(t) {
-                    t.enable();
-                });
-                return this;
-            },
-            disable: function disable() {
-                ttEach(this, function(t) {
-                    t.disable();
-                });
-                return this;
-            },
-            isActive: function isActive() {
-                var active;
-                ttEach(this.first(), function(t) {
-                    active = t.isActive();
-                });
-                return active;
-            },
-            activate: function activate() {
-                ttEach(this, function(t) {
-                    t.activate();
-                });
-                return this;
-            },
-            deactivate: function deactivate() {
-                ttEach(this, function(t) {
-                    t.deactivate();
-                });
-                return this;
-            },
-            isOpen: function isOpen() {
-                var open;
-                ttEach(this.first(), function(t) {
-                    open = t.isOpen();
-                });
-                return open;
-            },
-            open: function open() {
-                ttEach(this, function(t) {
-                    t.open();
-                });
-                return this;
-            },
-            close: function close() {
-                ttEach(this, function(t) {
-                    t.close();
-                });
-                return this;
-            },
-            select: function select(el) {
-                var success = false, $el = $(el);
-                ttEach(this.first(), function(t) {
-                    success = t.select($el);
-                });
-                return success;
-            },
-            autocomplete: function autocomplete(el) {
-                var success = false, $el = $(el);
-                ttEach(this.first(), function(t) {
-                    success = t.autocomplete($el);
-                });
-                return success;
-            },
-            moveCursor: function moveCursoe(delta) {
-                var success = false;
-                ttEach(this.first(), function(t) {
-                    success = t.moveCursor(delta);
-                });
-                return success;
-            },
-            val: function val(newVal) {
-                var query;
-                if (!arguments.length) {
-                    ttEach(this.first(), function(t) {
-                        query = t.getVal();
-                    });
-                    return query;
-                } else {
-                    ttEach(this, function(t) {
-                        t.setVal(_.toStr(newVal));
-                    });
-                    return this;
-                }
-            },
-            destroy: function destroy() {
-                ttEach(this, function(typeahead, $input) {
-                    revert($input);
-                    typeahead.destroy();
-                });
-                return this;
-            }
-        };
-        $.fn.typeahead = function(method) {
-            if (methods[method]) {
-                return methods[method].apply(this, [].slice.call(arguments, 1));
-            } else {
-                return methods.initialize.apply(this, arguments);
-            }
-        };
-        $.fn.typeahead.noConflict = function noConflict() {
-            $.fn.typeahead = old;
-            return this;
-        };
-        function ttEach($els, fn) {
-            $els.each(function() {
-                var $input = $(this), typeahead;
-                (typeahead = $input.data(keys.typeahead)) && fn(typeahead, $input);
-            });
-        }
-        function buildHintFromInput($input, www) {
-            return $input.clone().addClass(www.classes.hint).removeData().css(www.css.hint).css(getBackgroundStyles($input)).prop({
-                readonly: true,
-                required: false
-            }).removeAttr("id name placeholder").removeClass("required").attr({
-                spellcheck: "false",
-                tabindex: -1
-            });
-        }
-        function prepInput($input, www) {
-            $input.data(keys.attrs, {
-                dir: $input.attr("dir"),
-                autocomplete: $input.attr("autocomplete"),
-                spellcheck: $input.attr("spellcheck"),
-                style: $input.attr("style")
-            });
-            $input.addClass(www.classes.input).attr({
-                spellcheck: false
-            });
-            try {
-                !$input.attr("dir") && $input.attr("dir", "auto");
-            } catch (e) {}
-            return $input;
-        }
-        function getBackgroundStyles($el) {
-            return {
-                backgroundAttachment: $el.css("background-attachment"),
-                backgroundClip: $el.css("background-clip"),
-                backgroundColor: $el.css("background-color"),
-                backgroundImage: $el.css("background-image"),
-                backgroundOrigin: $el.css("background-origin"),
-                backgroundPosition: $el.css("background-position"),
-                backgroundRepeat: $el.css("background-repeat"),
-                backgroundSize: $el.css("background-size")
-            };
-        }
-        function revert($input) {
-            var www, $wrapper;
-            www = $input.data(keys.www);
-            $wrapper = $input.parent().filter(www.selectors.wrapper);
-            _.each($input.data(keys.attrs), function(val, key) {
-                _.isUndefined(val) ? $input.removeAttr(key) : $input.attr(key, val);
-            });
-            $input.removeData(keys.typeahead).removeData(keys.www).removeData(keys.attr).removeClass(www.classes.input);
-            if ($wrapper.length) {
-                $input.detach().insertAfter($wrapper);
-                $wrapper.remove();
-            }
-        }
-        function $elOrNull(obj) {
-            var isValid, $el;
-            isValid = _.isJQuery(obj) || _.isElement(obj);
-            $el = isValid ? $(obj).first() : [];
-            return $el.length ? $el : null;
-        }
-    })();
-});

+ 4 - 0
resources/assets/js/search.js

@@ -0,0 +1,4 @@
+Vue.component(
+    'search-results',
+    require('./components/SearchResults.vue').default
+);

+ 0 - 10
resources/assets/sass/custom.scss

@@ -37,16 +37,6 @@ body, button, input, textarea {
   color: #212529 !important;
 }
 
-.search-form {
-  width: 100%;
-}
-
-.search-form input,
-.search-form .form-inline,
-.search-form .form-control {
-  width: 100%;
-}
-
 .settings-nav .active {
   border-left: 2px solid #6c757d !important
 }

+ 13 - 0
resources/assets/sass/landing.scss

@@ -0,0 +1,13 @@
+// Landing Page bundle
+
+@import 'variables';
+@import '~bootstrap/scss/bootstrap';
+@import 'custom';
+@import 'landing/carousel';
+@import 'landing/devices';
+
+.container.slim {
+	width: auto;
+	max-width: 680px;
+	padding: 0 15px;
+}

+ 126 - 0
resources/assets/sass/landing/carousel.scss

@@ -0,0 +1,126 @@
+@-webkit-keyframes iosDeviceCarousel {
+ 0% {
+   opacity:1;
+ }
+ 17% {
+   opacity:1;
+ }
+ 25% {
+   opacity:0;
+ }
+ 92% {
+   opacity:0;
+ }
+ 100% {
+   opacity:1;
+ }
+}
+
+@-moz-keyframes iosDeviceCarousel {
+ 0% {
+   opacity:1;
+ }
+ 17% {
+   opacity:1;
+ }
+ 25% {
+   opacity:0;
+ }
+ 92% {
+   opacity:0;
+ }
+ 100% {
+   opacity:1;
+ }
+}
+
+@-o-keyframes iosDeviceCarousel {
+ 0% {
+   opacity:1;
+ }
+ 17% {
+   opacity:1;
+ }
+ 25% {
+   opacity:0;
+ }
+ 92% {
+   opacity:0;
+ }
+ 100% {
+   opacity:1;
+ }
+}
+
+@keyframes iosDeviceCarousel {
+ 0% {
+   opacity:1;
+ }
+ 17% {
+   opacity:1;
+ }
+ 25% {
+   opacity:0;
+ }
+ 92% {
+   opacity:0;
+ }
+ 100% {
+   opacity:1;
+ }
+}
+
+#iosDevice {
+  position:relative;
+  margin:0 auto;
+}
+#iosDevice img {
+  position:absolute;
+  left:0;
+}
+
+#iosDevice img {
+  -webkit-animation-name: iosDeviceCarousel;
+  -webkit-animation-timing-function: ease-in-out;
+  -webkit-animation-iteration-count: infinite;
+  -webkit-animation-duration: 16s;
+
+  -moz-animation-name: iosDeviceCarousel;
+  -moz-animation-timing-function: ease-in-out;
+  -moz-animation-iteration-count: infinite;
+  -moz-animation-duration: 16s;
+
+  -o-animation-name: iosDeviceCarousel;
+  -o-animation-timing-function: ease-in-out;
+  -o-animation-iteration-count: infinite;
+  -o-animation-duration: 16s;
+
+  animation-name: iosDeviceCarousel;
+  animation-timing-function: ease-in-out;
+  animation-iteration-count: infinite;
+  animation-duration: 16s;
+}
+#iosDevice img:nth-of-type(1) {
+  -webkit-animation-delay: 12s;
+  -moz-animation-delay: 12s;
+  -o-animation-delay: 12s;
+  animation-delay: 12s;
+}
+#iosDevice img:nth-of-type(2) {
+  -webkit-animation-delay: 8s;
+  -moz-animation-delay: 8s;
+  -o-animation-delay: 8s;
+  animation-delay: 8s;
+}
+#iosDevice img:nth-of-type(3) {
+  -webkit-animation-delay: 4s;
+  -moz-animation-delay: 4s;
+  -o-animation-delay: 4s;
+  animation-delay: 4s;
+}
+#iosDevice img:nth-of-type(4) {
+  -webkit-animation-delay: 0;
+  -moz-animation-delay: 0;
+  -o-animation-delay: 0;
+  animation-delay: 0;
+}

+ 593 - 0
resources/assets/sass/landing/devices.scss

@@ -0,0 +1,593 @@
+.marvel-device {
+    display: inline-block;
+    position: relative;
+    -webkit-box-sizing: content-box !important;
+    box-sizing: content-box !important
+}
+
+.marvel-device .screen {
+    width: 100%;
+    position: relative;
+    height: 100%;
+    z-index: 3;
+    background: white;
+    overflow: hidden;
+    display: block;
+    border-radius: 1px;
+    -webkit-box-shadow: 0 0 0 3px #111;
+    box-shadow: 0 0 0 3px #111
+}
+
+.marvel-device .top-bar,
+.marvel-device .bottom-bar {
+    height: 3px;
+    background: black;
+    width: 100%;
+    display: block
+}
+
+.marvel-device .middle-bar {
+    width: 3px;
+    height: 4px;
+    top: 0px;
+    left: 90px;
+    background: black;
+    position: absolute
+}
+
+.marvel-device.iphone-x {
+    width: 375px;
+    height: 812px;
+    padding: 26px;
+    background: #fdfdfd;
+    -webkit-box-shadow: inset 0 0 11px 0 black;
+    box-shadow: inset 0 0 11px 0 black;
+    border-radius: 66px
+}
+
+.marvel-device.iphone-x .overflow {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-radius: 66px;
+    overflow: hidden
+}
+
+.marvel-device.iphone-x .shadow {
+    border-radius: 100%;
+    width: 90px;
+    height: 90px;
+    position: absolute;
+    background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.6) 0%, rgba(255, 255, 255, 0) 60%)
+}
+
+.marvel-device.iphone-x .shadow--tl {
+    top: -20px;
+    left: -20px
+}
+
+.marvel-device.iphone-x .shadow--tr {
+    top: -20px;
+    right: -20px
+}
+
+.marvel-device.iphone-x .shadow--bl {
+    bottom: -20px;
+    left: -20px
+}
+
+.marvel-device.iphone-x .shadow--br {
+    bottom: -20px;
+    right: -20px
+}
+
+.marvel-device.iphone-x:before {
+    width: calc(100% - 10px);
+    height: calc(100% - 10px);
+    position: absolute;
+    top: 5px;
+    content: '';
+    left: 5px;
+    border-radius: 61px;
+    background: black;
+    z-index: 1
+}
+
+.marvel-device.iphone-x .inner-shadow {
+    width: calc(100% - 20px);
+    height: calc(100% - 20px);
+    position: absolute;
+    top: 10px;
+    overflow: hidden;
+    left: 10px;
+    border-radius: 56px;
+    -webkit-box-shadow: inset 0 0 15px 0 rgba(255, 255, 255, 0.66);
+    box-shadow: inset 0 0 15px 0 rgba(255, 255, 255, 0.66);
+    z-index: 1
+}
+
+.marvel-device.iphone-x .inner-shadow:before {
+    -webkit-box-shadow: inset 0 0 20px 0 #FFFFFF;
+    box-shadow: inset 0 0 20px 0 #FFFFFF;
+    width: 100%;
+    height: 116%;
+    position: absolute;
+    top: -8%;
+    content: '';
+    left: 0;
+    border-radius: 200px / 112px;
+    z-index: 2
+}
+
+.marvel-device.iphone-x .screen {
+    border-radius: 40px;
+    -webkit-box-shadow: none;
+    box-shadow: none
+}
+
+.marvel-device.iphone-x .top-bar,
+.marvel-device.iphone-x .bottom-bar {
+    width: 100%;
+    position: absolute;
+    height: 8px;
+    background: rgba(0, 0, 0, 0.1);
+    left: 0
+}
+
+.marvel-device.iphone-x .top-bar {
+    top: 80px
+}
+
+.marvel-device.iphone-x .bottom-bar {
+    bottom: 80px
+}
+
+.marvel-device.iphone-x .volume,
+.marvel-device.iphone-x .volume:before,
+.marvel-device.iphone-x .volume:after,
+.marvel-device.iphone-x .sleep {
+    width: 3px;
+    background: #b5b5b5;
+    position: absolute
+}
+
+.marvel-device.iphone-x .volume {
+    left: -3px;
+    top: 116px;
+    height: 32px
+}
+
+.marvel-device.iphone-x .volume:before {
+    height: 62px;
+    top: 62px;
+    content: '';
+    left: 0
+}
+
+.marvel-device.iphone-x .volume:after {
+    height: 62px;
+    top: 140px;
+    content: '';
+    left: 0
+}
+
+.marvel-device.iphone-x .sleep {
+    height: 96px;
+    top: 200px;
+    right: -3px
+}
+
+.marvel-device.iphone-x .camera {
+    width: 6px;
+    height: 6px;
+    top: 9px;
+    border-radius: 100%;
+    position: absolute;
+    left: 154px;
+    background: #0d4d71
+}
+
+.marvel-device.iphone-x .speaker {
+    height: 6px;
+    width: 60px;
+    left: 50%;
+    position: absolute;
+    top: 9px;
+    margin-left: -30px;
+    background: #171818;
+    border-radius: 6px
+}
+
+.marvel-device.iphone-x .notch {
+    position: absolute;
+    width: 210px;
+    height: 30px;
+    top: 26px;
+    left: 108px;
+    z-index: 4;
+    background: black;
+    border-bottom-left-radius: 24px;
+    border-bottom-right-radius: 24px
+}
+
+.marvel-device.iphone-x .notch:before,
+.marvel-device.iphone-x .notch:after {
+    content: '';
+    height: 8px;
+    position: absolute;
+    top: 0;
+    width: 8px
+}
+
+.marvel-device.iphone-x .notch:after {
+    background: radial-gradient(circle at bottom left, transparent 0, transparent 70%, black 70%, black 100%);
+    left: -8px
+}
+
+.marvel-device.iphone-x .notch:before {
+    background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
+    right: -8px
+}
+
+.marvel-device.iphone-x.landscape {
+    height: 375px;
+    width: 812px
+}
+
+.marvel-device.iphone-x.landscape .top-bar,
+.marvel-device.iphone-x.landscape .bottom-bar {
+    width: 8px;
+    height: 100%;
+    top: 0
+}
+
+.marvel-device.iphone-x.landscape .top-bar {
+    left: 80px
+}
+
+.marvel-device.iphone-x.landscape .bottom-bar {
+    right: 80px;
+    bottom: auto;
+    left: auto
+}
+
+.marvel-device.iphone-x.landscape .volume,
+.marvel-device.iphone-x.landscape .volume:before,
+.marvel-device.iphone-x.landscape .volume:after,
+.marvel-device.iphone-x.landscape .sleep {
+    height: 3px
+}
+
+.marvel-device.iphone-x.landscape .inner-shadow:before {
+    height: 100%;
+    width: 116%;
+    left: -8%;
+    top: 0;
+    border-radius: 112px / 200px
+}
+
+.marvel-device.iphone-x.landscape .volume {
+    bottom: -3px;
+    top: auto;
+    left: 116px;
+    width: 32px
+}
+
+.marvel-device.iphone-x.landscape .volume:before {
+    width: 62px;
+    left: 62px;
+    top: 0
+}
+
+.marvel-device.iphone-x.landscape .volume:after {
+    width: 62px;
+    left: 140px;
+    top: 0
+}
+
+.marvel-device.iphone-x.landscape .sleep {
+    width: 96px;
+    left: 200px;
+    top: -3px;
+    right: auto
+}
+
+.marvel-device.iphone-x.landscape .camera {
+    left: 9px;
+    bottom: 154px;
+    top: auto
+}
+
+.marvel-device.iphone-x.landscape .speaker {
+    width: 6px;
+    height: 60px;
+    left: 9px;
+    top: 50%;
+    margin-top: -30px;
+    margin-left: 0
+}
+
+.marvel-device.iphone-x.landscape .notch {
+    height: 210px;
+    width: 30px;
+    left: 26px;
+    bottom: 108px;
+    top: auto;
+    border-top-right-radius: 24px;
+    border-bottom-right-radius: 24px;
+    border-bottom-left-radius: 0
+}
+
+.marvel-device.iphone-x.landscape .notch:before,
+.marvel-device.iphone-x.landscape .notch:after {
+    left: 0
+}
+
+.marvel-device.iphone-x.landscape .notch:after {
+    background: radial-gradient(circle at bottom right, transparent 0, transparent 70%, black 70%, black 100%);
+    bottom: -8px;
+    top: auto
+}
+
+.marvel-device.iphone-x.landscape .notch:before {
+    background: radial-gradient(circle at top right, transparent 0, transparent 70%, black 70%, black 100%);
+    top: -8px
+}
+
+.marvel-device.note8 {
+    width: 400px;
+    height: 822px;
+    background: black;
+    border-radius: 34px;
+    padding: 45px 10px
+}
+
+.marvel-device.note8 .overflow {
+    width: 100%;
+    height: 100%;
+    position: absolute;
+    top: 0;
+    left: 0;
+    border-radius: 34px;
+    overflow: hidden
+}
+
+.marvel-device.note8 .speaker {
+    height: 8px;
+    width: 56px;
+    left: 50%;
+    position: absolute;
+    top: 25px;
+    margin-left: -28px;
+    background: #171818;
+    z-index: 1;
+    border-radius: 8px
+}
+
+.marvel-device.note8 .camera {
+    height: 18px;
+    width: 18px;
+    left: 86px;
+    position: absolute;
+    top: 18px;
+    background: #212b36;
+    z-index: 1;
+    border-radius: 100%
+}
+
+.marvel-device.note8 .camera:before {
+    content: '';
+    height: 8px;
+    width: 8px;
+    left: -22px;
+    position: absolute;
+    top: 5px;
+    background: #212b36;
+    z-index: 1;
+    border-radius: 100%
+}
+
+.marvel-device.note8 .sensors {
+    height: 10px;
+    width: 10px;
+    left: 120px;
+    position: absolute;
+    top: 22px;
+    background: #1d233b;
+    z-index: 1;
+    border-radius: 100%
+}
+
+.marvel-device.note8 .sensors:before {
+    content: '';
+    height: 10px;
+    width: 10px;
+    left: 18px;
+    position: absolute;
+    top: 0;
+    background: #1d233b;
+    z-index: 1;
+    border-radius: 100%
+}
+
+.marvel-device.note8 .more-sensors {
+    height: 16px;
+    width: 16px;
+    left: 285px;
+    position: absolute;
+    top: 18px;
+    background: #33244a;
+    -webkit-box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
+    box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.1);
+    z-index: 1;
+    border-radius: 100%
+}
+
+.marvel-device.note8 .more-sensors:before {
+    content: '';
+    height: 11px;
+    width: 11px;
+    left: 40px;
+    position: absolute;
+    top: 4px;
+    background: #214a61;
+    z-index: 1;
+    border-radius: 100%
+}
+
+.marvel-device.note8 .sleep {
+    width: 2px;
+    height: 56px;
+    background: black;
+    position: absolute;
+    top: 288px;
+    right: -2px
+}
+
+.marvel-device.note8 .volume {
+    width: 2px;
+    height: 120px;
+    background: black;
+    position: absolute;
+    top: 168px;
+    left: -2px
+}
+
+.marvel-device.note8 .volume:before {
+    content: '';
+    top: 168px;
+    width: 2px;
+    position: absolute;
+    left: 0;
+    background: black;
+    height: 56px
+}
+
+.marvel-device.note8 .inner {
+    width: 100%;
+    height: calc(100% - 8px);
+    position: absolute;
+    top: 2px;
+    content: '';
+    left: 0px;
+    border-radius: 34px;
+    border-top: 2px solid #9fa0a2;
+    border-bottom: 2px solid #9fa0a2;
+    background: black;
+    z-index: 1;
+    -webkit-box-shadow: inset 0 0 6px 0 rgba(255, 255, 255, 0.5);
+    box-shadow: inset 0 0 6px 0 rgba(255, 255, 255, 0.5)
+}
+
+.marvel-device.note8 .shadow {
+    -webkit-box-shadow: inset 0 0 60px 0 white, inset 0 0 30px 0 rgba(255, 255, 255, 0.5), 0 0 20px 0 white, 0 0 20px 0 rgba(255, 255, 255, 0.5);
+    box-shadow: inset 0 0 60px 0 white, inset 0 0 30px 0 rgba(255, 255, 255, 0.5), 0 0 20px 0 white, 0 0 20px 0 rgba(255, 255, 255, 0.5);
+    height: 101%;
+    position: absolute;
+    top: -0.5%;
+    content: '';
+    width: calc(100% - 20px);
+    left: 10px;
+    border-radius: 38px;
+    z-index: 5;
+    pointer-events: none
+}
+
+.marvel-device.note8 .screen {
+    border-radius: 14px;
+    -webkit-box-shadow: none;
+    box-shadow: none
+}
+
+.marvel-device.note8.landscape {
+    height: 400px;
+    width: 822px;
+    padding: 10px 45px
+}
+
+.marvel-device.note8.landscape .speaker {
+    height: 56px;
+    width: 8px;
+    top: 50%;
+    margin-top: -28px;
+    margin-left: 0;
+    right: 25px;
+    left: auto
+}
+
+.marvel-device.note8.landscape .camera {
+    top: 86px;
+    right: 18px;
+    left: auto
+}
+
+.marvel-device.note8.landscape .camera:before {
+    top: -22px;
+    left: 5px
+}
+
+.marvel-device.note8.landscape .sensors {
+    top: 120px;
+    right: 22px;
+    left: auto
+}
+
+.marvel-device.note8.landscape .sensors:before {
+    top: 18px;
+    left: 0
+}
+
+.marvel-device.note8.landscape .more-sensors {
+    top: 285px;
+    right: 18px;
+    left: auto
+}
+
+.marvel-device.note8.landscape .more-sensors:before {
+    top: 40px;
+    left: 4px
+}
+
+.marvel-device.note8.landscape .sleep {
+    bottom: -2px;
+    top: auto;
+    right: 288px;
+    width: 56px;
+    height: 2px
+}
+
+.marvel-device.note8.landscape .volume {
+    width: 120px;
+    height: 2px;
+    top: -2px;
+    right: 168px;
+    left: auto
+}
+
+.marvel-device.note8.landscape .volume:before {
+    right: 168px;
+    left: auto;
+    top: 0;
+    width: 56px;
+    height: 2px
+}
+
+.marvel-device.note8.landscape .inner {
+    height: 100%;
+    width: calc(100% - 8px);
+    left: 2px;
+    top: 0;
+    border-top: 0;
+    border-bottom: 0;
+    border-left: 2px solid #9fa0a2;
+    border-right: 2px solid #9fa0a2
+}
+
+.marvel-device.note8.landscape .shadow {
+    width: 101%;
+    height: calc(100% - 20px);
+    left: -0.5%;
+    top: 10px
+}

+ 4 - 4
resources/views/admin/settings/system.blade.php

@@ -13,7 +13,7 @@
   	<div class="col-12 col-md-3">
   		<div class="card mb-3 border-left-blue">
   			<div class="card-body text-center">
-  				<p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['pixelfed']}}</p>
+  				<p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['pixelfed']}}" data-toggle="tooltip">{{$sys['pixelfed']}}</p>
   			</div>
   			<div class="card-footer font-weight-bold py-0 text-center bg-white">Pixelfed</div>
       </div>
@@ -21,7 +21,7 @@
     <div class="col-12 col-md-3">
       <div class="card mb-3 border-left-blue">
         <div class="card-body text-center">
-          <p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['database']['version']}}</p>
+          <p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['database']['version']}}" data-toggle="tooltip">{{$sys['database']['version']}}</p>
         </div>
         <div class="card-footer font-weight-bold py-0 text-center bg-white">{{$sys['database']['name']}}</div>
       </div>
@@ -29,7 +29,7 @@
     <div class="col-12 col-md-3">
       <div class="card mb-3 border-left-blue">
         <div class="card-body text-center">
-          <p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['php']}}</p>
+          <p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['php']}}" data-toggle="tooltip">{{$sys['php']}}</p>
         </div>
         <div class="card-footer font-weight-bold py-0 text-center bg-white">PHP</div>
       </div>
@@ -37,7 +37,7 @@
   	<div class="col-12 col-md-3">
   		<div class="card mb-3 border-left-blue">
   			<div class="card-body text-center">
-  				<p class="font-weight-ultralight h2 mb-0 text-truncate">{{$sys['laravel']}}</p>
+  				<p class="font-weight-ultralight h2 mb-0 text-truncate" title="{{$sys['laravel']}}" data-toggle="tooltip">{{$sys['laravel']}}</p>
   			</div>
   			<div class="card-footer font-weight-bold py-0 text-center bg-white">Laravel</div>
   		</div>

+ 8 - 3
resources/views/layouts/partial/nav.blade.php

@@ -7,9 +7,14 @@
 
         <div class="collapse navbar-collapse" id="navbarSupportedContent">
             @auth
-            <ul class="navbar-nav ml-auto d-none d-md-block">
-              <form class="form-inline search-form">
-                <input class="form-control mr-sm-2 search-form-input" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off">
+            <ul class="navbar-nav mx-auto pr-3">
+              <form class="form-inline search-bar" method="get" action="/i/results">
+                <div class="input-group">
+                    <input class="form-control" name="q" placeholder="{{__('navmenu.search')}}" aria-label="search" autocomplete="off">
+                    <div class="input-group-append">
+                        <button class="btn btn-outline-primary" type="submit"><i class="fas fa-search"></i></button>
+                    </div>
+                </div>
               </form>
             </ul>
             @endauth

+ 3 - 2
resources/views/profile/show.blade.php

@@ -12,10 +12,11 @@
 @endsection
 
 @push('meta')<meta property="og:description" content="{{$profile->bio}}">
-    <meta property="og:image" content="{{$profile->avatarUrl()}}">
-    <link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on PixelFed" type="application/atom+xml">
   @if(false == $settings['crawlable'] || $profile->remote_url)
   <meta name="robots" content="noindex, nofollow">
+  @else  <meta property="og:image" content="{{$profile->avatarUrl()}}">
+    <link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on PixelFed" type="application/atom+xml">
+    <link href='{{$profile->permalink()}}' rel='alternate' type='application/activity+json'>
   @endif
 @endpush
 

+ 5 - 0
resources/views/pxtv/home.blade.php

@@ -0,0 +1,5 @@
+@extends('layouts.app')
+
+@section('content')
+
+@endsection

+ 3 - 1
resources/views/search/results.blade.php

@@ -1,10 +1,12 @@
 @extends('layouts.app')
 
 @section('content')
-	<search-results></search-results>
+	<search-results query="{{request()->query('q')}}" profile-id="{{Auth::user()->profile->id}}"></search-results>
 @endsection
 
 @push('scripts')
+<script type="text/javascript" src="{{mix('js/compose.js')}}"></script>
+<script type="text/javascript" src="{{mix('js/search.js')}}"></script>
 <script type="text/javascript">
 	new Vue({
 		el: '#content'

+ 150 - 8
resources/views/site/index.blade.php

@@ -22,11 +22,157 @@
     <meta name="apple-mobile-web-app-capable" content="yes">
     <link rel="shortcut icon" type="image/png" href="/img/favicon.png?v=2">
     <link rel="apple-touch-icon" type="image/png" href="/img/favicon.png?v=2">
-    <link href="{{ mix('css/app.css') }}" rel="stylesheet" data-stylesheet="light">
+    <link href="{{ mix('css/landing.css') }}" rel="stylesheet">
 </head>
 <body class="">
     <main id="content">
-        <landing-page></landing-page>
+        <section class="container">
+            <div class="row py-5 mb-5">
+                <div class="col-12 col-md-6 d-none d-md-block">
+                    <div class="m-md-4" style="position: absolute; transform: scale(0.66)">
+                        <div class="marvel-device note8" style="position: absolute;z-index:10;">
+                            <div class="inner"></div>
+                            <div class="overflow">
+                                <div class="shadow"></div>
+                            </div>
+                            <div class="speaker"></div>
+                            <div class="sensors"></div>
+                            <div class="more-sensors"></div>
+                            <div class="sleep"></div>
+                            <div class="volume"></div>
+                            <div class="camera"></div>
+                            <div class="screen">
+                                <img src="/img/landing/android_1.jpg" class="img-fluid">
+                            </div>
+                        </div>
+                        <div class="marvel-device iphone-x" style="position: absolute;z-index: 20;margin: 99px 0 0 151px;">
+                            <div class="notch">
+                                <div class="camera"></div>
+                                <div class="speaker"></div>
+                            </div>
+                            <div class="top-bar"></div>
+                            <div class="sleep"></div>
+                            <div class="bottom-bar"></div>
+                            <div class="volume"></div>
+                            <div class="overflow">
+                                <div class="shadow shadow--tr"></div>
+                                <div class="shadow shadow--tl"></div>
+                                <div class="shadow shadow--br"></div>
+                                <div class="shadow shadow--bl"></div>
+                            </div>
+                            <div class="inner-shadow"></div>
+                            <div class="screen">
+                                <div id="iosDevice">
+                                    <img v-if="!loading" src="/img/landing/ios_4.jpg" class="img-fluid">
+                                    <img v-if="!loading" src="/img/landing/ios_3.jpg" class="img-fluid">
+                                    <img v-if="!loading" src="/img/landing/ios_2.jpg" class="img-fluid">
+                                    <img src="/img/landing/ios_1.jpg" class="img-fluid">
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+                <div class="col-12 col-md-5 offset-md-1">
+                    <div>
+                        <div class="card my-4">
+                            <div class="card-body px-lg-5">
+                                <div class="text-center pt-3">
+                                    <img src="/img/pixelfed-icon-color.svg">
+                                </div>
+                                <div class="py-3 text-center">
+                                    <h3 class="font-weight-bold">Pixelfed</h3>
+                                    <p class="mb-0 lead">Photo sharing for everyone</p>
+                                </div>
+                                <div>
+                                    @if(true === config('pixelfed.open_registration'))
+                                    <form class="px-1" method="POST" action="{{ route('register') }}">
+                                        @csrf
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" placeholder="{{ __('Name') }}" required autofocus>
+
+                                                @if ($errors->has('name'))
+                                                <span class="invalid-feedback">
+                                                    <strong>{{ $errors->first('name') }}</strong>
+                                                </span>
+                                                @endif
+                                            </div>
+                                        </div>
+
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <input id="username" type="text" class="form-control{{ $errors->has('username') ? ' is-invalid' : '' }}" name="username" value="{{ old('username') }}" placeholder="{{ __('Username') }}" required>
+
+                                                @if ($errors->has('username'))
+                                                <span class="invalid-feedback">
+                                                    <strong>{{ $errors->first('username') }}</strong>
+                                                </span>
+                                                @endif
+                                            </div>
+                                        </div>
+
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <input id="email" type="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" placeholder="{{ __('E-Mail Address') }}" required>
+
+                                                @if ($errors->has('email'))
+                                                <span class="invalid-feedback">
+                                                    <strong>{{ $errors->first('email') }}</strong>
+                                                </span>
+                                                @endif
+                                            </div>
+                                        </div>
+
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <input id="password" type="password" class="form-control{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" placeholder="{{ __('Password') }}" required>
+
+                                                @if ($errors->has('password'))
+                                                <span class="invalid-feedback">
+                                                    <strong>{{ $errors->first('password') }}</strong>
+                                                </span>
+                                                @endif
+                                            </div>
+                                        </div>
+
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <input id="password-confirm" type="password" class="form-control" name="password_confirmation" placeholder="{{ __('Confirm Password') }}" required>
+                                            </div>
+                                        </div>
+                                        @if(config('pixelfed.recaptcha'))
+                                        <div class="row my-3">
+                                            {!! Recaptcha::render() !!}
+                                        </div>
+                                        @endif
+                                        <div class="form-group row">
+                                            <div class="col-md-12">
+                                                <button type="submit" class="btn btn-primary btn-block py-0 font-weight-bold">
+                                                    {{ __('Register') }}
+                                                </button>
+                                            </div>
+                                        </div>
+                                        <p class="mb-0 font-weight-bold text-lighter small">By signing up, you agree to our <a href="/site/terms" class="text-muted">Terms of Use</a> and <a href="/site/privacy" class="text-muted">Privacy Policy</a>.</p>
+                                    </form>
+                                    @else
+                                    <div style="min-height: 350px" class="d-flex justify-content-center align-items-center">
+                                        <div class="text-center">
+                                            <p class="lead">Registrations are closed.</p>
+                                            <p class="text-lighter small">You can find a list of other instances on <a href="https://the-federation.info/pixelfed" class="text-muted font-weight-bold">the-federation.info/pixelfed</a> or <a href="https://fediverse.network/pixelfed" class="text-muted font-weight-bold">fediverse.network/pixelfed</a></p>
+                                        </div>
+                                    </div>
+                                    @endif
+                                </div>
+                            </div>
+                        </div>
+                        <div class="card card-body">
+                            <p class="text-center mb-0 font-weight-bold">Have an account? <a href="/login">Log in</a></p>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+        </section>
     </main>
     <footer>
         <div class="container py-3">
@@ -38,13 +184,9 @@
                 <a href="{{route('site.privacy')}}" class="text-primary pr-3">{{__('site.privacy')}}</a>
                 <a href="{{route('site.platform')}}" class="text-primary pr-3">API</a>
                 <a href="{{route('site.language')}}" class="text-primary pr-3">{{__('site.language')}}</a>
-                <a href="https://pixelfed.org" class="text-muted float-right" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">Powered by PixelFed</a>
+                <a href="https://pixelfed.org" class="text-muted float-right" rel="noopener" title="version {{config('pixelfed.version')}}" data-toggle="tooltip">Powered by Pixelfed</a>
             </p>
         </div>
     </footer>
 </body>
-
-<script type="text/javascript" src="{{mix('js/app.js')}}"></script>
-<script type="text/javascript" src="{{mix('js/landing.js')}}"></script>
-</html>
-
+</html>

+ 2 - 0
routes/web.php

@@ -130,6 +130,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 
         Route::get('media/preview/{profileId}/{mediaId}', 'ApiController@showTempMedia')->name('temp-media');
 
+        Route::get('results', 'SearchController@results');
+        Route::post('visibility', 'StatusController@toggleVisibility');
 
         Route::group(['prefix' => 'report'], function () {
             Route::get('/', 'ReportController@showForm')->name('report.form');

+ 0 - 7
tests/Feature/InstalledTest.php

@@ -8,13 +8,6 @@ use Illuminate\Foundation\Testing\WithoutMiddleware;
 
 class InstalledTest extends TestCase
 {
-    /** @test */
-    public function landing_page()
-    {
-        $response = $this->get('/');
-        $response->assertStatus(200);
-    }
-
     /** @test */
     public function nodeinfo_api()
     {

+ 6 - 3
webpack.mix.js

@@ -28,16 +28,19 @@ mix.js('resources/assets/js/app.js', 'public/js')
 // Timeline component
 .js('resources/assets/js/timeline.js', 'public/js')
 
-// LandingPage component
-.js('resources/assets/js/landing.js', 'public/js')
-
 // ComposeModal component
 .js('resources/assets/js/compose.js', 'public/js')
 
+// SearchResults component
+.js('resources/assets/js/search.js', 'public/js')
+
 .sass('resources/assets/sass/app.scss', 'public/css', {
 	implementation: require('node-sass')
 })
 .sass('resources/assets/sass/appdark.scss', 'public/css', {
 	implementation: require('node-sass')
 })
+.sass('resources/assets/sass/landing.scss', 'public/css', {
+	implementation: require('node-sass')
+})
 .version();

Some files were not shown because too many files changed in this diff